Merge branch 'main' into Haven-Update-Dependencies
7
.github/workflows/cache_dependencies.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
187
.github/workflows/pr_test_build_linux.yml
vendored
Normal 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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div align="center">
|
||||
|
||||
<img height="100" src=".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
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 12 KiB |
BIN
assets/images/flags/arm.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 28 KiB |
|
@ -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
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
-
|
||||
uri: rpc.ankr.com
|
||||
is_default: true
|
||||
useSSL: true
|
||||
-
|
||||
uri: api.mainnet-beta.solana.com:443
|
||||
useSSL: true
|
|
@ -1,2 +1,3 @@
|
|||
Monero enhancements
|
||||
Scan and verify messages
|
||||
Synchronization enhancements
|
||||
Bug fixes
|
|
@ -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
|
@ -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.
|
|
@ -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.
|
35
com.cakewallet.CakeWallet.yml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
42
cw_core/lib/encryption_file_utils.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
119
cw_core/lib/wallet_keys_file.dart
Normal 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?,
|
||||
);
|
||||
}
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -7,6 +7,7 @@ class EthereumTransactionHistory extends EVMChainTransactionHistory {
|
|||
EthereumTransactionHistory({
|
||||
required super.walletInfo,
|
||||
required super.password,
|
||||
required super.encryptionFileUtils,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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('^\\\$'), '');
|
||||
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
flutter/ephemeral
|
138
cw_monero/example/linux/CMakeLists.txt
Normal 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()
|
88
cw_monero/example/linux/flutter/CMakeLists.txt
Normal 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}
|
||||
)
|
|
@ -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);
|
||||
}
|
|
@ -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_
|
24
cw_monero/example/linux/flutter/generated_plugins.cmake
Normal 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)
|
6
cw_monero/example/linux/main.cc
Normal 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);
|
||||
}
|
104
cw_monero/example/linux/my_application.cc
Normal 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));
|
||||
}
|
18
cw_monero/example/linux/my_application.h
Normal 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_
|
|
@ -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();
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
class ConnectionToNodeException implements Exception {
|
||||
ConnectionToNodeException({required this.message});
|
||||
|
||||
final String message;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import 'dart:ffi';
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
class Utf8Box extends Struct {
|
||||
external Pointer<Utf8> value;
|
||||
|
||||
String getValue() => value.toDartString();
|
||||
}
|
|
@ -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}) {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
import 'cw_monero_platform_interface.dart';
|
||||
|
||||
class CwMonero {
|
||||
Future<String?> getPlatformVersion() {
|
||||
return CwMoneroPlatform.instance.getPlatformVersion();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
}
|