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

View file

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

View file

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

2
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

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
useSSL: true
isDefault: true
isDefault: true
-
uri: litecoin.stackwallet.com:20063
useSSL: true
-
uri: electrum-ltc.bysh.me:50002
useSSL: true
-
uri: lightweight.fiatfaucet.com:50002
useSSL: true
-
uri: electrum.ltc.xurious.com:50002
useSSL: true
-
uri: backup.electrum-ltc.org:443
useSSL: true

View file

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

View file

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

View file

@ -1,3 +1,3 @@
Monero enhancements
Improvements for Tron and Nano wallets
Scan and verify messages
Synchronization enhancements
Bug fixes

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

@ -0,0 +1,176 @@
# Building CakeWallet for Linux
## Requirements and Setup
The following are the system requirements to build CakeWallet for your Linux device.
```
Ubuntu >= 16.04
Flutter 3.10.x
```
## Building CakeWallet on Linux
These steps will help you configure and execute a build of CakeWallet from its source code.
### 1. Installing Package Dependencies
CakeWallet requires some packages to be install on your build system. You may easily install them on your build system with the following command:
`$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool`
> [!WARNING]
>
> ### Check gcc version
>
> It is needed to use gcc 10 or 9 to successfully link dependencies with flutter.\
> To check what gcc version you are using:
>
> ```bash
> $ gcc --version
> $ g++ --version
> ```
>
> If you are using gcc version newer than 10, then you need to downgrade to version 10.4.0:
>
> ```bash
> $ sudo apt install gcc-10 g++-10
> $ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
> $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
> ```
> [!NOTE]
>
> Alternatively, you can use the [nix-shell](https://nixos.org/) with the `gcc10.nix` file\
> present on `scripts/linux` like so:
> ```bash
> $ nix-shell gcc10.nix
> ```
> This will get you in a nix environment with all the required dependencies that you can use to build the software from,\
> and it works in any linux distro.
### 2. Installing Flutter
Need to install flutter. For this please check section [How to install flutter on Linux](https://docs.flutter.dev/get-started/install/linux).
### 3. Verify Installations
Verify that the Flutter have been correctly installed on your system with the following command:
`$ flutter doctor`
The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
```
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8)
```
### 4. Acquiring the CakeWallet Source Code
Download CakeWallet source code
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch linux/password-direct-input`
Proceed into the source code before proceeding with the next steps:
`$ cd cake_wallet/scripts/linux/`
To configure some project properties run:
`$ ./cakewallet.sh`
Build the Monero libraries and their dependencies:
`$ ./build_all.sh`
Now the dependencies need to be copied into the CakeWallet project with this command:
`$ ./setup.sh`
It is now time to change back to the base directory of the CakeWallet source code:
`$ cd ../../`
Install Flutter package dependencies with this command:
`$ flutter pub get`
> #### If you will get an error like:
>
> ```
> The plugin `cw_shared_external` requires your app to be migrated to the Android embedding v2. Follow the steps on the migration doc above and re-run
> this command.
> ```
>
> Then need to config Android project settings. For this open `scripts/android` (`$ cd scripts/android`) directory and run followed commands:
>
> ```
> $ source ./app_env.sh cakewallet
> $ ./app_config.sh
> $ cd ../..
> ```
>
> Then re-configure Linux project again. For this open `scripts/linux` (`$cd scripts/linux`) directory and run:
> `$ ./cakewallet.sh`
> and back to project root directory:
> `$ cd ../..`
> and fetch dependecies again
> `$ flutter pub get`
Your CakeWallet binary will be built with some specific keys for iterate with 3rd party services. You may generate these secret keys placeholders with the following command:
`$ flutter packages pub run tool/generate_new_secrets.dart`
We will generate mobx models for the project.
`$ ./model_generator.sh`
Then we need to generate localization files.
`$ flutter packages pub run tool/generate_localization.dart`
### 5. Build!
`$ flutter build linux --release`
Path to executable file will be:
`build/linux/x64/release/bundle/cake_wallet`
> ### Troubleshooting
>
> If you got an error while building the application with `$ flutter build linux --release` command, add `-v` argument to the command (`$ flutter build linux -v --release`) to get details.\
> If you got in flutter build logs: undefined reference to `hid_free_enumeration`, or another error with undefined reference to `hid_*`, then rebuild monero lib without hidapi lib. Check does exists `libhidapi-dev` in your scope and remove it from your scope for build without it.
# Flatpak
For package the built application into flatpak you need fistly to install `flatpak` and `flatpak-builder`:
`$ sudo apt install flatpak flatpak-builder`
Then need to [add flathub](https://flatpak.org/setup/Ubuntu) (or just `$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`). Then need to install freedesktop runtime and sdk:
`$ flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08`
To build with using of `flatpak-build` directory run next:
`$ flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml`
And then export bundle:
`$ flatpak build-export export flatpak-build`
`$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet`
Result file: `cake_wallet.flatpak` should be generated in current directory.
For install generated flatpak file use:
`$ flatpak --user install cake_wallet.flatpak`
For run the installed application run:
`$ flatpak run com.cakewallet.CakeWallet`
Copyright (c) 2023 Cake Technologies LLC.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import 'dart:isolate';
import 'dart:math';
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:collection/collection.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_wallet_addresses.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/script_hash.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_priority.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_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart';
import 'package:sp_scanner/sp_scanner.dart';
@ -54,12 +52,13 @@ const int TWEAKS_COUNT = 25;
abstract class ElectrumWalletBase
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
with Store {
with Store, WalletKeysFile {
ElectrumWalletBase({
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required this.networkType,
required this.network,
required this.encryptionFileUtils,
String? xpub,
String? mnemonic,
Uint8List? seedBytes,
@ -70,7 +69,7 @@ abstract class ElectrumWalletBase
CryptoCurrency? currency,
this.alwaysScan,
}) : accountHD =
getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo),
getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_feeRates = <int>[],
@ -89,23 +88,22 @@ abstract class ElectrumWalletBase
}
: {}),
this.unspentCoinsInfo = unspentCoinsInfo,
this.network = _getNetwork(networkType, currency),
this.isTestnet = networkType == bitcoin.testnet,
this.isTestnet = !network.isMainnet,
this._mnemonic = mnemonic,
super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
transactionHistory = ElectrumTransactionHistory(
walletInfo: walletInfo,
password: password,
encryptionFileUtils: encryptionFileUtils,
);
reaction((_) => syncStatus, _syncStatusReaction);
}
static bitcoin.HDWallet getAccountHDWallet(
CryptoCurrency? currency,
bitcoin.NetworkType networkType,
Uint8List? seedBytes,
String? xpub,
DerivationInfo? derivationInfo) {
static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) {
if (seedBytes == null && xpub == null) {
throw Exception(
"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) {
return currency == CryptoCurrency.bch
? bitcoinCashHDWallet(seedBytes)
: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
.derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path));
: Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
as Bip32Slip10Secp256k1;
}
return bitcoin.HDWallet.fromBase58(xpub!);
return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
}
static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) =>
bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'");
static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10;
bool? alwaysScan;
final bitcoin.HDWallet accountHD;
final Bip32Slip10Secp256k1 accountHD;
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;
@override
@ -164,16 +166,22 @@ abstract class ElectrumWalletBase
.map((addr) => scriptHash(addr.address, network: network))
.toList();
String get xpub => accountHD.base58!;
String get xpub => accountHD.publicKey.toExtended;
@override
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;
@override
bool? isTestnet;
bool isTestnet;
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
@ -185,24 +193,21 @@ abstract class ElectrumWalletBase
bool _isTryingToConnect = false;
@action
Future<void> setSilentPaymentsScanning(bool active, bool usingElectrs) async {
Future<void> setSilentPaymentsScanning(bool active) async {
silentPaymentsScanningActive = active;
if (active) {
syncStatus = AttemptingSyncStatus();
syncStatus = StartingScanSyncStatus();
final tip = await getUpdatedChainTip();
if (tip == walletInfo.restoreHeight) {
syncStatus = SyncedTipSyncStatus(tip);
return;
}
if (tip > walletInfo.restoreHeight) {
_setListeners(
walletInfo.restoreHeight,
chainTipParam: _currentChainTip,
usingElectrs: usingElectrs,
);
_setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
}
} else {
alwaysScan = false;
@ -212,10 +217,7 @@ abstract class ElectrumWalletBase
if (electrumClient.isConnected) {
syncStatus = SyncedSyncStatus();
} else {
if (electrumClient.uri != null) {
await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL);
startSync();
}
syncStatus = NotConnectedSyncStatus();
}
}
}
@ -240,8 +242,11 @@ abstract class ElectrumWalletBase
}
@override
BitcoinWalletKeys get keys =>
BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!);
BitcoinWalletKeys get keys => BitcoinWalletKeys(
wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer),
privateKey: hd.privateKey.toHex(),
publicKey: hd.publicKey.toHex(),
);
String _password;
List<BitcoinUnspent> unspentCoins;
@ -256,8 +261,10 @@ abstract class ElectrumWalletBase
Future<Isolate>? _isolate;
void Function(FlutterErrorDetails)? _onError;
Timer? _reconnectTimer;
Timer? _autoSaveTimer;
static const int _autoSaveInterval = 30;
Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1;
Future<void> init() async {
await walletAddresses.init();
@ -265,7 +272,7 @@ abstract class ElectrumWalletBase
await save();
_autoSaveTimer =
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save());
}
@action
@ -273,7 +280,7 @@ abstract class ElectrumWalletBase
int height, {
int? chainTipParam,
bool? doSingleScan,
bool? usingElectrs,
bool? usingSupportedNode,
}) async {
final chainTip = chainTipParam ?? await getUpdatedChainTip();
@ -282,7 +289,7 @@ abstract class ElectrumWalletBase
return;
}
syncStatus = AttemptingSyncStatus();
syncStatus = StartingScanSyncStatus();
if (_isolate != null) {
final runningIsolate = await _isolate!;
@ -300,7 +307,9 @@ abstract class ElectrumWalletBase
chainTip: chainTip,
electrumClient: ElectrumClient(),
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,
labelIndexes: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1)
@ -388,7 +397,7 @@ abstract class ElectrumWalletBase
BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)),
)
: silentAddress.B_spend,
hrp: silentAddress.hrp,
network: network,
);
final addressRecord = walletAddresses.silentAddresses
@ -416,8 +425,10 @@ abstract class ElectrumWalletBase
await updateTransactions();
await updateAllUnspents();
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) {
_setListeners(walletInfo.restoreHeight);
@ -436,11 +447,64 @@ abstract class ElectrumWalletBase
final feeRates = await electrumClient.feeRates(network: network);
if (feeRates != [0, 0, 0]) {
_feeRates = feeRates;
} else if (isTestnet) {
_feeRates = [1, 1, 1];
}
}
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
@override
Future<void> connectToNode({required Node node}) async {
@ -502,13 +566,6 @@ abstract class ElectrumWalletBase
final hd =
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) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@ -519,17 +576,31 @@ abstract class ElectrumWalletBase
);
spendsSilentPayment = true;
isSilentPayment = true;
} else {
} else if (!isHardwareWallet) {
privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
}
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
inputPrivKeyInfos.add(ECPrivateInfo(
privkey,
address.type == SegwitAddresType.p2tr,
tweak: !isSilentPayment,
));
String pubKeyHex;
if (privkey != null) {
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(
UtxoWithAddress(
@ -541,7 +612,7 @@ abstract class ElectrumWalletBase
isSilentPayment: isSilentPayment,
),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
publicKey: pubKeyHex,
address: address,
),
),
@ -928,11 +999,29 @@ abstract class ElectrumWalletBase
bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.inputPrivKeyInfos
.firstWhereOrNull((element) => element.privkey.getPublic().toHex() == publicKey);
String error = "Cannot find private key.";
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) {
throw Exception("Cannot find private key");
throw Exception(error);
}
if (utxo.utxo.isP2tr()) {
@ -1073,8 +1162,13 @@ abstract class ElectrumWalletBase
@override
Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@ -1114,10 +1208,9 @@ abstract class ElectrumWalletBase
int? chainTip,
ScanData? scanData,
bool? doSingleScan,
bool? usingElectrs,
}) async {
silentPaymentsScanningActive = true;
_setListeners(height, doSingleScan: doSingleScan, usingElectrs: usingElectrs);
_setListeners(height, doSingleScan: doSingleScan);
}
@override
@ -1126,10 +1219,9 @@ abstract class ElectrumWalletBase
await electrumClient.close();
} catch (_) {}
_autoSaveTimer?.cancel();
_updateFeeRateTimer?.cancel();
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
@action
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
@ -1217,7 +1309,7 @@ abstract class ElectrumWalletBase
await Future.wait(unspents.map((unspent) async {
try {
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.confirmations = tx?.confirmations;
@ -1272,20 +1364,25 @@ abstract class ElectrumWalletBase
}
Future<bool> canReplaceByFee(String hash) async {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
final transactionHex = verboseTransaction['hex'] as String?;
final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
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 (transactionHex == null) {
if (transactionHex == null || transactionHex.isEmpty) {
return false;
}
final original = bitcoin.Transaction.fromHex(transactionHex);
return original.ins
.any((element) => element.sequence != null && element.sequence! < 4294967293);
return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
}
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;
// TODO: time is not always available, and calculating it from height is not always accurate.
// Add settings to choose API provider and use and http server instead of electrum for this.
int? time;
int confirmations = 0;
if (network == BitcoinNetwork.testnet) {
// Testnet public electrum server does not support verbose transaction fetching
int? confirmations;
final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
if (verboseTransaction.isEmpty) {
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 {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
transactionHex = verboseTransaction['hex'] as String;
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 ins = <BtcTransaction>[];
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(
original,
ins: ins,
time: time,
confirmations: confirmations,
confirmations: confirmations ?? 0,
);
}
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
{required String hash, required int height, bool? retryOnFailure}) async {
{required String hash, int? height, bool? retryOnFailure}) async {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash), walletInfo.type, network,
addresses: addressesSet, height: height);
await getTransactionExpanded(hash: hash, height: height),
walletInfo.type,
network,
addresses: addressesSet,
height: height,
);
} catch (e) {
if (e is FormatException && retryOnFailure == true) {
await Future.delayed(const Duration(seconds: 2));
@ -1638,8 +1758,8 @@ abstract class ElectrumWalletBase
await getCurrentChainTip();
transactionHistory.transactions.values.forEach((tx) async {
if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) {
tx.confirmations = await getCurrentChainTip() - tx.height + 1;
if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) {
tx.confirmations = await getCurrentChainTip() - tx.height! + 1;
}
});
@ -1755,15 +1875,79 @@ abstract class ElectrumWalletBase
final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message));
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
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 {
if (_chainTipUpdateSubject != null) return;
_currentChainTip = await getUpdatedChainTip();
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
await getUpdatedChainTip();
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) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
@action
void _onConnectionStatusChange(bool? isConnected) {
if (syncStatus is SyncingSyncStatus) return;
void _onConnectionStatusChange(ConnectionStatus status) {
switch (status) {
case ConnectionStatus.connected:
if (syncStatus is NotConnectedSyncStatus ||
syncStatus is LostConnectionSyncStatus ||
syncStatus is ConnectingSyncStatus) {
syncStatus = AttemptingSyncStatus();
startSync();
}
if (isConnected == true && syncStatus is! SyncedSyncStatus) {
syncStatus = ConnectedSyncStatus();
} else if (isConnected == false) {
syncStatus = LostConnectionSyncStatus();
} else if (isConnected != true && syncStatus is! ConnectingSyncStatus) {
syncStatus = NotConnectedSyncStatus();
break;
case ConnectionStatus.disconnected:
syncStatus = NotConnectedSyncStatus();
break;
case ConnectionStatus.failed:
syncStatus = LostConnectionSyncStatus();
break;
case ConnectionStatus.connecting:
syncStatus = ConnectingSyncStatus();
break;
default:
}
}
void _syncStatusReaction(SyncStatus syncStatus) async {
if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) {
silentPaymentsScanningActive = syncStatus is SyncingSyncStatus;
if (syncStatus is SyncingSyncStatus) {
return;
}
if (syncStatus is NotConnectedSyncStatus) {
if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) {
// Needs to re-subscribe to all scripthashes when reconnected
_scripthashesUpdateSubject = {};
@ -1827,7 +2007,8 @@ abstract class ElectrumWalletBase
_isTryingToConnect = true;
Future.delayed(Duration(seconds: 10), () {
_reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(seconds: 10), () {
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) {
this.electrumClient.connectToUri(
node!.uri,
@ -1939,8 +2120,8 @@ Future<void> startRefresh(ScanData scanData) async {
final tweaks = t as Map<String, dynamic>;
if (tweaks["message"] != null) {
// re-subscribe to continue receiving messages
electrumClient.tweaksSubscribe(height: syncHeight, count: count);
// re-subscribe to continue receiving messages, starting from the next unscanned height
electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count);
return;
}

View file

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

View file

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

View file

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

View file

@ -1,20 +1,28 @@
import 'dart:convert';
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_transaction_priority.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
import 'package:pointycastle/ecc/api.dart';
import 'package:pointycastle/ecc/curves/secp256k1.dart';
part 'litecoin_wallet.g.dart';
@ -27,6 +35,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
@ -37,10 +46,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
networkType: litecoinNetwork,
network: LitecoinNetwork.mainnet,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
currency: CryptoCurrency.ltc) {
walletAddresses = LitecoinWalletAddresses(
walletInfo,
@ -48,7 +58,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
sideHd: accountHD.derive(1),
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network,
);
autorun((_) {
@ -61,6 +71,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
@ -88,6 +99,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils,
seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
@ -95,25 +107,54 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
);
}
static Future<LitecoinWallet> open({
required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
}) async {
final snp =
await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet);
static Future<LitecoinWallet> open(
{required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required EncryptionFileUtils encryptionFileUtils}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
ElectrumWalletSnapshot? snp = null;
try {
snp = await ElectrumWalletSnapshot.load(
encryptionFileUtils,
name,
walletInfo.type,
password,
LitecoinNetwork.mainnet,
);
} catch (e) {
if (!hasKeysFile) rethrow;
}
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
keysData =
WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
return LitecoinWallet(
mnemonic: snp.mnemonic!,
mnemonic: keysData.mnemonic!,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses,
initialBalance: snp.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic!),
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
initialAddresses: snp?.addresses,
initialBalance: snp?.balance,
seedBytes: await mnemonicToSeedBytes(keysData.mnemonic!),
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp?.addressPageType,
);
}
@ -132,4 +173,127 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
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_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
@ -22,6 +22,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
@override
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);
}

View file

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

View file

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

View file

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

View file

@ -19,32 +19,24 @@ dependencies:
intl: ^0.18.0
cw_core:
path: ../cw_core
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v4
bitbox:
git:
url: https://github.com/cake-tech/bitbox-flutter.git
ref: Add-Support-For-OP-Return-data
rxdart: ^0.27.5
cryptography: ^2.0.5
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v3
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1
ref: cake-update-v2
ledger_flutter: ^1.0.1
ledger_bitcoin:
git:
url: https://github.com/cake-tech/ledger-bitcoin
sp_scanner:
git:
url: https://github.com/cake-tech/sp_scanner
ref: sp_v2.0.0
url: https://github.com/rafael-xmr/sp_scanner
ref: sp_v4.0.0
dev_dependencies:
@ -56,7 +48,15 @@ dev_dependencies:
hive_generator: ^1.1.3
dependency_overrides:
ledger_flutter:
git:
url: https://github.com/cake-tech/ledger-flutter.git
ref: cake-v3
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
# 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: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_transaction_priority.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/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
@ -29,6 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils,
BitcoinAddressType? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
@ -39,18 +40,19 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
networkType: bitcoin.bitcoin,
network: BitcoinCashNetwork.mainnet,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: seedBytes,
currency: CryptoCurrency.bch) {
currency: CryptoCurrency.bch,
encryptionFileUtils: encryptionFileUtils) {
walletAddresses = BitcoinCashWalletAddresses(
walletInfo,
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
sideHd: accountHD.derive(1),
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network,
initialAddressPageType: addressPageType,
);
@ -64,6 +66,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
@ -76,7 +79,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: await Mnemonic.toSeed(mnemonic),
seedBytes: await MnemonicBip39.toSeed(mnemonic),
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh,
@ -88,15 +92,44 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final snp = await ElectrumWalletSnapshot.load(
name, walletInfo.type, password, BitcoinCashNetwork.mainnet);
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
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(
mnemonic: snp.mnemonic!,
mnemonic: keysData.mnemonic!,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses.map((addr) {
initialAddresses: snp?.addresses.map((addr) {
try {
BitcoinCashAddress(addr.address);
return BitcoinAddressRecord(
@ -116,16 +149,19 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
);
}
}).toList(),
initialBalance: snp.balance,
seedBytes: await Mnemonic.toSeed(snp.mnemonic!),
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
initialBalance: snp?.balance,
seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!),
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh,
);
}
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
bitbox.ECPair.fromWIF(hd.derive(index).wif!);
bitbox.ECPair generateKeyPair({required Bip32Slip10Secp256k1 hd, required int index}) =>
bitbox.ECPair.fromPrivateKey(
Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw),
);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0;
@ -166,12 +202,17 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
@override
Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null
? walletAddresses.allAddresses
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))
.index
: null;
final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message));
int? index;
try {
index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
} catch (_) {}
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_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/utils.dart';
import 'package:cw_core/wallet_info.dart';
@ -23,6 +23,8 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
@override
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);
}

View file

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

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:bip39/bip39.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/unspent_coins_info.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:hive/hive.dart';
class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredentials,
BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> {
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
class BitcoinCashWalletService extends WalletService<
BitcoinCashNewWalletCredentials,
BitcoinCashRestoreWalletFromSeedCredentials,
BitcoinCashRestoreWalletFromWIFCredentials,
BitcoinCashNewWalletCredentials> {
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool isDirect;
@override
WalletType getType() => WalletType.bitcoinCash;
@ -30,12 +35,15 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final wallet = await BitcoinCashWalletBase.create(
mnemonic: await Mnemonic.generate(strength: strength),
password: credentials.password!,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource);
mnemonic: await MnemonicBip39.generate(strength: strength),
password: credentials.password!,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
return wallet;
}
@ -49,7 +57,9 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource);
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
saveBackup(name);
return wallet;
@ -59,7 +69,9 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource);
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
return wallet;
}
@ -81,7 +93,8 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: password,
name: currentName,
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource);
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
@ -95,7 +108,8 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
@override
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
@ -115,7 +129,8 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource);
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
await wallet.save();
await wallet.init();
return wallet;

View file

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

View file

@ -21,22 +21,14 @@ dependencies:
path: ../cw_core
cw_bitcoin:
path: ../cw_bitcoin
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v4
bitbox:
git:
url: https://github.com/cake-tech/bitbox-flutter.git
ref: Add-Support-For-OP-Return-data
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v3
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1
ref: cake-update-v2
dev_dependencies:
flutter_test:
@ -47,6 +39,10 @@ dev_dependencies:
dependency_overrides:
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
# 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';
throw ArgumentError.value(name, 'Fullname', s);
}
return CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()]!;
return CryptoCurrency._fullNameCurrencyMap[name.split("(").first.trim().toLowerCase()]!;
}
@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/
const bitcoinDates = {
"2024-08": 854889,
"2024-07": 850182,
"2024-06": 846005,
"2024-05": 841590,
"2024-04": 837182,
@ -371,7 +373,8 @@ const wowDates = {
int getWowneroHeightByDate({required DateTime date}) {
String closestKey =
wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => '');
wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => '');
return wowDates[closestKey] ?? 0;
}
}

View file

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

View file

@ -11,7 +11,8 @@ import 'package:http/io_client.dart' as ioc;
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)
class Node extends HiveObject with Keyable {
@ -72,6 +73,12 @@ class Node extends HiveObject with Keyable {
@HiveField(7, defaultValue: '')
String? path;
@HiveField(8)
bool? isElectrs;
@HiveField(9)
bool? supportsSilentPayments;
bool get isSSL => useSSL ?? false;
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;

View file

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

View file

@ -3,12 +3,13 @@ import 'package:cw_core/keyable.dart';
abstract class TransactionInfo extends Object with Keyable {
late String id;
late String txHash = id;
late int amount;
int? fee;
late TransactionDirection direction;
late bool isPending;
late DateTime date;
late int height;
int? height;
late int confirmations;
String amountFormatted();
String fiatAmount();
@ -24,4 +25,5 @@ abstract class TransactionInfo extends Object with Keyable {
dynamic get keyIndex => id;
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);
// void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded,
// {void Function() onFinished});
@ -84,13 +83,17 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
Future<void> changePassword(String password);
String get password;
Future<void>? updateBalance();
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
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 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.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);
}
}
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"
source: hosted
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:
dependency: transitive
description:
@ -169,6 +178,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -648,6 +673,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:

View file

@ -19,6 +19,11 @@ dependencies:
flutter_mobx: ^2.0.6+1
intl: ^0.18.0
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
unorm_dart: ^0.3.0
# tor:

View file

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

View file

@ -2,10 +2,12 @@ import 'dart:convert';
import 'package:cw_core/cake_hive.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/pathForWallet.dart';
import 'package:cw_core/transaction_direction.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/ethereum_client.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_wallet.dart';
import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/file.dart';
class EthereumWallet extends EVMChainWallet {
EthereumWallet({
@ -25,6 +26,7 @@ class EthereumWallet extends EVMChainWallet {
super.mnemonic,
super.initialBalance,
super.privateKey,
required super.encryptionFileUtils,
}) : super(nativeCurrency: CryptoCurrency.eth);
@override
@ -116,27 +118,57 @@ class EthereumWallet extends EVMChainWallet {
}
@override
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) {
return EthereumTransactionHistory(walletInfo: walletInfo, password: password);
EVMChainTransactionHistory setUpTransactionHistory(
WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils) {
return EthereumTransactionHistory(
walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils);
}
static Future<EthereumWallet> open(
{required String name, required String password, required WalletInfo walletInfo}) async {
static Future<EthereumWallet> open({
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 jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
Map<String, dynamic>? data;
try {
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} catch (e) {
if (!hasKeysFile) rethrow;
}
final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ??
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(
walletInfo: walletInfo,
password: password,
mnemonic: mnemonic,
privateKey: privateKey,
mnemonic: keysData.mnemonic,
privateKey: keysData.privateKey,
initialBalance: balance,
client: EthereumClient(),
encryptionFileUtils: encryptionFileUtils,
);
}
}

View file

@ -1,4 +1,5 @@
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_info.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';
class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
EthereumWalletService(super.walletInfoSource, {required this.client});
EthereumWalletService(super.walletInfoSource, super.isDirect, {required this.client});
late EthereumClient client;
@ -27,6 +28,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
mnemonic: mnemonic,
password: credentials.password!,
client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -46,6 +48,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -59,6 +62,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
await wallet.save();
@ -71,7 +75,11 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
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 saveBackup(newName);
@ -97,6 +105,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
walletInfo: credentials.walletInfo!,
password: credentials.password!,
client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -114,6 +123,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!,
client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -135,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
client: client,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();

View file

@ -2,7 +2,7 @@ import 'dart:typed_data';
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"}]',
'Erc20');
@ -13,7 +13,7 @@ class ERC20 extends web3.GeneratedContract {
required web3.EthereumAddress address,
required web3.Web3Client client,
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.
///

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/pending_evm_chain_transaction.dart';
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:http/http.dart';
import 'package:web3dart/web3dart.dart';
@ -65,16 +65,65 @@ abstract class EVMChainClient {
Future<int> getGasUnitPrice() async {
try {
final gasPrice = await _client!.getGasPrice();
return gasPrice.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int> getEstimatedGas() async {
Future<int?> getGasBaseFee() async {
try {
final estimatedGas = await _client!.estimateGas();
return estimatedGas.toInt();
final blockInfo = await _client!.getBlockInformation(isContainFullObj: false);
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 (_) {
return 0;
}
@ -84,7 +133,7 @@ abstract class EVMChainClient {
required Credentials privateKey,
required String toAddress,
required BigInt amount,
required int gas,
required BigInt gas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
required int exponent,
@ -97,8 +146,6 @@ abstract class EVMChainClient {
bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice();
final Transaction transaction = createTransaction(
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
@ -130,11 +177,10 @@ abstract class EVMChainClient {
_sendTransaction = () async => await sendTransaction(signedTransaction);
return PendingEVMChainTransaction(
signedTransaction: signedTransaction,
amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei,
fee: gas,
sendTransaction: _sendTransaction,
exponent: exponent,
);
@ -233,7 +279,6 @@ abstract class EVMChainClient {
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
final symbol = (decodedResponse['symbol'] ?? '') as String;
String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), '');

View file

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

View file

@ -7,6 +7,7 @@ import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/cake_hive.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/node.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_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_evm/evm_chain_client.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_wallet_addresses.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:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'evm_chain_transaction_info.dart';
import 'evm_erc20_balance.dart';
@ -57,7 +60,7 @@ abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet;
abstract class EVMChainWalletBase
extends WalletBase<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo>
with Store {
with Store, WalletKeysFile {
EVMChainWalletBase({
required WalletInfo walletInfo,
required EVMChainClient client,
@ -66,6 +69,7 @@ abstract class EVMChainWalletBase
String? privateKey,
required String password,
EVMChainERC20Balance? initialBalance,
required this.encryptionFileUtils,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
@ -81,7 +85,7 @@ abstract class EVMChainWalletBase
),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = setUpTransactionHistory(walletInfo, password);
transactionHistory = setUpTransactionHistory(walletInfo, password, encryptionFileUtils);
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
@ -93,6 +97,7 @@ abstract class EVMChainWalletBase
final String? _mnemonic;
final String? _hexPrivateKey;
final String _password;
final EncryptionFileUtils encryptionFileUtils;
late final Box<Erc20Token> erc20TokensBox;
@ -102,10 +107,14 @@ abstract class EVMChainWalletBase
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;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
@ -145,7 +154,11 @@ abstract class EVMChainWalletBase
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
@ -173,12 +186,70 @@ abstract class EVMChainWalletBase
@override
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 {
if (priority is EVMChainTransactionPriority) {
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;
} catch (e) {
return 0;
@ -194,6 +265,7 @@ abstract class EVMChainWalletBase
void close() {
_client.stop();
_transactionsUpdateTimer?.cancel();
_updateFeesTimer?.cancel();
}
@action
@ -225,13 +297,12 @@ abstract class EVMChainWalletBase
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic(
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10),
(timer) async => _estimatedGas = await _client.getEstimatedGas());
await _updateEstimatedGasFeeParams();
_updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
await _updateEstimatedGasFeeParams();
});
syncStatus = SyncedSyncStatus();
} 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
Future<PendingTransaction> createTransaction(Object credentials) async {
final _credentials = credentials as EVMChainTransactionCredentials;
@ -258,8 +342,17 @@ abstract class EVMChainWalletBase
final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
BigInt estimatedFeesForTransaction = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
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
if (hasMultiDestination) {
@ -271,35 +364,48 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
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) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
} else {
final output = outputs.first;
// since the fees are taken from Ethereum
// 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 {
if (!output.sendAll) {
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
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) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
@ -312,11 +418,9 @@ abstract class EVMChainWalletBase
final pendingEVMChainTransaction = await _client.signTransaction(
privateKey: _evmChainPrivateKey,
toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
toAddress: toAddress,
amount: totalAmount,
gas: _estimatedGas!,
gas: estimatedFeesForTransaction,
priority: _credentials.priority!,
currency: transactionCurrency,
exponent: exponent,
@ -400,7 +504,7 @@ abstract class EVMChainWalletBase
}
final methodSignature =
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
return methodSignatureToType[methodSignature];
}
@ -415,9 +519,14 @@ abstract class EVMChainWalletBase
@override
Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@ -429,7 +538,8 @@ abstract class EVMChainWalletBase
? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey)
: 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({
'mnemonic': _mnemonic,
@ -483,6 +593,7 @@ abstract class EVMChainWalletBase
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
}
@override
Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
@ -585,8 +696,24 @@ abstract class EVMChainWalletBase
}
@override
Future<String> signMessage(String message, {String? address}) async =>
bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
Future<String> signMessage(String message, {String? address}) async {
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();
@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';
class EVMChainNewWalletCredentials extends WalletCredentials {
EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password})
: super(name: name, walletInfo: walletInfo, password: password);
}
class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials {

View file

@ -15,9 +15,10 @@ abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletSer
EVMChainRestoreWalletFromSeedCredentials,
EVMChainRestoreWalletFromPrivateKey,
EVMChainRestoreWalletFromHardware> {
EVMChainWalletService(this.walletInfoSource);
EVMChainWalletService(this.walletInfoSource, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final bool isDirect;
@override
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:
sdk: flutter
web3dart: ^2.7.1
eth_sig_util: ^0.0.9
erc20: ^1.0.1
bip39: ^1.0.6
bip32: ^2.0.0
hex: ^0.2.0
@ -20,6 +22,7 @@ dependencies:
hive: ^2.2.3
collection: ^1.17.1
shared_preferences: ^2.0.15
mobx: ^2.0.7+4
cw_core:
path: ../cw_core
ledger_flutter: ^1.0.1
@ -35,7 +38,7 @@ dependency_overrides:
ledger_flutter:
git:
url: https://github.com/cake-tech/ledger-flutter.git
ref: cake
ref: cake-v3
watcher: ^1.1.0
dev_dependencies:

View file

@ -5,7 +5,7 @@ buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
@ -17,7 +17,7 @@ buildscript {
rootProject.allprojects {
repositories {
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_core/monero_wallet_utils.dart';
import 'package:cw_haven/api/structs/pending_transaction.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
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/transaction_history.dart' as transaction_history;
import 'package:cw_haven/api/monero_output.dart';
@ -38,9 +36,10 @@ class HavenWallet = HavenWalletBase with _$HavenWallet;
abstract class HavenWalletBase
extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store {
HavenWalletBase({required WalletInfo walletInfo})
HavenWalletBase({required WalletInfo walletInfo, String? password})
: balance = ObservableMap.of(getHavenBalance(accountIndex: 0)),
_isTransactionUpdating = false,
_password = password ?? '',
_hasSyncAfterStartup = false,
walletAddresses = HavenWalletAddresses(walletInfo),
syncStatus = NotConnectedSyncStatus(),
@ -56,6 +55,7 @@ abstract class HavenWalletBase
}
static const int _autoSaveInterval = 30;
final String _password;
@override
HavenWalletAddresses walletAddresses;
@ -111,7 +111,7 @@ abstract class HavenWalletBase
_onAccountChangeReaction?.reaction.dispose();
_autoSaveTimer?.cancel();
}
@override
Future<void> connectToNode({required Node node}) async {
try {
@ -121,7 +121,8 @@ abstract class HavenWalletBase
login: node.login,
password: node.password,
useSSL: node.useSSL ?? false,
isLightWallet: false, // FIXME: hardcoded value
isLightWallet: false,
// FIXME: hardcoded value
socksProxyAddress: node.socksProxyAddress);
haven_wallet.setTrustedDaemon(node.trusted);
@ -414,4 +415,15 @@ abstract class HavenWalletBase
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"
source: hosted
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:
dependency: transitive
description:
@ -169,6 +178,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -254,10 +279,10 @@ packages:
dependency: transitive
description:
name: glob
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
graphs:
dependency: transitive
description:
@ -514,14 +539,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
pub_semver:
dependency: transitive
description:
@ -647,6 +664,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:
@ -707,10 +732,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "0.2.0+3"
version: "1.0.4"
yaml:
dependency: transitive
description:

View file

@ -18,6 +18,9 @@ migration:
- platform: macos
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: linux
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
# 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!);
refreshAccounts();
int size = monero.SubaddressAccount_getAll_size(subaddressAccount!);
print("size: $size");
if (size == 0) {
monero.Wallet_addSubaddressAccount(wptr!);
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() {
final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
return List.generate(size, (index) {
final list = List.generate(size, (index) {
return Subaddress(
accountIndex: subaddress!.accountIndex,
addressIndex: index,
);
}).reversed.toList();
if (list.length == 0) {
list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0));
}
return list;
}
void addSubaddressSync({required int accountIndex, required String label}) {

View file

@ -1,4 +1,3 @@
import 'dart:ffi';
import 'dart:isolate';
@ -110,7 +109,10 @@ Future<PendingTransactionDescription> createTransactionSync(
})();
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);
}
@ -285,7 +287,7 @@ class Transaction {
};
}
// S finalubAddress? subAddress;
// final SubAddress? subAddress;
// List<Transfer> transfers = [];
// final int txIndex;
final monero.TransactionInfo txInfo;
@ -321,4 +323,4 @@ class Transaction {
required this.key,
required this.txInfo
});
}
}

View file

@ -39,16 +39,30 @@ String getSeed() {
if (polyseed != "") {
return polyseed;
}
final legacy = monero.Wallet_seed(wptr!, seedOffset: '');
final legacy = getSeedLegacy("English");
return legacy;
}
String getSeedLegacy(String? language) {
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) {
monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
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;
}
@ -121,9 +135,23 @@ void setRecoveringFromSeed({required bool isRecovery}) =>
monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
final storeMutex = Mutex();
int lastStorePointer = 0;
int lastStoreHeight = 0;
void storeSync() async {
await storeMutex.acquire();
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(() {
monero.Wallet_store(Pointer.fromAddress(addr));
});
@ -288,3 +316,7 @@ Future<bool> trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!);
String signMessage(String message, {String 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:io';
import 'dart:isolate';
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_restore_from_keys_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/transaction_history.dart';
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;
final monero.WalletManager wmPtr = Pointer.fromAddress((() {
try {
@ -21,6 +55,7 @@ final monero.WalletManager wmPtr = Pointer.fromAddress((() {
print("ptr: $_wmPtr");
} catch (e) {
print(e);
rethrow;
}
return _wmPtr!.address;
})());
@ -31,13 +66,14 @@ void createWalletSync(
required String language,
int nettype = 0}) {
txhistory = null;
wptr = monero.WalletManager_createWallet(wmPtr,
final newWptr = monero.WalletManager_createWallet(wmPtr,
path: path, password: password, language: language, networkType: 0);
final status = monero.Wallet_status(wptr!);
final status = monero.Wallet_status(newWptr);
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);
openedWalletsByPath[path] = wptr!;
@ -56,7 +92,7 @@ void restoreWalletFromSeedSync(
int nettype = 0,
int restoreHeight = 0}) {
txhistory = null;
wptr = monero.WalletManager_recoveryWallet(
final newWptr = monero.WalletManager_recoveryWallet(
wmPtr,
path: path,
password: password,
@ -66,12 +102,13 @@ void restoreWalletFromSeedSync(
networkType: 0,
);
final status = monero.Wallet_status(wptr!);
final status = monero.Wallet_status(newWptr);
if (status != 0) {
final error = monero.Wallet_errorString(wptr!);
final error = monero.Wallet_errorString(newWptr);
throw WalletRestoreFromSeedException(message: error);
}
wptr = newWptr;
openedWalletsByPath[path] = wptr!;
}
@ -86,7 +123,16 @@ void restoreWalletFromKeysSync(
int nettype = 0,
int restoreHeight = 0}) {
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,
path: path,
password: password,
@ -97,12 +143,14 @@ void restoreWalletFromKeysSync(
nettype: 0,
);
final status = monero.Wallet_status(wptr!);
final status = monero.Wallet_status(newWptr);
if (status != 0) {
throw WalletRestoreFromKeysException(
message: monero.Wallet_errorString(wptr!));
message: monero.Wallet_errorString(newWptr));
}
wptr = newWptr;
openedWalletsByPath[path] = wptr!;
}
@ -127,7 +175,7 @@ void restoreWalletFromSpendKeySync(
// );
txhistory = null;
wptr = monero.WalletManager_createDeterministicWalletFromSpendKey(
final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey(
wmPtr,
path: path,
password: password,
@ -137,14 +185,16 @@ void restoreWalletFromSpendKeySync(
restoreHeight: restoreHeight,
);
final status = monero.Wallet_status(wptr!);
final status = monero.Wallet_status(newWptr);
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
final err = monero.Wallet_errorString(newWptr);
print("err: $err");
throw WalletRestoreFromKeysException(message: err);
}
wptr = newWptr;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
storeSync();
@ -194,28 +244,25 @@ void loadWallet(
wptr = openedWalletsByPath[path]!;
return;
}
try {
if (wptr == null || path != _lastOpenedWallet) {
if (wptr != null) {
final addr = wptr!.address;
Isolate.run(() {
monero.Wallet_store(Pointer.fromAddress(addr));
});
}
txhistory = null;
wptr = monero.WalletManager_openWallet(wmPtr,
path: path, password: password);
openedWalletsByPath[path] = wptr!;
_lastOpenedWallet = path;
if (wptr == null || path != _lastOpenedWallet) {
if (wptr != null) {
final addr = wptr!.address;
Isolate.run(() {
monero.Wallet_store(Pointer.fromAddress(addr));
});
}
} catch (e) {
print(e);
}
final status = monero.Wallet_status(wptr!);
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
print(err);
throw WalletOpeningException(message: err);
txhistory = null;
final newWptr = monero.WalletManager_openWallet(wmPtr,
path: path, password: password);
_lastOpenedWallet = path;
final status = monero.Wallet_status(newWptr);
if (status != 0) {
final err = monero.Wallet_errorString(newWptr);
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