Merge branch 'mweb' of https://github.com/cake-tech/cake_wallet into mweb-bg-sync

This commit is contained in:
Matthew Fosse 2024-08-27 13:09:32 -04:00
commit dfea890c9e
405 changed files with 10822 additions and 7703 deletions

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<div align="center">
<img height="100" src=".github/assets/Logo_CakeWallet.png">
![logo](.github/assets/Logo_CakeWallet.png)
</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.
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

View file

@ -10,7 +10,18 @@ analyzer:
lib/generated/*.dart,
cw_monero/ios/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:
strict-casts: true
strict-raw-types: true

View file

@ -91,5 +91,4 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.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.provider.Settings;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
import java.security.SecureRandom;
public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24;
boolean isAppSecure = false;
@Override
@ -53,14 +49,6 @@ public class MainActivity extends FlutterFragmentActivity {
random.nextBytes(bytes);
handler.post(() -> result.success(bytes));
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":
isAppSecure = call.argument("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() {
String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);

View file

@ -19,14 +19,10 @@ import android.net.Uri;
import android.os.PowerManager;
import android.provider.Settings;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
import java.security.SecureRandom;
public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
@ -51,14 +47,6 @@ public class MainActivity extends FlutterFragmentActivity {
random.nextBytes(bytes);
handler.post(() -> result.success(bytes));
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":
disableBatteryOptimization();
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() {
String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);

View file

@ -19,14 +19,10 @@ import android.net.Uri;
import android.os.PowerManager;
import android.provider.Settings;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
import java.security.SecureRandom;
public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24;
boolean isAppSecure = false;
@Override
@ -52,14 +48,6 @@ public class MainActivity extends FlutterFragmentActivity {
random.nextBytes(bytes);
handler.post(() -> result.success(bytes));
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":
isAppSecure = call.argument("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() {
String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);

View file

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

Binary file not shown.

Before

(image error) Size: 3.4 KiB

After

(image error) Size: 9.9 KiB

Binary file not shown.

Before

(image error) Size: 3.4 KiB

After

(image error) Size: 12 KiB

Binary file not shown.

Before

(image error) Size: 287 B

After

(image error) Size: 376 B

Binary file not shown.

Before

(image error) Size: 340 B

After

(image error) Size: 1.2 KiB

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

Binary file not shown.

After

(image error) Size: 4.7 KiB

Binary file not shown.

Before

(image error) Size: 1.4 KiB

After

(image error) Size: 1.8 KiB

Binary file not shown.

Before

(image error) Size: 446 B

After

(image error) Size: 913 B

Binary file not shown.

Before

(image error) Size: 735 B

After

(image error) Size: 372 B

Binary file not shown.

Before

(image error) Size: 1.2 KiB

After

(image error) Size: 692 B

Binary file not shown.

Before

(image error) Size: 1,005 B

After

(image error) Size: 788 B

Binary file not shown.

Before

(image error) Size: 855 B

After

(image error) Size: 371 B

Binary file not shown.

Before

(image error) Size: 351 B

After

(image error) Size: 753 B

Binary file not shown.

Before

(image error) Size: 860 B

After

(image error) Size: 840 B

Binary file not shown.

Before

(image error) Size: 217 B

After

(image error) Size: 381 B

Binary file not shown.

Before

(image error) Size: 915 B

After

(image error) Size: 830 B

Binary file not shown.

Before

(image error) Size: 703 B

After

(image error) Size: 370 B

Binary file not shown.

Before

(image error) Size: 801 B

After

(image error) Size: 387 B

Binary file not shown.

Before

(image error) Size: 431 B

After

(image error) Size: 553 B

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 1.8 KiB

Binary file not shown.

Before

(image error) Size: 1,005 B

After

(image error) Size: 1.4 KiB

Binary file not shown.

Before

(image error) Size: 700 B

After

(image error) Size: 351 B

Binary file not shown.

Before

(image error) Size: 1.3 KiB

After

(image error) Size: 1.9 KiB

Binary file not shown.

Before

(image error) Size: 432 B

After

(image error) Size: 762 B

Binary file not shown.

Before

(image error) Size: 477 B

After

(image error) Size: 1.7 KiB

Binary file not shown.

Before

(image error) Size: 1,023 B

After

(image error) Size: 2 KiB

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 1.4 KiB

Binary file not shown.

Before

(image error) Size: 728 B

After

(image error) Size: 373 B

Binary file not shown.

Before

(image error) Size: 684 B

After

(image error) Size: 360 B

Binary file not shown.

Before

(image error) Size: 615 B

After

(image error) Size: 1.1 KiB

Binary file not shown.

Before

(image error) Size: 867 B

After

(image error) Size: 424 B

Binary file not shown.

Before

(image error) Size: 937 B

After

(image error) Size: 994 B

Binary file not shown.

Before

(image error) Size: 705 B

After

(image error) Size: 351 B

Binary file not shown.

Before

(image error) Size: 896 B

After

(image error) Size: 851 B

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 2.5 KiB

Binary file not shown.

Before

(image error) Size: 429 B

After

(image error) Size: 849 B

Binary file not shown.

Before

(image error) Size: 1,013 B

After

(image error) Size: 1.6 KiB

Binary file not shown.

Before

(image error) Size: 902 B

After

(image error) Size: 1 KiB

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 1.4 KiB

Binary file not shown.

Before

(image error) Size: 193 B

After

(image error) Size: 351 B

Binary file not shown.

Before

(image error) Size: 762 B

After

(image error) Size: 372 B

Binary file not shown.

Before

(image error) Size: 898 B

After

(image error) Size: 424 B

Binary file not shown.

Before

(image error) Size: 1.3 KiB

After

(image error) Size: 2 KiB

Binary file not shown.

Before

(image error) Size: 899 B

After

(image error) Size: 1.1 KiB

Binary file not shown.

Before

(image error) Size: 1.2 KiB

After

(image error) Size: 1.5 KiB

Binary file not shown.

Before

(image error) Size: 707 B

After

(image error) Size: 366 B

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 2.5 KiB

Binary file not shown.

Before

(image error) Size: 705 B

After

(image error) Size: 351 B

Binary file not shown.

Before

(image error) Size: 702 B

After

(image error) Size: 371 B

Binary file not shown.

Before

(image error) Size: 1.3 KiB

After

(image error) Size: 750 B

Binary file not shown.

Before

(image error) Size: 476 B

After

(image error) Size: 2.1 KiB

Binary file not shown.

Before

(image error) Size: 902 B

After

(image error) Size: 1.1 KiB

Binary file not shown.

Before

(image error) Size: 752 B

After

(image error) Size: 390 B

Binary file not shown.

Before

(image error) Size: 774 B

After

(image error) Size: 384 B

Binary file not shown.

Before

(image error) Size: 1 KiB

After

(image error) Size: 1.1 KiB

Binary file not shown.

Before

(image error) Size: 373 B

After

(image error) Size: 1,005 B

Binary file not shown.

Before

(image error) Size: 1.2 KiB

After

(image error) Size: 597 B

Binary file not shown.

Before

(image error) Size: 1,009 B

After

(image error) Size: 887 B

Binary file not shown.

Before

(image error) Size: 448 B

After

(image error) Size: 882 B

Binary file not shown.

Before

(image error) Size: 1.4 KiB

After

(image error) Size: 16 KiB

Binary file not shown.

Before

(image error) Size: 23 KiB

After

(image error) Size: 14 KiB

Binary file not shown.

Before

(image error) Size: 20 KiB

After

(image error) Size: 14 KiB

Binary file not shown.

Before

(image error) Size: 10 KiB

After

(image error) Size: 28 KiB

View file

@ -1,4 +1,19 @@
-
uri: ltc-electrum.cakewallet.com:50002
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
is_default: true
useSSL: true
-
uri: api.mainnet-beta.solana.com:443
useSSL: true

View file

@ -1,3 +1,3 @@
Monero enhancements
Synchronization improvements
Scan and verify messages
Synchronization enhancements
Bug fixes

View file

@ -1,5 +1,3 @@
Monero and Ethereum enhancements
Synchronization improvements
Exchange flow enhancements
Ledger improvements
Scan and verify messages
Synchronization enhancements
Bug fixes

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

@ -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"
ANDROID="android"
MACOS="macos"
LINUX="linux"
PLATFORMS=($IOS $ANDROID $MACOS)
PLATFORMS=($IOS $ANDROID $MACOS $LINUX)
PLATFORM=$1
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
fi
@ -27,9 +28,14 @@ if [ "$PLATFORM" == "$ANDROID" ]; then
cd scripts/android
fi
if [ "$PLATFORM" == "$LINUX" ]; then
echo "Configuring for linux"
cd scripts/linux
fi
source ./app_env.sh cakewallet
./app_config.sh
cd ../.. && flutter pub get
#flutter packages pub run tool/generate_localization.dart
flutter packages pub run tool/generate_localization.dart
./model_generator.sh
#cd macos && pod install
#cd macos && pod install

View file

@ -1,7 +1,7 @@
import 'dart:async';
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_core/hardware/hardware_account_data.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
@ -25,7 +25,8 @@ class BitcoinHardwareWalletService {
for (final i in indexRange) {
final derivationPath = "m/84'/0'/$i'";
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);

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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/utils.dart';
import 'package:cw_core/wallet_info.dart';
@ -24,7 +24,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
}) : super(walletInfo);
@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)
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';
class BitcoinNewWalletCredentials extends WalletCredentials {
BitcoinNewWalletCredentials(
{required String name,
WalletInfo? walletInfo,
DerivationType? derivationType,
String? derivationPath})
: super(
BitcoinNewWalletCredentials({
required String name,
WalletInfo? walletInfo,
String? password,
DerivationType? derivationType,
String? derivationPath,
String? passphrase,
}) : super(
name: name,
walletInfo: walletInfo,
password: password,
passphrase: passphrase,
);
}

View file

@ -1,8 +1,10 @@
import 'dart:io';
import 'package:bitcoin_base/bitcoin_base.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/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/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
@ -19,11 +21,12 @@ class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan);
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan;
final bool isDirect;
@override
WalletType getType() => WalletType.bitcoin;
@ -33,16 +36,32 @@ class BitcoinWalletService extends WalletService<
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
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(
mnemonic: await generateElectrumMnemonic(),
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
return wallet;
}
@ -61,6 +80,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
saveBackup(name);
@ -73,6 +93,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
return wallet;
@ -97,6 +118,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName);
@ -123,6 +145,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
@ -151,6 +174,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();

View file

@ -8,6 +8,8 @@ import 'package:cw_bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
enum ConnectionStatus { connected, disconnected, connecting, failed }
String jsonrpcparams(List<Object> params) {
final _params = params.map((val) => '"${val.toString()}"').join(',');
return '[$_params]';
@ -41,7 +43,7 @@ class ElectrumClient {
bool get isConnected => _isConnected;
Socket? socket;
void Function(bool?)? onConnectionStatusChange;
void Function(ConnectionStatus)? onConnectionStatusChange;
int _id;
final Map<String, SocketTask> _tasks;
Map<String, SocketTask> get tasks => _tasks;
@ -60,39 +62,72 @@ class ElectrumClient {
}
Future<void> connect({required String host, required int port, bool? useSSL}) async {
_setConnectionStatus(ConnectionStatus.connecting);
try {
await socket?.close();
socket = null;
} catch (_) {}
if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) {
socket = await Socket.connect(host, port, timeout: connectionTimeout);
} else {
socket = await SecureSocket.connect(host, port,
timeout: connectionTimeout, onBadCertificate: (_) => true);
}
_setIsConnected(true);
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());
try {
if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) {
socket = await Socket.connect(host, port, timeout: connectionTimeout);
} else {
socket = await SecureSocket.connect(
host,
port,
timeout: connectionTimeout,
onBadCertificate: (_) => true,
);
}
}, onError: (Object error) {
print(error.toString());
unterminatedString = '';
_setIsConnected(false);
}, onDone: () {
unterminatedString = '';
_setIsConnected(null);
});
} catch (_) {
_setConnectionStatus(ConnectionStatus.failed);
return;
}
if (socket == 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) {
socket = null;
final errorMsg = error.toString();
print(errorMsg);
unterminatedString = '';
final currentHost = socket?.address.host;
final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} ");
if (currentHost != null && isErrorForCurrentHost)
_setConnectionStatus(ConnectionStatus.failed);
},
onDone: () {
unterminatedString = '';
if (host == socket?.address.host) {
socket = null;
_setConnectionStatus(ConnectionStatus.disconnected);
}
},
cancelOnError: true,
);
keepAlive();
}
@ -144,9 +179,9 @@ class ElectrumClient {
Future<void> ping() async {
try {
await callWithTimeout(method: 'server.ping');
_setIsConnected(true);
_setConnectionStatus(ConnectionStatus.connected);
} on RequestFailedTimeoutException catch (_) {
_setIsConnected(null);
_setConnectionStatus(ConnectionStatus.disconnected);
}
}
@ -236,9 +271,24 @@ class ElectrumClient {
return [];
});
Future<Map<String, dynamic>> getTransactionRaw({required String hash}) async =>
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000)
.then((dynamic result) {
Future<dynamic> getTransaction({required String hash, required bool verbose}) async {
try {
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>) {
return result;
}
@ -246,9 +296,8 @@ class ElectrumClient {
return <String, dynamic>{};
});
Future<String> getTransactionHex({required String hash}) async =>
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000)
.then((dynamic result) {
Future<String> getTransactionHex({required String hash}) =>
getTransaction(hash: hash, verbose: false).then((dynamic result) {
if (result is String) {
return result;
}
@ -336,7 +385,7 @@ class ElectrumClient {
try {
final topDoubleString = await estimatefee(p: 1);
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 middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
@ -353,19 +402,18 @@ class ElectrumClient {
// "height": 520481,
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// }
BehaviorSubject<Map<String, dynamic>>? tipListener;
int? currentTip;
Future<int?> getCurrentBlockChainTip() async {
try {
final method = 'blockchain.headers.subscribe';
final cb = (result) => currentTip = result['height'] as int;
if (tipListener == null) {
tipListener = subscribe(id: method, method: method);
tipListener?.listen(cb);
callWithTimeout(method: method).then(cb);
final result = await callWithTimeout(method: 'blockchain.headers.subscribe');
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
return currentTip;
return null;
} on RequestFailedTimeoutException catch (_) {
return null;
} catch (e) {
print("getCurrentBlockChainTip: ${e.toString()}");
return null;
}
}
@ -387,6 +435,10 @@ class ElectrumClient {
BehaviorSubject<T>? subscribe<T>(
{required String id, required String method, List<Object> params = const []}) {
try {
if (socket == null) {
_setConnectionStatus(ConnectionStatus.failed);
return null;
}
final subscription = BehaviorSubject<T>();
_regisrySubscription(id, subscription);
socket!.write(jsonrpc(method: method, id: _id, params: params));
@ -400,6 +452,10 @@ class ElectrumClient {
Future<dynamic> call(
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
if (socket == null) {
_setConnectionStatus(ConnectionStatus.failed);
return null;
}
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
@ -413,6 +469,10 @@ class ElectrumClient {
Future<dynamic> callWithTimeout(
{required String method, List<Object> params = const [], int timeout = 4000}) async {
try {
if (socket == null) {
_setConnectionStatus(ConnectionStatus.failed);
return null;
}
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
@ -434,6 +494,7 @@ class ElectrumClient {
_aliveTimer?.cancel();
try {
await socket?.close();
socket = null;
} catch (_) {}
onConnectionStatusChange = null;
}
@ -488,12 +549,9 @@ class ElectrumClient {
}
}
void _setIsConnected(bool? isConnected) {
if (_isConnected != isConnected) {
onConnectionStatusChange?.call(isConnected);
}
_isConnected = isConnected ?? false;
void _setConnectionStatus(ConnectionStatus status) {
onConnectionStatusChange?.call(status);
_isConnected = status == ConnectionStatus.connected;
}
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 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_transaction_info.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/wallet_info.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';
@ -15,13 +18,15 @@ class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$Electru
abstract class ElectrumTransactionHistoryBase
extends TransactionHistoryBase<ElectrumTransactionInfo> with Store {
ElectrumTransactionHistoryBase({required this.walletInfo, required String password})
ElectrumTransactionHistoryBase(
{required this.walletInfo, required String password, required this.encryptionFileUtils})
: _password = password,
_height = 0 {
transactions = ObservableMap<String, ElectrumTransactionInfo>();
}
final WalletInfo walletInfo;
final EncryptionFileUtils encryptionFileUtils;
String _password;
int _height;
@ -44,7 +49,7 @@ abstract class ElectrumTransactionHistoryBase
txjson[tx.key] = tx.value.toJson();
}
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) {
print('Error while save bitcoin transaction history: ${e.toString()}');
}
@ -58,7 +63,7 @@ abstract class ElectrumTransactionHistoryBase
Future<Map<String, dynamic>> _read() async {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
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>;
}

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/address_from_output.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/format_amount.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hex/hex.dart';
class ElectrumTransactionBundle {
ElectrumTransactionBundle(this.originalTransaction,
{required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction;
final List<BtcTransaction> ins;
final int? time;
@ -22,7 +26,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
ElectrumTransactionInfo(this.type,
{required String id,
required int height,
int? height,
required int amount,
int? fee,
List<String>? inputAddresses,
@ -99,7 +103,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
factory ElectrumTransactionInfo.fromElectrumBundle(
ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network,
{required Set<String> addresses, required int height}) {
{required Set<String> addresses, int? height}) {
final date = bundle.time != null
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
: DateTime.now();
@ -125,7 +129,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt();
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) {
receivedAmounts.add(out.amount.toInt());
@ -235,6 +256,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
}
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)';
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
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/bitcoin_address_record.dart';
import 'package:cw_core/wallet_addresses.dart';
@ -31,7 +30,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Map<String, int>? initialChangeAddressIndex,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
bitcoin.HDWallet? masterHd,
Bip32Slip10Secp256k1? masterHd,
BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
addressesByReceiveType =
@ -54,9 +53,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
super(walletInfo) {
if (masterHd != null) {
silentAddress = SilentPaymentOwner.fromPrivateKeys(
b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privKey!),
b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!),
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp');
b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()),
b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()),
network: network,
);
if (silentAddresses.length == 0) {
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
@ -93,8 +93,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final ObservableList<BitcoinAddressRecord> changeAddresses;
final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
final BasedUtxoNetwork network;
final bitcoin.HDWallet mainHd;
final bitcoin.HDWallet sideHd;
final Bip32Slip10Secp256k1 mainHd;
final Bip32Slip10Secp256k1 sideHd;
@observable
SilentPaymentOwner? silentAddress;
@ -319,14 +319,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address;
}
String getAddress(
{required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) =>
String getAddress({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) =>
'';
Future<String> getAddressAsync(
{required int index,
required bitcoin.HDWallet hd,
BitcoinAddressType? addressType}) async =>
Future<String> getAddressAsync({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) async =>
getAddress(index: index, hd: hd, addressType: addressType);
void addBitcoinAddressTypes() {
@ -414,7 +418,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} else {
addressesMap[address] = 'Active - P2WPKH';
}
final lastMweb = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb));
if (lastMweb.address != address) {
@ -575,8 +579,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
void _validateAddresses() {
_addresses.forEach((element) {
if (!element.isHidden && element.address !=
getAddress(index: element.index, hd: mainHd, addressType: element.type)) {
if (!element.isHidden &&
element.address !=
getAddress(index: element.index, hd: mainHd, addressType: element.type)) {
element.isHidden = true;
} else if (element.isHidden &&
element.address !=
@ -598,7 +603,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
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 _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType 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:cw_bitcoin/bitcoin_address_record.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_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
@ -32,22 +33,28 @@ class ElectrumWalletSnapshot {
final WalletType type;
final String? addressPageType;
@deprecated
String? mnemonic;
@deprecated
String? xpub;
@deprecated
String? passphrase;
List<BitcoinAddressRecord> addresses;
List<BitcoinSilentPaymentAddressRecord> silentAddresses;
ElectrumBalance balance;
Map<String, int> regularAddressIndex;
Map<String, int> changeAddressIndex;
int silentAddressIndex;
String? passphrase;
DerivationType? derivationType;
String? derivationPath;
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 jsonSource = await read(path: path, password: password);
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List? ?? <Object>[];
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,39 +1,46 @@
import 'dart:async';
import 'dart:convert';
import 'package:convert/convert.dart' as convert;
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart';
import 'package:fixnum/fixnum.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.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/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.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_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:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/electrum_wallet.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:cw_mweb/cw_mweb.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
import 'package:pointycastle/ecc/api.dart';
import 'package:pointycastle/ecc/curves/secp256k1.dart';
part 'litecoin_wallet.g.dart';
@ -46,6 +53,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
@ -53,19 +62,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
Map<String, int>? initialChangeAddressIndex,
int? initialMwebHeight,
bool? alwaysScan,
}) : mwebHd =
bitcoin.HDWallet.fromSeed(seedBytes, network: litecoinNetwork).derivePath("m/1000'"),
super(
}) : super(
mnemonic: mnemonic,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
networkType: litecoinNetwork,
network: LitecoinNetwork.mainnet,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
currency: CryptoCurrency.ltc,
) {
mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
mwebEnabled = alwaysScan ?? false;
walletAddresses = LitecoinWalletAddresses(
walletInfo,
@ -73,31 +82,32 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
sideHd: accountHD.derive(1),
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network,
mwebHd: mwebHd,
mwebEnabled: mwebEnabled,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
CwMweb.stub().then((value) {
_stub = value;
});
}
final bitcoin.HDWallet mwebHd;
late final Bip32Slip10Secp256k1 mwebHd;
late final Box<MwebUtxo> mwebUtxosBox;
Timer? _syncTimer;
Timer? _feeRatesTimer;
StreamSubscription<Utxo>? _utxoStream;
int mwebUtxosHeight = 0;
late RpcClient _stub;
late bool mwebEnabled;
List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;
static Future<LitecoinWallet> create(
{required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
@ -115,7 +125,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic);
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break;
}
return LitecoinWallet(
@ -125,6 +135,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase,
seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
@ -138,20 +150,76 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required bool alwaysScan,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final snp =
await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet);
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
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(
mnemonic: snp.mnemonic!,
mnemonic: keysData.mnemonic!,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses,
initialBalance: snp.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic!),
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
initialAddresses: snp?.addresses,
initialBalance: snp?.balance,
seedBytes: seedBytes!,
passphrase: passphrase,
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp?.addressPageType,
alwaysScan: alwaysScan,
);
}
@ -159,57 +227,72 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@action
@override
Future<void> startSync() async {
print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
syncStatus = SyncronizingSyncStatus();
await subscribeForUpdates();
await updateTransactions();
await updateFeeRates();
_feeRatesTimer?.cancel();
_feeRatesTimer =
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
if (!mwebEnabled) {
syncStatus = SyncronizingSyncStatus();
await subscribeForUpdates();
await updateTransactions();
syncStatus = SyncedSyncStatus();
try {
await updateAllUnspents();
await updateBalance();
syncStatus = SyncedSyncStatus();
} catch (e, s) {
print(e);
print(s);
syncStatus = FailedSyncStatus();
}
return;
}
// await subscribeForUpdates();
// await updateTransactions();
// await updateFeeRates();
await getStub();
await updateUnspent();
await updateBalance();
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
_stub = await CwMweb.stub();
_syncTimer?.cancel();
_syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async {
if (syncStatus is FailedSyncStatus) return;
final height = await electrumClient.getCurrentBlockChainTip() ?? 0;
final resp = await _stub.status(StatusRequest());
if (resp.blockHeaderHeight < height) {
int h = resp.blockHeaderHeight;
syncStatus = SyncingSyncStatus(height - h, h / height);
} else if (resp.mwebHeaderHeight < height) {
int h = resp.mwebHeaderHeight;
syncStatus = SyncingSyncStatus(height - h, h / height);
} else if (resp.mwebUtxosHeight < height) {
syncStatus = SyncingSyncStatus(1, 0.999);
} else {
// prevent unnecessary reaction triggers:
if (syncStatus is! SyncedSyncStatus) {
syncStatus = SyncedSyncStatus();
}
// delay the timer by a second so we don't overrride the restoreheight if one is set
Timer(const Duration(seconds: 1), () async {
_syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async {
if (syncStatus is FailedSyncStatus) return;
final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
final resp = await _stub.status(StatusRequest());
if (resp.mwebUtxosHeight > mwebUtxosHeight) {
mwebUtxosHeight = resp.mwebUtxosHeight;
await checkMwebUtxosSpent();
// update the confirmations for each transaction:
for (final transaction in transactionHistory.transactions.values) {
if (transaction.isPending) continue;
final confirmations = mwebUtxosHeight - transaction.height + 1;
if (transaction.confirmations == confirmations) continue;
transaction.confirmations = confirmations;
transactionHistory.addOne(transaction);
if (resp.blockHeaderHeight < nodeHeight) {
int h = resp.blockHeaderHeight;
syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
} else if (resp.mwebHeaderHeight < nodeHeight) {
int h = resp.mwebHeaderHeight;
syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
} else if (resp.mwebUtxosHeight < nodeHeight) {
syncStatus = SyncingSyncStatus(1, 0.999);
} else {
// prevent unnecessary reaction triggers:
if (syncStatus is! SyncedSyncStatus) {
syncStatus = SyncedSyncStatus();
}
if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
await checkMwebUtxosSpent();
// update the confirmations for each transaction:
for (final transaction in transactionHistory.transactions.values) {
if (transaction.isPending) continue;
int txHeight = transaction.height ?? resp.mwebUtxosHeight;
final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
if (transaction.confirmations == confirmations) continue;
transaction.confirmations = confirmations;
transactionHistory.addOne(transaction);
}
await transactionHistory.save();
}
await transactionHistory.save();
}
}
});
});
// updateUnspent();
// fetchBalances();
// this runs in the background and processes new utxos as they come in:
processMwebUtxos();
}
@ -252,11 +335,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
bool? doSingleScan,
bool? usingElectrs,
}) async {
await mwebUtxosBox.clear();
transactionHistory.clear();
mwebUtxosHeight = height;
_syncTimer?.cancel();
int oldHeight = walletInfo.restoreHeight;
await walletInfo.updateRestoreHeight(height);
// go through mwebUtxos and clear any that are above the new restore height:
if (height == 0) {
await mwebUtxosBox.clear();
transactionHistory.clear();
} else {
for (final utxo in mwebUtxosBox.values) {
if (utxo.height > height) {
await mwebUtxosBox.delete(utxo.outputId);
}
}
// TODO: remove transactions that are above the new restore height!
}
// reset coin balances and txCount to 0:
unspentCoins.forEach((coin) {
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
@ -269,7 +364,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
addressRecord.txCount = 0;
}
print("STARTING SYNC");
await startSync();
}
@ -324,6 +418,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
if (addressRecord == null) {
print("we don't have this address in the wallet! ${utxo.address}");
return;
}
@ -348,27 +443,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
Future<void> processMwebUtxos() async {
final scanSecret = mwebHd.derive(0x80000000).privKey!;
int restoreHeight = walletInfo.restoreHeight;
print("SCANNING FROM HEIGHT: $restoreHeight");
final req = UtxosRequest(scanSecret: hex.decode(scanSecret), fromHeight: restoreHeight);
final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight);
// process old utxos:
for (final utxo in mwebUtxosBox.values) {
if (utxo.address.isEmpty) {
continue;
}
// for (final utxo in mwebUtxosBox.values) {
// if (utxo.address.isEmpty) {
// continue;
// }
// if (walletInfo.restoreHeight > utxo.height) {
// continue;
// }
// // if (walletInfo.restoreHeight > utxo.height) {
// // continue;
// // }
// // await handleIncoming(utxo, _stub);
await handleIncoming(utxo, _stub);
if (utxo.height > walletInfo.restoreHeight) {
await walletInfo.updateRestoreHeight(utxo.height);
}
}
// if (utxo.height > walletInfo.restoreHeight) {
// await walletInfo.updateRestoreHeight(utxo.height);
// }
// }
// process new utxos as they come in:
_utxoStream?.cancel();
@ -381,10 +474,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
value: sUtxo.value.toInt(),
);
if (mwebUtxosBox.containsKey(utxo.outputId)) {
// we've already stored this utxo, skip it:
return;
}
// if (mwebUtxosBox.containsKey(utxo.outputId)) {
// // we've already stored this utxo, skip it:
// return;
// }
// if (utxo.address.isEmpty) {
// await updateUnspent();
@ -425,7 +518,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (status.mwebUtxosHeight != height) return;
int amount = 0;
Set<String> inputAddresses = {};
var output = AccumulatorSink<Digest>();
var output = convert.AccumulatorSink<Digest>();
var input = sha256.startChunkedConversion(output);
for (final outputId in spent) {
final utxo = mwebUtxosBox.get(outputId);
@ -435,7 +528,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
if (!inputAddresses.contains(utxo.address)) {
addressRecord.txCount++;
// print("COUNT UPDATED HERE 3!!!!! ${addressRecord.address} ${addressRecord.txCount} !!!!!!");
}
addressRecord.balance -= utxo.value.toInt();
amount += utxo.value.toInt();
@ -494,69 +586,76 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return true;
}
@override
Future<void> updateUnspent() async {
await super.updateUnspent();
await checkMwebUtxosSpent();
await updateAllUnspents();
}
@override
@action
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
// get ltc unspents:
await super.updateAllUnspents();
await Future.wait(walletAddresses.allAddresses.map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address));
}));
if (mwebEnabled) {
// update mweb unspents:
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
mwebUtxosBox.keys.forEach((dynamic oId) {
final String outputId = oId as String;
final utxo = mwebUtxosBox.get(outputId);
if (utxo == null) {
return;
}
if (utxo.address.isEmpty) {
// not sure if a bug or a special case but we definitely ignore these
return;
}
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
if (addressRecord == null) {
print("utxo contains an address that is not in the wallet: ${utxo.address}");
return;
}
final unspent = BitcoinUnspent(
addressRecord,
outputId,
utxo.value.toInt(),
mwebAddrs.indexOf(utxo.address),
);
if (unspent.vout == 0) {
unspent.isChange = true;
}
updatedUnspentCoins.add(unspent);
});
if (!mwebEnabled) {
return;
}
await getStub();
unspentCoins = updatedUnspentCoins;
// add the mweb unspents to the list:
List<BitcoinUnspent> mwebUnspentCoins = [];
// update mweb unspents:
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
mwebUtxosBox.keys.forEach((dynamic oId) {
final String outputId = oId as String;
final utxo = mwebUtxosBox.get(outputId);
if (utxo == null) {
return;
}
if (utxo.address.isEmpty) {
// not sure if a bug or a special case but we definitely ignore these
return;
}
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
if (addressRecord == null) {
print("utxo contains an address that is not in the wallet: ${utxo.address}");
return;
}
final unspent = BitcoinUnspent(
addressRecord,
outputId,
utxo.value.toInt(),
mwebAddrs.indexOf(utxo.address),
);
if (unspent.vout == 0) {
unspent.isChange = true;
}
mwebUnspentCoins.add(unspent);
});
unspentCoins.addAll(mwebUnspentCoins);
}
@override
Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances();
var confirmed = balance.confirmed;
var unconfirmed = balance.unconfirmed;
mwebUtxosBox.values.forEach((utxo) {
if (utxo.height > 0) {
confirmed += utxo.value.toInt();
} else {
unconfirmed += utxo.value.toInt();
}
});
if (!mwebEnabled) {
return balance;
}
await getStub();
int confirmed = balance.confirmed;
int unconfirmed = balance.unconfirmed;
try {
mwebUtxosBox.values.forEach((utxo) {
if (utxo.height > 0) {
confirmed += utxo.value.toInt();
} else {
unconfirmed += utxo.value.toInt();
}
});
} catch (_) {}
// update unspent balances:
@ -566,6 +665,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
// coin.bitcoinAddressRecord.balance = 0;
// coin.bitcoinAddressRecord.txCount = 0;
// });
await updateUnspent();
for (var addressRecord in walletAddresses.allAddresses) {
addressRecord.balance = 0;
addressRecord.txCount = 0;
@ -594,10 +696,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
// update the txCount for each address using the tx history, since we can't rely on mwebd
// to have an accurate count, we should just keep it in sync with what we know from the tx history:
for (var tx in transactionHistory.transactions.values) {
if (tx.isPending) continue;
for (final tx in transactionHistory.transactions.values) {
// if (tx.isPending) continue;
if (tx.inputAddresses == null || tx.outputAddresses == null) {
continue;
}
final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
for (var address in txAddresses) {
for (final address in txAddresses) {
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == address);
if (addressRecord == null) {
@ -607,8 +712,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
}
await updateUnspent();
return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: balance.frozen);
}
@ -665,8 +768,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
final resp = await _stub.create(CreateRequest(
rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(),
scanSecret: hex.decode(mwebHd.derive(0x80000000).privKey!),
spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!),
scanSecret: scanSecret,
spendSecret: spendSecret,
feeRatePerKb: Int64(feeRate * 1000),
dryRun: true));
final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
@ -702,11 +805,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (!mwebEnabled) {
return tx;
}
await getStub();
final resp = await _stub.create(CreateRequest(
rawTx: hex.decode(tx.hex),
scanSecret: hex.decode(mwebHd.derive(0x80000000).privKey!),
spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!),
scanSecret: scanSecret,
spendSecret: spendSecret,
feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000,
));
final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
@ -768,7 +872,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
Future<void> close() async {
await super.close();
await mwebUtxosBox.close();
_syncTimer?.cancel();
_utxoStream?.cancel();
}
@ -779,6 +882,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
mwebEnabled = enabled;
(walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled;
if (enabled) {
// generate inital mweb addresses:
(walletAddresses as LitecoinWalletAddresses).topUpMweb(0);
}
stopSync();
startSync();
}
@ -793,4 +901,127 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final resp = await _stub.status(StatusRequest());
return resp;
}
@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,6 +1,6 @@
import 'package:convert/convert.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/electrum_wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
@ -17,29 +17,36 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required this.mwebHd,
required super.network,
required this.mwebHd,
required this.mwebEnabled,
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
}) : super(walletInfo) {
topUpMweb(0);
if (mwebEnabled) {
topUpMweb(0);
}
}
final HDWallet mwebHd;
final Bip32Slip10Secp256k1 mwebHd;
bool mwebEnabled;
List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendPubkey =>
mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed;
List<String> mwebAddrs = [];
Future<void> topUpMweb(int index) async {
while (mwebAddrs.length - index < 1000) {
final length = mwebAddrs.length;
final scanSecret = mwebHd.derive(0x80000000).privKey!;
final spendPubkey = mwebHd.derive(0x80000001).pubKey!;
final stub = await CwMweb.stub();
final resp = await stub.addresses(AddressRequest(
fromIndex: length,
toIndex: index + 1000,
scanSecret: hex.decode(scanSecret),
spendPubkey: hex.decode(spendPubkey),
scanSecret: scanSecret,
spendPubkey: spendPubkey,
));
if (mwebAddrs.length == length) {
mwebAddrs.addAll(resp.address);
@ -48,8 +55,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
}
@override
String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) {
if (addressType == SegwitAddresType.mweb) {
String getAddress({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) {
if (addressType == SegwitAddresType.mweb && mwebEnabled) {
topUpMweb(index);
return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
}
@ -57,9 +68,16 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
}
@override
Future<String> getAddressAsync(
{required int index, required HDWallet hd, BitcoinAddressType? addressType}) async {
if (addressType == SegwitAddresType.mweb) {
Future<String> getAddressAsync({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) async {
// if mweb isn't enabled we'll just return the regular address type which does effectively nothing
// sort of a hack but easier than trying to pull the mweb setting into the electrum_wallet_addresses initialization code
// (we want to avoid initializing the mweb.stub() if it's not enabled or we'd be starting the whole server for no reason and it's slow)
// TODO: find a way to do address generation without starting the whole mweb server
if (addressType == SegwitAddresType.mweb && mwebEnabled) {
await topUpMweb(index);
}
return getAddress(index: index, hd: hd, addressType: addressType);
@ -68,11 +86,10 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
@action
@override
Future<String> getChangeAddress() async {
// super.getChangeAddress();
// updateChangeAddresses();
// print("getChangeAddress @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
// this means all change addresses used will be mweb addresses!:
await topUpMweb(0);
return mwebAddrs[0];
if (mwebEnabled) {
await topUpMweb(0);
return mwebAddrs[0];
}
return super.getChangeAddress();
}
}

View file

@ -1,4 +1,6 @@
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:hive/hive.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
@ -19,10 +21,11 @@ class LitecoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinNewWalletCredentials> {
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan);
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect, this.alwaysScan);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool isDirect;
final bool alwaysScan;
@override
@ -30,12 +33,26 @@ class LitecoinWalletService extends WalletService<
@override
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(
mnemonic: await generateElectrumMnemonic(),
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
@ -59,6 +76,7 @@ class LitecoinWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
saveBackup(name);
@ -71,6 +89,7 @@ class LitecoinWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
return wallet;
@ -104,6 +123,7 @@ class LitecoinWalletService extends WalletService<
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName);
@ -135,11 +155,13 @@ class LitecoinWalletService extends WalletService<
}
final wallet = await LitecoinWalletBase.create(
password: credentials.password!,
passphrase: credentials.passphrase,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource);
password: credentials.password!,
passphrase: credentials.passphrase,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
return wallet;

View file

@ -1,68 +1,54 @@
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:hex/hex.dart';
bitcoin.PaymentData generatePaymentData({
required bitcoin.HDWallet hd,
required int index,
}) {
final pubKey = hd.derive(index).pubKey!;
return PaymentData(pubkey: Uint8List.fromList(HEX.decode(pubKey)));
}
import 'package:blockchain_utils/blockchain_utils.dart';
ECPrivate generateECPrivate({
required bitcoin.HDWallet hd,
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) {
final wif = hd.derive(index).wif!;
return ECPrivate.fromWif(wif, netVersion: network.wifNetVer);
}
}) =>
ECPrivate(hd.childKey(Bip32KeyIndex(index)).privateKey);
String generateP2WPKHAddress({
required bitcoin.HDWallet hd,
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) {
final pubKey = hd.derive(index).pubKey!;
return ECPublic.fromHex(pubKey).toP2wpkhAddress().toAddress(network);
}
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2wpkhAddress()
.toAddress(network);
String generateP2SHAddress({
required bitcoin.HDWallet hd,
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) {
final pubKey = hd.derive(index).pubKey!;
return ECPublic.fromHex(pubKey).toP2wpkhInP2sh().toAddress(network);
}
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2wpkhInP2sh()
.toAddress(network);
String generateP2WSHAddress({
required bitcoin.HDWallet hd,
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) {
final pubKey = hd.derive(index).pubKey!;
return ECPublic.fromHex(pubKey).toP2wshAddress().toAddress(network);
}
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2wshAddress()
.toAddress(network);
String generateP2PKHAddress({
required bitcoin.HDWallet hd,
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) {
final pubKey = hd.derive(index).pubKey!;
return ECPublic.fromHex(pubKey).toP2pkhAddress().toAddress(network);
}
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2pkhAddress()
.toAddress(network);
String generateP2TRAddress({
required bitcoin.HDWallet hd,
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) {
final pubKey = hd.derive(index).pubKey!;
return ECPublic.fromHex(pubKey).toTaprootAddress().toAddress(network);
}
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toTaprootAddress()
.toAddress(network);

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