Merge branch 'main' into CW-635--Show-BTC-LTC-BCH-keys-in-wallet-seeds-keys-page
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">
|
||||
![logo](.github/assets/Logo_CakeWallet.png)
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -161,7 +161,9 @@ The only parts to be translated, if needed, are the values m and s after the var
|
|||
|
||||
4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language.
|
||||
|
||||
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 digit localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
|
||||
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
|
||||
|
||||
6. Add the new language code to `tool/utils/translation/translation_constants.dart`
|
||||
|
||||
## Add a new fiat currency
|
||||
|
||||
|
|
|
@ -10,7 +10,18 @@ analyzer:
|
|||
lib/generated/*.dart,
|
||||
cw_monero/ios/External/**,
|
||||
cw_shared_external/**,
|
||||
shared_external/**]
|
||||
shared_external/**,
|
||||
lib/bitcoin/cw_bitcoin.dart,
|
||||
lib/bitcoin_cash/cw_bitcoin_cash.dart,
|
||||
lib/ethereum/cw_ethereum.dart,
|
||||
lib/haven/cw_haven.dart,
|
||||
lib/monero/cw_monero.dart,
|
||||
lib/nano/cw_nano.dart,
|
||||
lib/polygon/cw_polygon.dart,
|
||||
lib/solana/cw_solana.dart,
|
||||
lib/tron/cw_tron.dart,
|
||||
lib/wownero/cw_wownero.dart,
|
||||
]
|
||||
language:
|
||||
strict-casts: true
|
||||
strict-raw-types: true
|
||||
|
|
|
@ -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 |
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 376 B |
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 1.2 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: 1.8 KiB |
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 372 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 692 B |
Before Width: | Height: | Size: 1,005 B After Width: | Height: | Size: 788 B |
Before Width: | Height: | Size: 855 B After Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 753 B |
Before Width: | Height: | Size: 860 B After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 381 B |
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 830 B |
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 553 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1,005 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 432 B After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1,023 B After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 373 B |
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 360 B |
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 994 B |
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 896 B After Width: | Height: | Size: 851 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 849 B |
Before Width: | Height: | Size: 1,013 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 372 B |
Before Width: | Height: | Size: 898 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 366 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 702 B After Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 750 B |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 390 B |
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 384 B |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 373 B After Width: | Height: | Size: 1,005 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 597 B |
Before Width: | Height: | Size: 1,009 B After Width: | Height: | Size: 887 B |
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 882 B |
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,2 @@
|
|||
Monero enhancements
|
||||
Bug fixes
|
||||
Enhance auto-address generation for Monero
|
||||
Bug fixes and enhancements
|
|
@ -1,3 +1,4 @@
|
|||
Monero enhancements
|
||||
Improvements for Tron and Nano wallets
|
||||
Bug fixes
|
||||
Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page)
|
||||
Improve fee calculation for Bitcoin to protect against overpaying or underpaying
|
||||
Enhance auto-address generation for Monero
|
||||
Bug fixes and enhancements
|
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);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cryptography/cryptography.dart' as cryptography;
|
||||
import 'package:cw_core/sec_random_native.dart';
|
||||
|
@ -59,11 +60,7 @@ void maskBytes(Uint8List bytes, int bits) {
|
|||
}
|
||||
}
|
||||
|
||||
String bufferToBin(Uint8List data) {
|
||||
final q1 = data.map((e) => e.toRadixString(2).padLeft(8, '0'));
|
||||
final q2 = q1.join('');
|
||||
return q2;
|
||||
}
|
||||
String bufferToBin(Uint8List data) => data.map((e) => e.toRadixString(2).padLeft(8, '0')).join('');
|
||||
|
||||
String encode(Uint8List data) {
|
||||
final dataBitLen = data.length * 8;
|
||||
|
@ -112,17 +109,18 @@ Future<bool> checkIfMnemonicIsElectrum2(String mnemonic) async {
|
|||
Future<String> getMnemonicHash(String mnemonic) async {
|
||||
final hmacSha512 = Hmac(sha512, utf8.encode('Seed version'));
|
||||
final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic)));
|
||||
final hx = digest.toString();
|
||||
return hx;
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
Future<Uint8List> mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async {
|
||||
Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
|
||||
{String prefix = segwit, String passphrase = ''}) async {
|
||||
final pbkdf2 =
|
||||
cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512);
|
||||
final text = normalizeText(mnemonic);
|
||||
// pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce)
|
||||
final passphraseBytes = utf8.encode(normalizeText(passphrase));
|
||||
final key = await pbkdf2.deriveKey(
|
||||
secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits);
|
||||
secretKey: cryptography.SecretKey(text.codeUnits),
|
||||
nonce: [...'electrum'.codeUnits, ...passphraseBytes]);
|
||||
final bytes = await key.extractBytes();
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
|
||||
class Mnemonic {
|
||||
class MnemonicBip39 {
|
||||
/// Generate bip39 mnemonic
|
||||
static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength);
|
||||
|
||||
/// Create root seed from mnemonic
|
||||
static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic);
|
||||
static Uint8List toSeed(String mnemonic, {String? passphrase}) =>
|
||||
bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
|
||||
}
|
|
@ -2,10 +2,10 @@ import 'dart:convert';
|
|||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_bitcoin/electrum_derivations.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
|
@ -15,6 +15,7 @@ import 'package:cw_bitcoin/psbt_transaction_builder.dart';
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_keys_file.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
|
||||
|
@ -30,6 +31,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
Uint8List? seedBytes,
|
||||
String? mnemonic,
|
||||
String? xpub,
|
||||
|
@ -50,14 +52,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
networkType: networkParam == null
|
||||
? bitcoin.bitcoin
|
||||
network: networkParam == null
|
||||
? BitcoinNetwork.mainnet
|
||||
: networkParam == BitcoinNetwork.mainnet
|
||||
? bitcoin.bitcoin
|
||||
: bitcoin.testnet,
|
||||
? BitcoinNetwork.mainnet
|
||||
: BitcoinNetwork.testnet,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
currency:
|
||||
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
|
||||
alwaysScan: alwaysScan,
|
||||
|
@ -75,10 +78,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
initialSilentAddresses: initialSilentAddresses,
|
||||
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: accountHD.derive(1),
|
||||
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
|
||||
network: networkParam ?? network,
|
||||
masterHd:
|
||||
seedBytes != null ? bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : null,
|
||||
masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
|
||||
);
|
||||
|
||||
autorun((_) {
|
||||
|
@ -91,6 +93,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
BasedUtxoNetwork? network,
|
||||
|
@ -112,7 +115,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
}
|
||||
return BitcoinWallet(
|
||||
|
@ -125,6 +128,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
initialSilentAddresses: initialSilentAddresses,
|
||||
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||
initialBalance: initialBalance,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
seedBytes: seedBytes,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
|
@ -138,54 +142,87 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
required bool alwaysScan,
|
||||
}) async {
|
||||
final network = walletInfo.network != null
|
||||
? BasedUtxoNetwork.fromName(walletInfo.network!)
|
||||
: BitcoinNetwork.mainnet;
|
||||
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
|
||||
|
||||
walletInfo.derivationInfo ??= DerivationInfo(
|
||||
derivationType: snp.derivationType ?? DerivationType.electrum,
|
||||
derivationPath: snp.derivationPath,
|
||||
);
|
||||
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
|
||||
|
||||
ElectrumWalletSnapshot? snp = null;
|
||||
|
||||
try {
|
||||
snp = await ElectrumWalletSnapshot.load(
|
||||
encryptionFileUtils,
|
||||
name,
|
||||
walletInfo.type,
|
||||
password,
|
||||
network,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!hasKeysFile) rethrow;
|
||||
}
|
||||
|
||||
final WalletKeysData keysData;
|
||||
// Migrate wallet from the old scheme to then new .keys file scheme
|
||||
if (!hasKeysFile) {
|
||||
keysData = WalletKeysData(
|
||||
mnemonic: snp!.mnemonic,
|
||||
xPub: snp.xpub,
|
||||
passphrase: snp.passphrase,
|
||||
);
|
||||
} else {
|
||||
keysData = await WalletKeysFile.readKeysFile(
|
||||
name,
|
||||
walletInfo.type,
|
||||
password,
|
||||
encryptionFileUtils,
|
||||
);
|
||||
}
|
||||
|
||||
walletInfo.derivationInfo ??= DerivationInfo();
|
||||
|
||||
// set the default if not present:
|
||||
walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? electrum_path;
|
||||
walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum;
|
||||
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
|
||||
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
|
||||
|
||||
Uint8List? seedBytes = null;
|
||||
final mnemonic = keysData.mnemonic;
|
||||
final passphrase = keysData.passphrase;
|
||||
|
||||
if (snp.mnemonic != null) {
|
||||
if (mnemonic != null) {
|
||||
switch (walletInfo.derivationInfo!.derivationType) {
|
||||
case DerivationType.electrum:
|
||||
seedBytes = await mnemonicToSeedBytes(snp.mnemonic!);
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
case DerivationType.bip39:
|
||||
default:
|
||||
seedBytes = await bip39.mnemonicToSeed(
|
||||
snp.mnemonic!,
|
||||
passphrase: snp.passphrase ?? '',
|
||||
mnemonic,
|
||||
passphrase: passphrase ?? '',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return BitcoinWallet(
|
||||
mnemonic: snp.mnemonic,
|
||||
xpub: snp.xpub,
|
||||
mnemonic: mnemonic,
|
||||
xpub: keysData.xPub,
|
||||
password: password,
|
||||
passphrase: snp.passphrase,
|
||||
passphrase: passphrase,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialSilentAddresses: snp.silentAddresses,
|
||||
initialSilentAddressIndex: snp.silentAddressIndex,
|
||||
initialBalance: snp.balance,
|
||||
initialAddresses: snp?.addresses,
|
||||
initialSilentAddresses: snp?.silentAddresses,
|
||||
initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
|
||||
initialBalance: snp?.balance,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
seedBytes: seedBytes,
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex,
|
||||
addressPageType: snp.addressPageType,
|
||||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||
addressPageType: snp?.addressPageType,
|
||||
networkParam: network,
|
||||
alwaysScan: alwaysScan,
|
||||
);
|
||||
|
@ -235,7 +272,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
|
||||
|
||||
final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
|
||||
return BtcTransaction.fromRaw(hex.encode(rawHex));
|
||||
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -249,8 +286,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
final accountPath = walletInfo.derivationInfo?.derivationPath;
|
||||
final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null;
|
||||
|
||||
final signature = await _bitcoinLedgerApp!
|
||||
.signMessage(_ledgerDevice!, message: ascii.encode(message), signDerivationPath: derivationPath);
|
||||
final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!,
|
||||
message: ascii.encode(message), signDerivationPath: derivationPath);
|
||||
return base64Encode(signature);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -3,14 +3,18 @@ import 'package:cw_core/wallet_credentials.dart';
|
|||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class BitcoinNewWalletCredentials extends WalletCredentials {
|
||||
BitcoinNewWalletCredentials(
|
||||
{required String name,
|
||||
WalletInfo? walletInfo,
|
||||
DerivationType? derivationType,
|
||||
String? derivationPath})
|
||||
: super(
|
||||
BitcoinNewWalletCredentials({
|
||||
required String name,
|
||||
WalletInfo? walletInfo,
|
||||
String? password,
|
||||
DerivationType? derivationType,
|
||||
String? derivationPath,
|
||||
String? passphrase,
|
||||
}) : super(
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:io';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
|
@ -19,11 +21,12 @@ class BitcoinWalletService extends WalletService<
|
|||
BitcoinRestoreWalletFromSeedCredentials,
|
||||
BitcoinRestoreWalletFromWIFCredentials,
|
||||
BitcoinRestoreWalletFromHardware> {
|
||||
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan);
|
||||
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
|
||||
final bool alwaysScan;
|
||||
final bool isDirect;
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.bitcoin;
|
||||
|
@ -33,16 +36,32 @@ class BitcoinWalletService extends WalletService<
|
|||
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
||||
credentials.walletInfo?.network = network.value;
|
||||
|
||||
final String mnemonic;
|
||||
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
mnemonic = await generateElectrumMnemonic();
|
||||
break;
|
||||
}
|
||||
|
||||
final wallet = await BitcoinWalletBase.create(
|
||||
mnemonic: await generateElectrumMnemonic(),
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
passphrase: credentials.passphrase,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
network: network,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
|
@ -61,6 +80,7 @@ class BitcoinWalletService extends WalletService<
|
|||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
alwaysScan: alwaysScan,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.init();
|
||||
saveBackup(name);
|
||||
|
@ -73,6 +93,7 @@ class BitcoinWalletService extends WalletService<
|
|||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
alwaysScan: alwaysScan,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
|
@ -97,6 +118,7 @@ class BitcoinWalletService extends WalletService<
|
|||
walletInfo: currentWalletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
alwaysScan: alwaysScan,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
|
@ -123,6 +145,7 @@ class BitcoinWalletService extends WalletService<
|
|||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
networkParam: network,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
|
@ -151,6 +174,7 @@ class BitcoinWalletService extends WalletService<
|
|||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
network: network,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
|
|
|
@ -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,65 @@ 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) {
|
||||
final errorMsg = error.toString();
|
||||
print(errorMsg);
|
||||
unterminatedString = '';
|
||||
},
|
||||
onDone: () {
|
||||
unterminatedString = '';
|
||||
if (host == socket?.address.host) {
|
||||
socket = null;
|
||||
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
keepAlive();
|
||||
}
|
||||
|
||||
|
@ -144,9 +172,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 +264,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 +289,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 +378,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 +395,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 +428,9 @@ class ElectrumClient {
|
|||
BehaviorSubject<T>? subscribe<T>(
|
||||
{required String id, required String method, List<Object> params = const []}) {
|
||||
try {
|
||||
if (socket == null) {
|
||||
return null;
|
||||
}
|
||||
final subscription = BehaviorSubject<T>();
|
||||
_regisrySubscription(id, subscription);
|
||||
socket!.write(jsonrpc(method: method, id: _id, params: params));
|
||||
|
@ -392,6 +444,9 @@ class ElectrumClient {
|
|||
|
||||
Future<dynamic> call(
|
||||
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
|
||||
if (socket == null) {
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
|
@ -403,8 +458,11 @@ class ElectrumClient {
|
|||
}
|
||||
|
||||
Future<dynamic> callWithTimeout(
|
||||
{required String method, List<Object> params = const [], int timeout = 4000}) async {
|
||||
{required String method, List<Object> params = const [], int timeout = 5000}) async {
|
||||
try {
|
||||
if (socket == null) {
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
|
@ -426,6 +484,7 @@ class ElectrumClient {
|
|||
_aliveTimer?.cancel();
|
||||
try {
|
||||
await socket?.close();
|
||||
socket = null;
|
||||
} catch (_) {}
|
||||
onConnectionStatusChange = null;
|
||||
}
|
||||
|
@ -474,12 +533,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) {
|
||||
|
|
|
@ -109,5 +109,4 @@ Map<DerivationType, List<DerivationInfo>> electrum_derivations = {
|
|||
],
|
||||
};
|
||||
|
||||
|
||||
String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!;
|
||||
String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!;
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
|
@ -7,10 +9,12 @@ import 'package:cw_core/transaction_direction.dart';
|
|||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
|
||||
class ElectrumTransactionBundle {
|
||||
ElectrumTransactionBundle(this.originalTransaction,
|
||||
{required this.ins, required this.confirmations, this.time});
|
||||
|
||||
final BtcTransaction originalTransaction;
|
||||
final List<BtcTransaction> ins;
|
||||
final int? time;
|
||||
|
@ -22,7 +26,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
|
||||
ElectrumTransactionInfo(this.type,
|
||||
{required String id,
|
||||
required int height,
|
||||
int? height,
|
||||
required int amount,
|
||||
int? fee,
|
||||
List<String>? inputAddresses,
|
||||
|
@ -99,7 +103,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
|
||||
factory ElectrumTransactionInfo.fromElectrumBundle(
|
||||
ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network,
|
||||
{required Set<String> addresses, required int height}) {
|
||||
{required Set<String> addresses, int? height}) {
|
||||
final date = bundle.time != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
|
||||
: DateTime.now();
|
||||
|
@ -125,7 +129,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
for (final out in bundle.originalTransaction.outputs) {
|
||||
totalOutAmount += out.amount.toInt();
|
||||
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
||||
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) outputAddresses.add(address);
|
||||
|
||||
// Check if the script contains OP_RETURN
|
||||
final script = out.scriptPubKey.script;
|
||||
if (script.contains('OP_RETURN')) {
|
||||
final index = script.indexOf('OP_RETURN');
|
||||
if (index + 1 <= script.length) {
|
||||
try {
|
||||
final opReturnData = script[index + 1].toString();
|
||||
final decodedString = utf8.decode(HEX.decode(opReturnData));
|
||||
outputAddresses.add('OP_RETURN:$decodedString');
|
||||
} catch (_) {
|
||||
outputAddresses.add('OP_RETURN:');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addressExists) {
|
||||
receivedAmounts.add(out.amount.toInt());
|
||||
|
@ -235,6 +256,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
}
|
||||
|
||||
String toString() {
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import 'dart:isolate';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
|
@ -22,7 +23,6 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
|
|||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_bitcoin/electrum_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,17 +34,18 @@ 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';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
part 'electrum_wallet.g.dart';
|
||||
|
||||
|
@ -54,12 +55,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 +72,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 +91,24 @@ 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);
|
||||
|
||||
sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
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 +117,32 @@ 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;
|
||||
|
||||
@override
|
||||
final String? passphrase;
|
||||
|
||||
@override
|
||||
|
@ -164,16 +174,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;
|
||||
|
||||
|
@ -184,25 +200,29 @@ abstract class ElectrumWalletBase
|
|||
|
||||
bool _isTryingToConnect = false;
|
||||
|
||||
Completer<SharedPreferences> sharedPrefs = Completer();
|
||||
|
||||
Future<bool> checkIfMempoolAPIIsEnabled() async {
|
||||
bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true;
|
||||
return isMempoolAPIEnabled;
|
||||
}
|
||||
|
||||
@action
|
||||
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 +232,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,7 +286,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
void Function(FlutterErrorDetails)? _onError;
|
||||
Timer? _autoSaveTimer;
|
||||
static const int _autoSaveInterval = 30;
|
||||
Timer? _updateFeeRateTimer;
|
||||
static const int _autoSaveInterval = 1;
|
||||
|
||||
Future<void> init() async {
|
||||
await walletAddresses.init();
|
||||
|
@ -277,7 +295,7 @@ abstract class ElectrumWalletBase
|
|||
await save();
|
||||
|
||||
_autoSaveTimer =
|
||||
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
|
||||
Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save());
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -285,7 +303,7 @@ abstract class ElectrumWalletBase
|
|||
int height, {
|
||||
int? chainTipParam,
|
||||
bool? doSingleScan,
|
||||
bool? usingElectrs,
|
||||
bool? usingSupportedNode,
|
||||
}) async {
|
||||
final chainTip = chainTipParam ?? await getUpdatedChainTip();
|
||||
|
||||
|
@ -294,7 +312,7 @@ abstract class ElectrumWalletBase
|
|||
return;
|
||||
}
|
||||
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
syncStatus = StartingScanSyncStatus();
|
||||
|
||||
if (_isolate != null) {
|
||||
final runningIsolate = await _isolate!;
|
||||
|
@ -312,7 +330,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)
|
||||
|
@ -400,7 +420,7 @@ abstract class ElectrumWalletBase
|
|||
BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)),
|
||||
)
|
||||
: silentAddress.B_spend,
|
||||
hrp: silentAddress.hrp,
|
||||
network: network,
|
||||
);
|
||||
|
||||
final addressRecord = walletAddresses.silentAddresses
|
||||
|
@ -417,6 +437,10 @@ abstract class ElectrumWalletBase
|
|||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
if (syncStatus is SyncronizingSyncStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncStatus = SyncronizingSyncStatus();
|
||||
|
||||
if (hasSilentPaymentsScanning) {
|
||||
|
@ -428,8 +452,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);
|
||||
|
@ -445,14 +471,81 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
Future<void> updateFeeRates() async {
|
||||
if (await checkIfMempoolAPIIsEnabled()) {
|
||||
try {
|
||||
final response =
|
||||
await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended"));
|
||||
|
||||
final result = json.decode(response.body) as Map<String, num>;
|
||||
final slowFee = result['economyFee']?.toInt() ?? 0;
|
||||
final mediumFee = result['hourFee']?.toInt() ?? 0;
|
||||
final fastFee = result['fastestFee']?.toInt() ?? 0;
|
||||
_feeRates = [slowFee, mediumFee, fastFee];
|
||||
return;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final feeRates = await electrumClient.feeRates(network: network);
|
||||
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 {
|
||||
|
@ -514,13 +607,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;
|
||||
|
@ -531,17 +617,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(
|
||||
|
@ -553,7 +653,7 @@ abstract class ElectrumWalletBase
|
|||
isSilentPayment: isSilentPayment,
|
||||
),
|
||||
ownerDetails: UtxoAddressDetails(
|
||||
publicKey: privkey.getPublic().toHex(),
|
||||
publicKey: pubKeyHex,
|
||||
address: address,
|
||||
),
|
||||
),
|
||||
|
@ -940,11 +1040,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()) {
|
||||
|
@ -1085,8 +1203,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();
|
||||
}
|
||||
|
||||
|
@ -1126,10 +1249,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
|
||||
|
@ -1138,10 +1260,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 = [];
|
||||
|
@ -1229,7 +1350,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;
|
||||
|
||||
|
@ -1283,21 +1404,15 @@ 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?;
|
||||
|
||||
if (confirmations > 0) return false;
|
||||
|
||||
if (transactionHex == null) {
|
||||
Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
|
||||
try {
|
||||
final bundle = await getTransactionExpanded(hash: tx.txHash);
|
||||
_updateInputsAndOutputs(tx, bundle);
|
||||
if (bundle.confirmations > 0) return false;
|
||||
return bundle.originalTransaction.canReplaceByFee;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final original = bitcoin.Transaction.fromHex(transactionHex);
|
||||
|
||||
return original.ins
|
||||
.any((element) => element.sequence != null && element.sequence! < 4294967293);
|
||||
}
|
||||
|
||||
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
|
||||
|
@ -1338,6 +1453,7 @@ abstract class ElectrumWalletBase
|
|||
List<ECPrivate> privateKeys = [];
|
||||
|
||||
var allInputsAmount = 0;
|
||||
String? memo;
|
||||
|
||||
// Add inputs
|
||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
|
@ -1373,53 +1489,82 @@ abstract class ElectrumWalletBase
|
|||
);
|
||||
}
|
||||
|
||||
int totalOutAmount = bundle.originalTransaction.outputs
|
||||
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
|
||||
|
||||
var currentFee = allInputsAmount - totalOutAmount;
|
||||
int remainingFee = newFee - currentFee;
|
||||
|
||||
// Create a list of available outputs
|
||||
final outputs = <BitcoinOutput>[];
|
||||
|
||||
// Add outputs and deduct the fees from it
|
||||
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
|
||||
final out = bundle.originalTransaction.outputs[i];
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final btcAddress = addressTypeFromStr(address, network);
|
||||
|
||||
int newAmount;
|
||||
if (out.amount.toInt() >= remainingFee) {
|
||||
newAmount = out.amount.toInt() - remainingFee;
|
||||
remainingFee = 0;
|
||||
|
||||
// if new amount of output is less than dust amount, then don't add this output as well
|
||||
if (newAmount <= _dustAmount) {
|
||||
continue;
|
||||
for (final out in bundle.originalTransaction.outputs) {
|
||||
// Check if the script contains OP_RETURN
|
||||
final script = out.scriptPubKey.script;
|
||||
if (script.contains('OP_RETURN') && memo == null) {
|
||||
final index = script.indexOf('OP_RETURN');
|
||||
if (index + 1 <= script.length) {
|
||||
try {
|
||||
final opReturnData = script[index + 1].toString();
|
||||
memo = utf8.decode(HEX.decode(opReturnData));
|
||||
continue;
|
||||
} catch (_) {
|
||||
throw Exception('Cannot decode OP_RETURN data');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remainingFee -= out.amount.toInt();
|
||||
continue;
|
||||
}
|
||||
|
||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final btcAddress = addressTypeFromStr(address, network);
|
||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
|
||||
}
|
||||
|
||||
// Calculate the total amount and fees
|
||||
int totalOutAmount =
|
||||
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
|
||||
int currentFee = allInputsAmount - totalOutAmount;
|
||||
int remainingFee = newFee - currentFee;
|
||||
|
||||
if (remainingFee <= 0) {
|
||||
throw Exception("New fee must be higher than the current fee.");
|
||||
}
|
||||
|
||||
// Deduct Remaining Fee from Main Outputs
|
||||
if (remainingFee > 0) {
|
||||
for (int i = outputs.length - 1; i >= 0; i--) {
|
||||
int outputAmount = outputs[i].value.toInt();
|
||||
|
||||
if (outputAmount > _dustAmount) {
|
||||
int deduction = (outputAmount - _dustAmount >= remainingFee)
|
||||
? remainingFee
|
||||
: outputAmount - _dustAmount;
|
||||
outputs[i] = BitcoinOutput(
|
||||
address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
|
||||
remainingFee -= deduction;
|
||||
|
||||
if (remainingFee <= 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final check if the remaining fee couldn't be deducted
|
||||
if (remainingFee > 0) {
|
||||
throw Exception("Not enough funds to cover the fee.");
|
||||
}
|
||||
|
||||
// Identify all change outputs
|
||||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
||||
final List<BitcoinOutput> changeOutputs = outputs
|
||||
.where((output) => changeAddresses
|
||||
.any((element) => element.address == output.address.toAddress(network)))
|
||||
.toList();
|
||||
|
||||
// look for a change address in the outputs
|
||||
final changeOutput = outputs.firstWhereOrNull((output) =>
|
||||
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
|
||||
int totalChangeAmount =
|
||||
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
|
||||
|
||||
// deduct the change amount from the output amount
|
||||
if (changeOutput != null) {
|
||||
totalOutAmount -= changeOutput.value.toInt();
|
||||
}
|
||||
// The final amount that the receiver will receive
|
||||
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
|
||||
|
||||
final txb = BitcoinTransactionBuilder(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
fee: BigInt.from(newFee),
|
||||
network: network,
|
||||
memo: memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
enableRBF: true,
|
||||
);
|
||||
|
||||
|
@ -1442,10 +1587,10 @@ abstract class ElectrumWalletBase
|
|||
transaction,
|
||||
type,
|
||||
electrumClient: electrumClient,
|
||||
amount: totalOutAmount,
|
||||
amount: sendingAmount,
|
||||
fee: newFee,
|
||||
network: network,
|
||||
hasChange: changeOutput != null,
|
||||
hasChange: changeOutputs.isNotEmpty,
|
||||
feeRate: newFee.toString(),
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
|
@ -1456,50 +1601,73 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
|
||||
Future<ElectrumTransactionBundle> getTransactionExpanded(
|
||||
{required String hash, int? height}) async {
|
||||
String transactionHex;
|
||||
// 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));
|
||||
|
@ -1650,8 +1818,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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1767,15 +1935,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!);
|
||||
}
|
||||
|
||||
|
@ -1794,44 +2026,40 @@ abstract class ElectrumWalletBase
|
|||
});
|
||||
}
|
||||
|
||||
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
|
||||
if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
|
||||
return BitcoinCashNetwork.mainnet;
|
||||
}
|
||||
|
||||
if (networkType == litecoinNetwork) {
|
||||
return LitecoinNetwork.mainnet;
|
||||
}
|
||||
|
||||
if (networkType == bitcoin.testnet) {
|
||||
return BitcoinNetwork.testnet;
|
||||
}
|
||||
|
||||
return BitcoinNetwork.mainnet;
|
||||
}
|
||||
|
||||
static String _hardenedDerivationPath(String derivationPath) =>
|
||||
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 = {};
|
||||
|
||||
|
@ -1839,8 +2067,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
_isTryingToConnect = true;
|
||||
|
||||
Future.delayed(Duration(seconds: 10), () {
|
||||
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) {
|
||||
Timer(Duration(seconds: 10), () {
|
||||
if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) {
|
||||
this.electrumClient.connectToUri(
|
||||
node!.uri,
|
||||
useSSL: node!.useSSL ?? false,
|
||||
|
@ -1857,6 +2085,54 @@ abstract class ElectrumWalletBase
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) {
|
||||
tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList();
|
||||
|
||||
if (tx.inputAddresses == null ||
|
||||
tx.inputAddresses!.isEmpty ||
|
||||
tx.outputAddresses == null ||
|
||||
tx.outputAddresses!.isEmpty) {
|
||||
List<String> inputAddresses = [];
|
||||
List<String> outputAddresses = [];
|
||||
|
||||
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
final input = bundle.originalTransaction.inputs[i];
|
||||
final inputTransaction = bundle.ins[i];
|
||||
final vout = input.txIndex;
|
||||
final outTransaction = inputTransaction.outputs[vout];
|
||||
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) inputAddresses.add(address);
|
||||
}
|
||||
|
||||
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
|
||||
final out = bundle.originalTransaction.outputs[i];
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) outputAddresses.add(address);
|
||||
|
||||
// Check if the script contains OP_RETURN
|
||||
final script = out.scriptPubKey.script;
|
||||
if (script.contains('OP_RETURN')) {
|
||||
final index = script.indexOf('OP_RETURN');
|
||||
if (index + 1 <= script.length) {
|
||||
try {
|
||||
final opReturnData = script[index + 1].toString();
|
||||
final decodedString = utf8.decode(HEX.decode(opReturnData));
|
||||
outputAddresses.add('OP_RETURN:$decodedString');
|
||||
} catch (_) {
|
||||
outputAddresses.add('OP_RETURN:');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.inputAddresses = inputAddresses;
|
||||
tx.outputAddresses = outputAddresses;
|
||||
|
||||
transactionHistory.addOne(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScanNode {
|
||||
|
@ -1951,8 +2227,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,29 @@
|
|||
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_bitcoin/electrum_derivations.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 +36,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
|
@ -37,10 +48,12 @@ 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,
|
||||
passphrase: passphrase,
|
||||
currency: CryptoCurrency.ltc) {
|
||||
walletAddresses = LitecoinWalletAddresses(
|
||||
walletInfo,
|
||||
|
@ -48,7 +61,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 +74,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,
|
||||
|
@ -78,7 +92,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
}
|
||||
return LitecoinWallet(
|
||||
|
@ -88,6 +102,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase,
|
||||
seedBytes: seedBytes,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
|
@ -95,25 +111,80 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
walletInfo.derivationInfo ??= DerivationInfo();
|
||||
|
||||
// set the default if not present:
|
||||
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
|
||||
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
|
||||
|
||||
Uint8List? seedBytes = null;
|
||||
final mnemonic = keysData.mnemonic;
|
||||
final passphrase = keysData.passphrase;
|
||||
|
||||
if (mnemonic != null) {
|
||||
switch (walletInfo.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
seedBytes = await bip39.mnemonicToSeed(
|
||||
mnemonic,
|
||||
passphrase: passphrase ?? "",
|
||||
);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return LitecoinWallet(
|
||||
mnemonic: snp.mnemonic!,
|
||||
mnemonic: keysData.mnemonic!,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await mnemonicToSeedBytes(snp.mnemonic!),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex,
|
||||
addressPageType: snp.addressPageType,
|
||||
initialAddresses: snp?.addresses,
|
||||
initialBalance: snp?.balance,
|
||||
seedBytes: seedBytes!,
|
||||
passphrase: passphrase,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||
addressPageType: snp?.addressPageType,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -132,4 +203,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,6 @@
|
|||
import 'dart:io';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
|
@ -16,23 +18,40 @@ 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;
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final String mnemonic;
|
||||
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
mnemonic = await generateElectrumMnemonic();
|
||||
break;
|
||||
}
|
||||
|
||||
final wallet = await LitecoinWalletBase.create(
|
||||
mnemonic: await generateElectrumMnemonic(),
|
||||
password: credentials.password!,
|
||||
passphrase: credentials.passphrase,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
passphrase: credentials.passphrase,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
|
||||
|
@ -45,21 +64,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 +94,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 +124,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;
|
||||
|
|