Merge branch 'main' into Haven-Update-Dependencies

This commit is contained in:
Konstantin Ullrich 2024-08-22 16:59:51 +02:00 committed by GitHub
commit ec4aeb2ada
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
358 changed files with 10454 additions and 7565 deletions

View file

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

View file

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

View file

@ -0,0 +1,187 @@
name: PR Test Build linux
on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
branch:
description: "Branch name to build"
required: true
default: "main"
jobs:
PR_test_build:
runs-on: ubuntu-20.04
env:
STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet
PR_NUMBER: ${{ github.event.number }}
steps:
- name: is pr
if: github.event_name == 'pull_request'
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
- name: is not pr
if: github.event_name != 'pull_request'
run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENVg
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: "17.x"
- name: Configure placeholder git details
run: |
git config --global user.email "CI@cakewallet.com"
git config --global user.name "Cake Github Actions"
- name: Flutter action
uses: subosito/flutter-action@v1
with:
flutter-version: "3.19.6"
channel: stable
- name: Install package dependencies
run: |
sudo apt update
sudo apt-get install -y curl unzip automake build-essential file pkg-config git python-is-python3 libtool libtinfo5 cmake clang
- name: Install desktop dependencies
run: |
sudo apt update
sudo apt install -y ninja-build libgtk-3-dev gperf
- name: Execute Build and Setup Commands
run: |
sudo mkdir -p /opt/android
sudo chown $USER /opt/android
cd /opt/android
-y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install cargo-ndk
git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }}
cd scripts && ./gen_android_manifest.sh && cd ..
cd cake_wallet/scripts/android/
source ./app_env.sh cakewallet
./app_config.sh
cd ../../..
cd cake_wallet/scripts/linux/
source ./app_env.sh cakewallet
./app_config.sh
cd ../../..
- name: Cache Externals
id: cache-externals
uses: actions/cache@v3
with:
path: |
/opt/android/cake_wallet/cw_haven/android/.cxx
/opt/android/cake_wallet/scripts/monero_c/release
key: linux_${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
name: Generate Externals
run: |
cd /opt/android/cake_wallet/scripts/linux/
source ./app_env.sh cakewallet
./build_monero_all.sh
- name: Install Flutter dependencies
run: |
cd /opt/android/cake_wallet
flutter pub get
- name: Generate localization
run: |
cd /opt/android/cake_wallet
flutter packages pub run tool/generate_localization.dart
- name: Build generated code
run: |
cd /opt/android/cake_wallet
./model_generator.sh
- name: Add secrets
run: |
cd /opt/android/cake_wallet
touch lib/.secrets.g.dart
touch cw_evm/lib/.secrets.g.dart
touch cw_solana/lib/.secrets.g.dart
touch cw_core/lib/.secrets.g.dart
touch cw_nano/lib/.secrets.g.dart
touch cw_tron/lib/.secrets.g.dart
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart
echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart
echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart
echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart
echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart
echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart
echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart
echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart
echo "const moonPayApiKey = '${{ secrets.MOON_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const moonPaySecretKey = '${{ secrets.MOON_PAY_SECRET_KEY }}';" >> lib/.secrets.g.dart
echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart
echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart
echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart
echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart
echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart
echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart
echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart
echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart
echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
- name: Rename app
run: |
echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
- name: Build
run: |
cd /opt/android/cake_wallet
flutter build linux --release
- name: Prepare release zip file
run: |
cd /opt/android/cake_wallet/build/linux/x64/release
zip -r ${{env.BRANCH_NAME}}.zip bundle
- name: Upload Artifact
uses: kittaakos/upload-artifact-as-is@v0
with:
path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip
# Just as an artifact would be enough
# - name: Send Test APK
# continue-on-error: true
# uses: adrey/slack-file-upload-action@1.0.5
# with:
# token: ${{ secrets.SLACK_APP_TOKEN }}
# path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip
# channel: ${{ secrets.SLACK_APK_CHANNEL }}
# title: "${{ env.BRANCH_NAME }}_linux.zip"
# filename: ${{ env.BRANCH_NAME }}_linux.zip
# initial_comment: ${{ github.event.head_commit.message }}

2
.gitignore vendored
View file

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

View file

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

View file

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img height="100" src=".github/assets/Logo_CakeWallet.png"> ![logo](.github/assets/Logo_CakeWallet.png)
</div> </div>
@ -161,7 +161,9 @@ The only parts to be translated, if needed, are the values m and s after the var
4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language. 4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language.
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 digit localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. 5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
6. Add the new language code to `tool/utils/translation/translation_constants.dart`
## Add a new fiat currency ## Add a new fiat currency

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
Monero enhancements Scan and verify messages
Improvements for Tron and Nano wallets Synchronization enhancements
Bug fixes 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

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

View file

@ -0,0 +1,35 @@
app-id: com.cakewallet.CakeWallet
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: cake_wallet
separate-locales: false
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --socket=pulseaudio
- --share=network
- --filesystem=home
modules:
- name: cake_wallet
buildsystem: simple
only-arches:
- x86_64
build-commands:
- "cp -R bundle /app/cake_wallet"
- "chmod +x /app/cake_wallet/cake_wallet"
- "mkdir -p /app/bin"
- "ln -s /app/cake_wallet/cake_wallet /app/bin/cake_wallet"
- "mkdir -p /app/share/icons/hicolor/scalable/apps"
- "cp cakewallet_icon_180.png /app/share/icons/hicolor/scalable/apps/com.cakewallet.CakeWallet.png"
- "mkdir -p /app/share/applications"
- "cp com.cakewallet.CakeWallet.desktop /app/share/applications"
sources:
- type: dir
path: build/linux/x64/release
- type: file
path: assets/images/cakewallet_icon_180.png
- type: file
path: linux/com.cakewallet.CakeWallet.desktop

View file

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

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart';
@ -25,7 +25,8 @@ class BitcoinHardwareWalletService {
for (final i in indexRange) { for (final i in indexRange) {
final derivationPath = "m/84'/0'/$i'"; final derivationPath = "m/84'/0'/$i'";
final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath);
HDWallet hd = HDWallet.fromBase58(xpub).derive(0); Bip32Slip10Secp256k1 hd =
Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);

View file

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

View file

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

View file

@ -6,11 +6,13 @@ class BitcoinNewWalletCredentials extends WalletCredentials {
BitcoinNewWalletCredentials( BitcoinNewWalletCredentials(
{required String name, {required String name,
WalletInfo? walletInfo, WalletInfo? walletInfo,
String? password,
DerivationType? derivationType, DerivationType? derivationType,
String? derivationPath}) String? derivationPath})
: super( : super(
name: name, name: name,
walletInfo: walletInfo, walletInfo: walletInfo,
password: password,
); );
} }

View file

@ -3,6 +3,7 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
@ -19,11 +20,12 @@ class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> { BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan); BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan; final bool alwaysScan;
final bool isDirect;
@override @override
WalletType getType() => WalletType.bitcoin; WalletType getType() => WalletType.bitcoin;
@ -40,9 +42,12 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
network: network, network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
return wallet; return wallet;
} }
@ -61,6 +66,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
saveBackup(name); saveBackup(name);
@ -73,6 +79,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
return wallet; return wallet;
@ -97,6 +104,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: currentWalletInfo, walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
@ -123,6 +131,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network, networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
@ -151,6 +160,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
network: network, network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();

View file

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

View file

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

View file

@ -22,7 +22,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
ElectrumTransactionInfo(this.type, ElectrumTransactionInfo(this.type,
{required String id, {required String id,
required int height, int? height,
required int amount, required int amount,
int? fee, int? fee,
List<String>? inputAddresses, List<String>? inputAddresses,
@ -99,7 +99,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
factory ElectrumTransactionInfo.fromElectrumBundle( factory ElectrumTransactionInfo.fromElectrumBundle(
ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network, ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network,
{required Set<String> addresses, required int height}) { {required Set<String> addresses, int? height}) {
final date = bundle.time != null final date = bundle.time != null
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
: DateTime.now(); : DateTime.now();

View file

@ -5,7 +5,7 @@ import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_core/encryption_file_utils.dart';
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
@ -22,7 +22,6 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/script_hash.dart';
import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/utils.dart';
@ -34,14 +33,13 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/get_height_by_date.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
import 'package:sp_scanner/sp_scanner.dart'; import 'package:sp_scanner/sp_scanner.dart';
@ -54,12 +52,13 @@ const int TWEAKS_COUNT = 25;
abstract class ElectrumWalletBase abstract class ElectrumWalletBase
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo> extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
with Store { with Store, WalletKeysFile {
ElectrumWalletBase({ ElectrumWalletBase({
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required this.networkType, required this.network,
required this.encryptionFileUtils,
String? xpub, String? xpub,
String? mnemonic, String? mnemonic,
Uint8List? seedBytes, Uint8List? seedBytes,
@ -70,7 +69,7 @@ abstract class ElectrumWalletBase
CryptoCurrency? currency, CryptoCurrency? currency,
this.alwaysScan, this.alwaysScan,
}) : accountHD = }) : accountHD =
getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo), getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
_password = password, _password = password,
_feeRates = <int>[], _feeRates = <int>[],
@ -89,23 +88,22 @@ abstract class ElectrumWalletBase
} }
: {}), : {}),
this.unspentCoinsInfo = unspentCoinsInfo, this.unspentCoinsInfo = unspentCoinsInfo,
this.network = _getNetwork(networkType, currency), this.isTestnet = !network.isMainnet,
this.isTestnet = networkType == bitcoin.testnet,
this._mnemonic = mnemonic, this._mnemonic = mnemonic,
super(walletInfo) { super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient(); this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); transactionHistory = ElectrumTransactionHistory(
walletInfo: walletInfo,
password: password,
encryptionFileUtils: encryptionFileUtils,
);
reaction((_) => syncStatus, _syncStatusReaction); reaction((_) => syncStatus, _syncStatusReaction);
} }
static bitcoin.HDWallet getAccountHDWallet( static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
CryptoCurrency? currency, Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) {
bitcoin.NetworkType networkType,
Uint8List? seedBytes,
String? xpub,
DerivationInfo? derivationInfo) {
if (seedBytes == null && xpub == null) { if (seedBytes == null && xpub == null) {
throw Exception( throw Exception(
"To create a Wallet you need either a seed or an xpub. This should not happen"); "To create a Wallet you need either a seed or an xpub. This should not happen");
@ -114,25 +112,29 @@ abstract class ElectrumWalletBase
if (seedBytes != null) { if (seedBytes != null) {
return currency == CryptoCurrency.bch return currency == CryptoCurrency.bch
? bitcoinCashHDWallet(seedBytes) ? bitcoinCashHDWallet(seedBytes)
: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
.derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)); _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
as Bip32Slip10Secp256k1;
} }
return bitcoin.HDWallet.fromBase58(xpub!); return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
} }
static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'"); Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
static int estimatedTransactionSize(int inputsCount, int outputsCounts) => static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10; inputsCount * 68 + outputsCounts * 34 + 10;
bool? alwaysScan; bool? alwaysScan;
final bitcoin.HDWallet accountHD; final Bip32Slip10Secp256k1 accountHD;
final String? _mnemonic; final String? _mnemonic;
bitcoin.HDWallet get hd => accountHD.derive(0); Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
final EncryptionFileUtils encryptionFileUtils;
final String? passphrase; final String? passphrase;
@override @override
@ -164,16 +166,22 @@ abstract class ElectrumWalletBase
.map((addr) => scriptHash(addr.address, network: network)) .map((addr) => scriptHash(addr.address, network: network))
.toList(); .toList();
String get xpub => accountHD.base58!; String get xpub => accountHD.publicKey.toExtended;
@override @override
String? get seed => _mnemonic; String? get seed => _mnemonic;
bitcoin.NetworkType networkType; @override
WalletKeysData get walletKeysData =>
WalletKeysData(mnemonic: _mnemonic, xPub: xpub, passphrase: passphrase);
@override
String get password => _password;
BasedUtxoNetwork network; BasedUtxoNetwork network;
@override @override
bool? isTestnet; bool isTestnet;
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
@ -185,24 +193,21 @@ abstract class ElectrumWalletBase
bool _isTryingToConnect = false; bool _isTryingToConnect = false;
@action @action
Future<void> setSilentPaymentsScanning(bool active, bool usingElectrs) async { Future<void> setSilentPaymentsScanning(bool active) async {
silentPaymentsScanningActive = active; silentPaymentsScanningActive = active;
if (active) { if (active) {
syncStatus = AttemptingSyncStatus(); syncStatus = StartingScanSyncStatus();
final tip = await getUpdatedChainTip(); final tip = await getUpdatedChainTip();
if (tip == walletInfo.restoreHeight) { if (tip == walletInfo.restoreHeight) {
syncStatus = SyncedTipSyncStatus(tip); syncStatus = SyncedTipSyncStatus(tip);
return;
} }
if (tip > walletInfo.restoreHeight) { if (tip > walletInfo.restoreHeight) {
_setListeners( _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
walletInfo.restoreHeight,
chainTipParam: _currentChainTip,
usingElectrs: usingElectrs,
);
} }
} else { } else {
alwaysScan = false; alwaysScan = false;
@ -212,10 +217,7 @@ abstract class ElectrumWalletBase
if (electrumClient.isConnected) { if (electrumClient.isConnected) {
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} else { } else {
if (electrumClient.uri != null) { syncStatus = NotConnectedSyncStatus();
await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL);
startSync();
}
} }
} }
} }
@ -240,8 +242,11 @@ abstract class ElectrumWalletBase
} }
@override @override
BitcoinWalletKeys get keys => BitcoinWalletKeys get keys => BitcoinWalletKeys(
BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer),
privateKey: hd.privateKey.toHex(),
publicKey: hd.publicKey.toHex(),
);
String _password; String _password;
List<BitcoinUnspent> unspentCoins; List<BitcoinUnspent> unspentCoins;
@ -256,8 +261,10 @@ abstract class ElectrumWalletBase
Future<Isolate>? _isolate; Future<Isolate>? _isolate;
void Function(FlutterErrorDetails)? _onError; void Function(FlutterErrorDetails)? _onError;
Timer? _reconnectTimer;
Timer? _autoSaveTimer; Timer? _autoSaveTimer;
static const int _autoSaveInterval = 30; Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1;
Future<void> init() async { Future<void> init() async {
await walletAddresses.init(); await walletAddresses.init();
@ -265,7 +272,7 @@ abstract class ElectrumWalletBase
await save(); await save();
_autoSaveTimer = _autoSaveTimer =
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save());
} }
@action @action
@ -273,7 +280,7 @@ abstract class ElectrumWalletBase
int height, { int height, {
int? chainTipParam, int? chainTipParam,
bool? doSingleScan, bool? doSingleScan,
bool? usingElectrs, bool? usingSupportedNode,
}) async { }) async {
final chainTip = chainTipParam ?? await getUpdatedChainTip(); final chainTip = chainTipParam ?? await getUpdatedChainTip();
@ -282,7 +289,7 @@ abstract class ElectrumWalletBase
return; return;
} }
syncStatus = AttemptingSyncStatus(); syncStatus = StartingScanSyncStatus();
if (_isolate != null) { if (_isolate != null) {
final runningIsolate = await _isolate!; final runningIsolate = await _isolate!;
@ -300,7 +307,9 @@ abstract class ElectrumWalletBase
chainTip: chainTip, chainTip: chainTip,
electrumClient: ElectrumClient(), electrumClient: ElectrumClient(),
transactionHistoryIds: transactionHistory.transactions.keys.toList(), transactionHistoryIds: transactionHistory.transactions.keys.toList(),
node: usingElectrs == true ? ScanNode(node!.uri, node!.useSSL) : null, node: (await getNodeSupportsSilentPayments()) == true
? ScanNode(node!.uri, node!.useSSL)
: null,
labels: walletAddresses.labels, labels: walletAddresses.labels,
labelIndexes: walletAddresses.silentAddresses labelIndexes: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1)
@ -388,7 +397,7 @@ abstract class ElectrumWalletBase
BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)),
) )
: silentAddress.B_spend, : silentAddress.B_spend,
hrp: silentAddress.hrp, network: network,
); );
final addressRecord = walletAddresses.silentAddresses final addressRecord = walletAddresses.silentAddresses
@ -416,8 +425,10 @@ abstract class ElectrumWalletBase
await updateTransactions(); await updateTransactions();
await updateAllUnspents(); await updateAllUnspents();
await updateBalance(); await updateBalance();
await updateFeeRates();
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); _updateFeeRateTimer ??=
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
if (alwaysScan == true) { if (alwaysScan == true) {
_setListeners(walletInfo.restoreHeight); _setListeners(walletInfo.restoreHeight);
@ -436,11 +447,64 @@ abstract class ElectrumWalletBase
final feeRates = await electrumClient.feeRates(network: network); final feeRates = await electrumClient.feeRates(network: network);
if (feeRates != [0, 0, 0]) { if (feeRates != [0, 0, 0]) {
_feeRates = feeRates; _feeRates = feeRates;
} else if (isTestnet) {
_feeRates = [1, 1, 1];
} }
} }
Node? node; Node? node;
Future<bool> getNodeIsElectrs() async {
if (node == null) {
return false;
}
final version = await electrumClient.version();
if (version.isNotEmpty) {
final server = version[0];
if (server.toLowerCase().contains('electrs')) {
node!.isElectrs = true;
node!.save();
return node!.isElectrs!;
}
}
node!.isElectrs = false;
node!.save();
return node!.isElectrs!;
}
Future<bool> getNodeSupportsSilentPayments() async {
// As of today (august 2024), only ElectrumRS supports silent payments
if (!(await getNodeIsElectrs())) {
return false;
}
if (node == null) {
return false;
}
try {
final tweaksResponse = await electrumClient.getTweaks(height: 0);
if (tweaksResponse != null) {
node!.supportsSilentPayments = true;
node!.save();
return node!.supportsSilentPayments!;
}
} on RequestFailedTimeoutException catch (_) {
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
} catch (_) {}
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
}
@action @action
@override @override
Future<void> connectToNode({required Node node}) async { Future<void> connectToNode({required Node node}) async {
@ -502,13 +566,6 @@ abstract class ElectrumWalletBase
final hd = final hd =
utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
final derivationPath =
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!;
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@ -519,17 +576,31 @@ abstract class ElectrumWalletBase
); );
spendsSilentPayment = true; spendsSilentPayment = true;
isSilentPayment = true; isSilentPayment = true;
} else { } else if (!isHardwareWallet) {
privkey = privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
} }
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
inputPrivKeyInfos.add(ECPrivateInfo( String pubKeyHex;
privkey,
address.type == SegwitAddresType.p2tr, if (privkey != null) {
tweak: !isSilentPayment, inputPrivKeyInfos.add(ECPrivateInfo(
)); privkey,
address.type == SegwitAddresType.p2tr,
tweak: !isSilentPayment,
));
pubKeyHex = privkey.getPublic().toHex();
} else {
pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex();
}
final derivationPath =
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
utxos.add( utxos.add(
UtxoWithAddress( UtxoWithAddress(
@ -541,7 +612,7 @@ abstract class ElectrumWalletBase
isSilentPayment: isSilentPayment, isSilentPayment: isSilentPayment,
), ),
ownerDetails: UtxoAddressDetails( ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(), publicKey: pubKeyHex,
address: address, address: address,
), ),
), ),
@ -928,11 +999,29 @@ abstract class ElectrumWalletBase
bool hasTaprootInputs = false; bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.inputPrivKeyInfos String error = "Cannot find private key.";
.firstWhereOrNull((element) => element.privkey.getPublic().toHex() == publicKey);
ECPrivateInfo? key;
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
error += "\nNo private keys generated.";
} else {
error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";
key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) {
final elemPubkey = element.privkey.getPublic().toHex();
if (elemPubkey == publicKey) {
return true;
} else {
error += "\nExpected: $publicKey";
error += "\nPubkey: $elemPubkey";
return false;
}
});
}
if (key == null) { if (key == null) {
throw Exception("Cannot find private key"); throw Exception(error);
} }
if (utxo.utxo.isP2tr()) { if (utxo.utxo.isP2tr()) {
@ -1073,8 +1162,13 @@ abstract class ElectrumWalletBase
@override @override
Future<void> save() async { Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
final path = await makePath(); final path = await makePath();
await write(path: path, password: _password, data: toJSON()); await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save(); await transactionHistory.save();
} }
@ -1114,10 +1208,9 @@ abstract class ElectrumWalletBase
int? chainTip, int? chainTip,
ScanData? scanData, ScanData? scanData,
bool? doSingleScan, bool? doSingleScan,
bool? usingElectrs,
}) async { }) async {
silentPaymentsScanningActive = true; silentPaymentsScanningActive = true;
_setListeners(height, doSingleScan: doSingleScan, usingElectrs: usingElectrs); _setListeners(height, doSingleScan: doSingleScan);
} }
@override @override
@ -1126,10 +1219,9 @@ abstract class ElectrumWalletBase
await electrumClient.close(); await electrumClient.close();
} catch (_) {} } catch (_) {}
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
_updateFeeRateTimer?.cancel();
} }
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
@action @action
Future<void> updateAllUnspents() async { Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = []; List<BitcoinUnspent> updatedUnspentCoins = [];
@ -1217,7 +1309,7 @@ abstract class ElectrumWalletBase
await Future.wait(unspents.map((unspent) async { await Future.wait(unspents.map((unspent) async {
try { try {
final coin = BitcoinUnspent.fromJSON(address, unspent); final coin = BitcoinUnspent.fromJSON(address, unspent);
final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); final tx = await fetchTransactionInfo(hash: coin.hash);
coin.isChange = address.isHidden; coin.isChange = address.isHidden;
coin.confirmations = tx?.confirmations; coin.confirmations = tx?.confirmations;
@ -1272,20 +1364,25 @@ abstract class ElectrumWalletBase
} }
Future<bool> canReplaceByFee(String hash) async { Future<bool> canReplaceByFee(String hash) async {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
final transactionHex = verboseTransaction['hex'] as String?; final String? transactionHex;
int confirmations = 0;
if (verboseTransaction.isEmpty) {
transactionHex = await electrumClient.getTransactionHex(hash: hash);
} else {
confirmations = verboseTransaction['confirmations'] as int? ?? 0;
transactionHex = verboseTransaction['hex'] as String?;
}
if (confirmations > 0) return false; if (confirmations > 0) return false;
if (transactionHex == null) { if (transactionHex == null || transactionHex.isEmpty) {
return false; return false;
} }
final original = bitcoin.Transaction.fromHex(transactionHex); return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
return original.ins
.any((element) => element.sequence != null && element.sequence! < 4294967293);
} }
Future<bool> isChangeSufficientForFee(String txId, int newFee) async { Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
@ -1444,50 +1541,73 @@ abstract class ElectrumWalletBase
} }
} }
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async { Future<ElectrumTransactionBundle> getTransactionExpanded(
{required String hash, int? height}) async {
String transactionHex; String transactionHex;
// TODO: time is not always available, and calculating it from height is not always accurate.
// Add settings to choose API provider and use and http server instead of electrum for this.
int? time; int? time;
int confirmations = 0; int? confirmations;
if (network == BitcoinNetwork.testnet) {
// Testnet public electrum server does not support verbose transaction fetching final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
if (verboseTransaction.isEmpty) {
transactionHex = await electrumClient.getTransactionHex(hash: hash); transactionHex = await electrumClient.getTransactionHex(hash: hash);
final status = json.decode(
(await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body);
time = status["block_time"] as int?;
final height = status["block_height"] as int? ?? 0;
final tip = await getUpdatedChainTip();
if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0;
} else { } else {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
transactionHex = verboseTransaction['hex'] as String; transactionHex = verboseTransaction['hex'] as String;
time = verboseTransaction['time'] as int?; time = verboseTransaction['time'] as int?;
confirmations = verboseTransaction['confirmations'] as int? ?? 0; confirmations = verboseTransaction['confirmations'] as int?;
}
if (height != null) {
if (time == null) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
if (confirmations == null) {
final tip = await getUpdatedChainTip();
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
} }
final original = BtcTransaction.fromRaw(transactionHex); final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[]; final ins = <BtcTransaction>[];
for (final vin in original.inputs) { for (final vin in original.inputs) {
ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId))); final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
final String inputTransactionHex;
if (verboseTransaction.isEmpty) {
inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
} else {
inputTransactionHex = verboseTransaction['hex'] as String;
}
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
} }
return ElectrumTransactionBundle( return ElectrumTransactionBundle(
original, original,
ins: ins, ins: ins,
time: time, time: time,
confirmations: confirmations, confirmations: confirmations ?? 0,
); );
} }
Future<ElectrumTransactionInfo?> fetchTransactionInfo( Future<ElectrumTransactionInfo?> fetchTransactionInfo(
{required String hash, required int height, bool? retryOnFailure}) async { {required String hash, int? height, bool? retryOnFailure}) async {
try { try {
return ElectrumTransactionInfo.fromElectrumBundle( return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash), walletInfo.type, network, await getTransactionExpanded(hash: hash, height: height),
addresses: addressesSet, height: height); walletInfo.type,
network,
addresses: addressesSet,
height: height,
);
} catch (e) { } catch (e) {
if (e is FormatException && retryOnFailure == true) { if (e is FormatException && retryOnFailure == true) {
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
@ -1638,8 +1758,8 @@ abstract class ElectrumWalletBase
await getCurrentChainTip(); await getCurrentChainTip();
transactionHistory.transactions.values.forEach((tx) async { transactionHistory.transactions.values.forEach((tx) async {
if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) { if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) {
tx.confirmations = await getCurrentChainTip() - tx.height + 1; tx.confirmations = await getCurrentChainTip() - tx.height! + 1;
} }
}); });
@ -1755,15 +1875,79 @@ abstract class ElectrumWalletBase
final index = address != null final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null; : null;
final HD = index == null ? hd : hd.derive(index); final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
return base64Encode(HD.signMessage(message)); final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
String messagePrefix = '\x18Bitcoin Signed Message:\n';
final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
final decodedSig = hex.decode(hexEncoded);
return base64Encode(decodedSig);
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
if (address == null) {
return false;
}
List<int> sigDecodedBytes = [];
if (signature.endsWith('=')) {
sigDecodedBytes = base64.decode(signature);
} else {
sigDecodedBytes = hex.decode(signature);
}
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
throw ArgumentException(
"signature must be 64 bytes without recover-id or 65 bytes with recover-id");
}
String messagePrefix = '\x18Bitcoin Signed Message:\n';
final messageHash = QuickCrypto.sha256Hash(
BitcoinSignerUtils.magicMessage(utf8.encode(message), messagePrefix));
List<int> correctSignature =
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
List<int> rBytes = correctSignature.sublist(0, 32);
List<int> sBytes = correctSignature.sublist(32);
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
List<int> possibleRecoverIds = [0, 1];
final baseAddress = addressTypeFromStr(address, network);
for (int recoveryId in possibleRecoverIds) {
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
String? recoveredAddress;
if (baseAddress is P2pkAddress) {
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
} else if (baseAddress is P2pkhAddress) {
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
} else if (baseAddress is P2wshAddress) {
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
} else if (baseAddress is P2wpkhAddress) {
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
}
if (recoveredAddress == address) {
return true;
}
}
return false;
} }
Future<void> _setInitialHeight() async { Future<void> _setInitialHeight() async {
if (_chainTipUpdateSubject != null) return; if (_chainTipUpdateSubject != null) return;
_currentChainTip = await getUpdatedChainTip();
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
await getUpdatedChainTip();
await walletInfo.updateRestoreHeight(_currentChainTip!); await walletInfo.updateRestoreHeight(_currentChainTip!);
} }
@ -1782,44 +1966,40 @@ abstract class ElectrumWalletBase
}); });
} }
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
return BitcoinCashNetwork.mainnet;
}
if (networkType == litecoinNetwork) {
return LitecoinNetwork.mainnet;
}
if (networkType == bitcoin.testnet) {
return BitcoinNetwork.testnet;
}
return BitcoinNetwork.mainnet;
}
static String _hardenedDerivationPath(String derivationPath) => static String _hardenedDerivationPath(String derivationPath) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
@action @action
void _onConnectionStatusChange(bool? isConnected) { void _onConnectionStatusChange(ConnectionStatus status) {
if (syncStatus is SyncingSyncStatus) return; switch (status) {
case ConnectionStatus.connected:
if (syncStatus is NotConnectedSyncStatus ||
syncStatus is LostConnectionSyncStatus ||
syncStatus is ConnectingSyncStatus) {
syncStatus = AttemptingSyncStatus();
startSync();
}
if (isConnected == true && syncStatus is! SyncedSyncStatus) { break;
syncStatus = ConnectedSyncStatus(); case ConnectionStatus.disconnected:
} else if (isConnected == false) { syncStatus = NotConnectedSyncStatus();
syncStatus = LostConnectionSyncStatus(); break;
} else if (isConnected != true && syncStatus is! ConnectingSyncStatus) { case ConnectionStatus.failed:
syncStatus = NotConnectedSyncStatus(); syncStatus = LostConnectionSyncStatus();
break;
case ConnectionStatus.connecting:
syncStatus = ConnectingSyncStatus();
break;
default:
} }
} }
void _syncStatusReaction(SyncStatus syncStatus) async { void _syncStatusReaction(SyncStatus syncStatus) async {
if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) { if (syncStatus is SyncingSyncStatus) {
silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; return;
} }
if (syncStatus is NotConnectedSyncStatus) { if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) {
// Needs to re-subscribe to all scripthashes when reconnected // Needs to re-subscribe to all scripthashes when reconnected
_scripthashesUpdateSubject = {}; _scripthashesUpdateSubject = {};
@ -1827,7 +2007,8 @@ abstract class ElectrumWalletBase
_isTryingToConnect = true; _isTryingToConnect = true;
Future.delayed(Duration(seconds: 10), () { _reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(seconds: 10), () {
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) { if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) {
this.electrumClient.connectToUri( this.electrumClient.connectToUri(
node!.uri, node!.uri,
@ -1939,8 +2120,8 @@ Future<void> startRefresh(ScanData scanData) async {
final tweaks = t as Map<String, dynamic>; final tweaks = t as Map<String, dynamic>;
if (tweaks["message"] != null) { if (tweaks["message"] != null) {
// re-subscribe to continue receiving messages // re-subscribe to continue receiving messages, starting from the next unscanned height
electrumClient.tweaksSubscribe(height: syncHeight, count: count); electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count);
return; return;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,68 +1,54 @@
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:flutter/foundation.dart'; import 'package:blockchain_utils/blockchain_utils.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)));
}
ECPrivate generateECPrivate({ ECPrivate generateECPrivate({
required bitcoin.HDWallet hd, required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network, required BasedUtxoNetwork network,
required int index, required int index,
}) { }) =>
final wif = hd.derive(index).wif!; ECPrivate(hd.childKey(Bip32KeyIndex(index)).privateKey);
return ECPrivate.fromWif(wif, netVersion: network.wifNetVer);
}
String generateP2WPKHAddress({ String generateP2WPKHAddress({
required bitcoin.HDWallet hd, required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network, required BasedUtxoNetwork network,
required int index, required int index,
}) { }) =>
final pubKey = hd.derive(index).pubKey!; ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
return ECPublic.fromHex(pubKey).toP2wpkhAddress().toAddress(network); .toP2wpkhAddress()
} .toAddress(network);
String generateP2SHAddress({ String generateP2SHAddress({
required bitcoin.HDWallet hd, required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network, required BasedUtxoNetwork network,
required int index, required int index,
}) { }) =>
final pubKey = hd.derive(index).pubKey!; ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
return ECPublic.fromHex(pubKey).toP2wpkhInP2sh().toAddress(network); .toP2wpkhInP2sh()
} .toAddress(network);
String generateP2WSHAddress({ String generateP2WSHAddress({
required bitcoin.HDWallet hd, required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network, required BasedUtxoNetwork network,
required int index, required int index,
}) { }) =>
final pubKey = hd.derive(index).pubKey!; ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
return ECPublic.fromHex(pubKey).toP2wshAddress().toAddress(network); .toP2wshAddress()
} .toAddress(network);
String generateP2PKHAddress({ String generateP2PKHAddress({
required bitcoin.HDWallet hd, required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network, required BasedUtxoNetwork network,
required int index, required int index,
}) { }) =>
final pubKey = hd.derive(index).pubKey!; ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
return ECPublic.fromHex(pubKey).toP2pkhAddress().toAddress(network); .toP2pkhAddress()
} .toAddress(network);
String generateP2TRAddress({ String generateP2TRAddress({
required bitcoin.HDWallet hd, required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network, required BasedUtxoNetwork network,
required int index, required int index,
}) { }) =>
final pubKey = hd.derive(index).pubKey!; ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
return ECPublic.fromHex(pubKey).toTaprootAddress().toAddress(network); .toTaprootAddress()
} .toAddress(network);

View file

@ -41,15 +41,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
bech32:
dependency: transitive
description:
path: "."
ref: "cake-0.2.2"
resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192"
url: "https://github.com/cake-tech/bech32.git"
source: git
version: "0.2.2"
bip32: bip32:
dependency: transitive dependency: transitive
description: description:
@ -76,32 +67,23 @@ packages:
source: git source: git
version: "1.0.1" version: "1.0.1"
bitcoin_base: bitcoin_base:
dependency: "direct main" dependency: "direct overridden"
description: description:
path: "." path: "."
ref: cake-update-v3 ref: cake-update-v5
resolved-ref: cc99eedb1d28ee9376dda0465ef72aa627ac6149 resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380
url: "https://github.com/cake-tech/bitcoin_base" url: "https://github.com/cake-tech/bitcoin_base"
source: git source: git
version: "4.2.1" version: "4.7.0"
bitcoin_flutter:
dependency: "direct main"
description:
path: "."
ref: cake-update-v4
resolved-ref: e19ffb7e7977278a75b27e0479b3c6f4034223b3
url: "https://github.com/cake-tech/bitcoin_flutter.git"
source: git
version: "2.1.0"
blockchain_utils: blockchain_utils:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: cake-update-v1 ref: cake-update-v2
resolved-ref: cabd7e0e16c4da9920338c76eff3aeb8af0211f3 resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57"
url: "https://github.com/cake-tech/blockchain_utils" url: "https://github.com/cake-tech/blockchain_utils"
source: git source: git
version: "2.1.2" version: "3.3.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -182,6 +164,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.2" version: "8.9.2"
cake_backup:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38"
url: "https://github.com/cake-tech/cake_backup.git"
source: git
version: "1.0.0+1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -254,6 +245,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" version: "2.7.0"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
cw_core: cw_core:
dependency: "direct main" dependency: "direct main"
description: description:
@ -411,10 +410,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -499,11 +498,12 @@ packages:
ledger_flutter: ledger_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: ledger_flutter path: "."
sha256: f1680060ed6ff78f275837e0024ccaf667715a59ba7aa29fa7354bc7752e71c8 ref: cake-v3
url: "https://pub.dev" resolved-ref: "66469ff9dffe2417c70ae7287c9d76d2fe7157a4"
source: hosted url: "https://github.com/cake-tech/ledger-flutter.git"
version: "1.0.1" source: git
version: "1.0.2"
ledger_usb: ledger_usb:
dependency: transitive dependency: transitive
description: description:
@ -596,10 +596,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.4"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
@ -636,18 +636,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4" version: "3.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -700,10 +700,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pubspec_parse name: pubspec_parse
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.3.0"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
@ -761,10 +761,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: socks5_proxy name: socks5_proxy
sha256: "045cbba84f6e2b01c1c77634a63e926352bf110ef5f07fc462c6d43bbd4b6a83" sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5+dev.2" version: "1.0.6"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -793,9 +793,9 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "sp_v2.0.0" ref: "sp_v4.0.0"
resolved-ref: "62c152b9086cd968019128845371072f7e1168de" resolved-ref: "3b8ae38592c0584f53560071dc18bc570758fe13"
url: "https://github.com/cake-tech/sp_scanner" url: "https://github.com/rafael-xmr/sp_scanner"
source: git source: git
version: "0.0.1" version: "0.0.1"
stack_trace: stack_trace:
@ -854,6 +854,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -910,14 +918,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.5" version: "2.4.5"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View file

@ -19,32 +19,24 @@ dependencies:
intl: ^0.18.0 intl: ^0.18.0
cw_core: cw_core:
path: ../cw_core path: ../cw_core
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v4
bitbox: bitbox:
git: git:
url: https://github.com/cake-tech/bitbox-flutter.git url: https://github.com/cake-tech/bitbox-flutter.git
ref: Add-Support-For-OP-Return-data ref: Add-Support-For-OP-Return-data
rxdart: ^0.27.5 rxdart: ^0.27.5
cryptography: ^2.0.5 cryptography: ^2.0.5
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v3
blockchain_utils: blockchain_utils:
git: git:
url: https://github.com/cake-tech/blockchain_utils url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1 ref: cake-update-v2
ledger_flutter: ^1.0.1 ledger_flutter: ^1.0.1
ledger_bitcoin: ledger_bitcoin:
git: git:
url: https://github.com/cake-tech/ledger-bitcoin url: https://github.com/cake-tech/ledger-bitcoin
sp_scanner: sp_scanner:
git: git:
url: https://github.com/cake-tech/sp_scanner url: https://github.com/rafael-xmr/sp_scanner
ref: sp_v2.0.0 ref: sp_v4.0.0
dev_dependencies: dev_dependencies:
@ -56,7 +48,15 @@ dev_dependencies:
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides: dependency_overrides:
ledger_flutter:
git:
url: https://github.com/cake-tech/ledger-flutter.git
ref: cake-v3
watcher: ^1.1.0 watcher: ^1.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v5
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -1,8 +1,7 @@
import 'dart:convert';
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
@ -12,6 +11,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -29,6 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes, required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils,
BitcoinAddressType? addressPageType, BitcoinAddressType? addressPageType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
@ -39,18 +40,19 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
networkType: bitcoin.bitcoin, network: BitcoinCashNetwork.mainnet,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: seedBytes, seedBytes: seedBytes,
currency: CryptoCurrency.bch) { currency: CryptoCurrency.bch,
encryptionFileUtils: encryptionFileUtils) {
walletAddresses = BitcoinCashWalletAddresses( walletAddresses = BitcoinCashWalletAddresses(
walletInfo, walletInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd, mainHd: hd,
sideHd: accountHD.derive(1), sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network, network: network,
initialAddressPageType: addressPageType, initialAddressPageType: addressPageType,
); );
@ -64,6 +66,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? addressPageType, String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
@ -76,7 +79,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: await Mnemonic.toSeed(mnemonic), seedBytes: await MnemonicBip39.toSeed(mnemonic),
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh, addressPageType: P2pkhAddressType.p2pkh,
@ -88,15 +92,44 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password, required String password,
required EncryptionFileUtils encryptionFileUtils,
}) async { }) async {
final snp = await ElectrumWalletSnapshot.load( final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
name, walletInfo.type, password, BitcoinCashNetwork.mainnet);
ElectrumWalletSnapshot? snp = null;
try {
snp = await ElectrumWalletSnapshot.load(
encryptionFileUtils,
name,
walletInfo.type,
password,
BitcoinCashNetwork.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,
);
}
return BitcoinCashWallet( return BitcoinCashWallet(
mnemonic: snp.mnemonic!, mnemonic: keysData.mnemonic!,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses.map((addr) { initialAddresses: snp?.addresses.map((addr) {
try { try {
BitcoinCashAddress(addr.address); BitcoinCashAddress(addr.address);
return BitcoinAddressRecord( return BitcoinAddressRecord(
@ -116,16 +149,19 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
); );
} }
}).toList(), }).toList(),
initialBalance: snp.balance, initialBalance: snp?.balance,
seedBytes: await Mnemonic.toSeed(snp.mnemonic!), seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!),
initialRegularAddressIndex: snp.regularAddressIndex, encryptionFileUtils: encryptionFileUtils,
initialChangeAddressIndex: snp.changeAddressIndex, initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh, addressPageType: P2pkhAddressType.p2pkh,
); );
} }
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) => bitbox.ECPair generateKeyPair({required Bip32Slip10Secp256k1 hd, required int index}) =>
bitbox.ECPair.fromWIF(hd.derive(index).wif!); bitbox.ECPair.fromPrivateKey(
Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw),
);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0; int inputsCount = 0;
@ -166,12 +202,17 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
@override @override
Future<String> signMessage(String message, {String? address = null}) async { Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null int? index;
? walletAddresses.allAddresses try {
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) index = address != null
.index ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null; : null;
final HD = index == null ? hd : hd.derive(index); } catch (_) {}
return base64Encode(HD.signMessage(message)); final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
final priv = ECPrivate.fromWif(
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
netVersion: network.wifNetVer,
);
return priv.signMessage(StringUtils.encode(message));
} }
} }

View file

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

View file

@ -2,8 +2,8 @@ import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
class BitcoinCashNewWalletCredentials extends WalletCredentials { class BitcoinCashNewWalletCredentials extends WalletCredentials {
BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo}) BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password})
: super(name: name, walletInfo: walletInfo); : super(name: name, walletInfo: walletInfo, password: password);
} }
class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials {

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:bip39/bip39.dart'; import 'package:bip39/bip39.dart';
import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
@ -11,12 +12,16 @@ import 'package:cw_core/wallet_type.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredentials, class BitcoinCashWalletService extends WalletService<
BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> { BitcoinCashNewWalletCredentials,
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); BitcoinCashRestoreWalletFromSeedCredentials,
BitcoinCashRestoreWalletFromWIFCredentials,
BitcoinCashNewWalletCredentials> {
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool isDirect;
@override @override
WalletType getType() => WalletType.bitcoinCash; WalletType getType() => WalletType.bitcoinCash;
@ -30,12 +35,15 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final wallet = await BitcoinCashWalletBase.create( final wallet = await BitcoinCashWalletBase.create(
mnemonic: await Mnemonic.generate(strength: strength), mnemonic: await MnemonicBip39.generate(strength: strength),
password: credentials.password!, password: credentials.password!,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
return wallet; return wallet;
} }
@ -49,7 +57,9 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: password, password: password,
name: name, name: name,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init(); await wallet.init();
saveBackup(name); saveBackup(name);
return wallet; return wallet;
@ -59,7 +69,9 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: password, password: password,
name: name, name: name,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init(); await wallet.init();
return wallet; return wallet;
} }
@ -81,7 +93,8 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: password, password: password,
name: currentName, name: currentName,
walletInfo: currentWalletInfo, walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
await saveBackup(newName); await saveBackup(newName);
@ -95,7 +108,8 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
@override @override
Future<BitcoinCashWallet> restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) { Future<BitcoinCashWallet> restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Bitcoin Cash wallet from a hardware wallet is not yet supported!"); throw UnimplementedError(
"Restoring a Bitcoin Cash wallet from a hardware wallet is not yet supported!");
} }
@override @override
@ -115,7 +129,8 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: credentials.password!, password: credentials.password!,
mnemonic: credentials.mnemonic, mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
return wallet; return wallet;

View file

@ -2,7 +2,7 @@ import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
class Mnemonic { class MnemonicBip39 {
/// Generate bip39 mnemonic /// Generate bip39 mnemonic
static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength);

View file

@ -21,22 +21,14 @@ dependencies:
path: ../cw_core path: ../cw_core
cw_bitcoin: cw_bitcoin:
path: ../cw_bitcoin path: ../cw_bitcoin
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v4
bitbox: bitbox:
git: git:
url: https://github.com/cake-tech/bitbox-flutter.git url: https://github.com/cake-tech/bitbox-flutter.git
ref: Add-Support-For-OP-Return-data ref: Add-Support-For-OP-Return-data
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v3
blockchain_utils: blockchain_utils:
git: git:
url: https://github.com/cake-tech/blockchain_utils url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1 ref: cake-update-v2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -47,6 +39,10 @@ dev_dependencies:
dependency_overrides: dependency_overrides:
watcher: ^1.1.0 watcher: ^1.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v5
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -281,7 +281,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
final s = 'Unexpected token: $name for CryptoCurrency fromFullName'; final s = 'Unexpected token: $name for CryptoCurrency fromFullName';
throw ArgumentError.value(name, 'Fullname', s); throw ArgumentError.value(name, 'Fullname', s);
} }
return CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()]!; return CryptoCurrency._fullNameCurrencyMap[name.split("(").first.trim().toLowerCase()]!;
} }
@override @override

View file

@ -0,0 +1,42 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:cw_core/utils/file.dart' as file;
import 'package:cake_backup/backup.dart' as cwb;
EncryptionFileUtils encryptionFileUtilsFor(bool direct)
=> direct
? XChaCha20EncryptionFileUtils()
: Salsa20EncryhptionFileUtils();
abstract class EncryptionFileUtils {
Future<void> write({required String path, required String password, required String data});
Future<String> read({required String path, required String password});
}
class Salsa20EncryhptionFileUtils extends EncryptionFileUtils {
// Requires legacy complex key + iv as password
@override
Future<void> write({required String path, required String password, required String data}) async
=> await file.write(path: path, password: password, data: data);
// Requires legacy complex key + iv as password
@override
Future<String> read({required String path, required String password}) async
=> await file.read(path: path, password: password);
}
class XChaCha20EncryptionFileUtils extends EncryptionFileUtils {
@override
Future<void> write({required String path, required String password, required String data}) async {
final encrypted = await cwb.encrypt(password, Uint8List.fromList(data.codeUnits));
await File(path).writeAsBytes(encrypted);
}
@override
Future<String> read({required String path, required String password}) async {
final file = File(path);
final encrypted = await file.readAsBytes();
final bytes = await cwb.decrypt(password, encrypted);
return String.fromCharCodes(bytes);
}
}

View file

@ -245,6 +245,8 @@ Future<int> getHavenCurrentHeight() async {
// Data taken from https://timechaincalendar.com/ // Data taken from https://timechaincalendar.com/
const bitcoinDates = { const bitcoinDates = {
"2024-08": 854889,
"2024-07": 850182,
"2024-06": 846005, "2024-06": 846005,
"2024-05": 841590, "2024-05": 841590,
"2024-04": 837182, "2024-04": 837182,
@ -371,7 +373,8 @@ const wowDates = {
int getWowneroHeightByDate({required DateTime date}) { int getWowneroHeightByDate({required DateTime date}) {
String closestKey = String closestKey =
wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => ''); wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => '');
return wowDates[closestKey] ?? 0; return wowDates[closestKey] ?? 0;
} }

View file

@ -79,6 +79,7 @@ Future<bool> backupWalletFilesExists(String name) async {
backupAddressListFile.existsSync(); backupAddressListFile.existsSync();
} }
// WARNING: Transaction keys and your Polyseed CANNOT be recovered if this file is deleted
Future<void> removeCache(String name) async { Future<void> removeCache(String name) async {
final path = await pathForWallet(name: name, type: WalletType.monero); final path = await pathForWallet(name: name, type: WalletType.monero);
final cacheFile = File(path); final cacheFile = File(path);
@ -92,8 +93,8 @@ Future<void> restoreOrResetWalletFiles(String name) async {
final backupsExists = await backupWalletFilesExists(name); final backupsExists = await backupWalletFilesExists(name);
if (backupsExists) { if (backupsExists) {
await removeCache(name);
await restoreWalletFiles(name); await restoreWalletFiles(name);
} }
removeCache(name);
} }

View file

@ -11,7 +11,8 @@ import 'package:http/io_client.dart' as ioc;
part 'node.g.dart'; part 'node.g.dart';
Uri createUriFromElectrumAddress(String address, String path) => Uri.tryParse('tcp://$address$path')!; Uri createUriFromElectrumAddress(String address, String path) =>
Uri.tryParse('tcp://$address$path')!;
@HiveType(typeId: Node.typeId) @HiveType(typeId: Node.typeId)
class Node extends HiveObject with Keyable { class Node extends HiveObject with Keyable {
@ -72,6 +73,12 @@ class Node extends HiveObject with Keyable {
@HiveField(7, defaultValue: '') @HiveField(7, defaultValue: '')
String? path; String? path;
@HiveField(8)
bool? isElectrs;
@HiveField(9)
bool? supportsSilentPayments;
bool get isSSL => useSSL ?? false; bool get isSSL => useSSL ?? false;
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;

View file

@ -3,6 +3,11 @@ abstract class SyncStatus {
double progress(); double progress();
} }
class StartingScanSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
}
class SyncingSyncStatus extends SyncStatus { class SyncingSyncStatus extends SyncStatus {
SyncingSyncStatus(this.blocksLeft, this.ptc); SyncingSyncStatus(this.blocksLeft, this.ptc);

View file

@ -3,12 +3,13 @@ import 'package:cw_core/keyable.dart';
abstract class TransactionInfo extends Object with Keyable { abstract class TransactionInfo extends Object with Keyable {
late String id; late String id;
late String txHash = id;
late int amount; late int amount;
int? fee; int? fee;
late TransactionDirection direction; late TransactionDirection direction;
late bool isPending; late bool isPending;
late DateTime date; late DateTime date;
late int height; int? height;
late int confirmations; late int confirmations;
String amountFormatted(); String amountFormatted();
String fiatAmount(); String fiatAmount();
@ -24,4 +25,5 @@ abstract class TransactionInfo extends Object with Keyable {
dynamic get keyIndex => id; dynamic get keyIndex => id;
late Map<String, dynamic> additionalInfo; late Map<String, dynamic> additionalInfo;
} }

View file

@ -69,7 +69,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
int calculateEstimatedFee(TransactionPriority priority, int? amount); int calculateEstimatedFee(TransactionPriority priority, int? amount);
// void fetchTransactionsAsync( // void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded, // void Function(TransactionType transaction) onTransactionLoaded,
// {void Function() onFinished}); // {void Function() onFinished});
@ -84,13 +83,17 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
Future<void> changePassword(String password); Future<void> changePassword(String password);
String get password;
Future<void>? updateBalance(); Future<void>? updateBalance();
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
Future<void> renameWalletFiles(String newWalletName); Future<void> renameWalletFiles(String newWalletName);
Future<String> signMessage(String message, {String? address = null}) => throw UnimplementedError(); Future<String> signMessage(String message, {String? address = null});
bool? isTestnet; Future<bool> verifyMessage(String message, String signature, {String? address = null});
bool isTestnet = false;
} }

View file

@ -0,0 +1,119 @@
import 'dart:convert';
import 'dart:developer' as dev;
import 'dart:io';
import 'package:cw_core/balance.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
mixin WalletKeysFile<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
TransactionType extends TransactionInfo>
on WalletBase<BalanceType, HistoryType, TransactionType> {
Future<String> makePath() => pathForWallet(name: walletInfo.name, type: walletInfo.type);
// this needs to be overridden
WalletKeysData get walletKeysData;
Future<String> makeKeysFilePath() async => "${await makePath()}.keys";
Future<void> saveKeysFile(String password, EncryptionFileUtils encryptionFileUtils,
[bool isBackup = false]) async {
try {
final rootPath = await makeKeysFilePath();
final path = "$rootPath${isBackup ? ".backup" : ""}";
dev.log("Saving .keys file '$path'");
await encryptionFileUtils.write(
path: path, password: password, data: walletKeysData.toJSON());
} catch (_) {}
}
static Future<void> createKeysFile(String name, WalletType type, String password,
WalletKeysData walletKeysData, EncryptionFileUtils encryptionFileUtils,
[bool withBackup = true]) async {
try {
final rootPath = await pathForWallet(name: name, type: type);
final path = "$rootPath.keys";
dev.log("Saving .keys file '$path'");
await encryptionFileUtils.write(
path: path, password: password, data: walletKeysData.toJSON());
if (withBackup) {
dev.log("Saving .keys.backup file '$path.backup'");
await encryptionFileUtils.write(
path: "$path.backup", password: password, data: walletKeysData.toJSON());
}
} catch (_) {}
}
static Future<bool> hasKeysFile(String name, WalletType type) async {
try {
final path = await pathForWallet(name: name, type: type);
return File("$path.keys").existsSync() || File("$path.keys.backup").existsSync();
} catch (_) {
return false;
}
}
static Future<WalletKeysData> readKeysFile(
String name,
WalletType type,
String password,
EncryptionFileUtils encryptionFileUtils,
) async {
final path = await pathForWallet(name: name, type: type);
var readPath = "$path.keys";
try {
if (!File(readPath).existsSync()) throw Exception("No .keys file found for $name $type");
final jsonSource = await encryptionFileUtils.read(path: readPath, password: password);
final data = json.decode(jsonSource) as Map<String, dynamic>;
return WalletKeysData.fromJSON(data);
} catch (e) {
dev.log("Failed to read .keys file. Trying .keys.backup file...");
readPath = "$readPath.backup";
if (!File(readPath).existsSync())
throw Exception("No .keys nor a .keys.backup file found for $name $type");
final jsonSource = await encryptionFileUtils.read(path: readPath, password: password);
final data = json.decode(jsonSource) as Map<String, dynamic>;
final keysData = WalletKeysData.fromJSON(data);
dev.log("Restoring .keys from .keys.backup");
createKeysFile(name, type, password, keysData, encryptionFileUtils, false);
return keysData;
}
}
}
class WalletKeysData {
final String? privateKey;
final String? mnemonic;
final String? altMnemonic;
final String? passphrase;
final String? xPub;
WalletKeysData({this.privateKey, this.mnemonic, this.altMnemonic, this.passphrase, this.xPub});
String toJSON() => jsonEncode({
"privateKey": privateKey,
"mnemonic": mnemonic,
if (altMnemonic != null) "altMnemonic": altMnemonic,
if (passphrase != null) "passphrase": passphrase,
if (xPub != null) "xPub": xPub
});
static WalletKeysData fromJSON(Map<String, dynamic> json) => WalletKeysData(
privateKey: json["privateKey"] as String?,
mnemonic: json["mnemonic"] as String?,
altMnemonic: json["altMnemonic"] as String?,
passphrase: json["passphrase"] as String?,
xPub: json["xPub"] as String?,
);
}

View file

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -42,4 +44,21 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
await File(walletDirPath).copy(backupWalletDirPath); await File(walletDirPath).copy(backupWalletDirPath);
} }
} }
Future<String> getSeeds(String name, String password, WalletType type) async {
try {
final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password);
try {
final data = json.decode(jsonSource) as Map;
return data['mnemonic'] as String? ?? '';
} catch (_) {
// if not a valid json
return jsonSource.substring(0, 200);
}
} catch (_) {
// if the file couldn't be opened or read
return '';
}
}
} }

View file

@ -113,6 +113,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.8.1" version: "8.8.1"
cake_backup:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38"
url: "https://github.com/cake-tech/cake_backup.git"
source: git
version: "1.0.0+1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -169,6 +178,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
cryptography:
dependency: transitive
description:
name: cryptography
sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35
url: "https://pub.dev"
source: hosted
version: "2.5.0"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev"
source: hosted
version: "1.0.6"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -648,6 +673,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View file

@ -19,6 +19,11 @@ dependencies:
flutter_mobx: ^2.0.6+1 flutter_mobx: ^2.0.6+1
intl: ^0.18.0 intl: ^0.18.0
encrypt: ^5.0.1 encrypt: ^5.0.1
cake_backup:
git:
url: https://github.com/cake-tech/cake_backup.git
ref: main
version: 1.0.0
socks5_proxy: ^1.0.4 socks5_proxy: ^1.0.4
unorm_dart: ^0.3.0 unorm_dart: ^0.3.0
# tor: # tor:

View file

@ -7,6 +7,7 @@ class EthereumTransactionHistory extends EVMChainTransactionHistory {
EthereumTransactionHistory({ EthereumTransactionHistory({
required super.walletInfo, required super.walletInfo,
required super.password, required super.password,
required super.encryptionFileUtils,
}); });
@override @override

View file

@ -2,10 +2,12 @@ import 'dart:convert';
import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart';
import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_client.dart';
import 'package:cw_ethereum/ethereum_transaction_history.dart'; import 'package:cw_ethereum/ethereum_transaction_history.dart';
@ -15,7 +17,6 @@ import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_evm/evm_chain_wallet.dart';
import 'package:cw_evm/evm_erc20_balance.dart'; import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/file.dart';
class EthereumWallet extends EVMChainWallet { class EthereumWallet extends EVMChainWallet {
EthereumWallet({ EthereumWallet({
@ -25,6 +26,7 @@ class EthereumWallet extends EVMChainWallet {
super.mnemonic, super.mnemonic,
super.initialBalance, super.initialBalance,
super.privateKey, super.privateKey,
required super.encryptionFileUtils,
}) : super(nativeCurrency: CryptoCurrency.eth); }) : super(nativeCurrency: CryptoCurrency.eth);
@override @override
@ -116,27 +118,57 @@ class EthereumWallet extends EVMChainWallet {
} }
@override @override
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) { EVMChainTransactionHistory setUpTransactionHistory(
return EthereumTransactionHistory(walletInfo: walletInfo, password: password); WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils) {
return EthereumTransactionHistory(
walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils);
} }
static Future<EthereumWallet> open( static Future<EthereumWallet> open({
{required String name, required String password, required WalletInfo walletInfo}) async { required String name,
required String password,
required WalletInfo walletInfo,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
final path = await pathForWallet(name: name, type: walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map; Map<String, dynamic>? data;
final mnemonic = data['mnemonic'] as String?; try {
final privateKey = data['private_key'] as String?; final jsonSource = await encryptionFileUtils.read(path: path, password: password);
final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
data = json.decode(jsonSource) as Map<String, dynamic>;
} catch (e) {
if (!hasKeysFile) rethrow;
}
final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ??
EVMChainERC20Balance(BigInt.zero); EVMChainERC20Balance(BigInt.zero);
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
final mnemonic = data!['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
return EthereumWallet( return EthereumWallet(
walletInfo: walletInfo, walletInfo: walletInfo,
password: password, password: password,
mnemonic: mnemonic, mnemonic: keysData.mnemonic,
privateKey: privateKey, privateKey: keysData.privateKey,
initialBalance: balance, initialBalance: balance,
client: EthereumClient(), client: EthereumClient(),
encryptionFileUtils: encryptionFileUtils,
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -9,7 +10,7 @@ import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:cw_evm/evm_chain_wallet_service.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart';
class EthereumWalletService extends EVMChainWalletService<EthereumWallet> { class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
EthereumWalletService(super.walletInfoSource, {required this.client}); EthereumWalletService(super.walletInfoSource, super.isDirect, {required this.client});
late EthereumClient client; late EthereumClient client;
@ -27,6 +28,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
mnemonic: mnemonic, mnemonic: mnemonic,
password: credentials.password!, password: credentials.password!,
client: client, client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
@ -46,6 +48,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
name: name, name: name,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
@ -59,6 +62,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
name: name, name: name,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
await wallet.save(); await wallet.save();
@ -71,7 +75,11 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
final currentWalletInfo = walletInfoSource.values final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
final currentWallet = await EthereumWallet.open( final currentWallet = await EthereumWallet.open(
password: password, name: currentName, walletInfo: currentWalletInfo); password: password,
name: currentName,
walletInfo: currentWalletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
await saveBackup(newName); await saveBackup(newName);
@ -97,6 +105,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
password: credentials.password!, password: credentials.password!,
client: client, client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
@ -114,6 +123,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
privateKey: credentials.privateKey, privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
client: client, client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
@ -135,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
mnemonic: credentials.mnemonic, mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
client: client, client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();

View file

@ -2,7 +2,7 @@ import 'dart:typed_data';
import 'package:web3dart/web3dart.dart' as web3; import 'package:web3dart/web3dart.dart' as web3;
final _contractAbi = web3.ContractAbi.fromJson( final ethereumContractAbi = web3.ContractAbi.fromJson(
'[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]', '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]',
'Erc20'); 'Erc20');
@ -13,7 +13,7 @@ class ERC20 extends web3.GeneratedContract {
required web3.EthereumAddress address, required web3.EthereumAddress address,
required web3.Web3Client client, required web3.Web3Client client,
int? chainId, int? chainId,
}) : super(web3.DeployedContract(_contractAbi, address), client, chainId); }) : super(web3.DeployedContract(ethereumContractAbi, address), client, chainId);
/// Returns the remaining number of tokens that [spender] will be allowed to spend on behalf of [owner] through [transferFrom]. This is zero by default. This value changes when [approve] or [transferFrom] are called. /// Returns the remaining number of tokens that [spender] will be allowed to spend on behalf of [owner] through [transferFrom]. This is zero by default. This value changes when [approve] or [transferFrom] are called.
/// ///

View file

@ -10,7 +10,7 @@ import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_erc20_balance.dart'; import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/.secrets.g.dart' as secrets; import 'package:cw_evm/.secrets.g.dart' as secrets;
import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart';
import 'package:hex/hex.dart' as hex; import 'package:hex/hex.dart' as hex;
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
@ -65,16 +65,65 @@ abstract class EVMChainClient {
Future<int> getGasUnitPrice() async { Future<int> getGasUnitPrice() async {
try { try {
final gasPrice = await _client!.getGasPrice(); final gasPrice = await _client!.getGasPrice();
return gasPrice.getInWei.toInt(); return gasPrice.getInWei.toInt();
} catch (_) { } catch (_) {
return 0; return 0;
} }
} }
Future<int> getEstimatedGas() async { Future<int?> getGasBaseFee() async {
try { try {
final estimatedGas = await _client!.estimateGas(); final blockInfo = await _client!.getBlockInformation(isContainFullObj: false);
return estimatedGas.toInt(); final baseFee = blockInfo.baseFeePerGas;
return baseFee?.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int> getEstimatedGas({
String? contractAddress,
required EthereumAddress toAddress,
required EthereumAddress senderAddress,
required EtherAmount value,
EtherAmount? gasPrice,
// EtherAmount? maxFeePerGas,
// EtherAmount? maxPriorityFeePerGas,
}) async {
try {
if (contractAddress == null) {
final estimatedGas = await _client!.estimateGas(
sender: senderAddress,
gasPrice: gasPrice,
to: toAddress,
value: value,
// maxPriorityFeePerGas: maxPriorityFeePerGas,
// maxFeePerGas: maxFeePerGas,
);
return estimatedGas.toInt();
} else {
final contract = DeployedContract(
ethereumContractAbi,
EthereumAddress.fromHex(contractAddress),
);
final transfer = contract.function('transfer');
// Estimate gas units
final gasEstimate = await _client!.estimateGas(
sender: senderAddress,
to: EthereumAddress.fromHex(contractAddress),
data: transfer.encodeCall([
toAddress,
value.getInWei,
]),
);
return gasEstimate.toInt();
}
} catch (_) { } catch (_) {
return 0; return 0;
} }
@ -84,7 +133,7 @@ abstract class EVMChainClient {
required Credentials privateKey, required Credentials privateKey,
required String toAddress, required String toAddress,
required BigInt amount, required BigInt amount,
required int gas, required BigInt gas,
required EVMChainTransactionPriority priority, required EVMChainTransactionPriority priority,
required CryptoCurrency currency, required CryptoCurrency currency,
required int exponent, required int exponent,
@ -97,8 +146,6 @@ abstract class EVMChainClient {
bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice();
final Transaction transaction = createTransaction( final Transaction transaction = createTransaction(
from: privateKey.address, from: privateKey.address,
to: EthereumAddress.fromHex(toAddress), to: EthereumAddress.fromHex(toAddress),
@ -130,11 +177,10 @@ abstract class EVMChainClient {
_sendTransaction = () async => await sendTransaction(signedTransaction); _sendTransaction = () async => await sendTransaction(signedTransaction);
return PendingEVMChainTransaction( return PendingEVMChainTransaction(
signedTransaction: signedTransaction, signedTransaction: signedTransaction,
amount: amount.toString(), amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei, fee: gas,
sendTransaction: _sendTransaction, sendTransaction: _sendTransaction,
exponent: exponent, exponent: exponent,
); );
@ -233,7 +279,6 @@ abstract class EVMChainClient {
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>; final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
final symbol = (decodedResponse['symbol'] ?? '') as String; final symbol = (decodedResponse['symbol'] ?? '') as String;
String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), '');

View file

@ -1,10 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:core'; import 'dart:core';
import 'dart:developer'; import 'dart:developer';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_evm/evm_chain_transaction_info.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/file.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_history.dart';
@ -15,7 +15,8 @@ abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase
abstract class EVMChainTransactionHistoryBase abstract class EVMChainTransactionHistoryBase
extends TransactionHistoryBase<EVMChainTransactionInfo> with Store { extends TransactionHistoryBase<EVMChainTransactionInfo> with Store {
EVMChainTransactionHistoryBase({required this.walletInfo, required String password}) EVMChainTransactionHistoryBase(
{required this.walletInfo, required String password, required this.encryptionFileUtils})
: _password = password { : _password = password {
transactions = ObservableMap<String, EVMChainTransactionInfo>(); transactions = ObservableMap<String, EVMChainTransactionInfo>();
} }
@ -23,6 +24,7 @@ abstract class EVMChainTransactionHistoryBase
String _password; String _password;
final WalletInfo walletInfo; final WalletInfo walletInfo;
final EncryptionFileUtils encryptionFileUtils;
//! Method to be overridden by all child classes //! Method to be overridden by all child classes
@ -41,7 +43,7 @@ abstract class EVMChainTransactionHistoryBase
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
String path = '$dirPath/$transactionsHistoryFileNameForWallet'; String path = '$dirPath/$transactionsHistoryFileNameForWallet';
final data = json.encode({'transactions': transactions}); final data = json.encode({'transactions': transactions});
await writeData(path: path, password: _password, data: data); await encryptionFileUtils.write(path: path, password: _password, data: data);
} catch (e, s) { } catch (e, s) {
log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}');
log(s.toString()); log(s.toString());
@ -59,7 +61,7 @@ abstract class EVMChainTransactionHistoryBase
final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName();
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
String path = '$dirPath/$transactionsHistoryFileNameForWallet'; String path = '$dirPath/$transactionsHistoryFileNameForWallet';
final content = await read(path: path, password: _password); final content = await encryptionFileUtils.read(path: path, password: _password);
if (content.isEmpty) { if (content.isEmpty) {
return {}; return {};
} }

View file

@ -7,6 +7,7 @@ import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
@ -16,6 +17,7 @@ import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_evm/evm_chain_client.dart'; import 'package:cw_evm/evm_chain_client.dart';
import 'package:cw_evm/evm_chain_exceptions.dart'; import 'package:cw_evm/evm_chain_exceptions.dart';
@ -26,13 +28,14 @@ import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart';
import 'package:cw_evm/evm_ledger_credentials.dart'; import 'package:cw_evm/evm_ledger_credentials.dart';
import 'package:cw_evm/file.dart'; import 'package:flutter/foundation.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart'; import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'evm_chain_transaction_info.dart'; import 'evm_chain_transaction_info.dart';
import 'evm_erc20_balance.dart'; import 'evm_erc20_balance.dart';
@ -57,7 +60,7 @@ abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet;
abstract class EVMChainWalletBase abstract class EVMChainWalletBase
extends WalletBase<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo> extends WalletBase<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo>
with Store { with Store, WalletKeysFile {
EVMChainWalletBase({ EVMChainWalletBase({
required WalletInfo walletInfo, required WalletInfo walletInfo,
required EVMChainClient client, required EVMChainClient client,
@ -66,6 +69,7 @@ abstract class EVMChainWalletBase
String? privateKey, String? privateKey,
required String password, required String password,
EVMChainERC20Balance? initialBalance, EVMChainERC20Balance? initialBalance,
required this.encryptionFileUtils,
}) : syncStatus = const NotConnectedSyncStatus(), }) : syncStatus = const NotConnectedSyncStatus(),
_password = password, _password = password,
_mnemonic = mnemonic, _mnemonic = mnemonic,
@ -81,7 +85,7 @@ abstract class EVMChainWalletBase
), ),
super(walletInfo) { super(walletInfo) {
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = setUpTransactionHistory(walletInfo, password); transactionHistory = setUpTransactionHistory(walletInfo, password, encryptionFileUtils);
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter()); CakeHive.registerAdapter(Erc20TokenAdapter());
@ -93,6 +97,7 @@ abstract class EVMChainWalletBase
final String? _mnemonic; final String? _mnemonic;
final String? _hexPrivateKey; final String? _hexPrivateKey;
final String _password; final String _password;
final EncryptionFileUtils encryptionFileUtils;
late final Box<Erc20Token> erc20TokensBox; late final Box<Erc20Token> erc20TokensBox;
@ -102,10 +107,14 @@ abstract class EVMChainWalletBase
Credentials get evmChainPrivateKey => _evmChainPrivateKey; Credentials get evmChainPrivateKey => _evmChainPrivateKey;
late EVMChainClient _client; late final EVMChainClient _client;
int gasPrice = 0;
int? gasBaseFee = 0;
int estimatedGasUnits = 0;
Timer? _updateFeesTimer;
int? _gasPrice;
int? _estimatedGas;
bool _isTransactionUpdating; bool _isTransactionUpdating;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
@ -145,7 +154,11 @@ abstract class EVMChainWalletBase
Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath); Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath);
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password); EVMChainTransactionHistory setUpTransactionHistory(
WalletInfo walletInfo,
String password,
EncryptionFileUtils encryptionFileUtils,
);
//! Common Methods across child classes //! Common Methods across child classes
@ -173,12 +186,70 @@ abstract class EVMChainWalletBase
@override @override
int calculateEstimatedFee(TransactionPriority priority, int? amount) { int calculateEstimatedFee(TransactionPriority priority, int? amount) {
{
try {
if (priority is EVMChainTransactionPriority) {
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
int maxFeePerGas;
if (gasBaseFee != null) {
// MaxFeePerGas with EIP1559;
maxFeePerGas = gasBaseFee! + priorityFee;
} else {
// MaxFeePerGas with gasPrice;
maxFeePerGas = gasPrice;
debugPrint('MaxFeePerGas with gasPrice: $maxFeePerGas');
}
final totalGasFee = estimatedGasUnits * maxFeePerGas;
return totalGasFee;
}
return 0;
} catch (e) {
return 0;
}
}
}
/// Allows more customization to the fetch estimatedFees flow.
///
/// We are able to pass in:
/// - The exact amount the user wants to send,
/// - The addressHex for the receiving wallet,
/// - A contract address which would be essential in determining if to calcualate the estimate for ERC20 or native ETH
Future<int> calculateActualEstimatedFeeForCreateTransaction({
required amount,
required String? contractAddress,
required String receivingAddressHex,
required TransactionPriority priority,
}) async {
try { try {
if (priority is EVMChainTransactionPriority) { if (priority is EVMChainTransactionPriority) {
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
}
int maxFeePerGas;
if (gasBaseFee != null) {
// MaxFeePerGas with EIP1559;
maxFeePerGas = gasBaseFee! + priorityFee;
} else {
// MaxFeePerGas with gasPrice
maxFeePerGas = gasPrice;
}
final estimatedGas = await _client.getEstimatedGas(
contractAddress: contractAddress,
senderAddress: _evmChainPrivateKey.address,
value: EtherAmount.fromBigInt(EtherUnit.wei, amount!),
gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice),
toAddress: EthereumAddress.fromHex(receivingAddressHex),
// maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
// maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
);
final totalGasFee = estimatedGas * maxFeePerGas;
return totalGasFee;
}
return 0; return 0;
} catch (e) { } catch (e) {
return 0; return 0;
@ -194,6 +265,7 @@ abstract class EVMChainWalletBase
void close() { void close() {
_client.stop(); _client.stop();
_transactionsUpdateTimer?.cancel(); _transactionsUpdateTimer?.cancel();
_updateFeesTimer?.cancel();
} }
@action @action
@ -225,13 +297,12 @@ abstract class EVMChainWalletBase
syncStatus = AttemptingSyncStatus(); syncStatus = AttemptingSyncStatus();
await _updateBalance(); await _updateBalance();
await _updateTransactions(); await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic( await _updateEstimatedGasFeeParams();
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10), _updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
(timer) async => _estimatedGas = await _client.getEstimatedGas()); await _updateEstimatedGasFeeParams();
});
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} catch (e) { } catch (e) {
@ -239,6 +310,19 @@ abstract class EVMChainWalletBase
} }
} }
Future<void> _updateEstimatedGasFeeParams() async {
gasBaseFee = await _client.getGasBaseFee();
gasPrice = await _client.getGasUnitPrice();
estimatedGasUnits = await _client.getEstimatedGas(
senderAddress: _evmChainPrivateKey.address,
toAddress: _evmChainPrivateKey.address,
gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice),
value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.one),
);
}
@override @override
Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
final _credentials = credentials as EVMChainTransactionCredentials; final _credentials = credentials as EVMChainTransactionCredentials;
@ -258,8 +342,17 @@ abstract class EVMChainWalletBase
final erc20Balance = balance[transactionCurrency]!; final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero; BigInt totalAmount = BigInt.zero;
BigInt estimatedFeesForTransaction = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent); num amountToEVMChainMultiplier = pow(10, exponent);
String? contractAddress;
String toAddress = _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address;
if (transactionCurrency is Erc20Token) {
contractAddress = transactionCurrency.contractAddress;
}
// so far this can not be made with Ethereum as Ethereum does not support multiple recipients // so far this can not be made with Ethereum as Ethereum does not support multiple recipients
if (hasMultiDestination) { if (hasMultiDestination) {
@ -271,35 +364,48 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
final estimateFees = await calculateActualEstimatedFeeForCreateTransaction(
amount: totalAmount,
receivingAddressHex: toAddress,
priority: _credentials.priority!,
contractAddress: contractAddress,
);
estimatedFeesForTransaction = BigInt.from(estimateFees);
if (erc20Balance.balance < totalAmount) { if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency); throw EVMChainTransactionCreationException(transactionCurrency);
} }
} else { } else {
final output = outputs.first; final output = outputs.first;
// since the fees are taken from Ethereum if (!output.sendAll) {
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = erc20Balance.balance;
} else {
final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
if (estimatedFee > erc20Balance.balance) {
throw EVMChainTransactionFeesException();
}
allAmount = erc20Balance.balance - estimatedFee;
}
if (output.sendAll) {
totalAmount = allAmount;
} else {
final totalOriginalAmount = final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0); EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
} }
if (output.sendAll && transactionCurrency is Erc20Token) {
totalAmount = erc20Balance.balance;
}
final estimateFees = await calculateActualEstimatedFeeForCreateTransaction(
amount: totalAmount,
receivingAddressHex: toAddress,
priority: _credentials.priority!,
contractAddress: contractAddress,
);
estimatedFeesForTransaction = BigInt.from(estimateFees);
if (output.sendAll && transactionCurrency is! Erc20Token) {
totalAmount = (erc20Balance.balance - estimatedFeesForTransaction);
if (estimatedFeesForTransaction > erc20Balance.balance) {
throw EVMChainTransactionFeesException();
}
}
if (erc20Balance.balance < totalAmount) { if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency); throw EVMChainTransactionCreationException(transactionCurrency);
} }
@ -312,11 +418,9 @@ abstract class EVMChainWalletBase
final pendingEVMChainTransaction = await _client.signTransaction( final pendingEVMChainTransaction = await _client.signTransaction(
privateKey: _evmChainPrivateKey, privateKey: _evmChainPrivateKey,
toAddress: _credentials.outputs.first.isParsedAddress toAddress: toAddress,
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount, amount: totalAmount,
gas: _estimatedGas!, gas: estimatedFeesForTransaction,
priority: _credentials.priority!, priority: _credentials.priority!,
currency: transactionCurrency, currency: transactionCurrency,
exponent: exponent, exponent: exponent,
@ -400,7 +504,7 @@ abstract class EVMChainWalletBase
} }
final methodSignature = final methodSignature =
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null; transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
return methodSignatureToType[methodSignature]; return methodSignatureToType[methodSignature];
} }
@ -415,9 +519,14 @@ abstract class EVMChainWalletBase
@override @override
Future<void> save() async { Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
await walletAddresses.updateAddressesInBox(); await walletAddresses.updateAddressesInBox();
final path = await makePath(); final path = await makePath();
await write(path: path, password: _password, data: toJSON()); await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save(); await transactionHistory.save();
} }
@ -429,7 +538,8 @@ abstract class EVMChainWalletBase
? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey) ? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey)
: null; : null;
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); @override
WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey);
String toJSON() => json.encode({ String toJSON() => json.encode({
'mnemonic': _mnemonic, 'mnemonic': _mnemonic,
@ -483,6 +593,7 @@ abstract class EVMChainWalletBase
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>)); return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
} }
@override
Future<void>? updateBalance() async => await _updateBalance(); Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList(); List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
@ -585,8 +696,24 @@ abstract class EVMChainWalletBase
} }
@override @override
Future<String> signMessage(String message, {String? address}) async => Future<String> signMessage(String message, {String? address}) async {
bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message))); return bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
if (address == null) {
return false;
}
final recoveredAddress = EthSigUtil.recoverPersonalSignature(
message: ascii.encode(message),
signature: signature,
);
return recoveredAddress.toUpperCase() == address.toUpperCase();
}
Web3Client? getWeb3Client() => _client.getWeb3Client(); Web3Client? getWeb3Client() => _client.getWeb3Client();
@override
String get password => _password;
} }

View file

@ -3,8 +3,8 @@ import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
class EVMChainNewWalletCredentials extends WalletCredentials { class EVMChainNewWalletCredentials extends WalletCredentials {
EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo}) EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password})
: super(name: name, walletInfo: walletInfo); : super(name: name, walletInfo: walletInfo, password: password);
} }
class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials {

View file

@ -15,9 +15,10 @@ abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletSer
EVMChainRestoreWalletFromSeedCredentials, EVMChainRestoreWalletFromSeedCredentials,
EVMChainRestoreWalletFromPrivateKey, EVMChainRestoreWalletFromPrivateKey,
EVMChainRestoreWalletFromHardware> { EVMChainRestoreWalletFromHardware> {
EVMChainWalletService(this.walletInfoSource); EVMChainWalletService(this.walletInfoSource, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final bool isDirect;
@override @override
WalletType getType(); WalletType getType();

View file

@ -1,39 +0,0 @@
import 'dart:io';
import 'package:cw_core/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
Future<void> write(
{required String path,
required String password,
required String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: data);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<void> writeData(
{required String path,
required String password,
required String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: data);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<String> read({required String path, required String password}) async {
final file = File(path);
if (!file.existsSync()) {
file.createSync();
}
final encrypted = file.readAsStringSync();
return decode(password: password, data: encrypted);
}

View file

@ -13,6 +13,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
web3dart: ^2.7.1 web3dart: ^2.7.1
eth_sig_util: ^0.0.9
erc20: ^1.0.1
bip39: ^1.0.6 bip39: ^1.0.6
bip32: ^2.0.0 bip32: ^2.0.0
hex: ^0.2.0 hex: ^0.2.0
@ -20,6 +22,7 @@ dependencies:
hive: ^2.2.3 hive: ^2.2.3
collection: ^1.17.1 collection: ^1.17.1
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
mobx: ^2.0.7+4
cw_core: cw_core:
path: ../cw_core path: ../cw_core
ledger_flutter: ^1.0.1 ledger_flutter: ^1.0.1
@ -35,7 +38,7 @@ dependency_overrides:
ledger_flutter: ledger_flutter:
git: git:
url: https://github.com/cake-tech/ledger-flutter.git url: https://github.com/cake-tech/ledger-flutter.git
ref: cake ref: cake-v3
watcher: ^1.1.0 watcher: ^1.1.0
dev_dependencies: dev_dependencies:

View file

@ -5,7 +5,7 @@ buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.7.10'
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
@ -17,7 +17,7 @@ buildscript {
rootProject.allprojects { rootProject.allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }

View file

@ -10,10 +10,8 @@ import 'package:cw_haven/haven_transaction_info.dart';
import 'package:cw_haven/haven_wallet_addresses.dart'; import 'package:cw_haven/haven_wallet_addresses.dart';
import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_core/monero_wallet_utils.dart';
import 'package:cw_haven/api/structs/pending_transaction.dart'; import 'package:cw_haven/api/structs/pending_transaction.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history;
//import 'package:cw_haven/wallet.dart';
import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/wallet.dart' as haven_wallet;
import 'package:cw_haven/api/transaction_history.dart' as transaction_history; import 'package:cw_haven/api/transaction_history.dart' as transaction_history;
import 'package:cw_haven/api/monero_output.dart'; import 'package:cw_haven/api/monero_output.dart';
@ -38,9 +36,10 @@ class HavenWallet = HavenWalletBase with _$HavenWallet;
abstract class HavenWalletBase abstract class HavenWalletBase
extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store { extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store {
HavenWalletBase({required WalletInfo walletInfo}) HavenWalletBase({required WalletInfo walletInfo, String? password})
: balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)),
_isTransactionUpdating = false, _isTransactionUpdating = false,
_password = password ?? '',
_hasSyncAfterStartup = false, _hasSyncAfterStartup = false,
walletAddresses = HavenWalletAddresses(walletInfo), walletAddresses = HavenWalletAddresses(walletInfo),
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
@ -56,6 +55,7 @@ abstract class HavenWalletBase
} }
static const int _autoSaveInterval = 30; static const int _autoSaveInterval = 30;
final String _password;
@override @override
HavenWalletAddresses walletAddresses; HavenWalletAddresses walletAddresses;
@ -111,7 +111,7 @@ abstract class HavenWalletBase
_onAccountChangeReaction?.reaction.dispose(); _onAccountChangeReaction?.reaction.dispose();
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
} }
@override @override
Future<void> connectToNode({required Node node}) async { Future<void> connectToNode({required Node node}) async {
try { try {
@ -121,7 +121,8 @@ abstract class HavenWalletBase
login: node.login, login: node.login,
password: node.password, password: node.password,
useSSL: node.useSSL ?? false, useSSL: node.useSSL ?? false,
isLightWallet: false, // FIXME: hardcoded value isLightWallet: false,
// FIXME: hardcoded value
socksProxyAddress: node.socksProxyAddress); socksProxyAddress: node.socksProxyAddress);
haven_wallet.setTrustedDaemon(node.trusted); haven_wallet.setTrustedDaemon(node.trusted);
@ -414,4 +415,15 @@ abstract class HavenWalletBase
print(e.toString()); print(e.toString());
} }
} }
@override
String get password => _password;
@override
Future<String> signMessage(String message, {String? address = null}) =>
throw UnimplementedError();
@override
Future<bool> verifyMessage(String message, String signature, {String? address = null}) =>
throw UnimplementedError();
} }

View file

@ -113,6 +113,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.4.3" version: "8.4.3"
cake_backup:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38"
url: "https://github.com/cake-tech/cake_backup.git"
source: git
version: "1.0.0+1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -169,6 +178,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
cryptography:
dependency: transitive
description:
name: cryptography
sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35
url: "https://pub.dev"
source: hosted
version: "2.5.0"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev"
source: hosted
version: "1.0.6"
cw_core: cw_core:
dependency: "direct main" dependency: "direct main"
description: description:
@ -254,10 +279,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: glob name: glob
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -514,14 +539,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -647,6 +664,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -707,10 +732,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0+3" version: "1.0.4"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View file

@ -18,6 +18,9 @@ migration:
- platform: macos - platform: macos
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: linux
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
# User provided section # User provided section

1
cw_monero/example/linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

View file

@ -0,0 +1,138 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "cw_monero_example")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.cakewallet.cw_monero")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <cw_monero/cw_monero_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) cw_monero_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "CwMoneroPlugin");
cw_monero_plugin_register_with_registrar(cw_monero_registrar);
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
cw_monero
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View file

@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "cw_monero_example");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "cw_monero_example");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

View file

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View file

@ -34,7 +34,6 @@ List<monero.SubaddressAccountRow> getAllAccount() {
// final size = monero.Wallet_numSubaddressAccounts(wptr!); // final size = monero.Wallet_numSubaddressAccounts(wptr!);
refreshAccounts(); refreshAccounts();
int size = monero.SubaddressAccount_getAll_size(subaddressAccount!); int size = monero.SubaddressAccount_getAll_size(subaddressAccount!);
print("size: $size");
if (size == 0) { if (size == 0) {
monero.Wallet_addSubaddressAccount(wptr!); monero.Wallet_addSubaddressAccount(wptr!);
return getAllAccount(); return getAllAccount();

View file

@ -1,5 +0,0 @@
class ConnectionToNodeException implements Exception {
ConnectionToNodeException({required this.message});
final String message;
}

View file

@ -1,12 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
class AccountRow extends Struct {
@Int64()
external int id;
external Pointer<Utf8> label;
String getLabel() => label.toDartString();
int getId() => id;
}

View file

@ -1,73 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
class CoinsInfoRow extends Struct {
@Int64()
external int blockHeight;
external Pointer<Utf8> hash;
@Uint64()
external int internalOutputIndex;
@Uint64()
external int globalOutputIndex;
@Int8()
external int spent;
@Int8()
external int frozen;
@Uint64()
external int spentHeight;
@Uint64()
external int amount;
@Int8()
external int rct;
@Int8()
external int keyImageKnown;
@Uint64()
external int pkIndex;
@Uint32()
external int subaddrIndex;
@Uint32()
external int subaddrAccount;
external Pointer<Utf8> address;
external Pointer<Utf8> addressLabel;
external Pointer<Utf8> keyImage;
@Uint64()
external int unlockTime;
@Int8()
external int unlocked;
external Pointer<Utf8> pubKey;
@Int8()
external int coinbase;
external Pointer<Utf8> description;
String getHash() => hash.toDartString();
String getAddress() => address.toDartString();
String getAddressLabel() => addressLabel.toDartString();
String getKeyImage() => keyImage.toDartString();
String getPubKey() => pubKey.toDartString();
String getDescription() => description.toDartString();
}

View file

@ -1,15 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
class SubaddressRow extends Struct {
@Int64()
external int id;
external Pointer<Utf8> address;
external Pointer<Utf8> label;
String getLabel() => label.toDartString();
String getAddress() => address.toDartString();
int getId() => id;
}

View file

@ -1,41 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
class TransactionInfoRow extends Struct {
@Uint64()
external int amount;
@Uint64()
external int fee;
@Uint64()
external int blockHeight;
@Uint64()
external int confirmations;
@Uint32()
external int subaddrAccount;
@Int8()
external int direction;
@Int8()
external int isPending;
@Uint32()
external int subaddrIndex;
external Pointer<Utf8> hash;
external Pointer<Utf8> paymentId;
@Int64()
external int datetime;
int getDatetime() => datetime;
int getAmount() => amount >= 0 ? amount : amount * -1;
bool getIsPending() => isPending != 0;
String getHash() => hash.toDartString();
String getPaymentId() => paymentId.toDartString();
}

View file

@ -1,8 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
class Utf8Box extends Struct {
external Pointer<Utf8> value;
String getValue() => value.toDartString();
}

View file

@ -42,12 +42,16 @@ class Subaddress {
List<Subaddress> getAllSubaddresses() { List<Subaddress> getAllSubaddresses() {
final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
return List.generate(size, (index) { final list = List.generate(size, (index) {
return Subaddress( return Subaddress(
accountIndex: subaddress!.accountIndex, accountIndex: subaddress!.accountIndex,
addressIndex: index, addressIndex: index,
); );
}).reversed.toList(); }).reversed.toList();
if (list.length == 0) {
list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0));
}
return list;
} }
void addSubaddressSync({required int accountIndex, required String label}) { void addSubaddressSync({required int accountIndex, required String label}) {

View file

@ -1,4 +1,3 @@
import 'dart:ffi'; import 'dart:ffi';
import 'dart:isolate'; import 'dart:isolate';
@ -110,7 +109,10 @@ Future<PendingTransactionDescription> createTransactionSync(
})(); })();
if (error != null) { if (error != null) {
final message = error; String message = error;
if (message.contains("RPC error")) {
message = "Invalid node response, please try again or switch node\n\ntrace: $message";
}
throw CreationTransactionException(message: message); throw CreationTransactionException(message: message);
} }
@ -285,7 +287,7 @@ class Transaction {
}; };
} }
// S finalubAddress? subAddress; // final SubAddress? subAddress;
// List<Transfer> transfers = []; // List<Transfer> transfers = [];
// final int txIndex; // final int txIndex;
final monero.TransactionInfo txInfo; final monero.TransactionInfo txInfo;
@ -321,4 +323,4 @@ class Transaction {
required this.key, required this.key,
required this.txInfo required this.txInfo
}); });
} }

View file

@ -39,16 +39,30 @@ String getSeed() {
if (polyseed != "") { if (polyseed != "") {
return polyseed; return polyseed;
} }
final legacy = monero.Wallet_seed(wptr!, seedOffset: ''); final legacy = getSeedLegacy("English");
return legacy; return legacy;
} }
String getSeedLegacy(String? language) { String getSeedLegacy(String? language) {
var legacy = monero.Wallet_seed(wptr!, seedOffset: ''); var legacy = monero.Wallet_seed(wptr!, seedOffset: '');
switch (language) {
case "Chinese (Traditional)": language = "Chinese (simplified)"; break;
case "Chinese (Simplified)": language = "Chinese (simplified)"; break;
case "Korean": language = "English"; break;
case "Czech": language = "English"; break;
case "Japanese": language = "English"; break;
}
if (monero.Wallet_status(wptr!) != 0) { if (monero.Wallet_status(wptr!) != 0) {
monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
legacy = monero.Wallet_seed(wptr!, seedOffset: ''); legacy = monero.Wallet_seed(wptr!, seedOffset: '');
} }
if (monero.Wallet_status(wptr!) != 0) {
final err = monero.Wallet_errorString(wptr!);
if (legacy.isNotEmpty) {
return "$err\n\n$legacy";
}
return err;
}
return legacy; return legacy;
} }
@ -121,9 +135,23 @@ void setRecoveringFromSeed({required bool isRecovery}) =>
monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery); monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
final storeMutex = Mutex(); final storeMutex = Mutex();
int lastStorePointer = 0;
int lastStoreHeight = 0;
void storeSync() async { void storeSync() async {
await storeMutex.acquire();
final addr = wptr!.address; final addr = wptr!.address;
final synchronized = await Isolate.run(() {
return monero.Wallet_synchronized(Pointer.fromAddress(addr));
});
if (lastStorePointer == wptr!.address &&
lastStoreHeight + 5000 > monero.Wallet_blockChainHeight(wptr!) &&
!synchronized) {
return;
}
lastStorePointer = wptr!.address;
lastStoreHeight = monero.Wallet_blockChainHeight(wptr!);
await storeMutex.acquire();
await Isolate.run(() { await Isolate.run(() {
monero.Wallet_store(Pointer.fromAddress(addr)); monero.Wallet_store(Pointer.fromAddress(addr));
}); });
@ -288,3 +316,7 @@ Future<bool> trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!);
String signMessage(String message, {String address = ""}) { String signMessage(String message, {String address = ""}) {
return monero.Wallet_signMessage(wptr!, message: message, address: address); return monero.Wallet_signMessage(wptr!, message: message, address: address);
} }
bool verifyMessage(String message, String address, String signature) {
return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
}

View file

@ -1,4 +1,5 @@
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/account_list.dart';
@ -6,10 +7,43 @@ import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart';
import 'package:cw_monero/api/transaction_history.dart';
import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/api/transaction_history.dart';
import 'package:monero/monero.dart' as monero; import 'package:monero/monero.dart' as monero;
class MoneroCException implements Exception {
final String message;
MoneroCException(this.message);
@override
String toString() {
return message;
}
}
void checkIfMoneroCIsFine() {
final cppCsCpp = monero.MONERO_checksum_wallet2_api_c_cpp();
final cppCsH = monero.MONERO_checksum_wallet2_api_c_h();
final cppCsExp = monero.MONERO_checksum_wallet2_api_c_exp();
final dartCsCpp = monero.wallet2_api_c_cpp_sha256;
final dartCsH = monero.wallet2_api_c_h_sha256;
final dartCsExp = monero.wallet2_api_c_exp_sha256;
if (cppCsCpp != dartCsCpp) {
throw MoneroCException("monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'");
}
if (cppCsH != dartCsH) {
throw MoneroCException("monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'");
}
if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) {
throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'");
}
}
monero.WalletManager? _wmPtr; monero.WalletManager? _wmPtr;
final monero.WalletManager wmPtr = Pointer.fromAddress((() { final monero.WalletManager wmPtr = Pointer.fromAddress((() {
try { try {
@ -21,6 +55,7 @@ final monero.WalletManager wmPtr = Pointer.fromAddress((() {
print("ptr: $_wmPtr"); print("ptr: $_wmPtr");
} catch (e) { } catch (e) {
print(e); print(e);
rethrow;
} }
return _wmPtr!.address; return _wmPtr!.address;
})()); })());
@ -31,13 +66,14 @@ void createWalletSync(
required String language, required String language,
int nettype = 0}) { int nettype = 0}) {
txhistory = null; txhistory = null;
wptr = monero.WalletManager_createWallet(wmPtr, final newWptr = monero.WalletManager_createWallet(wmPtr,
path: path, password: password, language: language, networkType: 0); path: path, password: password, language: language, networkType: 0);
final status = monero.Wallet_status(wptr!); final status = monero.Wallet_status(newWptr);
if (status != 0) { if (status != 0) {
throw WalletCreationException(message: monero.Wallet_errorString(wptr!)); throw WalletCreationException(message: monero.Wallet_errorString(newWptr));
} }
wptr = newWptr;
monero.Wallet_store(wptr!, path: path); monero.Wallet_store(wptr!, path: path);
openedWalletsByPath[path] = wptr!; openedWalletsByPath[path] = wptr!;
@ -56,7 +92,7 @@ void restoreWalletFromSeedSync(
int nettype = 0, int nettype = 0,
int restoreHeight = 0}) { int restoreHeight = 0}) {
txhistory = null; txhistory = null;
wptr = monero.WalletManager_recoveryWallet( final newWptr = monero.WalletManager_recoveryWallet(
wmPtr, wmPtr,
path: path, path: path,
password: password, password: password,
@ -66,12 +102,13 @@ void restoreWalletFromSeedSync(
networkType: 0, networkType: 0,
); );
final status = monero.Wallet_status(wptr!); final status = monero.Wallet_status(newWptr);
if (status != 0) { if (status != 0) {
final error = monero.Wallet_errorString(wptr!); final error = monero.Wallet_errorString(newWptr);
throw WalletRestoreFromSeedException(message: error); throw WalletRestoreFromSeedException(message: error);
} }
wptr = newWptr;
openedWalletsByPath[path] = wptr!; openedWalletsByPath[path] = wptr!;
} }
@ -86,7 +123,16 @@ void restoreWalletFromKeysSync(
int nettype = 0, int nettype = 0,
int restoreHeight = 0}) { int restoreHeight = 0}) {
txhistory = null; txhistory = null;
wptr = monero.WalletManager_createWalletFromKeys( final newWptr = spendKey != ""
? monero.WalletManager_createDeterministicWalletFromSpendKey(
wmPtr,
path: path,
password: password,
language: language,
spendKeyString: spendKey,
newWallet: true, // TODO(mrcyjanek): safe to remove
restoreHeight: restoreHeight)
: monero.WalletManager_createWalletFromKeys(
wmPtr, wmPtr,
path: path, path: path,
password: password, password: password,
@ -97,12 +143,14 @@ void restoreWalletFromKeysSync(
nettype: 0, nettype: 0,
); );
final status = monero.Wallet_status(wptr!); final status = monero.Wallet_status(newWptr);
if (status != 0) { if (status != 0) {
throw WalletRestoreFromKeysException( throw WalletRestoreFromKeysException(
message: monero.Wallet_errorString(wptr!)); message: monero.Wallet_errorString(newWptr));
} }
wptr = newWptr;
openedWalletsByPath[path] = wptr!; openedWalletsByPath[path] = wptr!;
} }
@ -127,7 +175,7 @@ void restoreWalletFromSpendKeySync(
// ); // );
txhistory = null; txhistory = null;
wptr = monero.WalletManager_createDeterministicWalletFromSpendKey( final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey(
wmPtr, wmPtr,
path: path, path: path,
password: password, password: password,
@ -137,14 +185,16 @@ void restoreWalletFromSpendKeySync(
restoreHeight: restoreHeight, restoreHeight: restoreHeight,
); );
final status = monero.Wallet_status(wptr!); final status = monero.Wallet_status(newWptr);
if (status != 0) { if (status != 0) {
final err = monero.Wallet_errorString(wptr!); final err = monero.Wallet_errorString(newWptr);
print("err: $err"); print("err: $err");
throw WalletRestoreFromKeysException(message: err); throw WalletRestoreFromKeysException(message: err);
} }
wptr = newWptr;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
storeSync(); storeSync();
@ -194,28 +244,25 @@ void loadWallet(
wptr = openedWalletsByPath[path]!; wptr = openedWalletsByPath[path]!;
return; return;
} }
try { if (wptr == null || path != _lastOpenedWallet) {
if (wptr == null || path != _lastOpenedWallet) { if (wptr != null) {
if (wptr != null) { final addr = wptr!.address;
final addr = wptr!.address; Isolate.run(() {
Isolate.run(() { monero.Wallet_store(Pointer.fromAddress(addr));
monero.Wallet_store(Pointer.fromAddress(addr)); });
});
}
txhistory = null;
wptr = monero.WalletManager_openWallet(wmPtr,
path: path, password: password);
openedWalletsByPath[path] = wptr!;
_lastOpenedWallet = path;
} }
} catch (e) { txhistory = null;
print(e); final newWptr = monero.WalletManager_openWallet(wmPtr,
} path: path, password: password);
final status = monero.Wallet_status(wptr!); _lastOpenedWallet = path;
if (status != 0) { final status = monero.Wallet_status(newWptr);
final err = monero.Wallet_errorString(wptr!); if (status != 0) {
print(err); final err = monero.Wallet_errorString(newWptr);
throw WalletOpeningException(message: err); print(err);
throw WalletOpeningException(message: err);
}
wptr = newWptr;
openedWalletsByPath[path] = wptr!;
} }
} }

View file

@ -1,8 +0,0 @@
import 'cw_monero_platform_interface.dart';
class CwMonero {
Future<String?> getPlatformVersion() {
return CwMoneroPlatform.instance.getPlatformVersion();
}
}

View file

@ -1,17 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'cw_monero_platform_interface.dart';
/// An implementation of [CwMoneroPlatform] that uses method channels.
class MethodChannelCwMonero extends CwMoneroPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('cw_monero');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}

View file

@ -1,29 +0,0 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'cw_monero_method_channel.dart';
abstract class CwMoneroPlatform extends PlatformInterface {
/// Constructs a CwMoneroPlatform.
CwMoneroPlatform() : super(token: _token);
static final Object _token = Object();
static CwMoneroPlatform _instance = MethodChannelCwMonero();
/// The default instance of [CwMoneroPlatform] to use.
///
/// Defaults to [MethodChannelCwMonero].
static CwMoneroPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [CwMoneroPlatform] when
/// they register themselves.
static set instance(CwMoneroPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

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