Merge branch 'main' of https://github.com/cake-tech/cake_wallet into now_nodes_integration
57
.github/workflows/pr_test_build.yml
vendored
|
@ -6,9 +6,9 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch name to build'
|
||||
description: "Branch name to build"
|
||||
required: true
|
||||
default: 'main'
|
||||
default: "main"
|
||||
|
||||
jobs:
|
||||
PR_test_build:
|
||||
|
@ -16,6 +16,7 @@ jobs:
|
|||
env:
|
||||
STORE_PASS: test@cake_wallet
|
||||
KEY_PASS: test@cake_wallet
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
|
||||
steps:
|
||||
- name: is pr
|
||||
|
@ -104,22 +105,14 @@ jobs:
|
|||
- name: Build generated code
|
||||
run: |
|
||||
cd /opt/android/cake_wallet
|
||||
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && cd ..
|
||||
cd cw_polygon && flutter pub get && cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
./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
|
||||
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
|
||||
|
@ -150,49 +143,51 @@ jobs:
|
|||
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 robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> 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
|
||||
|
||||
- name: Rename app
|
||||
run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
|
||||
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 apk --release
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
# - name: Push to App Center
|
||||
# run: |
|
||||
# echo 'Installing App Center CLI tools'
|
||||
# npm install -g appcenter-cli
|
||||
# echo "Publishing test to App Center"
|
||||
# appcenter distribute release \
|
||||
# --group "Testers" \
|
||||
# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
|
||||
# --release-notes ${{ env.BRANCH_NAME }} \
|
||||
# --app Cake-Labs/Cake-Wallet \
|
||||
# --token ${{ secrets.APP_CENTER_TOKEN }} \
|
||||
# --quiet
|
||||
# - name: Push to App Center
|
||||
# run: |
|
||||
# echo 'Installing App Center CLI tools'
|
||||
# npm install -g appcenter-cli
|
||||
# echo "Publishing test to App Center"
|
||||
# appcenter distribute release \
|
||||
# --group "Testers" \
|
||||
# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
|
||||
# --release-notes ${{ env.BRANCH_NAME }} \
|
||||
# --app Cake-Labs/Cake-Wallet \
|
||||
# --token ${{ secrets.APP_CENTER_TOKEN }} \
|
||||
# --quiet
|
||||
|
||||
- name: Rename apk file
|
||||
run: |
|
||||
cd /opt/android/cake_wallet/build/app/outputs/apk/release
|
||||
cd /opt/android/cake_wallet/build/app/outputs/flutter-apk
|
||||
mkdir test-apk
|
||||
cp app-release.apk test-apk/${{env.BRANCH_NAME}}.apk
|
||||
cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: kittaakos/upload-artifact-as-is@v0
|
||||
with:
|
||||
path: /opt/android/cake_wallet/build/app/outputs/apk/release/test-apk/
|
||||
path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/
|
||||
|
||||
- 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/app/outputs/apk/release/app-release.apk
|
||||
path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/${{env.BRANCH_NAME}}.apk
|
||||
channel: ${{ secrets.SLACK_APK_CHANNEL }}
|
||||
title: "${{ env.BRANCH_NAME }}.apk"
|
||||
filename: ${{ env.BRANCH_NAME }}.apk
|
||||
|
|
4
.gitignore
vendored
|
@ -86,14 +86,17 @@ cw_monero/cw_monero/android/.cxx/
|
|||
**/*.g.dart
|
||||
|
||||
android/key.properties
|
||||
android/app/key.jks
|
||||
|
||||
**/tool/.secrets-prod.json
|
||||
**/tool/.secrets-test.json
|
||||
**/tool/.secrets-config.json
|
||||
**/tool/.evm-secrets-config.json
|
||||
**/tool/.ethereum-secrets-config.json
|
||||
**/tool/.solana-secrets-config.json
|
||||
**/lib/.secrets.g.dart
|
||||
**/cw_evm/lib/.secrets.g.dart
|
||||
**/cw_solana/lib/.secrets.g.dart
|
||||
|
||||
vendor/
|
||||
|
||||
|
@ -128,6 +131,7 @@ lib/ethereum/ethereum.dart
|
|||
lib/bitcoin_cash/bitcoin_cash.dart
|
||||
lib/nano/nano.dart
|
||||
lib/polygon/polygon.dart
|
||||
lib/solana/solana.dart
|
||||
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
|
@ -65,6 +66,7 @@
|
|||
<data android:scheme="polygon" />
|
||||
<data android:scheme="polygon-wallet" />
|
||||
<data android:scheme="polygon_wallet" />
|
||||
<data android:scheme="solana-wallet" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
|
|
|
@ -15,6 +15,10 @@ import android.os.Bundle;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.WindowManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.unstoppabledomains.resolution.DomainResolution;
|
||||
import com.unstoppabledomains.resolution.Resolution;
|
||||
|
@ -65,6 +69,14 @@ public class MainActivity extends FlutterFragmentActivity {
|
|||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
break;
|
||||
case "disableBatteryOptimization":
|
||||
disableBatteryOptimization();
|
||||
handler.post(() -> result.success(null));
|
||||
break;
|
||||
case "isBatteryOptimizationDisabled":
|
||||
boolean isDisabled = isBatteryOptimizationDisabled();
|
||||
handler.post(() -> result.success(isDisabled));
|
||||
break;
|
||||
default:
|
||||
handler.post(() -> result.notImplemented());
|
||||
}
|
||||
|
@ -89,4 +101,22 @@ public class MainActivity extends FlutterFragmentActivity {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void disableBatteryOptimization() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + packageName));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBatteryOptimizationDisabled() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
return pm.isIgnoringBatteryOptimizations(packageName);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,10 @@ import android.os.Build;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.WindowManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.unstoppabledomains.resolution.DomainResolution;
|
||||
import com.unstoppabledomains.resolution.Resolution;
|
||||
|
@ -55,6 +59,14 @@ public class MainActivity extends FlutterFragmentActivity {
|
|||
handler.post(() -> result.success(""));
|
||||
}
|
||||
break;
|
||||
case "disableBatteryOptimization":
|
||||
disableBatteryOptimization();
|
||||
handler.post(() -> result.success(null));
|
||||
break;
|
||||
case "isBatteryOptimizationDisabled":
|
||||
boolean isDisabled = isBatteryOptimizationDisabled();
|
||||
handler.post(() -> result.success(isDisabled));
|
||||
break;
|
||||
default:
|
||||
handler.post(() -> result.notImplemented());
|
||||
}
|
||||
|
@ -79,4 +91,22 @@ public class MainActivity extends FlutterFragmentActivity {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void disableBatteryOptimization() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + packageName));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBatteryOptimizationDisabled() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
return pm.isIgnoringBatteryOptimizations(packageName);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,10 @@ import android.os.Build;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.WindowManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.unstoppabledomains.resolution.DomainResolution;
|
||||
import com.unstoppabledomains.resolution.Resolution;
|
||||
|
@ -64,6 +68,14 @@ public class MainActivity extends FlutterFragmentActivity {
|
|||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
break;
|
||||
case "disableBatteryOptimization":
|
||||
disableBatteryOptimization();
|
||||
handler.post(() -> result.success(null));
|
||||
break;
|
||||
case "isBatteryOptimizationDisabled":
|
||||
boolean isDisabled = isBatteryOptimizationDisabled();
|
||||
handler.post(() -> result.success(isDisabled));
|
||||
break;
|
||||
default:
|
||||
handler.post(() -> result.notImplemented());
|
||||
}
|
||||
|
@ -88,4 +100,22 @@ public class MainActivity extends FlutterFragmentActivity {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void disableBatteryOptimization() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + packageName));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBatteryOptimizationDisabled() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
return pm.isIgnoringBatteryOptimizations(packageName);
|
||||
}
|
||||
|
||||
}
|
BIN
assets/images/avdo_icon.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
assets/images/bonk_icon.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
assets/images/digibyte.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/images/gmt_icon.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/images/hnt_icon.png
Normal file
After Width: | Height: | Size: 10 KiB |
69
assets/images/notification_icon.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="510px"
|
||||
height="510px"
|
||||
viewBox="0 0 510 510"
|
||||
style="enable-background:new 0 0 510 510;"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="notification_logo_tilt_white.svg"><metadata
|
||||
id="metadata42"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs40" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1090"
|
||||
id="namedview38"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.4627451"
|
||||
inkscape:cx="-849.27966"
|
||||
inkscape:cy="255"
|
||||
inkscape:window-x="-12"
|
||||
inkscape:window-y="58"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="notifications"
|
||||
inkscape:object-paths="true" /><g
|
||||
id="g3"
|
||||
transform="matrix(0.87658593,0,0,0.87658593,31.470588,31.470588)"><g
|
||||
id="notifications"
|
||||
transform="rotate(-20,255,255)"><path
|
||||
d="m 233.56017,502.19654 c 28.05,0 51,-22.95 51,-51 h -102 c 0,28.05 22.95,51 51,51 z m 165.75,-153 v -140.25 c 0,-79.05 -53.55,-142.8 -127.5,-160.65 v -17.85 c 0,-20.4 -17.85,-38.2499996 -38.25,-38.2499996 -20.4,0 -38.25,17.8499996 -38.25,38.2499996 v 17.85 c -73.95,17.85 -127.499999,81.6 -127.499999,160.65 v 140.25 l -51,51 v 25.5 H 450.31017 v -25.5 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:19.39342117;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></g></g><g
|
||||
id="g8" /><g
|
||||
id="g10" /><g
|
||||
id="g12" /><g
|
||||
id="g14" /><g
|
||||
id="g16" /><g
|
||||
id="g18" /><g
|
||||
id="g20" /><g
|
||||
id="g22" /><g
|
||||
id="g24" /><g
|
||||
id="g26" /><g
|
||||
id="g28" /><g
|
||||
id="g30" /><g
|
||||
id="g32" /><g
|
||||
id="g34" /><g
|
||||
id="g36" /></svg>
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/images/ray_icon.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/images/status_website_image.png
Normal file
After Width: | Height: | Size: 546 B |
4
assets/solana_node_list.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
-
|
||||
uri: rpc.ankr.com
|
||||
is_default: true
|
||||
useSSL: true
|
|
@ -1,3 +1,4 @@
|
|||
Security and Privacy enhancements
|
||||
Usability enhancements
|
||||
Bug fixes
|
||||
Monero enhancements
|
||||
In-App live status page for the app services
|
||||
Add Exolix exchange provider
|
||||
Bug fixes and enhancements
|
|
@ -1,4 +1 @@
|
|||
List previously used Bitcoin addresses
|
||||
Security and Privacy enhancements
|
||||
Usability enhancements
|
||||
Bug fixes
|
||||
Bug fixes and enhancements
|
|
@ -30,6 +30,7 @@ cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build
|
|||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && cd ..
|
||||
cd cw_polygon && flutter pub get && cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
|
||||
String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) {
|
||||
String addressFromOutputScript(Script script, BasedUtxoNetwork network) {
|
||||
try {
|
||||
return bitcoin.P2PKH(
|
||||
data: PaymentData(output: script),
|
||||
network: networkType)
|
||||
.data
|
||||
.address!;
|
||||
switch (script.getAddressType()) {
|
||||
case P2pkhAddressType.p2pkh:
|
||||
return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network);
|
||||
case P2shAddressType.p2pkInP2sh:
|
||||
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
|
||||
case SegwitAddresType.p2wpkh:
|
||||
return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network);
|
||||
case P2shAddressType.p2pkhInP2sh:
|
||||
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
|
||||
case SegwitAddresType.p2wsh:
|
||||
return P2wshAddress.fromScriptPubkey(script: script).toAddress(network);
|
||||
case SegwitAddresType.p2tr:
|
||||
return P2trAddress.fromScriptPubkey(script: script).toAddress(network);
|
||||
default:
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
return bitcoin.P2WPKH(
|
||||
data: PaymentData(output: script),
|
||||
network: networkType)
|
||||
.data
|
||||
.address!;
|
||||
} catch(_) {}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,12 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||
import 'package:bitcoin_flutter/src/utils/constants/op.dart';
|
||||
import 'package:bitcoin_flutter/src/utils/script.dart' as bscript;
|
||||
import 'package:bitcoin_flutter/src/address.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
|
||||
|
||||
Uint8List p2shAddressToOutputScript(String address) {
|
||||
final decodeBase58 = bs58check.decode(address);
|
||||
final hash = decodeBase58.sublist(1);
|
||||
return bscript.compile(<dynamic>[OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]);
|
||||
}
|
||||
|
||||
Uint8List addressToOutputScript(
|
||||
String address, bitcoin.NetworkType networkType) {
|
||||
List<int> addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) {
|
||||
try {
|
||||
// FIXME: improve validation for p2sh addresses
|
||||
// 3 for bitcoin
|
||||
// m for litecoin
|
||||
if (address.startsWith('3') || address.toLowerCase().startsWith('m')) {
|
||||
return p2shAddressToOutputScript(address);
|
||||
if (network == bitcoin.BitcoinCashNetwork.mainnet) {
|
||||
return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes();
|
||||
}
|
||||
|
||||
return Address.addressToOutputScript(address, networkType);
|
||||
return bitcoin.addressToOutputScript(address: address, network: network);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
return Uint8List(0);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart' as sh;
|
||||
|
||||
class BitcoinAddressRecord {
|
||||
BitcoinAddressRecord(
|
||||
|
@ -10,30 +12,47 @@ class BitcoinAddressRecord {
|
|||
int balance = 0,
|
||||
String name = '',
|
||||
bool isUsed = false,
|
||||
required this.type,
|
||||
String? scriptHash,
|
||||
required this.network,
|
||||
}) : _txCount = txCount,
|
||||
_balance = balance,
|
||||
_name = name,
|
||||
_isUsed = isUsed;
|
||||
_isUsed = isUsed,
|
||||
scriptHash = scriptHash ?? sh.scriptHash(address, network: network);
|
||||
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) {
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
return BitcoinAddressRecord(decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
name: decoded['name'] as String? ?? '',
|
||||
balance: decoded['balance'] as int? ?? 0);
|
||||
return BitcoinAddressRecord(
|
||||
decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
name: decoded['name'] as String? ?? '',
|
||||
balance: decoded['balance'] as int? ?? 0,
|
||||
type: decoded['type'] != null && decoded['type'] != ''
|
||||
? BitcoinAddressType.values
|
||||
.firstWhere((type) => type.toString() == decoded['type'] as String)
|
||||
: SegwitAddresType.p2wpkh,
|
||||
scriptHash: decoded['scriptHash'] as String?,
|
||||
network: network,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address;
|
||||
|
||||
final String address;
|
||||
final bool isHidden;
|
||||
bool isHidden;
|
||||
final int index;
|
||||
int _txCount;
|
||||
int _balance;
|
||||
String _name;
|
||||
bool _isUsed;
|
||||
String? scriptHash;
|
||||
BasedUtxoNetwork network;
|
||||
|
||||
int get txCount => _txCount;
|
||||
|
||||
|
@ -50,21 +69,25 @@ class BitcoinAddressRecord {
|
|||
void setAsUsed() => _isUsed = true;
|
||||
void setNewName(String label) => _name = label;
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address;
|
||||
|
||||
@override
|
||||
int get hashCode => address.hashCode;
|
||||
|
||||
String get cashAddr => bitbox.Address.toCashAddress(address);
|
||||
BitcoinAddressType type;
|
||||
|
||||
String updateScriptHash(BasedUtxoNetwork network) {
|
||||
scriptHash = sh.scriptHash(address, network: network);
|
||||
return scriptHash!;
|
||||
}
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'address': address,
|
||||
'index': index,
|
||||
'isHidden': isHidden,
|
||||
'isUsed': isUsed,
|
||||
'txCount': txCount,
|
||||
'name': name,
|
||||
'isUsed': isUsed,
|
||||
'balance': balance,
|
||||
'type': type.toString(),
|
||||
'scriptHash': scriptHash,
|
||||
});
|
||||
}
|
||||
|
|
42
cw_bitcoin/lib/bitcoin_receive_page_option.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
|
||||
class BitcoinReceivePageOption implements ReceivePageOption {
|
||||
static const p2wpkh = BitcoinReceivePageOption._('Segwit (P2WPKH) (Default)');
|
||||
static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible (P2SH)');
|
||||
static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)');
|
||||
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
|
||||
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
|
||||
|
||||
const BitcoinReceivePageOption._(this.value);
|
||||
|
||||
final String value;
|
||||
|
||||
String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static const all = [
|
||||
BitcoinReceivePageOption.p2wpkh,
|
||||
BitcoinReceivePageOption.p2tr,
|
||||
BitcoinReceivePageOption.p2wsh,
|
||||
BitcoinReceivePageOption.p2sh,
|
||||
BitcoinReceivePageOption.p2pkh
|
||||
];
|
||||
|
||||
factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) {
|
||||
switch (type) {
|
||||
case SegwitAddresType.p2tr:
|
||||
return BitcoinReceivePageOption.p2tr;
|
||||
case SegwitAddresType.p2wsh:
|
||||
return BitcoinReceivePageOption.p2wsh;
|
||||
case P2pkhAddressType.p2pkh:
|
||||
return BitcoinReceivePageOption.p2pkh;
|
||||
case P2shAddressType.p2wpkhInP2sh:
|
||||
return BitcoinReceivePageOption.p2sh;
|
||||
case SegwitAddresType.p2wpkh:
|
||||
default:
|
||||
return BitcoinReceivePageOption.p2wpkh;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,10 +6,9 @@ class BitcoinUnspent extends Unspent {
|
|||
: bitcoinAddressRecord = addressRecord,
|
||||
super(addressRecord.address, hash, value, vout, null);
|
||||
|
||||
factory BitcoinUnspent.fromJSON(
|
||||
BitcoinAddressRecord address, Map<String, dynamic> json) =>
|
||||
BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int,
|
||||
json['tx_pos'] as int);
|
||||
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) =>
|
||||
BitcoinUnspent(
|
||||
address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int);
|
||||
|
||||
final BitcoinAddressRecord bitcoinAddressRecord;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
|
@ -17,36 +18,42 @@ part 'bitcoin_wallet.g.dart';
|
|||
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
|
||||
|
||||
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
BitcoinWalletBase(
|
||||
{required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(
|
||||
BitcoinWalletBase({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
String? addressPageType,
|
||||
BasedUtxoNetwork? networkParam,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
}) : super(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
networkType: bitcoin.bitcoin,
|
||||
networkType: networkParam == null
|
||||
? bitcoin.bitcoin
|
||||
: networkParam == BitcoinNetwork.mainnet
|
||||
? bitcoin.bitcoin
|
||||
: bitcoin.testnet,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.btc) {
|
||||
walletAddresses = BitcoinWalletAddresses(
|
||||
walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
|
||||
.derivePath("m/0'/1"),
|
||||
networkType: networkType);
|
||||
walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
|
||||
network: networkParam ?? network,
|
||||
);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
|
@ -57,21 +64,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
String? addressPageType,
|
||||
BasedUtxoNetwork? network,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
}) async {
|
||||
return BitcoinWallet(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex);
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
addressPageType: addressPageType,
|
||||
networkParam: network,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<BitcoinWallet> open({
|
||||
|
@ -80,16 +92,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
}) async {
|
||||
final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password);
|
||||
final network = walletInfo.network != null
|
||||
? BasedUtxoNetwork.fromName(walletInfo.network!)
|
||||
: BitcoinNetwork.mainnet;
|
||||
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
|
||||
|
||||
return BitcoinWallet(
|
||||
mnemonic: snp.mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex);
|
||||
mnemonic: snp.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,
|
||||
networkParam: network,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
@ -11,24 +10,31 @@ part 'bitcoin_wallet_addresses.g.dart';
|
|||
class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses;
|
||||
|
||||
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||
BitcoinWalletAddressesBase(WalletInfo walletInfo,
|
||||
{required bitcoin.HDWallet mainHd,
|
||||
required bitcoin.HDWallet sideHd,
|
||||
required bitcoin.NetworkType networkType,
|
||||
required ElectrumClient electrumClient,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: mainHd,
|
||||
sideHd: sideHd,
|
||||
electrumClient: electrumClient,
|
||||
networkType: networkType);
|
||||
BitcoinWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required super.mainHd,
|
||||
required super.sideHd,
|
||||
required super.network,
|
||||
required super.electrumClient,
|
||||
super.initialAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
}) : super(walletInfo);
|
||||
|
||||
@override
|
||||
String getAddress({required int index, required bitcoin.HDWallet hd}) =>
|
||||
generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
|
||||
String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) {
|
||||
if (addressType == P2pkhAddressType.p2pkh)
|
||||
return generateP2PKHAddress(hd: hd, index: index, network: network);
|
||||
|
||||
if (addressType == SegwitAddresType.p2tr)
|
||||
return generateP2TRAddress(hd: hd, index: index, network: network);
|
||||
|
||||
if (addressType == SegwitAddresType.p2wsh)
|
||||
return generateP2WSHAddress(hd: hd, index: index, network: network);
|
||||
|
||||
if (addressType == P2shAddressType.p2wpkhInP2sh)
|
||||
return generateP2SHAddress(hd: hd, index: index, network: network);
|
||||
|
||||
return generateP2WPKHAddress(hd: hd, index: index, network: network);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
@ -12,10 +13,8 @@ import 'package:cw_core/wallet_type.dart';
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class BitcoinWalletService extends WalletService<
|
||||
BitcoinNewWalletCredentials,
|
||||
BitcoinRestoreWalletFromSeedCredentials,
|
||||
BitcoinRestoreWalletFromWIFCredentials> {
|
||||
class BitcoinWalletService extends WalletService<BitcoinNewWalletCredentials,
|
||||
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> {
|
||||
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
@ -25,12 +24,17 @@ class BitcoinWalletService extends WalletService<
|
|||
WalletType getType() => WalletType.bitcoin;
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials) async {
|
||||
Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
||||
credentials.walletInfo?.network = network.value;
|
||||
|
||||
final wallet = await BitcoinWalletBase.create(
|
||||
mnemonic: await generateMnemonic(),
|
||||
password: credentials.password!,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
mnemonic: await generateMnemonic(),
|
||||
password: credentials.password!,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
network: network,
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
|
@ -42,28 +46,41 @@ class BitcoinWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<BitcoinWallet> openWallet(String name, String password) async {
|
||||
final walletInfo = walletInfoSource.values.firstWhereOrNull(
|
||||
(info) => info.id == WalletBase.idFor(name, getType()))!;
|
||||
final wallet = await BitcoinWalletBase.open(
|
||||
password: password, name: name, walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
|
||||
try {
|
||||
final wallet = await BitcoinWalletBase.open(
|
||||
password: password,
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
saveBackup(name);
|
||||
return wallet;
|
||||
} catch (_) {
|
||||
await restoreWalletFilesFromBackup(name);
|
||||
final wallet = await BitcoinWalletBase.open(
|
||||
password: password,
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@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 BitcoinWalletBase.open(
|
||||
password: password,
|
||||
name: currentName,
|
||||
|
@ -71,6 +88,7 @@ class BitcoinWalletService extends WalletService<
|
|||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
@ -80,24 +98,29 @@ class BitcoinWalletService extends WalletService<
|
|||
}
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> restoreFromKeys(
|
||||
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
|
||||
Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials,
|
||||
{bool? isTestnet}) async =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> restoreFromSeed(
|
||||
BitcoinRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
if (!validateMnemonic(credentials.mnemonic)) {
|
||||
throw BitcoinMnemonicIsIncorrectException();
|
||||
}
|
||||
|
||||
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
||||
credentials.walletInfo?.network = network.value;
|
||||
|
||||
final wallet = await BitcoinWalletBase.create(
|
||||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
network: network,
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
String jsonrpcparams(List<Object> params) {
|
||||
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
|
||||
|
@ -22,10 +22,7 @@ String jsonrpc(
|
|||
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
|
||||
|
||||
class SocketTask {
|
||||
SocketTask({
|
||||
required this.isSubscription,
|
||||
this.completer,
|
||||
this.subject});
|
||||
SocketTask({required this.isSubscription, this.completer, this.subject});
|
||||
|
||||
final Completer<dynamic>? completer;
|
||||
final BehaviorSubject<dynamic>? subject;
|
||||
|
@ -51,8 +48,7 @@ class ElectrumClient {
|
|||
Timer? _aliveTimer;
|
||||
String unterminatedString;
|
||||
|
||||
Future<void> connectToUri(Uri uri) async =>
|
||||
await connect(host: uri.host, port: uri.port);
|
||||
Future<void> connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port);
|
||||
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
try {
|
||||
|
@ -104,21 +100,20 @@ class ElectrumClient {
|
|||
}
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response =
|
||||
json.decode(unterminatedString) as Map<String, dynamic>;
|
||||
final response = json.decode(unterminatedString) as Map<String, dynamic>;
|
||||
_handleResponse(response);
|
||||
unterminatedString = '';
|
||||
}
|
||||
} on TypeError catch (e) {
|
||||
if (!e.toString().contains('Map<String, Object>') && !e.toString().contains('Map<String, dynamic>')) {
|
||||
if (!e.toString().contains('Map<String, Object>') &&
|
||||
!e.toString().contains('Map<String, dynamic>')) {
|
||||
return;
|
||||
}
|
||||
|
||||
unterminatedString += message;
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response =
|
||||
json.decode(unterminatedString) as Map<String, dynamic>;
|
||||
final response = json.decode(unterminatedString) as Map<String, dynamic>;
|
||||
_handleResponse(response);
|
||||
// unterminatedString = null;
|
||||
unterminatedString = '';
|
||||
|
@ -142,8 +137,7 @@ class ElectrumClient {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<String>> version() =>
|
||||
call(method: 'server.version').then((dynamic result) {
|
||||
Future<List<String>> version() => call(method: 'server.version').then((dynamic result) {
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) => val.toString()).toList();
|
||||
}
|
||||
|
@ -178,11 +172,10 @@ class ElectrumClient {
|
|||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
|
||||
String address, NetworkType networkType) =>
|
||||
String address, BasedUtxoNetwork network) =>
|
||||
call(
|
||||
method: 'blockchain.scripthash.listunspent',
|
||||
params: [scriptHash(address, networkType: networkType)])
|
||||
.then((dynamic result) {
|
||||
method: 'blockchain.scripthash.listunspent',
|
||||
params: [scriptHash(address, network: network)]).then((dynamic result) {
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) {
|
||||
if (val is Map<String, dynamic>) {
|
||||
|
@ -229,8 +222,7 @@ class ElectrumClient {
|
|||
return [];
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> getTransactionRaw(
|
||||
{required String hash}) async =>
|
||||
Future<Map<String, dynamic>> getTransactionRaw({required String hash}) async =>
|
||||
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000)
|
||||
.then((dynamic result) {
|
||||
if (result is Map<String, dynamic>) {
|
||||
|
@ -240,8 +232,7 @@ class ElectrumClient {
|
|||
return <String, dynamic>{};
|
||||
});
|
||||
|
||||
Future<String> getTransactionHex(
|
||||
{required String hash}) async =>
|
||||
Future<String> getTransactionHex({required String hash}) async =>
|
||||
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000)
|
||||
.then((dynamic result) {
|
||||
if (result is String) {
|
||||
|
@ -252,29 +243,40 @@ class ElectrumClient {
|
|||
});
|
||||
|
||||
Future<String> broadcastTransaction(
|
||||
{required String transactionRaw}) async =>
|
||||
call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
|
||||
.then((dynamic result) {
|
||||
if (result is String) {
|
||||
return result;
|
||||
{required String transactionRaw, BasedUtxoNetwork? network}) async {
|
||||
if (network == BitcoinNetwork.testnet) {
|
||||
return http
|
||||
.post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'),
|
||||
headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'},
|
||||
body: transactionRaw)
|
||||
.then((http.Response response) {
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return '';
|
||||
throw Exception('Failed to broadcast transaction: ${response.body}');
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getMerkle(
|
||||
{required String hash, required int height}) async =>
|
||||
await call(
|
||||
method: 'blockchain.transaction.get_merkle',
|
||||
params: [hash, height]) as Map<String, dynamic>;
|
||||
return call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
|
||||
.then((dynamic result) {
|
||||
if (result is String) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getHeader({required int height}) async =>
|
||||
await call(method: 'blockchain.block.get_header', params: [height])
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
|
||||
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
|
||||
as Map<String, dynamic>;
|
||||
|
||||
Future<Map<String, dynamic>> getHeader({required int height}) async =>
|
||||
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
|
||||
|
||||
Future<double> estimatefee({required int p}) =>
|
||||
call(method: 'blockchain.estimatefee', params: [p])
|
||||
.then((dynamic result) {
|
||||
call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) {
|
||||
if (result is double) {
|
||||
return result;
|
||||
}
|
||||
|
@ -314,20 +316,17 @@ class ElectrumClient {
|
|||
return [];
|
||||
});
|
||||
|
||||
Future<List<int>> feeRates() async {
|
||||
Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
|
||||
if (network == BitcoinNetwork.testnet) {
|
||||
return [1, 1, 1];
|
||||
}
|
||||
try {
|
||||
final topDoubleString = await estimatefee(p: 1);
|
||||
final middleDoubleString = await estimatefee(p: 5);
|
||||
final bottomDoubleString = await estimatefee(p: 100);
|
||||
final top =
|
||||
(stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000)
|
||||
.round();
|
||||
final middle =
|
||||
(stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000)
|
||||
.round();
|
||||
final bottom =
|
||||
(stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000)
|
||||
.round();
|
||||
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
|
||||
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
|
||||
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
|
||||
|
||||
return [bottom, middle, top];
|
||||
} catch (_) {
|
||||
|
@ -335,6 +334,21 @@ class ElectrumClient {
|
|||
}
|
||||
}
|
||||
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
|
||||
// example response:
|
||||
// {
|
||||
// "height": 520481,
|
||||
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
|
||||
// }
|
||||
Future<int?> getCurrentBlockChainTip() =>
|
||||
call(method: 'blockchain.headers.subscribe').then((result) {
|
||||
if (result is Map<String, dynamic>) {
|
||||
return result["height"] as int;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
|
||||
_id += 1;
|
||||
return subscribe<Object>(
|
||||
|
@ -344,16 +358,14 @@ class ElectrumClient {
|
|||
}
|
||||
|
||||
BehaviorSubject<T>? subscribe<T>(
|
||||
{required String id,
|
||||
required String method,
|
||||
List<Object> params = const []}) {
|
||||
{required String id, required String method, List<Object> params = const []}) {
|
||||
try {
|
||||
final subscription = BehaviorSubject<T>();
|
||||
_regisrySubscription(id, subscription);
|
||||
socket!.write(jsonrpc(method: method, id: _id, params: params));
|
||||
|
||||
return subscription;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
return null;
|
||||
}
|
||||
|
@ -370,9 +382,7 @@ 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 = 4000}) async {
|
||||
try {
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
|
@ -386,7 +396,7 @@ class ElectrumClient {
|
|||
});
|
||||
|
||||
return completer.future;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
|
@ -397,8 +407,8 @@ class ElectrumClient {
|
|||
onConnectionStatusChange = null;
|
||||
}
|
||||
|
||||
void _registryTask(int id, Completer<dynamic> completer) => _tasks[id.toString()] =
|
||||
SocketTask(completer: completer, isSubscription: false);
|
||||
void _registryTask(int id, Completer<dynamic> completer) =>
|
||||
_tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false);
|
||||
|
||||
void _regisrySubscription(String id, BehaviorSubject<dynamic> subject) =>
|
||||
_tasks[id] = SocketTask(subject: subject, isSubscription: true);
|
||||
|
@ -419,8 +429,7 @@ class ElectrumClient {
|
|||
}
|
||||
}
|
||||
|
||||
void _methodHandler(
|
||||
{required String method, required Map<String, dynamic> request}) {
|
||||
void _methodHandler({required String method, required Map<String, dynamic> request}) {
|
||||
switch (method) {
|
||||
case 'blockchain.scripthash.subscribe':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
|
@ -451,8 +460,8 @@ class ElectrumClient {
|
|||
_methodHandler(method: method, request: response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != null){
|
||||
|
||||
if (id != null) {
|
||||
_finish(id, result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
|
@ -10,13 +11,12 @@ import 'package:cw_core/wallet_type.dart';
|
|||
|
||||
class ElectrumTransactionBundle {
|
||||
ElectrumTransactionBundle(this.originalTransaction,
|
||||
{required this.ins,
|
||||
required this.confirmations,
|
||||
this.time});
|
||||
final bitcoin.Transaction originalTransaction;
|
||||
final List<bitcoin.Transaction> ins;
|
||||
{required this.ins, required this.confirmations, this.time, required this.height});
|
||||
final BtcTransaction originalTransaction;
|
||||
final List<BtcTransaction> ins;
|
||||
final int? time;
|
||||
final int confirmations;
|
||||
final int height;
|
||||
}
|
||||
|
||||
class ElectrumTransactionInfo extends TransactionInfo {
|
||||
|
@ -39,8 +39,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
this.confirmations = confirmations;
|
||||
}
|
||||
|
||||
factory ElectrumTransactionInfo.fromElectrumVerbose(
|
||||
Map<String, Object> obj, WalletType type,
|
||||
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
|
||||
{required List<BitcoinAddressRecord> addresses, required int height}) {
|
||||
final addressesSet = addresses.map((addr) => addr.address).toSet();
|
||||
final id = obj['txid'] as String;
|
||||
|
@ -58,10 +57,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
for (dynamic vin in vins) {
|
||||
final vout = vin['vout'] as int;
|
||||
final out = vin['tx']['vout'][vout] as Map;
|
||||
final outAddresses =
|
||||
(out['scriptPubKey']['addresses'] as List<Object>?)?.toSet();
|
||||
inputsAmount +=
|
||||
stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString());
|
||||
final outAddresses = (out['scriptPubKey']['addresses'] as List<Object>?)?.toSet();
|
||||
inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString());
|
||||
|
||||
if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) {
|
||||
direction = TransactionDirection.outgoing;
|
||||
|
@ -69,11 +66,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
}
|
||||
|
||||
for (dynamic out in vout) {
|
||||
final outAddresses =
|
||||
out['scriptPubKey']['addresses'] as List<Object>? ?? [];
|
||||
final outAddresses = out['scriptPubKey']['addresses'] as List<Object>? ?? [];
|
||||
final ntrs = outAddresses.toSet().intersection(addressesSet);
|
||||
final value = stringDoubleToBitcoinAmount(
|
||||
(out['value'] as double? ?? 0.0).toString());
|
||||
final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString());
|
||||
totalOutAmount += value;
|
||||
|
||||
if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
|
||||
|
@ -96,44 +91,50 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
}
|
||||
|
||||
factory ElectrumTransactionInfo.fromElectrumBundle(
|
||||
ElectrumTransactionBundle bundle,
|
||||
WalletType type,
|
||||
bitcoin.NetworkType networkType,
|
||||
{required Set<String> addresses,
|
||||
required int height}) {
|
||||
ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network,
|
||||
{required Set<String> addresses, required int height}) {
|
||||
final date = bundle.time != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
|
||||
: DateTime.now();
|
||||
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
|
||||
: DateTime.now();
|
||||
var direction = TransactionDirection.incoming;
|
||||
var amount = 0;
|
||||
var inputAmount = 0;
|
||||
var totalOutAmount = 0;
|
||||
|
||||
for (var i = 0; i < bundle.originalTransaction.ins.length; i++) {
|
||||
final input = bundle.originalTransaction.ins[i];
|
||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
final input = bundle.originalTransaction.inputs[i];
|
||||
final inputTransaction = bundle.ins[i];
|
||||
final vout = input.index;
|
||||
final outTransaction = inputTransaction.outs[vout!];
|
||||
final address = addressFromOutput(outTransaction.script!, networkType);
|
||||
inputAmount += outTransaction.value!;
|
||||
if (addresses.contains(address)) {
|
||||
final outTransaction = inputTransaction.outputs[input.txIndex];
|
||||
inputAmount += outTransaction.amount.toInt();
|
||||
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
|
||||
direction = TransactionDirection.outgoing;
|
||||
}
|
||||
}
|
||||
|
||||
for (final out in bundle.originalTransaction.outs) {
|
||||
totalOutAmount += out.value!;
|
||||
final address = addressFromOutput(out.script!, networkType);
|
||||
final addressExists = addresses.contains(address);
|
||||
final receivedAmounts = <int>[];
|
||||
for (final out in bundle.originalTransaction.outputs) {
|
||||
totalOutAmount += out.amount.toInt();
|
||||
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
||||
|
||||
if (addressExists) {
|
||||
receivedAmounts.add(out.amount.toInt());
|
||||
}
|
||||
|
||||
if ((direction == TransactionDirection.incoming && addressExists) ||
|
||||
(direction == TransactionDirection.outgoing && !addressExists)) {
|
||||
amount += out.value!;
|
||||
amount += out.amount.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
if (receivedAmounts.length == bundle.originalTransaction.outputs.length) {
|
||||
// Self-send
|
||||
direction = TransactionDirection.incoming;
|
||||
amount = receivedAmounts.reduce((a, b) => a + b);
|
||||
}
|
||||
|
||||
final fee = inputAmount - totalOutAmount;
|
||||
return ElectrumTransactionInfo(type,
|
||||
id: bundle.originalTransaction.getId(),
|
||||
id: bundle.originalTransaction.txId(),
|
||||
height: height,
|
||||
isPending: bundle.confirmations == 0,
|
||||
fee: fee,
|
||||
|
@ -152,8 +153,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
if (addresses != null) {
|
||||
tx.outs.forEach((out) {
|
||||
try {
|
||||
final p2pkh = bitcoin.P2PKH(
|
||||
data: PaymentData(output: out.script), network: bitcoin.bitcoin);
|
||||
final p2pkh =
|
||||
bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin);
|
||||
exist = addresses.contains(p2pkh.data.address);
|
||||
|
||||
if (exist) {
|
||||
|
@ -163,9 +164,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
});
|
||||
}
|
||||
|
||||
final date = timestamp != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
|
||||
: DateTime.now();
|
||||
final date =
|
||||
timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now();
|
||||
|
||||
return ElectrumTransactionInfo(type,
|
||||
id: tx.getId(),
|
||||
|
@ -178,8 +178,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
confirmations: confirmations);
|
||||
}
|
||||
|
||||
factory ElectrumTransactionInfo.fromJson(
|
||||
Map<String, dynamic> data, WalletType type) {
|
||||
factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
|
||||
return ElectrumTransactionInfo(type,
|
||||
id: data['id'] as String,
|
||||
height: data['height'] as int,
|
||||
|
|
|
@ -3,9 +3,10 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_bitcoin/address_to_output_script.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
|
||||
|
@ -18,6 +19,7 @@ import 'package:cw_bitcoin/electrum_balance.dart';
|
|||
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/litecoin_network.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
|
@ -33,10 +35,10 @@ import 'package:cw_core/utils/file.dart';
|
|||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:rxdart/subjects.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
part 'electrum_wallet.g.dart';
|
||||
|
||||
|
@ -73,6 +75,8 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
: {}),
|
||||
this.unspentCoinsInfo = unspentCoinsInfo,
|
||||
this.network = _getNetwork(networkType, currency),
|
||||
this.isTestnet = networkType == bitcoin.testnet,
|
||||
super(walletInfo) {
|
||||
this.electrumClient = electrumClient ?? ElectrumClient();
|
||||
this.walletInfo = walletInfo;
|
||||
|
@ -106,13 +110,13 @@ abstract class ElectrumWalletBase
|
|||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
List<String> get scriptHashes => walletAddresses.addresses
|
||||
.map((addr) => scriptHash(addr.address, networkType: networkType))
|
||||
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
|
||||
.map((addr) => scriptHash(addr.address, network: network))
|
||||
.toList();
|
||||
|
||||
List<String> get publicScriptHashes => walletAddresses.addresses
|
||||
List<String> get publicScriptHashes => walletAddresses.allAddresses
|
||||
.where((addr) => !addr.isHidden)
|
||||
.map((addr) => scriptHash(addr.address, networkType: networkType))
|
||||
.map((addr) => scriptHash(addr.address, network: network))
|
||||
.toList();
|
||||
|
||||
String get xpub => hd.base58!;
|
||||
|
@ -121,6 +125,10 @@ abstract class ElectrumWalletBase
|
|||
String get seed => mnemonic;
|
||||
|
||||
bitcoin.NetworkType networkType;
|
||||
BasedUtxoNetwork network;
|
||||
|
||||
@override
|
||||
bool? isTestnet;
|
||||
|
||||
@override
|
||||
BitcoinWalletKeys get keys =>
|
||||
|
@ -145,12 +153,11 @@ abstract class ElectrumWalletBase
|
|||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await walletAddresses.discoverAddresses();
|
||||
await updateTransactions();
|
||||
_subscribeForUpdates();
|
||||
await updateUnspent();
|
||||
await updateBalance();
|
||||
_feeRates = await electrumClient.feeRates();
|
||||
_feeRates = await electrumClient.feeRates(network: network);
|
||||
|
||||
Timer.periodic(
|
||||
const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
|
||||
|
@ -181,183 +188,212 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
const minAmount = 546;
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final inputs = <BitcoinUnspent>[];
|
||||
final outputs = transactionCredentials.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
Future<EstimatedTxResult> estimateTxFeeAndInputsToUse(
|
||||
int credentialsAmount,
|
||||
bool sendAll,
|
||||
List<BitcoinBaseAddress> outputAddresses,
|
||||
List<BitcoinOutput> outputs,
|
||||
int? feeRate,
|
||||
BitcoinTransactionPriority? priority,
|
||||
{int? inputsCount}) async {
|
||||
final utxos = <UtxoWithAddress>[];
|
||||
List<ECPrivate> privateKeys = [];
|
||||
|
||||
var leftAmount = credentialsAmount;
|
||||
var allInputsAmount = 0;
|
||||
|
||||
if (unspentCoins.isEmpty) {
|
||||
await updateUnspent();
|
||||
}
|
||||
for (int i = 0; i < unspentCoins.length; i++) {
|
||||
final utx = unspentCoins[i];
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
allInputsAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.isEmpty) {
|
||||
throw BitcoinTransactionNoInputsException();
|
||||
}
|
||||
|
||||
final allAmountFee = transactionCredentials.feeRate != null
|
||||
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
|
||||
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
|
||||
|
||||
final allAmount = allInputsAmount - allAmountFee;
|
||||
|
||||
var credentialsAmount = 0;
|
||||
var amount = 0;
|
||||
var fee = 0;
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
|
||||
|
||||
if (allAmount - credentialsAmount < minAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
amount = credentialsAmount;
|
||||
|
||||
if (transactionCredentials.feeRate != null) {
|
||||
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
|
||||
outputsCount: outputs.length + 1);
|
||||
} else {
|
||||
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
|
||||
outputsCount: outputs.length + 1);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
|
||||
|
||||
if (credentialsAmount > allAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
amount = output.sendAll || allAmount - credentialsAmount < minAmount
|
||||
? allAmount
|
||||
: credentialsAmount;
|
||||
|
||||
if (output.sendAll || amount == allAmount) {
|
||||
fee = allAmountFee;
|
||||
} else if (transactionCredentials.feeRate != null) {
|
||||
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
|
||||
} else {
|
||||
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
final totalAmount = amount + fee;
|
||||
|
||||
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
final txb = bitcoin.TransactionBuilder(network: networkType);
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
var leftAmount = totalAmount;
|
||||
var totalInputAmount = 0;
|
||||
|
||||
inputs.clear();
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
leftAmount = leftAmount - utx.value;
|
||||
totalInputAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
|
||||
if (leftAmount <= 0) {
|
||||
final address = addressTypeFromStr(utx.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: utx.bitcoinAddressRecord.index,
|
||||
network: network);
|
||||
|
||||
privateKeys.add(privkey);
|
||||
|
||||
utxos.add(
|
||||
UtxoWithAddress(
|
||||
utxo: BitcoinUtxo(
|
||||
txHash: utx.hash,
|
||||
value: BigInt.from(utx.value),
|
||||
vout: utx.vout,
|
||||
scriptType: _getScriptType(address),
|
||||
),
|
||||
ownerDetails:
|
||||
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
|
||||
),
|
||||
);
|
||||
|
||||
bool amountIsAcquired = !sendAll && leftAmount <= 0;
|
||||
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.isEmpty) {
|
||||
if (utxos.isEmpty) {
|
||||
throw BitcoinTransactionNoInputsException();
|
||||
}
|
||||
|
||||
if (amount <= 0 || totalInputAmount < totalAmount) {
|
||||
var changeValue = allInputsAmount - credentialsAmount;
|
||||
|
||||
if (!sendAll) {
|
||||
if (changeValue > 0) {
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
final address = addressTypeFromStr(changeAddress, network);
|
||||
outputAddresses.add(address);
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue)));
|
||||
}
|
||||
}
|
||||
|
||||
final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos, outputs: outputs, network: network);
|
||||
|
||||
int fee = feeRate != null
|
||||
? feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize)
|
||||
: feeAmountForPriority(priority!, 0, 0, size: estimatedSize);
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
txb.setVersion(1);
|
||||
inputs.forEach((input) {
|
||||
if (input.isP2wpkh) {
|
||||
final p2wpkh = bitcoin
|
||||
.P2WPKH(
|
||||
data: generatePaymentData(
|
||||
hd: input.bitcoinAddressRecord.isHidden
|
||||
? walletAddresses.sideHd
|
||||
: walletAddresses.mainHd,
|
||||
index: input.bitcoinAddressRecord.index),
|
||||
network: networkType)
|
||||
.data;
|
||||
var amount = credentialsAmount;
|
||||
|
||||
txb.addInput(input.hash, input.vout, null, p2wpkh.output);
|
||||
} else {
|
||||
txb.addInput(input.hash, input.vout);
|
||||
final lastOutput = outputs.last;
|
||||
if (!sendAll) {
|
||||
if (changeValue > fee) {
|
||||
// Here, lastOutput is change, deduct the fee from it
|
||||
outputs[outputs.length - 1] =
|
||||
BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
|
||||
}
|
||||
});
|
||||
|
||||
outputs.forEach((item) {
|
||||
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
|
||||
final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
|
||||
txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!);
|
||||
});
|
||||
|
||||
final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1);
|
||||
var feeAmount = 0;
|
||||
|
||||
if (transactionCredentials.feeRate != null) {
|
||||
feeAmount = transactionCredentials.feeRate! * estimatedSize;
|
||||
} else {
|
||||
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
|
||||
// Here, if sendAll, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount for change
|
||||
amount = allInputsAmount - fee;
|
||||
outputs[outputs.length - 1] =
|
||||
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount));
|
||||
}
|
||||
|
||||
final changeValue = totalInputAmount - amount - feeAmount;
|
||||
final totalAmount = amount + fee;
|
||||
|
||||
if (changeValue > minAmount) {
|
||||
txb.addOutput(changeAddress, changeValue);
|
||||
if (totalAmount > balance[currency]!.confirmed) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
final input = inputs[i];
|
||||
final keyPair = generateKeyPair(
|
||||
hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: input.bitcoinAddressRecord.index,
|
||||
network: networkType);
|
||||
final witnessValue = input.isP2wpkh ? input.value : null;
|
||||
if (totalAmount > allInputsAmount) {
|
||||
if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
} else {
|
||||
if (changeValue > fee) {
|
||||
outputAddresses.removeLast();
|
||||
outputs.removeLast();
|
||||
}
|
||||
|
||||
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
|
||||
return estimateTxFeeAndInputsToUse(
|
||||
credentialsAmount, sendAll, outputAddresses, outputs, feeRate, priority,
|
||||
inputsCount: utxos.length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return PendingBitcoinTransaction(txb.build(), type,
|
||||
electrumClient: electrumClient, amount: amount, fee: fee)
|
||||
..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
try {
|
||||
final outputs = <BitcoinOutput>[];
|
||||
final outputAddresses = <BitcoinBaseAddress>[];
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final hasMultiDestination = transactionCredentials.outputs.length > 1;
|
||||
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
|
||||
|
||||
var credentialsAmount = 0;
|
||||
|
||||
for (final out in transactionCredentials.outputs) {
|
||||
final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
|
||||
final address = addressTypeFromStr(outputAddress, network);
|
||||
|
||||
outputAddresses.add(address);
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (out.sendAll || out.formattedCryptoAmount! <= 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
credentialsAmount += outputAmount;
|
||||
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
|
||||
} else {
|
||||
if (!sendAll) {
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
credentialsAmount += outputAmount;
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
|
||||
} else {
|
||||
// The value will be changed after estimating the Tx size and deducting the fee from the total
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final estimatedTx = await estimateTxFeeAndInputsToUse(
|
||||
credentialsAmount,
|
||||
sendAll,
|
||||
outputAddresses,
|
||||
outputs,
|
||||
transactionCredentials.feeRate,
|
||||
transactionCredentials.priority,
|
||||
);
|
||||
|
||||
final txb = BitcoinTransactionBuilder(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: outputs,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network);
|
||||
|
||||
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
|
||||
final key = estimatedTx.privateKeys
|
||||
.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
|
||||
|
||||
if (key == null) {
|
||||
throw Exception("Cannot find private key");
|
||||
}
|
||||
|
||||
if (utxo.utxo.isP2tr()) {
|
||||
return key.signTapRoot(txDigest, sighash: sighash);
|
||||
} else {
|
||||
return key.signInput(txDigest, sigHash: sighash);
|
||||
}
|
||||
});
|
||||
|
||||
return PendingBitcoinTransaction(transaction, type,
|
||||
electrumClient: electrumClient,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
network: network)
|
||||
..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': mnemonic,
|
||||
'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
|
||||
'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
|
||||
'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
|
||||
'balance': balance[currency]?.toJSON()
|
||||
'account_index': walletAddresses.currentReceiveAddressIndexByType,
|
||||
'change_address_index': walletAddresses.currentChangeAddressIndexByType,
|
||||
'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
|
||||
'address_page_type': walletInfo.addressPageType == null
|
||||
? SegwitAddresType.p2wpkh.toString()
|
||||
: walletInfo.addressPageType.toString(),
|
||||
'balance': balance[currency]?.toJSON(),
|
||||
});
|
||||
|
||||
int feeRate(TransactionPriority priority) {
|
||||
|
@ -372,24 +408,29 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
int feeAmountForPriority(
|
||||
BitcoinTransactionPriority priority, int inputsCount, int outputsCount) =>
|
||||
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
|
||||
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
|
||||
{int? size}) =>
|
||||
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
|
||||
|
||||
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) =>
|
||||
feeRate * estimatedTransactionSize(inputsCount, outputsCount);
|
||||
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
|
||||
feeRate * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
|
||||
|
||||
@override
|
||||
int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount}) {
|
||||
int calculateEstimatedFee(TransactionPriority? priority, int? amount,
|
||||
{int? outputsCount, int? size}) {
|
||||
if (priority is BitcoinTransactionPriority) {
|
||||
return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount,
|
||||
outputsCount: outputsCount);
|
||||
outputsCount: outputsCount, size: size);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) {
|
||||
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
|
||||
if (size != null) {
|
||||
return feeAmountWithFeeRate(feeRate, 0, 0, size: size);
|
||||
}
|
||||
|
||||
int inputsCount = 0;
|
||||
|
||||
if (amount != null) {
|
||||
|
@ -457,9 +498,6 @@ abstract class ElectrumWalletBase
|
|||
await transactionHistory.changePassword(password);
|
||||
}
|
||||
|
||||
bitcoin.ECPair keyPairFor({required int index}) =>
|
||||
generateKeyPair(hd: hd, index: index, network: networkType);
|
||||
|
||||
@override
|
||||
Future<void> rescan({required int height}) async => throw UnimplementedError();
|
||||
|
||||
|
@ -473,20 +511,23 @@ abstract class ElectrumWalletBase
|
|||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
Future<void> updateUnspent() async {
|
||||
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
|
||||
.getListUnspentWithAddress(address.address, networkType)
|
||||
.then((unspent) => unspent.map((unspent) {
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
|
||||
.getListUnspentWithAddress(address.address, network)
|
||||
.then((unspent) => Future.forEach<Map<String, dynamic>>(unspent, (unspent) async {
|
||||
try {
|
||||
return BitcoinUnspent.fromJSON(address, unspent);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}).whereNotNull())));
|
||||
unspentCoins = unspent.expand((e) => e).toList();
|
||||
unspentCoins.forEach((coin) async {
|
||||
final tx = await fetchTransactionInfo(hash: coin.hash, height: 0);
|
||||
coin.isChange = tx?.direction == TransactionDirection.outgoing;
|
||||
});
|
||||
final coin = BitcoinUnspent.fromJSON(address, unspent);
|
||||
final tx = await fetchTransactionInfo(
|
||||
hash: coin.hash, height: 0, myAddresses: addressesSet);
|
||||
coin.isChange = tx?.direction == TransactionDirection.outgoing;
|
||||
updatedUnspentCoins.add(coin);
|
||||
} catch (_) {}
|
||||
}))));
|
||||
|
||||
unspentCoins = updatedUnspentCoins;
|
||||
|
||||
if (unspentCoinsInfo.isEmpty) {
|
||||
unspentCoins.forEach((coin) => _addCoinInfo(coin));
|
||||
|
@ -495,8 +536,10 @@ abstract class ElectrumWalletBase
|
|||
|
||||
if (unspentCoins.isNotEmpty) {
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values
|
||||
.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
|
||||
final coinInfoList = unspentCoinsInfo.values.where((element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.vout == coin.vout);
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
|
@ -537,7 +580,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
if (currentWalletUnspentCoins.isNotEmpty) {
|
||||
currentWalletUnspentCoins.forEach((element) {
|
||||
final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash));
|
||||
final existUnspentCoins = unspentCoins
|
||||
.where((coin) => element.hash.contains(coin.hash) && element.vout == coin.vout);
|
||||
|
||||
if (existUnspentCoins.isEmpty) {
|
||||
keys.add(element.key);
|
||||
|
@ -555,95 +599,145 @@ abstract class ElectrumWalletBase
|
|||
|
||||
Future<ElectrumTransactionBundle> getTransactionExpanded(
|
||||
{required String hash, required int height}) async {
|
||||
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
|
||||
final transactionHex = verboseTransaction['hex'] as String;
|
||||
final original = bitcoin.Transaction.fromHex(transactionHex);
|
||||
final ins = <bitcoin.Transaction>[];
|
||||
final time = verboseTransaction['time'] as int?;
|
||||
final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
|
||||
String transactionHex;
|
||||
int? time;
|
||||
int confirmations = 0;
|
||||
if (network == BitcoinNetwork.testnet) {
|
||||
// Testnet public electrum server does not support verbose transaction fetching
|
||||
transactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
|
||||
for (final vin in original.ins) {
|
||||
final id = HEX.encode(vin.hash!.reversed.toList());
|
||||
final txHex = await electrumClient.getTransactionHex(hash: id);
|
||||
final tx = bitcoin.Transaction.fromHex(txHex);
|
||||
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 tip = await electrumClient.getCurrentBlockChainTip() ?? 0;
|
||||
confirmations = tip - (status["block_height"] as int? ?? 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;
|
||||
}
|
||||
|
||||
final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex);
|
||||
final ins = <bitcoin_base.BtcTransaction>[];
|
||||
|
||||
for (final vin in original.inputs) {
|
||||
final txHex = await electrumClient.getTransactionHex(hash: vin.txId);
|
||||
final tx = bitcoin_base.BtcTransaction.fromRaw(txHex);
|
||||
ins.add(tx);
|
||||
}
|
||||
|
||||
return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations);
|
||||
return ElectrumTransactionBundle(original,
|
||||
ins: ins, time: time, confirmations: confirmations, height: height);
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
|
||||
{required String hash, required int height}) async {
|
||||
{required String hash,
|
||||
required int height,
|
||||
required Set<String> myAddresses,
|
||||
bool? retryOnFailure}) async {
|
||||
try {
|
||||
final tx = await getTransactionExpanded(hash: hash, height: height);
|
||||
final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet();
|
||||
return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType,
|
||||
addresses: addresses, height: height);
|
||||
} catch (_) {
|
||||
return ElectrumTransactionInfo.fromElectrumBundle(
|
||||
await getTransactionExpanded(hash: hash, height: height), walletInfo.type, network,
|
||||
addresses: myAddresses, height: height);
|
||||
} catch (e) {
|
||||
if (e is FormatException && retryOnFailure == true) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
||||
final addressHashes = <String, BitcoinAddressRecord>{};
|
||||
final normalizedHistories = <Map<String, dynamic>>[];
|
||||
final newTxCounts = <String, int>{};
|
||||
|
||||
walletAddresses.addresses.forEach((addressRecord) {
|
||||
final sh = scriptHash(addressRecord.address, networkType: networkType);
|
||||
addressHashes[sh] = addressRecord;
|
||||
newTxCounts[sh] = 0;
|
||||
});
|
||||
|
||||
try {
|
||||
final histories = addressHashes.keys.map((scriptHash) =>
|
||||
electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
|
||||
final historyResults = await Future.wait(histories);
|
||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
||||
final currentHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
|
||||
|
||||
await Future.wait(ADDRESS_TYPES.map((type) {
|
||||
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
|
||||
|
||||
return Future.wait(addressesByType.map((addressRecord) async {
|
||||
final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight);
|
||||
|
||||
historyResults.forEach((history) {
|
||||
history.entries.forEach((historyItem) {
|
||||
if (historyItem.value.isNotEmpty) {
|
||||
final address = addressHashes[historyItem.key];
|
||||
address?.setAsUsed();
|
||||
newTxCounts[historyItem.key] = historyItem.value.length;
|
||||
normalizedHistories.addAll(historyItem.value);
|
||||
if (history.isNotEmpty) {
|
||||
addressRecord.txCount = history.length;
|
||||
historiesWithDetails.addAll(history);
|
||||
|
||||
final matchedAddresses =
|
||||
addressesByType.where((addr) => addr.isHidden == addressRecord.isHidden);
|
||||
|
||||
final isLastUsedAddress =
|
||||
history.isNotEmpty && addressRecord.address == matchedAddresses.last.address;
|
||||
|
||||
if (isLastUsedAddress) {
|
||||
await walletAddresses.discoverAddresses(
|
||||
matchedAddresses.toList(),
|
||||
addressRecord.isHidden,
|
||||
(address, addressesSet) =>
|
||||
_fetchAddressHistory(address, addressesSet, currentHeight)
|
||||
.then((history) => history.isNotEmpty ? address.address : null),
|
||||
type: type);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (var sh in addressHashes.keys) {
|
||||
var balanceData = await electrumClient.getBalance(sh);
|
||||
var addressRecord = addressHashes[sh];
|
||||
if (addressRecord != null) {
|
||||
addressRecord.balance = balanceData['confirmed'] as int? ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
addressHashes.forEach((sh, addressRecord) {
|
||||
addressRecord.txCount = newTxCounts[sh] ?? 0;
|
||||
});
|
||||
|
||||
final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
|
||||
try {
|
||||
return fetchTransactionInfo(
|
||||
hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
|
||||
} catch (_) {
|
||||
return Future.value(null);
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
|
||||
<String, ElectrumTransactionInfo>{}, (acc, tx) {
|
||||
if (tx == null) {
|
||||
return acc;
|
||||
}
|
||||
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
|
||||
return acc;
|
||||
});
|
||||
return historiesWithDetails;
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
|
||||
BitcoinAddressRecord addressRecord, Set<String> addressesSet, int currentHeight) async {
|
||||
try {
|
||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
|
||||
final history = await electrumClient
|
||||
.getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network));
|
||||
|
||||
if (history.isNotEmpty) {
|
||||
addressRecord.setAsUsed();
|
||||
|
||||
await Future.wait(history.map((transaction) async {
|
||||
final txid = transaction['tx_hash'] as String;
|
||||
final height = transaction['height'] as int;
|
||||
final storedTx = transactionHistory.transactions[txid];
|
||||
|
||||
if (storedTx != null) {
|
||||
if (height > 0) {
|
||||
storedTx.height = height;
|
||||
// the tx's block itself is the first confirmation so add 1
|
||||
storedTx.confirmations = currentHeight - height + 1;
|
||||
storedTx.isPending = storedTx.confirmations == 0;
|
||||
}
|
||||
|
||||
historiesWithDetails[txid] = storedTx;
|
||||
} else {
|
||||
final tx = await fetchTransactionInfo(
|
||||
hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true);
|
||||
|
||||
if (tx != null) {
|
||||
historiesWithDetails[txid] = tx;
|
||||
|
||||
// Got a new transaction fetched, add it to the transaction history
|
||||
// instead of waiting all to finish, and next time it will be faster
|
||||
transactionHistory.addOne(tx);
|
||||
await transactionHistory.save();
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(null);
|
||||
}));
|
||||
}
|
||||
|
||||
return historiesWithDetails;
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
return {};
|
||||
|
@ -657,10 +751,8 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
final transactions = await fetchTransactions();
|
||||
transactionHistory.addMany(transactions);
|
||||
await fetchTransactions();
|
||||
walletAddresses.updateReceiveAddresses();
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
} catch (e, stacktrace) {
|
||||
print(stacktrace);
|
||||
|
@ -691,11 +783,11 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
Future<ElectrumBalance> _fetchBalances() async {
|
||||
final addresses = walletAddresses.addresses.toList();
|
||||
final addresses = walletAddresses.allAddresses.toList();
|
||||
final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
for (var i = 0; i < addresses.length; i++) {
|
||||
final addressRecord = addresses[i];
|
||||
final sh = scriptHash(addressRecord.address, networkType: networkType);
|
||||
final sh = scriptHash(addressRecord.address, network: network);
|
||||
final balanceFuture = electrumClient.getBalance(sh);
|
||||
balanceFutures.add(balanceFuture);
|
||||
}
|
||||
|
@ -704,6 +796,7 @@ abstract class ElectrumWalletBase
|
|||
unspentCoinsInfo.values.forEach((info) {
|
||||
unspentCoins.forEach((element) {
|
||||
if (element.hash == info.hash &&
|
||||
element.vout == info.vout &&
|
||||
info.isFrozen &&
|
||||
element.bitcoinAddressRecord.address == info.address &&
|
||||
element.value == info.value) {
|
||||
|
@ -741,10 +834,10 @@ abstract class ElectrumWalletBase
|
|||
String getChangeAddress() {
|
||||
const minCountOfHiddenAddresses = 5;
|
||||
final random = Random();
|
||||
var addresses = walletAddresses.addresses.where((addr) => addr.isHidden).toList();
|
||||
var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList();
|
||||
|
||||
if (addresses.length < minCountOfHiddenAddresses) {
|
||||
addresses = walletAddresses.addresses.toList();
|
||||
addresses = walletAddresses.allAddresses.toList();
|
||||
}
|
||||
|
||||
return addresses[random.nextInt(addresses.length)].address;
|
||||
|
@ -756,9 +849,78 @@ abstract class ElectrumWalletBase
|
|||
@override
|
||||
String signMessage(String message, {String? address = null}) {
|
||||
final index = address != null
|
||||
? walletAddresses.addresses.firstWhere((element) => element.address == address).index
|
||||
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||
: null;
|
||||
final HD = index == null ? hd : hd.derive(index);
|
||||
return base64Encode(HD.signMessage(message));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class EstimateTxParams {
|
||||
EstimateTxParams(
|
||||
{required this.amount,
|
||||
required this.feeRate,
|
||||
required this.priority,
|
||||
required this.outputsCount,
|
||||
required this.size});
|
||||
|
||||
final int amount;
|
||||
final int feeRate;
|
||||
final TransactionPriority priority;
|
||||
final int outputsCount;
|
||||
final int size;
|
||||
}
|
||||
|
||||
class EstimatedTxResult {
|
||||
EstimatedTxResult(
|
||||
{required this.utxos, required this.privateKeys, required this.fee, required this.amount});
|
||||
|
||||
final List<UtxoWithAddress> utxos;
|
||||
final List<ECPrivate> privateKeys;
|
||||
final int fee;
|
||||
final int amount;
|
||||
}
|
||||
|
||||
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
|
||||
if (P2pkhAddress.regex.hasMatch(address)) {
|
||||
return P2pkhAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2shAddress.regex.hasMatch(address)) {
|
||||
return P2shAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2wshAddress.regex.hasMatch(address)) {
|
||||
return P2wshAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2trAddress.regex.hasMatch(address)) {
|
||||
return P2trAddress.fromAddress(address: address, network: network);
|
||||
} else {
|
||||
return P2wpkhAddress.fromAddress(address: address, network: network);
|
||||
}
|
||||
}
|
||||
|
||||
BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
|
||||
if (type is P2pkhAddress) {
|
||||
return P2pkhAddressType.p2pkh;
|
||||
} else if (type is P2shAddress) {
|
||||
return P2shAddressType.p2wpkhInP2sh;
|
||||
} else if (type is P2wshAddress) {
|
||||
return SegwitAddresType.p2wsh;
|
||||
} else if (type is P2trAddress) {
|
||||
return SegwitAddresType.p2tr;
|
||||
} else {
|
||||
return SegwitAddresType.p2wpkh;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
@ -12,65 +11,90 @@ part 'electrum_wallet_addresses.g.dart';
|
|||
|
||||
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
||||
|
||||
const List<BitcoinAddressType> ADDRESS_TYPES = [
|
||||
SegwitAddresType.p2wpkh,
|
||||
P2pkhAddressType.p2pkh,
|
||||
SegwitAddresType.p2tr,
|
||||
SegwitAddresType.p2wsh,
|
||||
P2shAddressType.p2wpkhInP2sh,
|
||||
];
|
||||
|
||||
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
ElectrumWalletAddressesBase(WalletInfo walletInfo,
|
||||
{required this.mainHd,
|
||||
required this.sideHd,
|
||||
required this.electrumClient,
|
||||
required this.networkType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
ElectrumWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required this.mainHd,
|
||||
required this.sideHd,
|
||||
required this.electrumClient,
|
||||
required this.network,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
BitcoinAddressType? initialAddressPageType,
|
||||
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
addressesByReceiveType =
|
||||
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
|
||||
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
||||
.toSet()),
|
||||
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
|
||||
.toSet()),
|
||||
currentReceiveAddressIndex = initialRegularAddressIndex,
|
||||
currentChangeAddressIndex = initialChangeAddressIndex,
|
||||
super(walletInfo);
|
||||
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
|
||||
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
|
||||
_addressPageType = initialAddressPageType ??
|
||||
(walletInfo.addressPageType != null
|
||||
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
|
||||
: SegwitAddresType.p2wpkh),
|
||||
super(walletInfo) {
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
static const defaultReceiveAddressesCount = 22;
|
||||
static const defaultChangeAddressesCount = 17;
|
||||
static const gap = 20;
|
||||
|
||||
static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
|
||||
|
||||
static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
|
||||
|
||||
final ObservableList<BitcoinAddressRecord> addresses;
|
||||
final ObservableList<BitcoinAddressRecord> _addresses;
|
||||
// Matched by addressPageType
|
||||
late ObservableList<BitcoinAddressRecord> addressesByReceiveType;
|
||||
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
||||
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
||||
final ElectrumClient electrumClient;
|
||||
final bitcoin.NetworkType networkType;
|
||||
final BasedUtxoNetwork network;
|
||||
final bitcoin.HDWallet mainHd;
|
||||
final bitcoin.HDWallet sideHd;
|
||||
|
||||
@observable
|
||||
late BitcoinAddressType _addressPageType;
|
||||
|
||||
@computed
|
||||
BitcoinAddressType get addressPageType => _addressPageType;
|
||||
|
||||
@computed
|
||||
List<BitcoinAddressRecord> get allAddresses => _addresses;
|
||||
|
||||
@override
|
||||
@computed
|
||||
String get address {
|
||||
if (isEnabledAutoGenerateSubaddress) {
|
||||
if (receiveAddresses.isEmpty) {
|
||||
final newAddress = generateNewAddress().address;
|
||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress;
|
||||
}
|
||||
final receiveAddress = receiveAddresses.first.address;
|
||||
String receiveAddress;
|
||||
|
||||
return walletInfo.type == WalletType.bitcoinCash
|
||||
? toCashAddr(receiveAddress)
|
||||
: receiveAddress;
|
||||
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
|
||||
|
||||
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
|
||||
typeMatchingReceiveAddresses.isEmpty) {
|
||||
receiveAddress = generateNewAddress().address;
|
||||
} else {
|
||||
final receiveAddress = (receiveAddresses.first.address != addresses.first.address &&
|
||||
previousAddressRecord != null)
|
||||
? previousAddressRecord!.address
|
||||
: addresses.first.address;
|
||||
final previousAddressMatchesType =
|
||||
previousAddressRecord != null && previousAddressRecord!.type == addressPageType;
|
||||
|
||||
return walletInfo.type == WalletType.bitcoinCash
|
||||
? toCashAddr(receiveAddress)
|
||||
: receiveAddress;
|
||||
if (previousAddressMatchesType &&
|
||||
typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) {
|
||||
receiveAddress = previousAddressRecord!.address;
|
||||
} else {
|
||||
receiveAddress = typeMatchingReceiveAddresses.first.address;
|
||||
}
|
||||
}
|
||||
|
||||
return receiveAddress;
|
||||
}
|
||||
|
||||
@observable
|
||||
|
@ -78,10 +102,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@override
|
||||
set address(String addr) {
|
||||
if (addr.startsWith('bitcoincash:')) {
|
||||
addr = toLegacy(addr);
|
||||
}
|
||||
final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr);
|
||||
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
|
||||
|
||||
previousAddressRecord = addressRecord;
|
||||
receiveAddresses.remove(addressRecord);
|
||||
|
@ -89,16 +110,29 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
}
|
||||
|
||||
@override
|
||||
String get primaryAddress => getAddress(index: 0, hd: mainHd);
|
||||
String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType);
|
||||
|
||||
int currentReceiveAddressIndex;
|
||||
int currentChangeAddressIndex;
|
||||
Map<String, int> currentReceiveAddressIndexByType;
|
||||
|
||||
int get currentReceiveAddressIndex =>
|
||||
currentReceiveAddressIndexByType[_addressPageType.toString()] ?? 0;
|
||||
|
||||
void set currentReceiveAddressIndex(int index) =>
|
||||
currentReceiveAddressIndexByType[_addressPageType.toString()] = index;
|
||||
|
||||
Map<String, int> currentChangeAddressIndexByType;
|
||||
|
||||
int get currentChangeAddressIndex =>
|
||||
currentChangeAddressIndexByType[_addressPageType.toString()] ?? 0;
|
||||
|
||||
void set currentChangeAddressIndex(int index) =>
|
||||
currentChangeAddressIndexByType[_addressPageType.toString()] = index;
|
||||
|
||||
@observable
|
||||
BitcoinAddressRecord? previousAddressRecord;
|
||||
|
||||
@computed
|
||||
int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) {
|
||||
int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) {
|
||||
if (!addressRecord.isHidden) {
|
||||
return acc + 1;
|
||||
}
|
||||
|
@ -106,22 +140,27 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
});
|
||||
|
||||
@computed
|
||||
int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) {
|
||||
int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) {
|
||||
if (addressRecord.isHidden) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
|
||||
Future<void> discoverAddresses() async {
|
||||
await _discoverAddresses(mainHd, false);
|
||||
await _discoverAddresses(sideHd, true);
|
||||
await updateAddressesInBox();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
await _generateInitialAddresses();
|
||||
if (walletInfo.type == WalletType.bitcoinCash) {
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
} else if (walletInfo.type == WalletType.litecoin) {
|
||||
await _generateInitialAddresses();
|
||||
} else if (walletInfo.type == WalletType.bitcoin) {
|
||||
await _generateInitialAddresses();
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
|
||||
}
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
await updateAddressesInBox();
|
||||
|
@ -141,10 +180,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
if (changeAddresses.isEmpty) {
|
||||
final newAddresses = await _createNewAddresses(gap,
|
||||
hd: sideHd,
|
||||
startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
|
||||
isHidden: true);
|
||||
_addAddresses(newAddresses);
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
|
||||
if (currentChangeAddressIndex >= changeAddresses.length) {
|
||||
|
@ -157,25 +195,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
return address;
|
||||
}
|
||||
|
||||
BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) {
|
||||
final isHidden = hd == sideHd;
|
||||
BitcoinAddressRecord generateNewAddress({String label = ''}) {
|
||||
final newAddressIndex = addressesByReceiveType.fold(
|
||||
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
|
||||
|
||||
final newAddressIndex = addresses.fold(
|
||||
0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
|
||||
|
||||
final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd),
|
||||
index: newAddressIndex, isHidden: isHidden, name: label ?? '');
|
||||
addresses.add(address);
|
||||
final address = BitcoinAddressRecord(
|
||||
getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType),
|
||||
index: newAddressIndex,
|
||||
isHidden: false,
|
||||
name: label,
|
||||
type: addressPageType,
|
||||
network: network,
|
||||
);
|
||||
_addresses.add(address);
|
||||
updateAddressesByMatch();
|
||||
return address;
|
||||
}
|
||||
|
||||
String getAddress({required int index, required bitcoin.HDWallet hd}) => '';
|
||||
String getAddress(
|
||||
{required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) =>
|
||||
'';
|
||||
|
||||
@override
|
||||
Future<void> updateAddressesInBox() async {
|
||||
try {
|
||||
addressesMap.clear();
|
||||
addressesMap[address] = '';
|
||||
_addresses.forEach((addressRecord) {
|
||||
addressesMap[addressRecord.address] = addressRecord.name;
|
||||
});
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
|
@ -184,116 +231,138 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@action
|
||||
void updateAddress(String address, String label) {
|
||||
if (address.startsWith('bitcoincash:')) {
|
||||
address = toLegacy(address);
|
||||
}
|
||||
final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address);
|
||||
final addressRecord =
|
||||
_addresses.firstWhere((addressRecord) => addressRecord.address == address);
|
||||
addressRecord.setNewName(label);
|
||||
final index = addresses.indexOf(addressRecord);
|
||||
addresses.remove(addressRecord);
|
||||
addresses.insert(index, addressRecord);
|
||||
final index = _addresses.indexOf(addressRecord);
|
||||
_addresses.remove(addressRecord);
|
||||
_addresses.insert(index, addressRecord);
|
||||
}
|
||||
|
||||
@action
|
||||
void updateAddressesByMatch() {
|
||||
addressesByReceiveType.clear();
|
||||
addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList());
|
||||
}
|
||||
|
||||
@action
|
||||
void updateReceiveAddresses() {
|
||||
receiveAddresses.removeRange(0, receiveAddresses.length);
|
||||
final newAddresses =
|
||||
addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
|
||||
_addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
|
||||
receiveAddresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
@action
|
||||
void updateChangeAddresses() {
|
||||
changeAddresses.removeRange(0, changeAddresses.length);
|
||||
final newAddresses =
|
||||
addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
|
||||
final newAddresses = _addresses.where((addressRecord) =>
|
||||
addressRecord.isHidden &&
|
||||
!addressRecord.isUsed &&
|
||||
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
|
||||
(walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
|
||||
changeAddresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
Future<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async {
|
||||
var hasAddrUse = true;
|
||||
List<BitcoinAddressRecord> addrs;
|
||||
|
||||
if (addresses.isNotEmpty) {
|
||||
addrs = addresses.where((addr) => addr.isHidden == isHidden).toList();
|
||||
} else {
|
||||
addrs = await _createNewAddresses(
|
||||
isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount,
|
||||
startIndex: 0,
|
||||
hd: hd,
|
||||
isHidden: isHidden);
|
||||
@action
|
||||
Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
|
||||
Future<String?> Function(BitcoinAddressRecord, Set<String>) getAddressHistory,
|
||||
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
|
||||
if (!isHidden) {
|
||||
_validateSideHdAddresses(addressList.toList());
|
||||
}
|
||||
|
||||
while (hasAddrUse) {
|
||||
final addr = addrs.last.address;
|
||||
hasAddrUse = await _hasAddressUsed(addr);
|
||||
final newAddresses = await _createNewAddresses(gap,
|
||||
startIndex: addressList.length, isHidden: isHidden, type: type);
|
||||
addAddresses(newAddresses);
|
||||
|
||||
if (!hasAddrUse) {
|
||||
break;
|
||||
}
|
||||
final addressesWithHistory = await Future.wait(newAddresses
|
||||
.map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet())));
|
||||
final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
|
||||
|
||||
final start = addrs.length;
|
||||
final count = start + gap;
|
||||
final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden);
|
||||
addrs.addAll(batch);
|
||||
}
|
||||
|
||||
if (addresses.length < addrs.length) {
|
||||
_addAddresses(addrs);
|
||||
if (isLastAddressUsed) {
|
||||
discoverAddresses(addressList, isHidden, getAddressHistory, type: type);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _generateInitialAddresses() async {
|
||||
Future<void> _generateInitialAddresses(
|
||||
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
|
||||
var countOfReceiveAddresses = 0;
|
||||
var countOfHiddenAddresses = 0;
|
||||
|
||||
addresses.forEach((addr) {
|
||||
if (addr.isHidden) {
|
||||
countOfHiddenAddresses += 1;
|
||||
return;
|
||||
}
|
||||
_addresses.forEach((addr) {
|
||||
if (addr.type == type) {
|
||||
if (addr.isHidden) {
|
||||
countOfHiddenAddresses += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
countOfReceiveAddresses += 1;
|
||||
countOfReceiveAddresses += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
|
||||
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
|
||||
final newAddresses = await _createNewAddresses(addressesCount,
|
||||
startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false);
|
||||
addresses.addAll(newAddresses);
|
||||
startIndex: countOfReceiveAddresses, isHidden: false, type: type);
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
|
||||
if (countOfHiddenAddresses < defaultChangeAddressesCount) {
|
||||
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
|
||||
final newAddresses = await _createNewAddresses(addressesCount,
|
||||
startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true);
|
||||
addresses.addAll(newAddresses);
|
||||
startIndex: countOfHiddenAddresses, isHidden: true, type: type);
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
|
||||
{required bitcoin.HDWallet hd, int startIndex = 0, bool isHidden = false}) async {
|
||||
{int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async {
|
||||
final list = <BitcoinAddressRecord>[];
|
||||
|
||||
for (var i = startIndex; i < count + startIndex; i++) {
|
||||
final address =
|
||||
BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden);
|
||||
final address = BitcoinAddressRecord(
|
||||
getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
|
||||
index: i,
|
||||
isHidden: isHidden,
|
||||
type: type ?? addressPageType,
|
||||
network: network,
|
||||
);
|
||||
list.add(address);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
void _addAddresses(Iterable<BitcoinAddressRecord> addresses) {
|
||||
final addressesSet = this.addresses.toSet();
|
||||
@action
|
||||
void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
|
||||
final addressesSet = this._addresses.toSet();
|
||||
addressesSet.addAll(addresses);
|
||||
this.addresses.removeRange(0, this.addresses.length);
|
||||
this.addresses.addAll(addressesSet);
|
||||
this._addresses.clear();
|
||||
this._addresses.addAll(addressesSet);
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
Future<bool> _hasAddressUsed(String address) async {
|
||||
final sh = scriptHash(address, networkType: networkType);
|
||||
final transactionHistory = await electrumClient.getHistory(sh);
|
||||
return transactionHistory.isNotEmpty;
|
||||
void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) {
|
||||
addrWithTransactions.forEach((element) {
|
||||
if (element.address !=
|
||||
getAddress(index: element.index, hd: mainHd, addressType: element.type))
|
||||
element.isHidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> setAddressType(BitcoinAddressType type) async {
|
||||
_addressPageType = type;
|
||||
updateAddressesByMatch();
|
||||
walletInfo.addressPageType = addressPageType.toString();
|
||||
await walletInfo.save();
|
||||
}
|
||||
|
||||
bool _isAddressPageTypeMatch(BitcoinAddressRecord addressRecord) {
|
||||
return _isAddressByType(addressRecord, addressPageType);
|
||||
}
|
||||
|
||||
bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd;
|
||||
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
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/pathForWallet.dart';
|
||||
import 'package:cw_core/utils/file.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
class ElectrumWallletSnapshot {
|
||||
ElectrumWallletSnapshot({
|
||||
class ElectrumWalletSnapshot {
|
||||
ElectrumWalletSnapshot({
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.password,
|
||||
|
@ -14,19 +15,23 @@ class ElectrumWallletSnapshot {
|
|||
required this.addresses,
|
||||
required this.balance,
|
||||
required this.regularAddressIndex,
|
||||
required this.changeAddressIndex});
|
||||
required this.changeAddressIndex,
|
||||
required this.addressPageType,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String password;
|
||||
final WalletType type;
|
||||
final String? addressPageType;
|
||||
|
||||
String mnemonic;
|
||||
List<BitcoinAddressRecord> addresses;
|
||||
ElectrumBalance balance;
|
||||
int regularAddressIndex;
|
||||
int changeAddressIndex;
|
||||
Map<String, int> regularAddressIndex;
|
||||
Map<String, int> changeAddressIndex;
|
||||
|
||||
static Future<ElectrumWallletSnapshot> load(String name, WalletType type, String password) async {
|
||||
static Future<ElectrumWalletSnapshot> load(
|
||||
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 data = json.decode(jsonSource) as Map;
|
||||
|
@ -34,26 +39,38 @@ class ElectrumWallletSnapshot {
|
|||
final mnemonic = data['mnemonic'] as String;
|
||||
final addresses = addressesTmp
|
||||
.whereType<String>()
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr))
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network))
|
||||
.toList();
|
||||
final balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
|
||||
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
||||
var regularAddressIndex = 0;
|
||||
var changeAddressIndex = 0;
|
||||
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
|
||||
try {
|
||||
regularAddressIndex = int.parse(data['account_index'] as String? ?? '0');
|
||||
changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0');
|
||||
} catch (_) {}
|
||||
regularAddressIndexByType = {
|
||||
SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0')
|
||||
};
|
||||
changeAddressIndexByType = {
|
||||
SegwitAddresType.p2wpkh.toString():
|
||||
int.parse(data['change_address_index'] as String? ?? '0')
|
||||
};
|
||||
} catch (_) {
|
||||
try {
|
||||
regularAddressIndexByType = data["account_index"] as Map<String, int>? ?? {};
|
||||
changeAddressIndexByType = data["change_address_index"] as Map<String, int>? ?? {};
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return ElectrumWallletSnapshot(
|
||||
return ElectrumWalletSnapshot(
|
||||
name: name,
|
||||
type: type,
|
||||
password: password,
|
||||
mnemonic: mnemonic,
|
||||
addresses: addresses,
|
||||
balance: balance,
|
||||
regularAddressIndex: regularAddressIndex,
|
||||
changeAddressIndex: changeAddressIndex);
|
||||
regularAddressIndex: regularAddressIndexByType,
|
||||
changeAddressIndex: changeAddressIndexByType,
|
||||
addressPageType: data['address_page_type'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
@ -20,17 +21,18 @@ part 'litecoin_wallet.g.dart';
|
|||
class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet;
|
||||
|
||||
abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
LitecoinWalletBase(
|
||||
{required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(
|
||||
LitecoinWalletBase({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
}) : super(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
|
@ -41,41 +43,42 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.ltc) {
|
||||
walletAddresses = LitecoinWalletAddresses(
|
||||
walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet
|
||||
.fromSeed(seedBytes, network: networkType)
|
||||
.derivePath("m/0'/1"),
|
||||
networkType: networkType,);
|
||||
walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
|
||||
network: network,
|
||||
);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<LitecoinWallet> create({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0
|
||||
}) async {
|
||||
static Future<LitecoinWallet> create(
|
||||
{required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex}) async {
|
||||
return LitecoinWallet(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex);
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
addressPageType: addressPageType,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<LitecoinWallet> open({
|
||||
|
@ -84,17 +87,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
}) async {
|
||||
final snp = await ElectrumWallletSnapshot.load (name, walletInfo.type, password);
|
||||
final snp =
|
||||
await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet);
|
||||
return LitecoinWallet(
|
||||
mnemonic: snp.mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex);
|
||||
mnemonic: snp.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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,39 +1,28 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'litecoin_wallet_addresses.g.dart';
|
||||
|
||||
class LitecoinWalletAddresses = LitecoinWalletAddressesBase
|
||||
with _$LitecoinWalletAddresses;
|
||||
class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses;
|
||||
|
||||
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
|
||||
with Store {
|
||||
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||
LitecoinWalletAddressesBase(
|
||||
WalletInfo walletInfo,
|
||||
{required bitcoin.HDWallet mainHd,
|
||||
required bitcoin.HDWallet sideHd,
|
||||
required bitcoin.NetworkType networkType,
|
||||
required ElectrumClient electrumClient,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(
|
||||
walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: mainHd,
|
||||
sideHd: sideHd,
|
||||
electrumClient: electrumClient,
|
||||
networkType: networkType);
|
||||
WalletInfo walletInfo, {
|
||||
required super.mainHd,
|
||||
required super.sideHd,
|
||||
required super.network,
|
||||
required super.electrumClient,
|
||||
super.initialAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
}) : super(walletInfo);
|
||||
|
||||
@override
|
||||
String getAddress({required int index, required bitcoin.HDWallet hd}) =>
|
||||
generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
|
||||
}
|
||||
String getAddress(
|
||||
{required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) =>
|
||||
generateP2WPKHAddress(hd: hd, index: index, network: network);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:io';
|
|||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
||||
import 'package:cw_bitcoin/litecoin_wallet.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
|
@ -25,7 +25,7 @@ class LitecoinWalletService extends WalletService<
|
|||
WalletType getType() => WalletType.litecoin;
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials) async {
|
||||
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final wallet = await LitecoinWalletBase.create(
|
||||
mnemonic: await generateMnemonic(),
|
||||
password: credentials.password!,
|
||||
|
@ -45,11 +45,22 @@ class LitecoinWalletService extends WalletService<
|
|||
Future<LitecoinWallet> openWallet(String name, String password) async {
|
||||
final walletInfo = walletInfoSource.values.firstWhereOrNull(
|
||||
(info) => info.id == WalletBase.idFor(name, getType()))!;
|
||||
final wallet = await LitecoinWalletBase.open(
|
||||
password: password, name: name, walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
|
||||
try {
|
||||
final wallet = await LitecoinWalletBase.open(
|
||||
password: password, name: name, walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
saveBackup(name);
|
||||
return wallet;
|
||||
} catch (_) {
|
||||
await restoreWalletFilesFromBackup(name);
|
||||
final wallet = await LitecoinWalletBase.open(
|
||||
password: password, name: name, walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -72,6 +83,7 @@ class LitecoinWalletService extends WalletService<
|
|||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
@ -82,14 +94,14 @@ class LitecoinWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<LitecoinWallet> restoreFromKeys(
|
||||
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
|
||||
BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> restoreFromSeed(
|
||||
BitcoinRestoreWalletFromSeedCredentials credentials) async {
|
||||
BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async {
|
||||
if (!validateMnemonic(credentials.mnemonic)) {
|
||||
throw BitcoinMnemonicIsIncorrectException();
|
||||
throw LitecoinMnemonicIsIncorrectException();
|
||||
}
|
||||
|
||||
final wallet = await LitecoinWalletBase.create(
|
||||
|
|
|
@ -3,3 +3,9 @@ class BitcoinMnemonicIsIncorrectException implements Exception {
|
|||
String toString() =>
|
||||
'Bitcoin mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
|
||||
}
|
||||
|
||||
class LitecoinMnemonicIsIncorrectException implements Exception {
|
||||
@override
|
||||
String toString() =>
|
||||
'Litecoin mnemonic has incorrect format. Mnemonic should contain 24 words separated by space.';
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
|
@ -9,22 +9,21 @@ import 'package:cw_core/wallet_type.dart';
|
|||
|
||||
class PendingBitcoinTransaction with PendingTransaction {
|
||||
PendingBitcoinTransaction(this._tx, this.type,
|
||||
{required this.electrumClient,
|
||||
required this.amount,
|
||||
required this.fee})
|
||||
{required this.electrumClient, required this.amount, required this.fee, this.network})
|
||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
final WalletType type;
|
||||
final bitcoin.Transaction _tx;
|
||||
final BtcTransaction _tx;
|
||||
final ElectrumClient electrumClient;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final BasedUtxoNetwork? network;
|
||||
|
||||
@override
|
||||
String get id => _tx.getId();
|
||||
String get id => _tx.txId();
|
||||
|
||||
@override
|
||||
String get hex => _tx.toHex();
|
||||
String get hex => _tx.serialize();
|
||||
|
||||
@override
|
||||
String get amountFormatted => bitcoinAmountToString(amount: amount);
|
||||
|
@ -36,18 +35,16 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
final result =
|
||||
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
|
||||
final result = await electrumClient.broadcastTransaction(transactionRaw: hex, network: network);
|
||||
|
||||
if (result.isEmpty) {
|
||||
throw BitcoinCommitTransactionException();
|
||||
}
|
||||
|
||||
_listeners?.forEach((listener) => listener(transactionInfo()));
|
||||
_listeners.forEach((listener) => listener(transactionInfo()));
|
||||
}
|
||||
|
||||
void addListener(
|
||||
void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
_listeners.add(listener);
|
||||
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/address_to_output_script.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
|
||||
|
||||
String scriptHash(String address, {required bitcoin.NetworkType networkType}) {
|
||||
final outputScript =
|
||||
bitcoin.Address.addressToOutputScript(address, networkType);
|
||||
String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) {
|
||||
final outputScript = addressToOutputScript(address, network);
|
||||
final parts = sha256.convert(outputScript).toString().split('');
|
||||
var res = '';
|
||||
|
||||
|
|
|
@ -1,55 +1,33 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
|
||||
import 'package:hex/hex.dart';
|
||||
|
||||
bitcoin.PaymentData generatePaymentData(
|
||||
{required bitcoin.HDWallet hd, required int index}) =>
|
||||
PaymentData(
|
||||
pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!)));
|
||||
bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, required int index}) =>
|
||||
PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!)));
|
||||
|
||||
bitcoin.ECPair generateKeyPair(
|
||||
{required bitcoin.HDWallet hd,
|
||||
required int index,
|
||||
required bitcoin.NetworkType network}) =>
|
||||
bitcoin.ECPair.fromWIF(hd.derive(index).wif!, network: network);
|
||||
ECPrivate generateECPrivate(
|
||||
{required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) =>
|
||||
ECPrivate.fromWif(hd.derive(index).wif!, netVersion: network.wifNetVer);
|
||||
|
||||
String generateP2WPKHAddress(
|
||||
{required bitcoin.HDWallet hd,
|
||||
required int index,
|
||||
required bitcoin.NetworkType networkType}) =>
|
||||
bitcoin
|
||||
.P2WPKH(
|
||||
data: PaymentData(
|
||||
pubkey:
|
||||
Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))),
|
||||
network: networkType)
|
||||
.data
|
||||
.address!;
|
||||
{required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) =>
|
||||
ECPublic.fromHex(hd.derive(index).pubKey!).toP2wpkhAddress().toAddress(network);
|
||||
|
||||
String generateP2WPKHAddressByPath(
|
||||
{required bitcoin.HDWallet hd,
|
||||
required String path,
|
||||
required bitcoin.NetworkType networkType}) =>
|
||||
bitcoin
|
||||
.P2WPKH(
|
||||
data: PaymentData(
|
||||
pubkey:
|
||||
Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey!))),
|
||||
network: networkType)
|
||||
.data
|
||||
.address!;
|
||||
String generateP2SHAddress(
|
||||
{required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) =>
|
||||
ECPublic.fromHex(hd.derive(index).pubKey!).toP2wpkhInP2sh().toAddress(network);
|
||||
|
||||
String generateP2WSHAddress(
|
||||
{required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) =>
|
||||
ECPublic.fromHex(hd.derive(index).pubKey!).toP2wshAddress().toAddress(network);
|
||||
|
||||
String generateP2PKHAddress(
|
||||
{required bitcoin.HDWallet hd,
|
||||
required int index,
|
||||
required bitcoin.NetworkType networkType}) =>
|
||||
bitcoin
|
||||
.P2PKH(
|
||||
data: PaymentData(
|
||||
pubkey:
|
||||
Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))),
|
||||
network: networkType)
|
||||
.data
|
||||
.address!;
|
||||
{required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) =>
|
||||
ECPublic.fromHex(hd.derive(index).pubKey!).toP2pkhAddress().toAddress(network);
|
||||
|
||||
String generateP2TRAddress(
|
||||
{required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) =>
|
||||
ECPublic.fromHex(hd.derive(index).pubKey!).toTaprootAddress().toAddress(network);
|
||||
|
|
|
@ -21,18 +21,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.2"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039
|
||||
sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -75,6 +75,15 @@ packages:
|
|||
url: "https://github.com/cake-tech/bitbox-flutter.git"
|
||||
source: git
|
||||
version: "1.0.1"
|
||||
bitcoin_base:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v2
|
||||
resolved-ref: "3fd81d238b990bb767fc7a4fdd5053a22a142e2e"
|
||||
url: "https://github.com/cake-tech/bitcoin_base.git"
|
||||
source: git
|
||||
version: "4.2.0"
|
||||
bitcoin_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -84,6 +93,14 @@ packages:
|
|||
url: "https://github.com/cake-tech/bitcoin_flutter.git"
|
||||
source: git
|
||||
version: "2.1.0"
|
||||
blockchain_utils:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: blockchain_utils
|
||||
sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -104,10 +121,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
|
||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.4.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -120,10 +137,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf"
|
||||
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.0.1"
|
||||
build_resolvers:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -136,18 +153,18 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
|
||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.4.8"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
|
||||
sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
version: "7.2.10"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -160,10 +177,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725"
|
||||
sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.3"
|
||||
version: "8.9.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -176,10 +193,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -192,10 +209,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe"
|
||||
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "4.10.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -216,18 +233,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
cryptography:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cryptography
|
||||
sha256: e0e37f79665cd5c86e8897f9abe1accfe813c0cc5299dab22256e22fddc1fef8
|
||||
sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.5.0"
|
||||
cw_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -247,10 +264,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: encrypt
|
||||
sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb"
|
||||
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -263,10 +280,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.1.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -292,10 +309,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_mobx
|
||||
sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e"
|
||||
sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6+5"
|
||||
version: "2.2.0+2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -313,18 +330,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
|
||||
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.3.1"
|
||||
hex:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -401,18 +418,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
version: "4.8.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -449,18 +466,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: mobx
|
||||
sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a
|
||||
sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3+1"
|
||||
version: "2.3.0+1"
|
||||
mobx_codegen:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mobx_codegen
|
||||
sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181"
|
||||
sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.3.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -481,26 +506,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
|
||||
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -513,10 +538,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -529,26 +554,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.2"
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -557,30 +582,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
process:
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||
name: provider
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
version: "6.1.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17"
|
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a"
|
||||
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.3"
|
||||
rxdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -593,18 +618,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.4"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -702,10 +727,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
unorm_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -726,42 +751,42 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0"
|
||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.1.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
||||
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "5.0.9"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0+3"
|
||||
version: "1.0.4"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
|
|
@ -30,6 +30,11 @@ dependencies:
|
|||
rxdart: ^0.27.5
|
||||
unorm_dart: ^0.2.0
|
||||
cryptography: ^2.0.5
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base.git
|
||||
ref: cake-update-v2
|
||||
blockchain_utils: ^2.1.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -28,17 +28,18 @@ part 'bitcoin_cash_wallet.g.dart';
|
|||
class BitcoinCashWallet = BitcoinCashWalletBase with _$BitcoinCashWallet;
|
||||
|
||||
abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||
BitcoinCashWalletBase(
|
||||
{required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(
|
||||
BitcoinCashWalletBase({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
BitcoinAddressType? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
}) : super(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
|
@ -48,40 +49,44 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.bch) {
|
||||
walletAddresses = BitcoinCashWalletAddresses(walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes)
|
||||
.derivePath("m/44'/145'/0'/1"),
|
||||
networkType: networkType);
|
||||
walletAddresses = BitcoinCashWalletAddresses(
|
||||
walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"),
|
||||
network: network,
|
||||
initialAddressPageType: addressPageType,
|
||||
);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static Future<BitcoinCashWallet> create(
|
||||
{required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0}) async {
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex}) async {
|
||||
return BitcoinCashWallet(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await Mnemonic.toSeed(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex);
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await Mnemonic.toSeed(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
addressPageType: P2pkhAddressType.p2pkh,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<BitcoinCashWallet> open({
|
||||
|
@ -90,17 +95,39 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
}) async {
|
||||
final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password);
|
||||
final snp = await ElectrumWalletSnapshot.load(
|
||||
name, walletInfo.type, password, BitcoinCashNetwork.mainnet);
|
||||
return BitcoinCashWallet(
|
||||
mnemonic: snp.mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await Mnemonic.toSeed(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex);
|
||||
mnemonic: snp.mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses.map((addr) {
|
||||
try {
|
||||
BitcoinCashAddress(addr.address);
|
||||
return BitcoinAddressRecord(
|
||||
addr.address,
|
||||
index: addr.index,
|
||||
isHidden: addr.isHidden,
|
||||
type: P2pkhAddressType.p2pkh,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
);
|
||||
} catch (_) {
|
||||
return BitcoinAddressRecord(
|
||||
AddressUtils.getCashAddrFormat(addr.address),
|
||||
index: addr.index,
|
||||
isHidden: addr.isHidden,
|
||||
type: P2pkhAddressType.p2pkh,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await Mnemonic.toSeed(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex,
|
||||
addressPageType: P2pkhAddressType.p2pkh,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -270,20 +297,18 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
electrumClient: electrumClient, amount: amount, fee: fee);
|
||||
}
|
||||
|
||||
bitbox.ECPair generateKeyPair(
|
||||
{required bitcoin.HDWallet hd,
|
||||
required int index}) =>
|
||||
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
|
||||
bitbox.ECPair.fromWIF(hd.derive(index).wif!);
|
||||
|
||||
@override
|
||||
int feeAmountForPriority(
|
||||
BitcoinTransactionPriority priority, int inputsCount, int outputsCount) =>
|
||||
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
|
||||
{int? size}) =>
|
||||
feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
|
||||
|
||||
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) =>
|
||||
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
|
||||
feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
|
||||
|
||||
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) {
|
||||
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
|
||||
int inputsCount = 0;
|
||||
int totalValue = 0;
|
||||
|
||||
|
@ -323,9 +348,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
@override
|
||||
String signMessage(String message, {String? address = null}) {
|
||||
final index = address != null
|
||||
? walletAddresses.addresses
|
||||
? walletAddresses.allAddresses
|
||||
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))
|
||||
.index : null;
|
||||
.index
|
||||
: null;
|
||||
final HD = index == null ? hd : hd.derive(index);
|
||||
return base64Encode(HD.signMessage(message));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
@ -11,24 +10,20 @@ part 'bitcoin_cash_wallet_addresses.g.dart';
|
|||
class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$BitcoinCashWalletAddresses;
|
||||
|
||||
abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||
BitcoinCashWalletAddressesBase(WalletInfo walletInfo,
|
||||
{required bitcoin.HDWallet mainHd,
|
||||
required bitcoin.HDWallet sideHd,
|
||||
required bitcoin.NetworkType networkType,
|
||||
required ElectrumClient electrumClient,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: mainHd,
|
||||
sideHd: sideHd,
|
||||
electrumClient: electrumClient,
|
||||
networkType: networkType);
|
||||
BitcoinCashWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required super.mainHd,
|
||||
required super.sideHd,
|
||||
required super.network,
|
||||
required super.electrumClient,
|
||||
super.initialAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
super.initialAddressPageType,
|
||||
}) : super(walletInfo);
|
||||
|
||||
@override
|
||||
String getAddress({required int index, required bitcoin.HDWallet hd}) =>
|
||||
generateP2PKHAddress(hd: hd, index: index, networkType: networkType);
|
||||
String getAddress(
|
||||
{required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) =>
|
||||
generateP2PKHAddress(hd: hd, index: index, network: network);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:bip39/bip39.dart';
|
||||
import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
@ -15,8 +12,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:hive/hive.dart';
|
||||
|
||||
class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredentials,
|
||||
BitcoinCashRestoreWalletFromSeedCredentials,
|
||||
BitcoinCashRestoreWalletFromWIFCredentials> {
|
||||
BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials> {
|
||||
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
@ -30,13 +26,9 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
|
|||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<BitcoinCashWallet> create(
|
||||
credentials) async {
|
||||
final strength = (credentials.seedPhraseLength == 12)
|
||||
? 128
|
||||
: (credentials.seedPhraseLength == 24)
|
||||
? 256
|
||||
: 128;
|
||||
Future<BitcoinCashWallet> create(credentials, {bool? isTestnet}) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final wallet = await BitcoinCashWalletBase.create(
|
||||
mnemonic: await Mnemonic.generate(strength: strength),
|
||||
password: credentials.password!,
|
||||
|
@ -49,28 +41,42 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
|
|||
|
||||
@override
|
||||
Future<BitcoinCashWallet> openWallet(String name, String password) async {
|
||||
final walletInfo = walletInfoSource.values.firstWhereOrNull(
|
||||
(info) => info.id == WalletBase.idFor(name, getType()))!;
|
||||
final wallet = await BitcoinCashWalletBase.open(
|
||||
password: password, name: name, walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
|
||||
|
||||
try {
|
||||
final wallet = await BitcoinCashWalletBase.open(
|
||||
password: password,
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
saveBackup(name);
|
||||
return wallet;
|
||||
} catch (_) {
|
||||
await restoreWalletFilesFromBackup(name);
|
||||
final wallet = await BitcoinCashWalletBase.open(
|
||||
password: password,
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@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 BitcoinCashWalletBase.open(
|
||||
password: password,
|
||||
name: currentName,
|
||||
|
@ -78,6 +84,7 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
|
|||
unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
@ -87,15 +94,14 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
|
|||
}
|
||||
|
||||
@override
|
||||
Future<BitcoinCashWallet>
|
||||
restoreFromKeys(credentials) {
|
||||
Future<BitcoinCashWallet> restoreFromKeys(credentials, {bool? isTestnet}) {
|
||||
// TODO: implement restoreFromKeys
|
||||
throw UnimplementedError('restoreFromKeys() is not implemented');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BitcoinCashWallet> restoreFromSeed(
|
||||
BitcoinCashRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<BitcoinCashWallet> restoreFromSeed(BitcoinCashRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
if (!validateMnemonic(credentials.mnemonic)) {
|
||||
throw BitcoinCashMnemonicIsIncorrectException();
|
||||
}
|
||||
|
|
|
@ -29,7 +29,10 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||
ref: master
|
||||
bitcoin_base: ^3.0.1
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base.git
|
||||
ref: cake-update-v2
|
||||
|
||||
|
||||
|
||||
|
|
22
cw_core/lib/battery_optimization_native.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
const MethodChannel _channel = MethodChannel('com.cake_wallet/native_utils');
|
||||
|
||||
Future<void> requestDisableBatteryOptimization() async {
|
||||
try {
|
||||
await _channel.invokeMethod('disableBatteryOptimization');
|
||||
} on PlatformException catch (e) {
|
||||
print("Failed to disable battery optimization: '${e.message}'.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isBatteryOptimizationDisabled() async {
|
||||
try {
|
||||
final bool isDisabled = await _channel.invokeMethod('isBatteryOptimizationDisabled') as bool;
|
||||
print('It\'s actually disabled? $isDisabled');
|
||||
return isDisabled;
|
||||
} on PlatformException catch (e) {
|
||||
print("Failed to check battery optimization status: '${e.message}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
required this.decimals,
|
||||
this.fullName,
|
||||
this.iconPath,
|
||||
this.tag})
|
||||
this.tag,
|
||||
this.enabled = false,
|
||||
})
|
||||
: super(title: title, raw: raw);
|
||||
|
||||
final String name;
|
||||
|
@ -17,6 +19,9 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
final String? fullName;
|
||||
final String? iconPath;
|
||||
final int decimals;
|
||||
final bool enabled;
|
||||
|
||||
set enabled(bool value) => this.enabled = value;
|
||||
|
||||
static const all = [
|
||||
CryptoCurrency.xmr,
|
||||
|
@ -96,6 +101,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
CryptoCurrency.usdtPoly,
|
||||
CryptoCurrency.usdcEPoly,
|
||||
CryptoCurrency.kaspa,
|
||||
CryptoCurrency.digibyte,
|
||||
CryptoCurrency.usdtSol,
|
||||
];
|
||||
|
||||
static const havenCurrencies = [
|
||||
|
@ -207,7 +214,9 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29);
|
||||
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
|
||||
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kaspa', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
|
||||
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
|
||||
static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8);
|
||||
static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
|
||||
|
||||
|
||||
static final Map<int, CryptoCurrency> _rawCurrencyMap =
|
||||
|
@ -238,7 +247,16 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
return CryptoCurrency._rawCurrencyMap[raw]!;
|
||||
}
|
||||
|
||||
static CryptoCurrency fromString(String name) {
|
||||
// TODO: refactor this
|
||||
static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) {
|
||||
try {
|
||||
return CryptoCurrency.all.firstWhere((element) =>
|
||||
element.title.toLowerCase() == name &&
|
||||
(element.tag == null ||
|
||||
element.tag == walletCurrency?.title ||
|
||||
element.tag == walletCurrency?.tag));
|
||||
} catch (_) {}
|
||||
|
||||
if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) {
|
||||
final s = 'Unexpected token: $name for CryptoCurrency fromString';
|
||||
throw ArgumentError.value(name, 'name', s);
|
||||
|
|
|
@ -21,6 +21,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
|
|||
return CryptoCurrency.banano;
|
||||
case WalletType.polygon:
|
||||
return CryptoCurrency.maticpoly;
|
||||
case WalletType.solana:
|
||||
return CryptoCurrency.sol;
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
|
||||
}
|
||||
|
|
13
cw_core/lib/enumerate.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
abstract class Enumerate {
|
||||
String get value;
|
||||
|
||||
@override
|
||||
operator ==(other) {
|
||||
if (identical(other, this)) return true;
|
||||
if (other is! Enumerate) return false;
|
||||
return other.runtimeType == runtimeType && value == other.value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
}
|
|
@ -13,4 +13,5 @@ const ADDRESS_INFO_TYPE_ID = 11;
|
|||
const ERC20_TOKEN_TYPE_ID = 12;
|
||||
const NANO_ACCOUNT_TYPE_ID = 13;
|
||||
const POW_NODE_TYPE_ID = 14;
|
||||
const DERIVATION_TYPE_TYPE_ID = 15;
|
||||
const DERIVATION_TYPE_TYPE_ID = 15;
|
||||
const SPL_TOKEN_TYPE_ID = 16;
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
//import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/enumerable_item.dart';
|
||||
|
||||
class MoneroTransactionPriority extends TransactionPriority {
|
||||
const MoneroTransactionPriority({required String title, required int raw})
|
||||
|
@ -12,21 +9,20 @@ class MoneroTransactionPriority extends TransactionPriority {
|
|||
MoneroTransactionPriority.automatic,
|
||||
MoneroTransactionPriority.medium,
|
||||
MoneroTransactionPriority.fast,
|
||||
MoneroTransactionPriority.fastest
|
||||
MoneroTransactionPriority.fastest,
|
||||
];
|
||||
static const slow = MoneroTransactionPriority(title: 'Slow', raw: 0);
|
||||
static const automatic = MoneroTransactionPriority(title: 'Automatic', raw: 1);
|
||||
static const automatic = MoneroTransactionPriority(title: 'Automatic', raw: 0);
|
||||
static const slow = MoneroTransactionPriority(title: 'Slow', raw: 1);
|
||||
static const medium = MoneroTransactionPriority(title: 'Medium', raw: 2);
|
||||
static const fast = MoneroTransactionPriority(title: 'Fast', raw: 3);
|
||||
static const fastest = MoneroTransactionPriority(title: 'Fastest', raw: 4);
|
||||
static const standard = slow;
|
||||
|
||||
static MoneroTransactionPriority deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
case 0:
|
||||
return slow;
|
||||
case 1:
|
||||
return automatic;
|
||||
case 1:
|
||||
return slow;
|
||||
case 2:
|
||||
return medium;
|
||||
case 3:
|
||||
|
|
|
@ -54,6 +54,17 @@ Future<void> restoreWalletFiles(String name) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> resetCache(String name) async {
|
||||
await removeCache(name);
|
||||
|
||||
final walletDirPath = await pathForWalletDir(name: name, type: WalletType.monero);
|
||||
final cacheFilePath = '$walletDirPath/$name';
|
||||
final backupCacheFile = File(backupFileName(cacheFilePath));
|
||||
if (backupCacheFile.existsSync()) {
|
||||
await backupCacheFile.copy(cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> backupWalletFilesExists(String name) async {
|
||||
final walletDirPath = await pathForWalletDir(name: name, type: WalletType.monero);
|
||||
final cacheFilePath = '$walletDirPath/$name';
|
||||
|
@ -63,9 +74,9 @@ Future<bool> backupWalletFilesExists(String name) async {
|
|||
final backupKeysFile = File(backupFileName(keysFilePath));
|
||||
final backupAddressListFile = File(backupFileName(addressListFilePath));
|
||||
|
||||
return backupCacheFile.existsSync()
|
||||
&& backupKeysFile.existsSync()
|
||||
&& backupAddressListFile.existsSync();
|
||||
return backupCacheFile.existsSync() &&
|
||||
backupKeysFile.existsSync() &&
|
||||
backupAddressListFile.existsSync();
|
||||
}
|
||||
|
||||
Future<void> removeCache(String name) async {
|
||||
|
@ -85,4 +96,4 @@ Future<void> restoreOrResetWalletFiles(String name) async {
|
|||
}
|
||||
|
||||
removeCache(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,15 +75,10 @@ class Node extends HiveObject with Keyable {
|
|||
Uri get uri {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return Uri.http(uriRaw, '');
|
||||
case WalletType.bitcoin:
|
||||
return createUriFromElectrumAddress(uriRaw);
|
||||
case WalletType.litecoin:
|
||||
return createUriFromElectrumAddress(uriRaw);
|
||||
case WalletType.haven:
|
||||
return Uri.http(uriRaw, '');
|
||||
case WalletType.ethereum:
|
||||
return Uri.https(uriRaw, '');
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return createUriFromElectrumAddress(uriRaw);
|
||||
case WalletType.nano:
|
||||
|
@ -93,7 +88,9 @@ class Node extends HiveObject with Keyable {
|
|||
} else {
|
||||
return Uri.http(uriRaw, '');
|
||||
}
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.solana:
|
||||
return Uri.https(uriRaw, '');
|
||||
default:
|
||||
throw Exception('Unexpected type ${type.toString()} for Node uri');
|
||||
|
@ -139,21 +136,17 @@ class Node extends HiveObject with Keyable {
|
|||
try {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return requestMoneroNode();
|
||||
case WalletType.bitcoin:
|
||||
return requestElectrumServer();
|
||||
case WalletType.litecoin:
|
||||
return requestElectrumServer();
|
||||
case WalletType.haven:
|
||||
return requestMoneroNode();
|
||||
case WalletType.ethereum:
|
||||
return requestElectrumServer();
|
||||
case WalletType.bitcoinCash:
|
||||
return requestElectrumServer();
|
||||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
return requestNanoNode();
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.solana:
|
||||
return requestElectrumServer();
|
||||
default:
|
||||
return false;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<String> pathForWalletDir({required String name, required WalletType type}) async {
|
||||
|
|
21
cw_core/lib/receive_page_option.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'package:cw_core/enumerate.dart';
|
||||
|
||||
class ReceivePageOption implements Enumerate {
|
||||
static const mainnet = ReceivePageOption._('mainnet');
|
||||
static const anonPayInvoice = ReceivePageOption._('anonPayInvoice');
|
||||
static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink');
|
||||
|
||||
const ReceivePageOption._(this.value);
|
||||
|
||||
final String value;
|
||||
|
||||
String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const ReceivePageOptions = [
|
||||
ReceivePageOption.mainnet,
|
||||
ReceivePageOption.anonPayInvoice,
|
||||
ReceivePageOption.anonPayDonationLink
|
||||
];
|
|
@ -88,4 +88,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
Future<void> renameWalletFiles(String newWalletName);
|
||||
|
||||
String signMessage(String message, {String? address = null}) => throw UnimplementedError();
|
||||
|
||||
bool? isTestnet;
|
||||
}
|
||||
|
|
|
@ -148,6 +148,12 @@ class WalletInfo extends HiveObject {
|
|||
@HiveField(17)
|
||||
String? derivationPath;
|
||||
|
||||
@HiveField(18)
|
||||
String? addressPageType;
|
||||
|
||||
@HiveField(19)
|
||||
String? network;
|
||||
|
||||
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
|
||||
|
||||
set yatLastUsedAddress(String address) {
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import 'package:cw_core/node.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
|
||||
RFK extends WalletCredentials> {
|
||||
WalletType getType();
|
||||
|
||||
Future<WalletBase> create(N credentials);
|
||||
Future<WalletBase> create(N credentials, {bool? isTestnet});
|
||||
|
||||
Future<WalletBase> restoreFromSeed(RFS credentials);
|
||||
Future<WalletBase> restoreFromSeed(RFS credentials, {bool? isTestnet});
|
||||
|
||||
Future<WalletBase> restoreFromKeys(RFK credentials);
|
||||
Future<WalletBase> restoreFromKeys(RFK credentials, {bool? isTestnet});
|
||||
|
||||
Future<WalletBase> openWallet(String name, String password);
|
||||
|
||||
|
@ -21,4 +22,22 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
|||
Future<void> remove(String wallet);
|
||||
|
||||
Future<void> rename(String currentName, String password, String newName);
|
||||
|
||||
Future<void> restoreWalletFilesFromBackup(String name) async {
|
||||
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
|
||||
final walletDirPath = await pathForWalletDir(name: name, type: getType());
|
||||
|
||||
if (File(backupWalletDirPath).existsSync()) {
|
||||
await File(backupWalletDirPath).copy(walletDirPath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveBackup(String name) async {
|
||||
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
|
||||
final walletDirPath = await pathForWalletDir(name: name, type: getType());
|
||||
|
||||
if (File(walletDirPath).existsSync()) {
|
||||
await File(walletDirPath).copy(backupWalletDirPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ const walletTypes = [
|
|||
WalletType.nano,
|
||||
WalletType.banano,
|
||||
WalletType.polygon,
|
||||
WalletType.solana,
|
||||
];
|
||||
|
||||
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
|
||||
|
@ -46,7 +47,10 @@ enum WalletType {
|
|||
bitcoinCash,
|
||||
|
||||
@HiveField(9)
|
||||
polygon
|
||||
polygon,
|
||||
|
||||
@HiveField(10)
|
||||
solana
|
||||
}
|
||||
|
||||
int serializeToInt(WalletType type) {
|
||||
|
@ -69,6 +73,8 @@ int serializeToInt(WalletType type) {
|
|||
return 7;
|
||||
case WalletType.polygon:
|
||||
return 8;
|
||||
case WalletType.solana:
|
||||
return 9;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
@ -94,6 +100,8 @@ WalletType deserializeFromInt(int raw) {
|
|||
return WalletType.bitcoinCash;
|
||||
case 8:
|
||||
return WalletType.polygon;
|
||||
case 9:
|
||||
return WalletType.solana;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
|
||||
}
|
||||
|
@ -119,6 +127,8 @@ String walletTypeToString(WalletType type) {
|
|||
return 'Banano';
|
||||
case WalletType.polygon:
|
||||
return 'Polygon';
|
||||
case WalletType.solana:
|
||||
return 'Solana';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -144,6 +154,8 @@ String walletTypeToDisplayName(WalletType type) {
|
|||
return 'Banano (BAN)';
|
||||
case WalletType.polygon:
|
||||
return 'Polygon (MATIC)';
|
||||
case WalletType.solana:
|
||||
return 'Solana (SOL)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -169,6 +181,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
|
|||
return CryptoCurrency.banano;
|
||||
case WalletType.polygon:
|
||||
return CryptoCurrency.maticpoly;
|
||||
case WalletType.solana:
|
||||
return CryptoCurrency.sol;
|
||||
default:
|
||||
throw Exception(
|
||||
'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
|
||||
|
|
|
@ -5,34 +5,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8"
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "47.0.0"
|
||||
version: "64.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80"
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "6.2.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.2"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039
|
||||
sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -53,10 +53,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
|
||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.4.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -69,34 +69,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf"
|
||||
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.0.1"
|
||||
build_resolvers:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6"
|
||||
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.10"
|
||||
version: "2.4.2"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
|
||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.4.8"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
|
||||
sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
version: "7.2.11"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -109,10 +109,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725"
|
||||
sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.3"
|
||||
version: "8.8.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -125,10 +125,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -141,10 +141,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe"
|
||||
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "4.10.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -165,26 +165,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4"
|
||||
sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.3.4"
|
||||
encrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: encrypt
|
||||
sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb"
|
||||
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -197,10 +197,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.1.0"
|
||||
file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -226,10 +226,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_mobx
|
||||
sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e"
|
||||
sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6+5"
|
||||
version: "2.2.0+2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -247,18 +247,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
|
||||
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.3.1"
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -327,18 +327,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
version: "4.8.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -375,18 +375,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: mobx
|
||||
sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a
|
||||
sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3+1"
|
||||
version: "2.3.0+1"
|
||||
mobx_codegen:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mobx_codegen
|
||||
sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181"
|
||||
sha256: b26c7f9c20b38f0ea572c1ed3f29d8e027cb265538bbd1aed3ec198642cfca42
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.6.0+1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -407,26 +415,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
|
||||
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -439,10 +447,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -455,26 +463,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.2"
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -483,46 +491,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
process:
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||
name: provider
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
version: "6.1.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17"
|
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a"
|
||||
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.3"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.4"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -540,18 +548,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d"
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.6"
|
||||
version: "1.5.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f"
|
||||
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
version: "1.3.4"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -620,10 +628,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -636,42 +644,42 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0"
|
||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.1.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
||||
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "5.0.9"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0+3"
|
||||
version: "1.0.4"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
|
|
@ -16,7 +16,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
WalletType getType() => WalletType.ethereum;
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> create(EVMChainNewWalletCredentials credentials) async {
|
||||
Future<EthereumWallet> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final mnemonic = bip39.generateMnemonic(strength: strength);
|
||||
|
@ -39,16 +39,30 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
Future<EthereumWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await EthereumWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
try {
|
||||
final wallet = await EthereumWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
return wallet;
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
saveBackup(name);
|
||||
return wallet;
|
||||
} catch (_) {
|
||||
await restoreWalletFilesFromBackup(name);
|
||||
|
||||
final wallet = await EthereumWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -59,6 +73,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
@ -68,7 +83,8 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async {
|
||||
Future<EthereumWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials,
|
||||
{bool? isTestnet}) async {
|
||||
final wallet = EthereumWallet(
|
||||
password: credentials.password!,
|
||||
privateKey: credentials.privateKey,
|
||||
|
@ -84,8 +100,8 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> restoreFromSeed(
|
||||
EVMChainRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<EthereumWallet> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw EthereumMnemonicIsIncorrectException();
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ abstract class EVMChainWalletBase
|
|||
privateKey: _hexPrivateKey,
|
||||
password: _password,
|
||||
);
|
||||
walletAddresses.address = _evmChainPrivateKey.address.toString();
|
||||
walletAddresses.address = _evmChainPrivateKey.address.hexEip55;
|
||||
await save();
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
|
|||
super(walletInfo);
|
||||
|
||||
@override
|
||||
@observable
|
||||
String address;
|
||||
|
||||
@override
|
||||
|
|
|
@ -22,7 +22,7 @@ abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletSer
|
|||
WalletType getType();
|
||||
|
||||
@override
|
||||
Future<T> create(EVMChainNewWalletCredentials credentials);
|
||||
Future<T> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet});
|
||||
|
||||
@override
|
||||
Future<T> openWallet(String name, String password);
|
||||
|
@ -31,10 +31,10 @@ abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletSer
|
|||
Future<void> rename(String currentName, String password, String newName);
|
||||
|
||||
@override
|
||||
Future<T> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials);
|
||||
Future<T> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, {bool? isTestnet});
|
||||
|
||||
@override
|
||||
Future<T> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials);
|
||||
Future<T> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, {bool? isTestnet});
|
||||
|
||||
@override
|
||||
Future<bool> isWalletExit(String name) async =>
|
||||
|
|
|
@ -68,7 +68,7 @@ class HavenWalletService extends WalletService<
|
|||
WalletType getType() => WalletType.haven;
|
||||
|
||||
@override
|
||||
Future<HavenWallet> create(HavenNewWalletCredentials credentials) async {
|
||||
Future<HavenWallet> create(HavenNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
await haven_wallet_manager.createWallet(
|
||||
|
@ -163,6 +163,7 @@ class HavenWalletService extends WalletService<
|
|||
final currentWallet = HavenWallet(walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
@ -173,7 +174,7 @@ class HavenWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<HavenWallet> restoreFromKeys(
|
||||
HavenRestoreWalletFromKeysCredentials credentials) async {
|
||||
HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
await haven_wallet_manager.restoreFromKeys(
|
||||
|
@ -197,7 +198,7 @@ class HavenWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<HavenWallet> restoreFromSeed(
|
||||
HavenRestoreWalletFromSeedCredentials credentials) async {
|
||||
HavenRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
await haven_wallet_manager.restoreFromSeed(
|
||||
|
|
|
@ -36,6 +36,8 @@ import 'package:mobx/mobx.dart';
|
|||
part 'monero_wallet.g.dart';
|
||||
|
||||
const moneroBlockSize = 1000;
|
||||
// not sure if this should just be 0 but setting it higher feels safer / should catch more cases:
|
||||
const MIN_RESTORE_HEIGHT = 1000;
|
||||
|
||||
class MoneroWallet = MoneroWalletBase with _$MoneroWallet;
|
||||
|
||||
|
@ -79,7 +81,7 @@ abstract class MoneroWalletBase
|
|||
|
||||
Box<UnspentCoinsInfo> unspentCoinsInfo;
|
||||
|
||||
void Function(FlutterErrorDetails)? _onError;
|
||||
void Function(FlutterErrorDetails)? onError;
|
||||
|
||||
@override
|
||||
late MoneroWalletAddresses walletAddresses;
|
||||
|
@ -171,7 +173,21 @@ abstract class MoneroWalletBase
|
|||
Future<void> startSync() async {
|
||||
try {
|
||||
_setInitialHeight();
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
// our restore height wasn't correct, so lets see if using the backup works:
|
||||
try {
|
||||
await resetCache(name);
|
||||
_setInitialHeight();
|
||||
} catch (e) {
|
||||
// we still couldn't get a valid height from the backup?!:
|
||||
// try to use the date instead:
|
||||
try {
|
||||
_setHeightFromDate();
|
||||
} catch (_) {
|
||||
// we still couldn't get a valid sync height :/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
|
@ -339,6 +355,8 @@ abstract class MoneroWalletBase
|
|||
if (currentAddressListFile.existsSync()) {
|
||||
await currentAddressListFile.rename('$newWalletPath.address.txt');
|
||||
}
|
||||
|
||||
await backupWalletFiles(newWalletName);
|
||||
} catch (e) {
|
||||
final currentWalletPath = await pathForWallet(name: name, type: type);
|
||||
|
||||
|
@ -402,9 +420,7 @@ abstract class MoneroWalletBase
|
|||
if (coin.spent == 0) {
|
||||
final unspent = MoneroUnspent.fromCoinsInfoRow(coin);
|
||||
if (unspent.hash.isNotEmpty) {
|
||||
unspent.isChange = transaction_history
|
||||
.getTransaction(unspent.hash)
|
||||
.direction == 1;
|
||||
unspent.isChange = transaction_history.getTransaction(unspent.hash).direction == 1;
|
||||
}
|
||||
unspentCoins.add(unspent);
|
||||
}
|
||||
|
@ -418,7 +434,7 @@ abstract class MoneroWalletBase
|
|||
if (unspentCoins.isNotEmpty) {
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where((element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.walletId.contains(id) &&
|
||||
element.accountIndex == walletAddresses.account!.id &&
|
||||
element.keyImage!.contains(coin.keyImage!));
|
||||
|
||||
|
@ -438,7 +454,7 @@ abstract class MoneroWalletBase
|
|||
_askForUpdateBalance();
|
||||
} catch (e, s) {
|
||||
print(e.toString());
|
||||
_onError?.call(FlutterErrorDetails(
|
||||
onError?.call(FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: s,
|
||||
library: this.runtimeType.toString(),
|
||||
|
@ -534,18 +550,34 @@ abstract class MoneroWalletBase
|
|||
_listener = monero_wallet.setListeners(_onNewBlock, _onNewTransaction);
|
||||
}
|
||||
|
||||
// check if the height is correct:
|
||||
void _setInitialHeight() {
|
||||
if (walletInfo.isRecovery) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentHeight = monero_wallet.getCurrentHeight();
|
||||
final height = monero_wallet.getCurrentHeight();
|
||||
|
||||
if (currentHeight <= 1) {
|
||||
final height = _getHeightByDate(walletInfo.date);
|
||||
monero_wallet.setRecoveringFromSeed(isRecovery: true);
|
||||
monero_wallet.setRefreshFromBlockHeight(height: height);
|
||||
if (height > MIN_RESTORE_HEIGHT) {
|
||||
// the restore height is probably correct, so we do nothing:
|
||||
return;
|
||||
}
|
||||
|
||||
throw Exception("height isn't > $MIN_RESTORE_HEIGHT!");
|
||||
}
|
||||
|
||||
void _setHeightFromDate() {
|
||||
if (walletInfo.isRecovery) {
|
||||
return;
|
||||
}
|
||||
|
||||
int height = 0;
|
||||
try {
|
||||
height = _getHeightByDate(walletInfo.date);
|
||||
} catch (_) {}
|
||||
|
||||
monero_wallet.setRecoveringFromSeed(isRecovery: true);
|
||||
monero_wallet.setRefreshFromBlockHeight(height: height);
|
||||
}
|
||||
|
||||
int _getHeightDistance(DateTime date) {
|
||||
|
@ -561,7 +593,8 @@ abstract class MoneroWalletBase
|
|||
final heightDistance = _getHeightDistance(date);
|
||||
|
||||
if (nodeHeight <= 0) {
|
||||
return 0;
|
||||
// the node returned 0 (an error state)
|
||||
throw Exception("nodeHeight is <= 0!");
|
||||
}
|
||||
|
||||
return nodeHeight - heightDistance;
|
||||
|
@ -650,7 +683,7 @@ abstract class MoneroWalletBase
|
|||
}
|
||||
|
||||
@override
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) e) => onError = e;
|
||||
|
||||
@override
|
||||
String signMessage(String message, {String? address}) {
|
||||
|
|
|
@ -11,11 +11,13 @@ import 'package:cw_core/get_height_by_date.dart';
|
|||
import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart';
|
||||
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:polyseed/polyseed.dart';
|
||||
|
||||
class MoneroNewWalletCredentials extends WalletCredentials {
|
||||
MoneroNewWalletCredentials({required String name, required this.language, required this.isPolyseed, String? password})
|
||||
MoneroNewWalletCredentials(
|
||||
{required String name, required this.language, required this.isPolyseed, String? password})
|
||||
: super(name: name, password: password);
|
||||
|
||||
final String language;
|
||||
|
@ -52,10 +54,8 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
|
|||
final String spendKey;
|
||||
}
|
||||
|
||||
class MoneroWalletService extends WalletService<
|
||||
MoneroNewWalletCredentials,
|
||||
MoneroRestoreWalletFromSeedCredentials,
|
||||
MoneroRestoreWalletFromKeysCredentials> {
|
||||
class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
|
||||
MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials> {
|
||||
MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
@ -68,7 +68,7 @@ class MoneroWalletService extends WalletService<
|
|||
WalletType getType() => WalletType.monero;
|
||||
|
||||
@override
|
||||
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials) async {
|
||||
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
|
||||
|
@ -112,6 +112,7 @@ class MoneroWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<MoneroWallet> openWallet(String name, String password) async {
|
||||
MoneroWallet? wallet;
|
||||
try {
|
||||
final path = await pathForWallet(name: name, type: getType());
|
||||
|
||||
|
@ -119,11 +120,10 @@ class MoneroWalletService extends WalletService<
|
|||
await repairOldAndroidWallet(name);
|
||||
}
|
||||
|
||||
await monero_wallet_manager
|
||||
.openWalletAsync({'path': path, 'password': password});
|
||||
final walletInfo = walletInfoSource.values.firstWhere(
|
||||
(info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await monero_wallet_manager.openWalletAsync({'path': path, 'password': password});
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
final isValid = wallet.walletAddresses.validate();
|
||||
|
||||
if (!isValid) {
|
||||
|
@ -135,7 +135,7 @@ class MoneroWalletService extends WalletService<
|
|||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
// TODO: Implement Exception for wallet list service.
|
||||
|
||||
final bool isBadAlloc = e.toString().contains('bad_alloc') ||
|
||||
|
@ -156,16 +156,18 @@ class MoneroWalletService extends WalletService<
|
|||
final bool invalidSignature = e.toString().contains('invalid signature') ||
|
||||
(e is WalletOpeningException && e.message.contains('invalid signature'));
|
||||
|
||||
if (isBadAlloc ||
|
||||
doesNotCorrespond ||
|
||||
isMissingCacheFilesIOS ||
|
||||
isMissingCacheFilesAndroid ||
|
||||
invalidSignature) {
|
||||
await restoreOrResetWalletFiles(name);
|
||||
return openWallet(name, password);
|
||||
if (!isBadAlloc &&
|
||||
!doesNotCorrespond &&
|
||||
!isMissingCacheFilesIOS &&
|
||||
!isMissingCacheFilesAndroid &&
|
||||
!invalidSignature &&
|
||||
wallet != null &&
|
||||
wallet.onError != null) {
|
||||
wallet.onError!(FlutterErrorDetails(exception: e, stack: s));
|
||||
}
|
||||
|
||||
rethrow;
|
||||
await restoreOrResetWalletFiles(name);
|
||||
return openWallet(name, password);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,10 +187,9 @@ class MoneroWalletService extends WalletService<
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> rename(
|
||||
String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = walletInfoSource.values.firstWhere(
|
||||
(info) => info.id == WalletBase.idFor(currentName, getType()));
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = walletInfoSource.values
|
||||
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
|
||||
final currentWallet =
|
||||
MoneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
|
||||
|
@ -202,8 +203,8 @@ class MoneroWalletService extends WalletService<
|
|||
}
|
||||
|
||||
@override
|
||||
Future<MoneroWallet> restoreFromKeys(
|
||||
MoneroRestoreWalletFromKeysCredentials credentials) async {
|
||||
Future<MoneroWallet> restoreFromKeys(MoneroRestoreWalletFromKeysCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
await monero_wallet_manager.restoreFromKeys(
|
||||
|
@ -227,9 +228,8 @@ class MoneroWalletService extends WalletService<
|
|||
}
|
||||
|
||||
@override
|
||||
Future<MoneroWallet> restoreFromSeed(
|
||||
MoneroRestoreWalletFromSeedCredentials credentials) async {
|
||||
|
||||
Future<MoneroWallet> restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
// Restore from Polyseed
|
||||
if (Polyseed.isValidSeed(credentials.mnemonic)) {
|
||||
return restoreFromPolyseed(credentials);
|
||||
|
@ -254,14 +254,16 @@ class MoneroWalletService extends WalletService<
|
|||
}
|
||||
}
|
||||
|
||||
Future<MoneroWallet> restoreFromPolyseed(MoneroRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<MoneroWallet> restoreFromPolyseed(
|
||||
MoneroRestoreWalletFromSeedCredentials credentials) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
final polyseedCoin = PolyseedCoin.POLYSEED_MONERO;
|
||||
final lang = PolyseedLang.getByPhrase(credentials.mnemonic);
|
||||
final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
|
||||
|
||||
return _restoreFromPolyseed(path, credentials.password!, polyseed, credentials.walletInfo!, lang);
|
||||
return _restoreFromPolyseed(
|
||||
path, credentials.password!, polyseed, credentials.walletInfo!, lang);
|
||||
} catch (e) {
|
||||
// TODO: Implement Exception for wallet list service.
|
||||
print('MoneroWalletsManager Error: $e');
|
||||
|
@ -269,11 +271,11 @@ class MoneroWalletService extends WalletService<
|
|||
}
|
||||
}
|
||||
|
||||
Future<MoneroWallet> _restoreFromPolyseed(String path, String password, Polyseed polyseed,
|
||||
WalletInfo walletInfo, PolyseedLang lang,
|
||||
Future<MoneroWallet> _restoreFromPolyseed(
|
||||
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
|
||||
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async {
|
||||
final height = overrideHeight ?? getMoneroHeigthByDate(
|
||||
date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000));
|
||||
final height = overrideHeight ??
|
||||
getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000));
|
||||
final spendKey = polyseed.generateKey(coin, 32).toHexString();
|
||||
final seed = polyseed.encode(lang, coin);
|
||||
|
||||
|
@ -288,8 +290,7 @@ class MoneroWalletService extends WalletService<
|
|||
restoreHeight: height,
|
||||
spendKey: spendKey);
|
||||
|
||||
final wallet = MoneroWallet(
|
||||
walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
|
||||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
|
@ -301,16 +302,14 @@ class MoneroWalletService extends WalletService<
|
|||
return;
|
||||
}
|
||||
|
||||
final oldAndroidWalletDirPath =
|
||||
await outdatedAndroidPathForWalletDir(name: name);
|
||||
final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name);
|
||||
final dir = Directory(oldAndroidWalletDirPath);
|
||||
|
||||
if (!dir.existsSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newWalletDirPath =
|
||||
await pathForWalletDir(name: name, type: getType());
|
||||
final newWalletDirPath = await pathForWalletDir(name: name, type: getType());
|
||||
|
||||
dir.listSync().forEach((f) {
|
||||
final file = File(f.path);
|
||||
|
|
|
@ -26,7 +26,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
WalletType getType() => WalletType.nano;
|
||||
|
||||
@override
|
||||
Future<WalletBase> create(NanoNewWalletCredentials credentials) async {
|
||||
Future<WalletBase> create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
// nano standard:
|
||||
DerivationType derivationType = DerivationType.nano;
|
||||
String seedKey = NanoSeeds.generateSeed();
|
||||
|
@ -69,6 +69,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
NanoWallet(walletInfo: currentWalletInfo, password: password, mnemonic: randomWords);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
@ -78,7 +79,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
}
|
||||
|
||||
@override
|
||||
Future<NanoWallet> restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials) async {
|
||||
Future<NanoWallet> restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async {
|
||||
if (credentials.seedKey.contains(' ')) {
|
||||
throw Exception("Invalid key!");
|
||||
} else {
|
||||
|
@ -112,7 +113,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
}
|
||||
|
||||
@override
|
||||
Future<NanoWallet> restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<NanoWallet> restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async {
|
||||
if (credentials.mnemonic.contains(' ')) {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw nm.NanoMnemonicIsIncorrectException();
|
||||
|
@ -150,14 +151,29 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
Future<NanoWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await NanoWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
return wallet;
|
||||
try {
|
||||
final wallet = await NanoWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
saveBackup(name);
|
||||
return wallet;
|
||||
} catch (_) {
|
||||
await restoreWalletFilesFromBackup(name);
|
||||
final wallet = await NanoWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
|
|||
WalletType getType() => WalletType.polygon;
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> create(EVMChainNewWalletCredentials credentials) async {
|
||||
Future<PolygonWallet> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final mnemonic = bip39.generateMnemonic(strength: strength);
|
||||
|
@ -42,21 +42,36 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
|
|||
Future<PolygonWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await PolygonWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
try {
|
||||
final wallet = await PolygonWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
return wallet;
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
saveBackup(name);
|
||||
return wallet;
|
||||
} catch (_) {
|
||||
await restoreWalletFilesFromBackup(name);
|
||||
|
||||
final wallet = await PolygonWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async {
|
||||
|
||||
Future<PolygonWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials,
|
||||
{bool? isTestnet}) async {
|
||||
final wallet = PolygonWallet(
|
||||
password: credentials.password!,
|
||||
privateKey: credentials.privateKey,
|
||||
|
@ -72,8 +87,8 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> restoreFromSeed(
|
||||
EVMChainRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<PolygonWallet> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw PolygonMnemonicIsIncorrectException();
|
||||
}
|
||||
|
@ -100,6 +115,7 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
|
|||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
|
30
cw_solana/.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
10
cw_solana/.metadata
Normal file
|
@ -0,0 +1,10 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
3
cw_solana/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
cw_solana/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
39
cw_solana/README.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
4
cw_solana/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
7
cw_solana/lib/cw_solana.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
library cw_solana;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
107
cw_solana/lib/default_spl_tokens.dart
Normal file
|
@ -0,0 +1,107 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_solana/spl_token.dart';
|
||||
|
||||
class DefaultSPLTokens {
|
||||
final List<SPLToken> _defaultTokens = [
|
||||
SPLToken(
|
||||
name: 'USDT Tether',
|
||||
symbol: 'USDT',
|
||||
mintAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
|
||||
decimal: 6,
|
||||
mint: 'usdtsol',
|
||||
enabled: true,
|
||||
),
|
||||
SPLToken(
|
||||
name: 'USD Coin',
|
||||
symbol: 'USDC',
|
||||
mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
decimal: 6,
|
||||
mint: 'usdcsol',
|
||||
enabled: true,
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Wrapped Ethereum (Sollet)',
|
||||
symbol: 'soETH',
|
||||
mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk',
|
||||
decimal: 6,
|
||||
mint: 'soEth',
|
||||
iconPath: 'assets/images/eth_icon.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Wrapped SOL',
|
||||
symbol: 'WSOL',
|
||||
mintAddress: 'So11111111111111111111111111111111111111112',
|
||||
decimal: 9,
|
||||
mint: 'WSOL',
|
||||
iconPath: 'assets/images/sol_icon.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Wrapped Bitcoin (Sollet)',
|
||||
symbol: 'BTC',
|
||||
mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
|
||||
decimal: 6,
|
||||
mint: 'btcsol',
|
||||
iconPath: 'assets/images/btc.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Bonk',
|
||||
symbol: 'Bonk',
|
||||
mintAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
|
||||
decimal: 5,
|
||||
mint: 'Bonk',
|
||||
iconPath: 'assets/images/bonk_icon.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Helium Network Token',
|
||||
symbol: 'HNT',
|
||||
mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux',
|
||||
decimal: 8,
|
||||
mint: 'hnt',
|
||||
iconPath: 'assets/images/hnt_icon.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Pyth Network',
|
||||
symbol: 'PYTH',
|
||||
mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
|
||||
decimal: 6,
|
||||
mint: 'pyth',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'Raydium',
|
||||
symbol: 'RAY',
|
||||
mintAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
|
||||
decimal: 6,
|
||||
mint: 'ray',
|
||||
iconPath: 'assets/images/ray_icon.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'GMT',
|
||||
symbol: 'GMT',
|
||||
mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx',
|
||||
decimal: 6,
|
||||
mint: 'ray',
|
||||
iconPath: 'assets/images/gmt_icon.png',
|
||||
),
|
||||
SPLToken(
|
||||
name: 'AvocadoCoin',
|
||||
symbol: 'AVDO',
|
||||
mintAddress: 'EE5L8cMU4itTsCSuor7NLK6RZx6JhsBe8GGV3oaAHm3P',
|
||||
decimal: 8,
|
||||
mint: 'avdo',
|
||||
iconPath: 'assets/images/avdo_icon.png',
|
||||
),
|
||||
];
|
||||
|
||||
List<SPLToken> get initialSPLTokens => _defaultTokens.map((token) {
|
||||
String? iconPath;
|
||||
if (token.iconPath != null) return token;
|
||||
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
return SPLToken.copyWith(token, iconPath, 'SOL');
|
||||
}).toList();
|
||||
}
|
39
cw_solana/lib/file.dart
Normal file
|
@ -0,0 +1,39 @@
|
|||
import 'dart:io';
|
||||
import 'package:cw_core/key.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
Future<void> write(
|
||||
{required String path,
|
||||
required String password,
|
||||
required String data}) async {
|
||||
final keys = extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypted = await encode(key: key, iv: iv, data: data);
|
||||
final f = File(path);
|
||||
f.writeAsStringSync(encrypted);
|
||||
}
|
||||
|
||||
Future<void> writeData(
|
||||
{required String path,
|
||||
required String password,
|
||||
required String data}) async {
|
||||
final keys = extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypted = await encode(key: key, iv: iv, data: data);
|
||||
final f = File(path);
|
||||
f.writeAsStringSync(encrypted);
|
||||
}
|
||||
|
||||
Future<String> read({required String path, required String password}) async {
|
||||
final file = File(path);
|
||||
|
||||
if (!file.existsSync()) {
|
||||
file.createSync();
|
||||
}
|
||||
|
||||
final encrypted = file.readAsStringSync();
|
||||
|
||||
return decode(password: password, data: encrypted);
|
||||
}
|
43
cw_solana/lib/pending_solana_transaction.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:solana/encoder.dart';
|
||||
|
||||
class PendingSolanaTransaction with PendingTransaction {
|
||||
final double amount;
|
||||
final SignedTx signedTransaction;
|
||||
final String destinationAddress;
|
||||
final Function sendTransaction;
|
||||
final double fee;
|
||||
|
||||
PendingSolanaTransaction({
|
||||
required this.fee,
|
||||
required this.amount,
|
||||
required this.signedTransaction,
|
||||
required this.destinationAddress,
|
||||
required this.sendTransaction,
|
||||
});
|
||||
|
||||
@override
|
||||
String get amountFormatted {
|
||||
String stringifiedAmount = amount.toString();
|
||||
|
||||
if (stringifiedAmount.toString().length >= 6) {
|
||||
stringifiedAmount = stringifiedAmount.substring(0, 6);
|
||||
}
|
||||
|
||||
return stringifiedAmount;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
return await sendTransaction();
|
||||
}
|
||||
|
||||
@override
|
||||
String get feeFormatted => fee.toString();
|
||||
|
||||
@override
|
||||
String get hex => signedTransaction.encode();
|
||||
|
||||
@override
|
||||
String get id => '';
|
||||
}
|
39
cw_solana/lib/solana_balance.dart
Normal file
|
@ -0,0 +1,39 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/balance.dart';
|
||||
|
||||
class SolanaBalance extends Balance {
|
||||
SolanaBalance(this.balance) : super(balance.toInt(), balance.toInt());
|
||||
|
||||
final double balance;
|
||||
|
||||
@override
|
||||
String get formattedAdditionalBalance => _balanceFormatted();
|
||||
|
||||
@override
|
||||
String get formattedAvailableBalance => _balanceFormatted();
|
||||
|
||||
String _balanceFormatted() {
|
||||
String stringBalance = balance.toString();
|
||||
if (stringBalance.toString().length >= 6) {
|
||||
stringBalance = stringBalance.substring(0, 6);
|
||||
}
|
||||
return stringBalance;
|
||||
}
|
||||
|
||||
static SolanaBalance? fromJSON(String? jsonSource) {
|
||||
if (jsonSource == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
try {
|
||||
return SolanaBalance(decoded['balance']);
|
||||
} catch (e) {
|
||||
return SolanaBalance(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
String toJSON() => json.encode({'balance': balance.toString()});
|
||||
}
|
478
cw_solana/lib/solana_client.dart
Normal file
|
@ -0,0 +1,478 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_solana/pending_solana_transaction.dart';
|
||||
import 'package:cw_solana/solana_balance.dart';
|
||||
import 'package:cw_solana/solana_transaction_model.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:solana/dto.dart';
|
||||
import 'package:solana/encoder.dart';
|
||||
import 'package:solana/solana.dart';
|
||||
import '.secrets.g.dart' as secrets;
|
||||
|
||||
class SolanaWalletClient {
|
||||
final httpClient = http.Client();
|
||||
SolanaClient? _client;
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
Uri? rpcUri;
|
||||
String webSocketUrl;
|
||||
bool isModifiedNodeUri = false;
|
||||
|
||||
if (node.uriRaw == 'rpc.ankr.com') {
|
||||
isModifiedNodeUri = true;
|
||||
String ankrApiKey = secrets.ankrApiKey;
|
||||
|
||||
rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
|
||||
webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
|
||||
} else {
|
||||
webSocketUrl = 'wss://${node.uriRaw}';
|
||||
}
|
||||
|
||||
_client = SolanaClient(
|
||||
rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri,
|
||||
websocketUrl: Uri.parse(webSocketUrl),
|
||||
timeout: const Duration(minutes: 2),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<double> getBalance(String address) async {
|
||||
try {
|
||||
final balance = await _client!.rpcClient.getBalance(address);
|
||||
|
||||
final solBalance = balance.value / lamportsPerSol;
|
||||
|
||||
return solBalance;
|
||||
} catch (_) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProgramAccountsResult?> getSPLTokenAccounts(String mintAddress, String publicKey) async {
|
||||
try {
|
||||
final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner(
|
||||
publicKey,
|
||||
TokenAccountsFilter.byMint(mintAddress),
|
||||
commitment: Commitment.confirmed,
|
||||
encoding: Encoding.jsonParsed,
|
||||
);
|
||||
return tokenAccounts;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<SolanaBalance?> getSplTokenBalance(String mintAddress, String publicKey) async {
|
||||
// Fetch the token accounts (a token can have multiple accounts for various uses)
|
||||
final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey);
|
||||
|
||||
// Handle scenario where there is no token account
|
||||
if (tokenAccounts == null || tokenAccounts.value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sum the balances of all accounts with the specified mint address
|
||||
double totalBalance = 0.0;
|
||||
|
||||
for (var programAccount in tokenAccounts.value) {
|
||||
final tokenAmountResult =
|
||||
await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey);
|
||||
|
||||
final balance = tokenAmountResult.value.uiAmountString;
|
||||
|
||||
final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0;
|
||||
|
||||
totalBalance += balanceAsDouble;
|
||||
}
|
||||
|
||||
return SolanaBalance(totalBalance);
|
||||
}
|
||||
|
||||
Future<double> getGasForMessage(String message) async {
|
||||
try {
|
||||
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
|
||||
final fee = gasPrice / lamportsPerSol;
|
||||
return fee;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the Address's transactions into the account
|
||||
Future<List<SolanaTransactionModel>> fetchTransactions(
|
||||
Ed25519HDPublicKey publicKey, {
|
||||
String? splTokenSymbol,
|
||||
int? splTokenDecimal,
|
||||
}) async {
|
||||
List<SolanaTransactionModel> transactions = [];
|
||||
|
||||
try {
|
||||
final response = await _client!.rpcClient.getTransactionsList(
|
||||
publicKey,
|
||||
commitment: Commitment.confirmed,
|
||||
limit: 1000,
|
||||
);
|
||||
|
||||
for (final tx in response) {
|
||||
if (tx.transaction is ParsedTransaction) {
|
||||
final parsedTx = (tx.transaction as ParsedTransaction);
|
||||
final message = parsedTx.message;
|
||||
|
||||
final fee = (tx.meta?.fee ?? 0) / lamportsPerSol;
|
||||
|
||||
for (final instruction in message.instructions) {
|
||||
if (instruction is ParsedInstruction) {
|
||||
instruction.map(
|
||||
system: (systemData) {
|
||||
systemData.parsed.map(
|
||||
transfer: (transferData) {
|
||||
ParsedSystemTransferInformation transfer = transferData.info;
|
||||
bool isOutgoingTx = transfer.source == publicKey.toBase58();
|
||||
|
||||
double amount = transfer.lamports.toDouble() / lamportsPerSol;
|
||||
|
||||
transactions.add(
|
||||
SolanaTransactionModel(
|
||||
id: parsedTx.signatures.first,
|
||||
from: transfer.source,
|
||||
to: transfer.destination,
|
||||
amount: amount,
|
||||
isOutgoingTx: isOutgoingTx,
|
||||
blockTimeInInt: tx.blockTime!,
|
||||
fee: fee,
|
||||
programId: SystemProgram.programId,
|
||||
tokenSymbol: 'SOL',
|
||||
),
|
||||
);
|
||||
},
|
||||
transferChecked: (_) {},
|
||||
unsupported: (_) {},
|
||||
);
|
||||
},
|
||||
splToken: (splTokenData) {
|
||||
if (splTokenSymbol != null) {
|
||||
splTokenData.parsed.map(
|
||||
transfer: (transferData) {
|
||||
SplTokenTransferInfo transfer = transferData.info;
|
||||
bool isOutgoingTx = transfer.source == publicKey.toBase58();
|
||||
|
||||
double amount = (double.tryParse(transfer.amount) ?? 0.0) /
|
||||
pow(10, splTokenDecimal ?? 9);
|
||||
|
||||
transactions.add(
|
||||
SolanaTransactionModel(
|
||||
id: parsedTx.signatures.first,
|
||||
fee: fee,
|
||||
from: transfer.source,
|
||||
to: transfer.destination,
|
||||
amount: amount,
|
||||
isOutgoingTx: isOutgoingTx,
|
||||
programId: TokenProgram.programId,
|
||||
blockTimeInInt: tx.blockTime!,
|
||||
tokenSymbol: splTokenSymbol,
|
||||
),
|
||||
);
|
||||
},
|
||||
transferChecked: (transferCheckedData) {
|
||||
SplTokenTransferCheckedInfo transfer = transferCheckedData.info;
|
||||
bool isOutgoingTx = transfer.source == publicKey.toBase58();
|
||||
double amount =
|
||||
double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0;
|
||||
|
||||
transactions.add(
|
||||
SolanaTransactionModel(
|
||||
id: parsedTx.signatures.first,
|
||||
fee: fee,
|
||||
from: transfer.source,
|
||||
to: transfer.destination,
|
||||
amount: amount,
|
||||
isOutgoingTx: isOutgoingTx,
|
||||
programId: TokenProgram.programId,
|
||||
blockTimeInInt: tx.blockTime!,
|
||||
tokenSymbol: splTokenSymbol,
|
||||
),
|
||||
);
|
||||
},
|
||||
generic: (genericData) {},
|
||||
);
|
||||
}
|
||||
},
|
||||
memo: (_) {},
|
||||
unsupported: (a) {},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SolanaTransactionModel>> getSPLTokenTransfers(
|
||||
String address,
|
||||
String splTokenSymbol,
|
||||
int splTokenDecimal,
|
||||
Ed25519HDKeyPair ownerKeypair,
|
||||
) async {
|
||||
final tokenMint = Ed25519HDPublicKey.fromBase58(address);
|
||||
|
||||
ProgramAccount? associatedTokenAccount;
|
||||
|
||||
try {
|
||||
associatedTokenAccount = await _client!.getAssociatedTokenAccount(
|
||||
mint: tokenMint,
|
||||
owner: ownerKeypair.publicKey,
|
||||
commitment: Commitment.confirmed,
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
if (associatedTokenAccount == null) return [];
|
||||
|
||||
final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey);
|
||||
|
||||
final tokenTransactions = await fetchTransactions(
|
||||
accountPublicKey,
|
||||
splTokenSymbol: splTokenSymbol,
|
||||
splTokenDecimal: splTokenDecimal,
|
||||
);
|
||||
|
||||
return tokenTransactions;
|
||||
}
|
||||
|
||||
void stop() {}
|
||||
|
||||
SolanaClient? get getSolanaClient => _client;
|
||||
|
||||
Future<PendingSolanaTransaction> signSolanaTransaction({
|
||||
required String tokenTitle,
|
||||
required int tokenDecimals,
|
||||
String? tokenMint,
|
||||
required double inputAmount,
|
||||
required String destinationAddress,
|
||||
required Ed25519HDKeyPair ownerKeypair,
|
||||
List<String> references = const [],
|
||||
}) async {
|
||||
const commitment = Commitment.confirmed;
|
||||
|
||||
final latestBlockhash =
|
||||
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
|
||||
|
||||
final recentBlockhash = RecentBlockhash(
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
feeCalculator: const FeeCalculator(
|
||||
lamportsPerSignature: 500,
|
||||
),
|
||||
);
|
||||
|
||||
if (tokenTitle == CryptoCurrency.sol.title) {
|
||||
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
|
||||
tokenTitle: tokenTitle,
|
||||
tokenDecimals: tokenDecimals,
|
||||
inputAmount: inputAmount,
|
||||
destinationAddress: destinationAddress,
|
||||
ownerKeypair: ownerKeypair,
|
||||
recentBlockhash: recentBlockhash,
|
||||
commitment: commitment,
|
||||
);
|
||||
return pendingNativeTokenTransaction;
|
||||
} else {
|
||||
final pendingSPLTokenTransaction = _signSPLTokenTransaction(
|
||||
tokenTitle: tokenTitle,
|
||||
tokenDecimals: tokenDecimals,
|
||||
tokenMint: tokenMint!,
|
||||
inputAmount: inputAmount,
|
||||
destinationAddress: destinationAddress,
|
||||
ownerKeypair: ownerKeypair,
|
||||
recentBlockhash: recentBlockhash,
|
||||
commitment: commitment,
|
||||
);
|
||||
return pendingSPLTokenTransaction;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
|
||||
required String tokenTitle,
|
||||
required int tokenDecimals,
|
||||
required double inputAmount,
|
||||
required String destinationAddress,
|
||||
required Ed25519HDKeyPair ownerKeypair,
|
||||
required RecentBlockhash recentBlockhash,
|
||||
required Commitment commitment,
|
||||
}) async {
|
||||
// Convert SOL to lamport
|
||||
int lamports = (inputAmount * lamportsPerSol).toInt();
|
||||
|
||||
final instructions = [
|
||||
SystemInstruction.transfer(
|
||||
fundingAccount: ownerKeypair.publicKey,
|
||||
recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress),
|
||||
lamports: lamports,
|
||||
),
|
||||
];
|
||||
|
||||
final message = Message(instructions: instructions);
|
||||
final signers = [ownerKeypair];
|
||||
|
||||
final signedTx = await _signTransactionInternal(
|
||||
message: message,
|
||||
signers: signers,
|
||||
commitment: commitment,
|
||||
recentBlockhash: recentBlockhash,
|
||||
);
|
||||
|
||||
final fee = await _getFeeFromCompiledMessage(
|
||||
message,
|
||||
recentBlockhash,
|
||||
signers.first.publicKey,
|
||||
);
|
||||
|
||||
sendTx() async => await sendTransaction(
|
||||
signedTransaction: signedTx,
|
||||
commitment: commitment,
|
||||
);
|
||||
|
||||
final pendingTransaction = PendingSolanaTransaction(
|
||||
amount: inputAmount,
|
||||
signedTransaction: signedTx,
|
||||
destinationAddress: destinationAddress,
|
||||
sendTransaction: sendTx,
|
||||
fee: fee,
|
||||
);
|
||||
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
Future<PendingSolanaTransaction> _signSPLTokenTransaction({
|
||||
required String tokenTitle,
|
||||
required int tokenDecimals,
|
||||
required String tokenMint,
|
||||
required double inputAmount,
|
||||
required String destinationAddress,
|
||||
required Ed25519HDKeyPair ownerKeypair,
|
||||
required RecentBlockhash recentBlockhash,
|
||||
required Commitment commitment,
|
||||
}) async {
|
||||
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
|
||||
final mint = Ed25519HDPublicKey.fromBase58(tokenMint);
|
||||
|
||||
ProgramAccount? associatedRecipientAccount;
|
||||
ProgramAccount? associatedSenderAccount;
|
||||
|
||||
associatedRecipientAccount = await _client!.getAssociatedTokenAccount(
|
||||
mint: mint,
|
||||
owner: destinationOwner,
|
||||
commitment: commitment,
|
||||
);
|
||||
|
||||
associatedSenderAccount = await _client!.getAssociatedTokenAccount(
|
||||
owner: ownerKeypair.publicKey,
|
||||
mint: mint,
|
||||
commitment: commitment,
|
||||
);
|
||||
|
||||
// Throw an appropriate exception if the sender has no associated
|
||||
// token account
|
||||
if (associatedSenderAccount == null) {
|
||||
throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
|
||||
}
|
||||
|
||||
try {
|
||||
associatedRecipientAccount ??= await _client!.createAssociatedTokenAccount(
|
||||
mint: mint,
|
||||
owner: destinationOwner,
|
||||
funder: ownerKeypair,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Insufficient lamports balance to complete this transaction');
|
||||
}
|
||||
|
||||
// Input by the user
|
||||
final amount = (inputAmount * pow(10, tokenDecimals)).toInt();
|
||||
|
||||
final instruction = TokenInstruction.transfer(
|
||||
source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey),
|
||||
destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey),
|
||||
owner: ownerKeypair.publicKey,
|
||||
amount: amount,
|
||||
);
|
||||
|
||||
final message = Message(instructions: [instruction]);
|
||||
final signers = [ownerKeypair];
|
||||
|
||||
final signedTx = await _signTransactionInternal(
|
||||
message: message,
|
||||
signers: signers,
|
||||
commitment: commitment,
|
||||
recentBlockhash: recentBlockhash,
|
||||
);
|
||||
|
||||
final fee = await _getFeeFromCompiledMessage(
|
||||
message,
|
||||
recentBlockhash,
|
||||
signers.first.publicKey,
|
||||
);
|
||||
|
||||
sendTx() async => await sendTransaction(
|
||||
signedTransaction: signedTx,
|
||||
commitment: commitment,
|
||||
);
|
||||
|
||||
final pendingTransaction = PendingSolanaTransaction(
|
||||
amount: inputAmount,
|
||||
signedTransaction: signedTx,
|
||||
destinationAddress: destinationAddress,
|
||||
sendTransaction: sendTx,
|
||||
fee: fee,
|
||||
);
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
Future<double> _getFeeFromCompiledMessage(
|
||||
Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
|
||||
final compile = message.compile(
|
||||
recentBlockhash: recentBlockhash.blockhash,
|
||||
feePayer: feePayer,
|
||||
);
|
||||
|
||||
final base64Message = base64Encode(compile.toByteArray().toList());
|
||||
|
||||
final fee = await getGasForMessage(base64Message);
|
||||
return fee;
|
||||
}
|
||||
|
||||
Future<SignedTx> _signTransactionInternal({
|
||||
required Message message,
|
||||
required List<Ed25519HDKeyPair> signers,
|
||||
required Commitment commitment,
|
||||
required RecentBlockhash recentBlockhash,
|
||||
}) async {
|
||||
final signedTx = await signTransaction(recentBlockhash, message, signers);
|
||||
|
||||
return signedTx;
|
||||
}
|
||||
|
||||
Future<String> sendTransaction({
|
||||
required SignedTx signedTransaction,
|
||||
required Commitment commitment,
|
||||
}) async {
|
||||
final signature = await _client!.rpcClient.sendTransaction(
|
||||
signedTransaction.encode(),
|
||||
preflightCommitment: commitment,
|
||||
);
|
||||
|
||||
_client!.waitForSignatureStatus(signature, status: commitment);
|
||||
|
||||
return signature;
|
||||
}
|
||||
}
|
21
cw_solana/lib/solana_exceptions.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class SolanaTransactionCreationException implements Exception {
|
||||
final String exceptionMessage;
|
||||
|
||||
SolanaTransactionCreationException(CryptoCurrency currency)
|
||||
: exceptionMessage = 'Error creating ${currency.title} transaction.';
|
||||
|
||||
@override
|
||||
String toString() => exceptionMessage;
|
||||
}
|
||||
|
||||
class SolanaTransactionWrongBalanceException implements Exception {
|
||||
final String exceptionMessage;
|
||||
|
||||
SolanaTransactionWrongBalanceException(CryptoCurrency currency)
|
||||
: exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
|
||||
|
||||
@override
|
||||
String toString() => exceptionMessage;
|
||||
}
|
2058
cw_solana/lib/solana_mnemonics.dart
Normal file
12
cw_solana/lib/solana_transaction_credentials.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
|
||||
class SolanaTransactionCredentials {
|
||||
SolanaTransactionCredentials(
|
||||
this.outputs, {
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
final List<OutputInfo> outputs;
|
||||
final CryptoCurrency currency;
|
||||
}
|
78
cw_solana/lib/solana_transaction_history.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_solana/file.dart';
|
||||
import 'package:cw_solana/solana_transaction_info.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
|
||||
part 'solana_transaction_history.g.dart';
|
||||
|
||||
const transactionsHistoryFileName = 'solana_transactions.json';
|
||||
|
||||
class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTransactionHistory;
|
||||
|
||||
abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<SolanaTransactionInfo>
|
||||
with Store {
|
||||
SolanaTransactionHistoryBase({required this.walletInfo, required String password})
|
||||
: _password = password {
|
||||
transactions = ObservableMap<String, SolanaTransactionInfo>();
|
||||
}
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
String _password;
|
||||
|
||||
Future<void> init() async => await _load();
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
try {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
|
||||
final data = json.encode({'transactions': transactionMaps});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
print('Error while saving solana transaction history: ${e.toString()}');
|
||||
print(s);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addOne(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, SolanaTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
|
||||
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);
|
||||
if (content.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
return json.decode(content) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, dynamic>) {
|
||||
final tx = SolanaTransactionInfo.fromJson(val);
|
||||
_update(tx);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _update(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
}
|
78
cw_solana/lib/solana_transaction_info.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
||||
class SolanaTransactionInfo extends TransactionInfo {
|
||||
SolanaTransactionInfo({
|
||||
required this.id,
|
||||
required this.blockTime,
|
||||
required this.to,
|
||||
required this.from,
|
||||
required this.direction,
|
||||
required this.solAmount,
|
||||
this.tokenSymbol = "SOL",
|
||||
required this.isPending,
|
||||
required this.txFee,
|
||||
}) : amount = solAmount.toInt();
|
||||
|
||||
final String id;
|
||||
final String? to;
|
||||
final String? from;
|
||||
final int amount;
|
||||
final bool isPending;
|
||||
final double solAmount;
|
||||
final String tokenSymbol;
|
||||
final DateTime blockTime;
|
||||
final double txFee;
|
||||
final TransactionDirection direction;
|
||||
|
||||
String? _fiatAmount;
|
||||
|
||||
@override
|
||||
DateTime get date => blockTime;
|
||||
|
||||
@override
|
||||
String amountFormatted() {
|
||||
String stringBalance = solAmount.toString();
|
||||
|
||||
if (stringBalance.toString().length >= 6) {
|
||||
stringBalance = stringBalance.substring(0, 6);
|
||||
}
|
||||
return '$stringBalance $tokenSymbol';
|
||||
}
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
||||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() => '${txFee.toString()} SOL';
|
||||
|
||||
factory SolanaTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||
return SolanaTransactionInfo(
|
||||
id: data['id'] as String,
|
||||
solAmount: data['solAmount'],
|
||||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
tokenSymbol: data['tokenSymbol'] as String,
|
||||
to: data['to'],
|
||||
from: data['from'],
|
||||
txFee: data['txFee'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'solAmount': solAmount,
|
||||
'direction': direction.index,
|
||||
'blockTime': blockTime.millisecondsSinceEpoch,
|
||||
'isPending': isPending,
|
||||
'tokenSymbol': tokenSymbol,
|
||||
'to': to,
|
||||
'from': from,
|
||||
'txFee': txFee,
|
||||
};
|
||||
}
|
47
cw_solana/lib/solana_transaction_model.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
class SolanaTransactionModel {
|
||||
final String id;
|
||||
|
||||
final String from;
|
||||
|
||||
final String to;
|
||||
|
||||
final double amount;
|
||||
|
||||
// If this is an outgoing transaction
|
||||
final bool isOutgoingTx;
|
||||
|
||||
// The Program ID of this transaction, e.g, System Program, Token Program...
|
||||
final String programId;
|
||||
|
||||
// The DateTime from the UNIX timestamp of the block where the transaction was included
|
||||
final DateTime blockTime;
|
||||
|
||||
// The Transaction fee
|
||||
final double fee;
|
||||
|
||||
// The token symbol
|
||||
final String tokenSymbol;
|
||||
|
||||
SolanaTransactionModel({
|
||||
required this.id,
|
||||
required this.to,
|
||||
required this.from,
|
||||
required this.amount,
|
||||
required this.programId,
|
||||
required int blockTimeInInt,
|
||||
this.isOutgoingTx = false,
|
||||
required this.tokenSymbol,
|
||||
required this.fee,
|
||||
}) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000);
|
||||
|
||||
factory SolanaTransactionModel.fromJson(Map<String, dynamic> json) => SolanaTransactionModel(
|
||||
id: json['id'],
|
||||
blockTimeInInt: int.parse(json["timeStamp"]) * 1000,
|
||||
from: json["from"],
|
||||
to: json["to"],
|
||||
amount: double.parse(json["value"]),
|
||||
programId: json["programId"],
|
||||
fee: json['fee'],
|
||||
tokenSymbol: json['tokenSymbol'],
|
||||
);
|
||||
}
|
498
cw_solana/lib/solana_wallet.dart
Normal file
|
@ -0,0 +1,498 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_solana/default_spl_tokens.dart';
|
||||
import 'package:cw_solana/file.dart';
|
||||
import 'package:cw_solana/solana_balance.dart';
|
||||
import 'package:cw_solana/solana_client.dart';
|
||||
import 'package:cw_solana/solana_exceptions.dart';
|
||||
import 'package:cw_solana/solana_transaction_credentials.dart';
|
||||
import 'package:cw_solana/solana_transaction_history.dart';
|
||||
import 'package:cw_solana/solana_transaction_info.dart';
|
||||
import 'package:cw_solana/solana_transaction_model.dart';
|
||||
import 'package:cw_solana/solana_wallet_addresses.dart';
|
||||
import 'package:cw_solana/spl_token.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solana/metaplex.dart' as metaplex;
|
||||
import 'package:solana/solana.dart';
|
||||
|
||||
part 'solana_wallet.g.dart';
|
||||
|
||||
class SolanaWallet = SolanaWalletBase with _$SolanaWallet;
|
||||
|
||||
abstract class SolanaWalletBase
|
||||
extends WalletBase<SolanaBalance, SolanaTransactionHistory, SolanaTransactionInfo> with Store {
|
||||
SolanaWalletBase({
|
||||
required WalletInfo walletInfo,
|
||||
String? mnemonic,
|
||||
String? privateKey,
|
||||
required String password,
|
||||
SolanaBalance? initialBalance,
|
||||
}) : syncStatus = const NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_mnemonic = mnemonic,
|
||||
_hexPrivateKey = privateKey,
|
||||
_client = SolanaWalletClient(),
|
||||
walletAddresses = SolanaWalletAddresses(walletInfo),
|
||||
balance = ObservableMap<CryptoCurrency, SolanaBalance>.of(
|
||||
{CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}),
|
||||
super(walletInfo) {
|
||||
this.walletInfo = walletInfo;
|
||||
transactionHistory = SolanaTransactionHistory(walletInfo: walletInfo, password: password);
|
||||
|
||||
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
|
||||
CakeHive.registerAdapter(SPLTokenAdapter());
|
||||
}
|
||||
|
||||
_sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
final String _password;
|
||||
final String? _mnemonic;
|
||||
final String? _hexPrivateKey;
|
||||
|
||||
// The Solana WalletPair
|
||||
Ed25519HDKeyPair? _walletKeyPair;
|
||||
|
||||
Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair;
|
||||
|
||||
// To access the privateKey bytes.
|
||||
Ed25519HDKeyPairData? _keyPairData;
|
||||
|
||||
late SolanaWalletClient _client;
|
||||
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
late final Box<SPLToken> splTokensBox;
|
||||
|
||||
@override
|
||||
WalletAddresses walletAddresses;
|
||||
|
||||
@override
|
||||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
@override
|
||||
@observable
|
||||
late ObservableMap<CryptoCurrency, SolanaBalance> balance;
|
||||
|
||||
Completer<SharedPreferences> _sharedPrefs = Completer();
|
||||
|
||||
@override
|
||||
Ed25519HDKeyPairData get keys {
|
||||
if (_keyPairData == null) {
|
||||
return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([]));
|
||||
}
|
||||
|
||||
return _keyPairData!;
|
||||
}
|
||||
|
||||
@override
|
||||
String? get seed => _mnemonic;
|
||||
|
||||
@override
|
||||
String get privateKey => HEX.encode(_keyPairData!.bytes);
|
||||
|
||||
Future<void> init() async {
|
||||
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
|
||||
|
||||
splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
|
||||
|
||||
// Create WalletPair using either the mnemonic or the privateKey
|
||||
_walletKeyPair = await getWalletPair(
|
||||
mnemonic: _mnemonic,
|
||||
privateKey: _hexPrivateKey,
|
||||
);
|
||||
|
||||
// Extract the keyPairData containing both the privateKey bytes and the publicKey hex.
|
||||
_keyPairData = await _walletKeyPair!.extract();
|
||||
|
||||
walletInfo.address = _walletKeyPair!.address;
|
||||
|
||||
await walletAddresses.init();
|
||||
await transactionHistory.init();
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
|
||||
assert(mnemonic != null || privateKey != null);
|
||||
|
||||
if (privateKey != null) {
|
||||
final privateKeyBytes = HEX.decode(privateKey);
|
||||
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
|
||||
}
|
||||
|
||||
return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_client.stop();
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> connectToNode({required Node node}) async {
|
||||
try {
|
||||
syncStatus = ConnectingSyncStatus();
|
||||
|
||||
final isConnected = _client.connect(node);
|
||||
|
||||
if (!isConnected) {
|
||||
throw Exception("Solana Node connection failed");
|
||||
}
|
||||
|
||||
_setTransactionUpdateTimer();
|
||||
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final solCredentials = credentials as SolanaTransactionCredentials;
|
||||
|
||||
final outputs = solCredentials.outputs;
|
||||
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
|
||||
await _updateBalance();
|
||||
|
||||
final CryptoCurrency transactionCurrency =
|
||||
balance.keys.firstWhere((element) => element.title == solCredentials.currency.title);
|
||||
|
||||
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
|
||||
|
||||
double totalAmount = 0.0;
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||
}
|
||||
|
||||
final totalAmountFromCredentials =
|
||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
|
||||
|
||||
totalAmount = totalAmountFromCredentials.toDouble();
|
||||
|
||||
if (walletBalanceForCurrency < totalAmount) {
|
||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
|
||||
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
|
||||
|
||||
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
|
||||
|
||||
if (walletBalanceForCurrency < totalAmount) {
|
||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||
}
|
||||
}
|
||||
|
||||
String? tokenMint;
|
||||
// Token Mint is only needed for transactions that are not native tokens(non-SOL transactions)
|
||||
if (transactionCurrency.title != CryptoCurrency.sol.title) {
|
||||
tokenMint = (transactionCurrency as SPLToken).mintAddress;
|
||||
}
|
||||
|
||||
final pendingSolanaTransaction = await _client.signSolanaTransaction(
|
||||
tokenMint: tokenMint,
|
||||
tokenTitle: transactionCurrency.title,
|
||||
inputAmount: totalAmount,
|
||||
ownerKeypair: _walletKeyPair!,
|
||||
tokenDecimals: transactionCurrency.decimals,
|
||||
destinationAddress: solCredentials.outputs.first.isParsedAddress
|
||||
? solCredentials.outputs.first.extractedAddress!
|
||||
: solCredentials.outputs.first.address,
|
||||
);
|
||||
|
||||
return pendingSolanaTransaction;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
|
||||
|
||||
/// Fetches the native SOL transactions linked to the wallet Public Key
|
||||
Future<void> _updateNativeSOLTransactions() async {
|
||||
final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address);
|
||||
|
||||
final transactions = await _client.fetchTransactions(address);
|
||||
|
||||
final Map<String, SolanaTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
result[transactionModel.id] = SolanaTransactionInfo(
|
||||
id: transactionModel.id,
|
||||
to: transactionModel.to,
|
||||
from: transactionModel.from,
|
||||
blockTime: transactionModel.blockTime,
|
||||
direction: transactionModel.isOutgoingTx
|
||||
? TransactionDirection.outgoing
|
||||
: TransactionDirection.incoming,
|
||||
solAmount: transactionModel.amount,
|
||||
isPending: false,
|
||||
txFee: transactionModel.fee,
|
||||
tokenSymbol: transactionModel.tokenSymbol,
|
||||
);
|
||||
}
|
||||
|
||||
transactionHistory.addMany(result);
|
||||
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
/// Fetches the SPL Tokens transactions linked to the token account Public Key
|
||||
Future<void> _updateSPLTokenTransactions() async {
|
||||
List<SolanaTransactionModel> splTokenTransactions = [];
|
||||
|
||||
for (var token in balance.keys) {
|
||||
if (token is SPLToken) {
|
||||
final tokenTxs = await _client.getSPLTokenTransfers(
|
||||
token.mintAddress,
|
||||
token.symbol,
|
||||
token.decimal,
|
||||
_walletKeyPair!,
|
||||
);
|
||||
|
||||
splTokenTransactions.addAll(tokenTxs);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, SolanaTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in splTokenTransactions) {
|
||||
result[transactionModel.id] = SolanaTransactionInfo(
|
||||
id: transactionModel.id,
|
||||
to: transactionModel.to,
|
||||
from: transactionModel.from,
|
||||
blockTime: transactionModel.blockTime,
|
||||
direction: transactionModel.isOutgoingTx
|
||||
? TransactionDirection.outgoing
|
||||
: TransactionDirection.incoming,
|
||||
solAmount: transactionModel.amount,
|
||||
isPending: false,
|
||||
txFee: transactionModel.fee,
|
||||
tokenSymbol: transactionModel.tokenSymbol,
|
||||
);
|
||||
}
|
||||
|
||||
transactionHistory.addMany(result);
|
||||
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
await walletAddresses.updateAddressesInBox();
|
||||
final path = await makePath();
|
||||
await write(path: path, password: _password, data: toJSON());
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
|
||||
await Future.wait([
|
||||
_updateBalance(),
|
||||
_updateNativeSOLTransactions(),
|
||||
_updateSPLTokenTransactions(),
|
||||
]);
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': _mnemonic,
|
||||
'private_key': privateKey,
|
||||
'balance': balance[currency]!.toJSON(),
|
||||
});
|
||||
|
||||
static Future<SolanaWallet> open({
|
||||
required String name,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
}) async {
|
||||
final path = await pathForWallet(name: name, type: walletInfo.type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final mnemonic = data['mnemonic'] as String?;
|
||||
final privateKey = data['private_key'] as String?;
|
||||
final balance = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0);
|
||||
|
||||
return SolanaWallet(
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
mnemonic: mnemonic,
|
||||
privateKey: privateKey,
|
||||
initialBalance: balance,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
balance[currency] = await _fetchSOLBalance();
|
||||
await _fetchSPLTokensBalances();
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<SolanaBalance> _fetchSOLBalance() async {
|
||||
final balance = await _client.getBalance(_walletKeyPair!.address);
|
||||
|
||||
return SolanaBalance(balance);
|
||||
}
|
||||
|
||||
Future<void> _fetchSPLTokensBalances() async {
|
||||
for (var token in splTokensBox.values) {
|
||||
if (token.enabled) {
|
||||
try {
|
||||
final tokenBalance =
|
||||
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
|
||||
balance[token] ??
|
||||
SolanaBalance(0.0);
|
||||
balance[token] = tokenBalance;
|
||||
} catch (e) {
|
||||
print('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
|
||||
}
|
||||
} else {
|
||||
balance.remove(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
|
||||
|
||||
void addInitialTokens() {
|
||||
final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
|
||||
|
||||
for (var token in initialSPLTokens) {
|
||||
splTokensBox.put(token.mintAddress, token);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addSPLToken(SPLToken token) async {
|
||||
await splTokensBox.put(token.mintAddress, token);
|
||||
|
||||
if (token.enabled) {
|
||||
final tokenBalance =
|
||||
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
|
||||
balance[token] ??
|
||||
SolanaBalance(0.0);
|
||||
|
||||
balance[token] = tokenBalance;
|
||||
} else {
|
||||
balance.remove(token);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteSPLToken(SPLToken token) async {
|
||||
await token.delete();
|
||||
|
||||
balance.remove(token);
|
||||
_updateBalance();
|
||||
}
|
||||
|
||||
Future<SPLToken?> getSPLToken(String mintAddress) async {
|
||||
// Convert SPL token mint address to public key
|
||||
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
|
||||
|
||||
// Fetch token's metadata account
|
||||
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
|
||||
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SPLToken.fromMetadata(
|
||||
name: token.name,
|
||||
mint: token.mint,
|
||||
symbol: token.symbol,
|
||||
mintAddress: mintAddress,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
||||
final currentWalletFile = File(currentWalletPath);
|
||||
|
||||
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
|
||||
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
|
||||
|
||||
// Copies current wallet files into new wallet name's dir and files
|
||||
if (currentWalletFile.existsSync()) {
|
||||
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
|
||||
await currentWalletFile.copy(newWalletPath);
|
||||
}
|
||||
if (currentTransactionsFile.existsSync()) {
|
||||
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
|
||||
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
|
||||
}
|
||||
|
||||
// Delete old name's dir and files
|
||||
await Directory(currentDirPath).delete(recursive: true);
|
||||
}
|
||||
|
||||
void _setTransactionUpdateTimer() {
|
||||
if (_transactionsUpdateTimer?.isActive ?? false) {
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
|
||||
_updateSPLTokenTransactions();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateBalance();
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> signSolanaMessage(String message) async {
|
||||
// Convert the message to bytes
|
||||
final messageBytes = utf8.encode(message);
|
||||
|
||||
// Sign the message bytes with the wallet's private key
|
||||
final signature = await _walletKeyPair!.sign(messageBytes);
|
||||
|
||||
// Convert the signature to a hexadecimal string
|
||||
final hex = HEX.encode(signature.bytes);
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
SolanaClient? get solanaClient => _client.getSolanaClient;
|
||||
}
|
33
cw_solana/lib/solana_wallet_addresses.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'solana_wallet_addresses.g.dart';
|
||||
|
||||
class SolanaWalletAddresses = SolanaWalletAddressesBase with _$SolanaWalletAddresses;
|
||||
|
||||
abstract class SolanaWalletAddressesBase extends WalletAddresses with Store {
|
||||
SolanaWalletAddressesBase(WalletInfo walletInfo)
|
||||
: address = '',
|
||||
super(walletInfo);
|
||||
|
||||
@override
|
||||
String address;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
address = walletInfo.address;
|
||||
await updateAddressesInBox();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAddressesInBox() async {
|
||||
try {
|
||||
addressesMap.clear();
|
||||
addressesMap[address] = '';
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
29
cw_solana/lib/solana_wallet_creation_credentials.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class SolanaNewWalletCredentials extends WalletCredentials {
|
||||
SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo})
|
||||
: super(name: name, walletInfo: walletInfo);
|
||||
}
|
||||
|
||||
class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
SolanaRestoreWalletFromSeedCredentials(
|
||||
{required String name,
|
||||
required String password,
|
||||
required this.mnemonic,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
||||
class SolanaRestoreWalletFromPrivateKey extends WalletCredentials {
|
||||
SolanaRestoreWalletFromPrivateKey(
|
||||
{required String name,
|
||||
required String password,
|
||||
required this.privateKey,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String privateKey;
|
||||
}
|
120
cw_solana/lib/solana_wallet_service.dart
Normal file
|
@ -0,0 +1,120 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_solana/solana_mnemonics.dart';
|
||||
import 'package:cw_solana/solana_wallet.dart';
|
||||
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
||||
SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey> {
|
||||
SolanaWalletService(this.walletInfoSource);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
||||
@override
|
||||
Future<SolanaWallet> create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final mnemonic = bip39.generateMnemonic(strength: strength);
|
||||
|
||||
final wallet = SolanaWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
wallet.addInitialTokens();
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.solana;
|
||||
|
||||
@override
|
||||
Future<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<SolanaWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await SolanaWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@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()))!;
|
||||
await walletInfoSource.delete(walletInfo.key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SolanaWallet> restoreFromKeys(SolanaRestoreWalletFromPrivateKey credentials,
|
||||
{bool? isTestnet}) async {
|
||||
final wallet = SolanaWallet(
|
||||
password: credentials.password!,
|
||||
privateKey: credentials.privateKey,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
wallet.addInitialTokens();
|
||||
await wallet.save();
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SolanaWallet> restoreFromSeed(SolanaRestoreWalletFromSeedCredentials credentials,
|
||||
{bool? isTestnet}) async {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw SolanaMnemonicIsIncorrectException();
|
||||
}
|
||||
|
||||
final wallet = SolanaWallet(
|
||||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
wallet.addInitialTokens();
|
||||
await wallet.save();
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = walletInfoSource.values
|
||||
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
|
||||
final currentWallet = await SolanaWalletBase.open(
|
||||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
newWalletInfo.name = newName;
|
||||
|
||||
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
|
||||
}
|
||||
}
|
146
cw_solana/lib/spl_token.dart
Normal file
|
@ -0,0 +1,146 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:solana/metaplex.dart';
|
||||
|
||||
part 'spl_token.g.dart';
|
||||
|
||||
@HiveType(typeId: SPLToken.typeId)
|
||||
class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
@HiveField(2)
|
||||
final String mintAddress;
|
||||
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
@HiveField(4, defaultValue: false)
|
||||
bool _enabled;
|
||||
|
||||
@HiveField(5)
|
||||
final String mint;
|
||||
|
||||
@HiveField(6)
|
||||
final String? iconPath;
|
||||
|
||||
@HiveField(7)
|
||||
final String? tag;
|
||||
|
||||
SPLToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.mintAddress,
|
||||
required this.decimal,
|
||||
required this.mint,
|
||||
this.iconPath,
|
||||
this.tag = 'SOL',
|
||||
bool enabled = false,
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: mint.toLowerCase(),
|
||||
title: symbol.toUpperCase(),
|
||||
fullName: name,
|
||||
tag: tag,
|
||||
iconPath: iconPath,
|
||||
decimals: decimal,
|
||||
);
|
||||
|
||||
factory SPLToken.fromMetadata({
|
||||
required String name,
|
||||
required String mint,
|
||||
required String symbol,
|
||||
required String mintAddress,
|
||||
}) {
|
||||
return SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: 0,
|
||||
mint: mint,
|
||||
iconPath: '',
|
||||
);
|
||||
}
|
||||
|
||||
factory SPLToken.cryptoCurrency({
|
||||
required String name,
|
||||
required String symbol,
|
||||
required int decimals,
|
||||
required String iconPath,
|
||||
required String mint,
|
||||
}) {
|
||||
return SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
decimal: decimals,
|
||||
mint: mint,
|
||||
iconPath: iconPath,
|
||||
mintAddress: '',
|
||||
);
|
||||
}
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
SPLToken.copyWith(SPLToken other, String? icon, String? tag)
|
||||
: name = other.name,
|
||||
symbol = other.symbol,
|
||||
mintAddress = other.mintAddress,
|
||||
decimal = other.decimal,
|
||||
_enabled = other.enabled,
|
||||
mint = other.mint,
|
||||
tag = other.tag,
|
||||
iconPath = icon,
|
||||
super(
|
||||
title: other.symbol.toUpperCase(),
|
||||
name: other.symbol.toLowerCase(),
|
||||
decimals: other.decimal,
|
||||
fullName: other.name,
|
||||
tag: other.tag,
|
||||
iconPath: icon,
|
||||
);
|
||||
|
||||
static const typeId = SPL_TOKEN_TYPE_ID;
|
||||
static const boxName = 'SPLTokens';
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
(other is SPLToken && other.mintAddress == mintAddress) ||
|
||||
(other is CryptoCurrency && other.title == title);
|
||||
|
||||
@override
|
||||
int get hashCode => mintAddress.hashCode;
|
||||
}
|
||||
|
||||
class NFT extends SPLToken {
|
||||
final ImageInfo? imageInfo;
|
||||
|
||||
NFT(
|
||||
String mint,
|
||||
String name,
|
||||
String symbol,
|
||||
String mintAddress,
|
||||
int decimal,
|
||||
String iconPath,
|
||||
this.imageInfo,
|
||||
) : super(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: decimal,
|
||||
mint: mint,
|
||||
iconPath: iconPath,
|
||||
);
|
||||
}
|
||||
|
||||
class ImageInfo {
|
||||
final String uri;
|
||||
final OffChainMetadata? data;
|
||||
|
||||
const ImageInfo(this.uri, this.data);
|
||||
}
|
36
cw_solana/pubspec.yaml
Normal file
|
@ -0,0 +1,36 @@
|
|||
name: cw_solana
|
||||
description: A new Flutter package project.
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
homepage: https://cakewallet.com
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.6 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
solana: ^0.30.1
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
http: ^1.1.0
|
||||
hive: ^2.2.3
|
||||
bip39: ^1.0.6
|
||||
mobx: ^2.3.0+1
|
||||
shared_preferences: ^2.0.15
|
||||
bip32: ^2.0.0
|
||||
hex: ^0.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
build_runner: ^2.1.11
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^1.1.3
|
||||
|
||||
flutter:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
12
cw_solana/test/cw_solana_test.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cw_solana/cw_solana.dart';
|
||||
|
||||
void main() {
|
||||
test('adds one to input values', () {
|
||||
final calculator = Calculator();
|
||||
expect(calculator.addOne(2), 3);
|
||||
expect(calculator.addOne(-7), -6);
|
||||
expect(calculator.addOne(0), 1);
|
||||
});
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '11.0'
|
||||
platform :ios, '12.0'
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
|
@ -44,7 +44,7 @@ post_install do |installer|
|
|||
flutter_additional_ios_build_settings(target)
|
||||
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ PODS:
|
|||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- CryptoSwift (1.7.1)
|
||||
- CryptoSwift (1.8.1)
|
||||
- cw_haven (0.0.1):
|
||||
- cw_haven/Boost (= 0.0.1)
|
||||
- cw_haven/Haven (= 0.0.1)
|
||||
|
@ -132,9 +132,9 @@ PODS:
|
|||
- permission_handler_apple (9.1.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- SDWebImage (5.16.0):
|
||||
- SDWebImage/Core (= 5.16.0)
|
||||
- SDWebImage/Core (5.16.0)
|
||||
- SDWebImage (5.18.11):
|
||||
- SDWebImage/Core (= 5.18.11)
|
||||
- SDWebImage/Core (5.18.11)
|
||||
- sensitive_clipboard (0.0.1):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
|
@ -142,9 +142,9 @@ PODS:
|
|||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SwiftProtobuf (1.22.0)
|
||||
- SwiftProtobuf (1.25.2)
|
||||
- SwiftyGif (5.4.4)
|
||||
- Toast (4.0.0)
|
||||
- Toast (4.1.0)
|
||||
- uni_links (0.0.1):
|
||||
- Flutter
|
||||
- UnstoppableDomainsResolution (4.0.0):
|
||||
|
@ -262,8 +262,8 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0
|
||||
BigInt: f668a80089607f521586bbe29513d708491ef2f7
|
||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||
CryptoSwift: d3d18dc357932f7e6d580689e065cf1f176007c1
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
CryptoSwift: b9c701d6f5011df23794dbf7f2e480a77835d83d
|
||||
cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a
|
||||
cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d
|
||||
cw_shared_external: 2972d872b8917603478117c9957dfca611845a92
|
||||
|
@ -277,7 +277,7 @@ SPEC CHECKSUMS:
|
|||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||
fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
|
@ -287,19 +287,19 @@ SPEC CHECKSUMS:
|
|||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6
|
||||
SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457
|
||||
sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989
|
||||
SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
|
||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
|
||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||
UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: 09df1114e7c360f55770d35a79356bf5446e0100
|
||||
PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
|
|
@ -377,7 +377,7 @@
|
|||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -523,7 +523,7 @@
|
|||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -561,7 +561,7 @@
|
|||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -607,9 +607,4 @@
|
|||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -180,6 +180,16 @@
|
|||
<string>polygon-wallet</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>solana-wallet</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solana-wallet</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
|
|
|
@ -1,181 +1,234 @@
|
|||
part of 'bitcoin.dart';
|
||||
|
||||
class CWBitcoin extends Bitcoin {
|
||||
@override
|
||||
TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({
|
||||
required String name,
|
||||
required String mnemonic,
|
||||
required String password})
|
||||
=> BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password);
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({
|
||||
required String name,
|
||||
required String password,
|
||||
required String wif,
|
||||
WalletInfo? walletInfo})
|
||||
=> BitcoinRestoreWalletFromWIFCredentials(name: name, password: password, wif: wif, walletInfo: walletInfo);
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinNewWalletCredentials({
|
||||
required String name,
|
||||
WalletInfo? walletInfo})
|
||||
=> BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo);
|
||||
|
||||
@override
|
||||
List<String> getWordList() => wordlist;
|
||||
|
||||
@override
|
||||
Map<String, String> getWalletKeys(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
|
||||
return <String, String>{
|
||||
'wif': keys.wif,
|
||||
'privateKey': keys.privateKey,
|
||||
'publicKey': keys.publicKey
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getTransactionPriorities()
|
||||
=> BitcoinTransactionPriority.all;
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getLitecoinTransactionPriorities()
|
||||
=> LitecoinTransactionPriority.all;
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeBitcoinTransactionPriority(int raw)
|
||||
=> BitcoinTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeLitecoinTransactionPriority(int raw)
|
||||
=> LitecoinTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
@override
|
||||
int getFeeRate(Object wallet, TransactionPriority priority) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.feeRate(priority);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> generateNewAddress(Object wallet, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAddress(Object wallet,String address, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
bitcoinWallet.walletAddresses.updateAddress(address, label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate})
|
||||
=> BitcoinTransactionCredentials(
|
||||
outputs.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority? priority, required int feeRate})
|
||||
=> BitcoinTransactionCredentials(
|
||||
outputs,
|
||||
priority: priority != null ? priority as BitcoinTransactionPriority : null,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
List<String> getAddresses(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.addresses
|
||||
.map((BitcoinAddressRecord addr) => addr.address)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@computed
|
||||
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
return electrumWallet.walletAddresses.addresses
|
||||
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
|
||||
id: addr.index,
|
||||
name: addr.name,
|
||||
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
|
||||
txCount: addr.txCount,
|
||||
balance: addr.balance,
|
||||
isChange: addr.isHidden))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
String getAddress(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.address;
|
||||
}
|
||||
|
||||
@override
|
||||
String formatterBitcoinAmountToString({required int amount})
|
||||
=> bitcoinAmountToString(amount: amount);
|
||||
|
||||
@override
|
||||
double formatterBitcoinAmountToDouble({required int amount})
|
||||
=> bitcoinAmountToDouble(amount: amount);
|
||||
|
||||
@override
|
||||
int formatterStringDoubleToBitcoinAmount(String amount)
|
||||
=> stringDoubleToBitcoinAmount(amount);
|
||||
@override
|
||||
TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate)
|
||||
=> (priority as BitcoinTransactionPriority).labelWithRate(rate);
|
||||
|
||||
@override
|
||||
List<BitcoinUnspent> getUnspents(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.unspentCoins;
|
||||
}
|
||||
|
||||
Future<void> updateUnspents(Object wallet) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.updateUnspent();
|
||||
}
|
||||
|
||||
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
|
||||
return BitcoinWalletService(walletInfoSource, unspentCoinSource);
|
||||
}
|
||||
|
||||
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
|
||||
return LitecoinWalletService(walletInfoSource, unspentCoinSource);
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityMedium()
|
||||
=> BitcoinTransactionPriority.medium;
|
||||
WalletCredentials createBitcoinRestoreWalletFromSeedCredentials(
|
||||
{required String name, required String mnemonic, required String password}) =>
|
||||
BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password);
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPriorityMedium()
|
||||
=> LitecoinTransactionPriority.medium;
|
||||
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials(
|
||||
{required String name,
|
||||
required String password,
|
||||
required String wif,
|
||||
WalletInfo? walletInfo}) =>
|
||||
BitcoinRestoreWalletFromWIFCredentials(
|
||||
name: name, password: password, wif: wif, walletInfo: walletInfo);
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPrioritySlow()
|
||||
=> BitcoinTransactionPriority.slow;
|
||||
|
||||
WalletCredentials createBitcoinNewWalletCredentials(
|
||||
{required String name, WalletInfo? walletInfo}) =>
|
||||
BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo);
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPrioritySlow()
|
||||
=> LitecoinTransactionPriority.slow;
|
||||
}
|
||||
List<String> getWordList() => wordlist;
|
||||
|
||||
@override
|
||||
Map<String, String> getWalletKeys(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
|
||||
return <String, String>{
|
||||
'wif': keys.wif,
|
||||
'privateKey': keys.privateKey,
|
||||
'publicKey': keys.publicKey
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all;
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeBitcoinTransactionPriority(int raw) =>
|
||||
BitcoinTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeLitecoinTransactionPriority(int raw) =>
|
||||
LitecoinTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
@override
|
||||
int getFeeRate(Object wallet, TransactionPriority priority) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.feeRate(priority);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> generateNewAddress(Object wallet, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAddress(Object wallet, String address, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
bitcoinWallet.walletAddresses.updateAddress(address, label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentials(List<Output> outputs,
|
||||
{required TransactionPriority priority, int? feeRate}) =>
|
||||
BitcoinTransactionCredentials(
|
||||
outputs
|
||||
.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs,
|
||||
{TransactionPriority? priority, required int feeRate}) =>
|
||||
BitcoinTransactionCredentials(outputs,
|
||||
priority: priority != null ? priority as BitcoinTransactionPriority : null,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
List<String> getAddresses(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.addressesByReceiveType
|
||||
.map((BitcoinAddressRecord addr) => addr.address)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@computed
|
||||
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
return electrumWallet.walletAddresses.addressesByReceiveType
|
||||
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
|
||||
id: addr.index,
|
||||
name: addr.name,
|
||||
address: addr.address,
|
||||
txCount: addr.txCount,
|
||||
balance: addr.balance,
|
||||
isChange: addr.isHidden))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
final sk = ECPrivate.random();
|
||||
|
||||
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
|
||||
final p2wpkhAddr = sk.getPublic().toP2wpkhAddress();
|
||||
try {
|
||||
final estimatedTx = await electrumWallet.estimateTxFeeAndInputsToUse(
|
||||
0,
|
||||
true,
|
||||
// Deposit address + change address
|
||||
[p2shAddr, p2wpkhAddr],
|
||||
[
|
||||
BitcoinOutput(address: p2shAddr, value: BigInt.zero),
|
||||
BitcoinOutput(address: p2wpkhAddr, value: BigInt.zero)
|
||||
],
|
||||
null,
|
||||
priority as BitcoinTransactionPriority);
|
||||
|
||||
return estimatedTx.amount;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String getAddress(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.address;
|
||||
}
|
||||
|
||||
@override
|
||||
String formatterBitcoinAmountToString({required int amount}) =>
|
||||
bitcoinAmountToString(amount: amount);
|
||||
|
||||
@override
|
||||
double formatterBitcoinAmountToDouble({required int amount}) =>
|
||||
bitcoinAmountToDouble(amount: amount);
|
||||
|
||||
@override
|
||||
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
|
||||
|
||||
@override
|
||||
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) =>
|
||||
(priority as BitcoinTransactionPriority).labelWithRate(rate);
|
||||
|
||||
@override
|
||||
List<BitcoinUnspent> getUnspents(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.unspentCoins;
|
||||
}
|
||||
|
||||
Future<void> updateUnspents(Object wallet) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.updateUnspent();
|
||||
}
|
||||
|
||||
WalletService createBitcoinWalletService(
|
||||
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
|
||||
return BitcoinWalletService(walletInfoSource, unspentCoinSource);
|
||||
}
|
||||
|
||||
WalletService createLitecoinWalletService(
|
||||
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
|
||||
return LitecoinWalletService(walletInfoSource, unspentCoinSource);
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow;
|
||||
|
||||
@override
|
||||
Future<void> setAddressType(Object wallet, dynamic option) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.walletAddresses.setAddressType(option as BitcoinAddressType);
|
||||
}
|
||||
|
||||
@override
|
||||
ReceivePageOption getSelectedAddressType(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType);
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
|
||||
|
||||
@override
|
||||
BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) {
|
||||
switch (option) {
|
||||
case BitcoinReceivePageOption.p2pkh:
|
||||
return P2pkhAddressType.p2pkh;
|
||||
case BitcoinReceivePageOption.p2sh:
|
||||
return P2shAddressType.p2wpkhInP2sh;
|
||||
case BitcoinReceivePageOption.p2tr:
|
||||
return SegwitAddresType.p2tr;
|
||||
case BitcoinReceivePageOption.p2wsh:
|
||||
return SegwitAddresType.p2wsh;
|
||||
case BitcoinReceivePageOption.p2wpkh:
|
||||
default:
|
||||
return SegwitAddresType.p2wpkh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
part of 'bitcoin_cash.dart';
|
||||
|
||||
class CWBitcoinCash extends BitcoinCash {
|
||||
@override
|
||||
String getMnemonic(int? strength) => Mnemonic.generate();
|
||||
|
||||
@override
|
||||
Uint8List getSeedFromMnemonic(String seed) => Mnemonic.toSeed(seed);
|
||||
|
||||
@override
|
||||
String getCashAddrFormat(String address) => AddressUtils.getCashAddrFormat(address);
|
||||
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_wallet/buy/buy_amount.dart';
|
||||
import 'package:cake_wallet/buy/buy_exception.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider_description.dart';
|
||||
import 'package:cake_wallet/buy/order.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/palette.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
|
@ -6,19 +15,11 @@ import 'package:cake_wallet/store/settings_store.dart';
|
|||
import 'package:cake_wallet/themes/theme_base.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cake_wallet/buy/buy_exception.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:cake_wallet/buy/buy_amount.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider_description.dart';
|
||||
import 'package:cake_wallet/buy/order.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MoonPaySellProvider extends BuyProvider {
|
||||
|
@ -34,6 +35,7 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
|
||||
static const _baseTestUrl = 'sell-sandbox.moonpay.com';
|
||||
static const _baseProductUrl = 'sell.moonpay.com';
|
||||
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
|
||||
|
||||
@override
|
||||
String get providerDescription =>
|
||||
|
@ -60,9 +62,29 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
|
||||
static String get _apiKey => secrets.moonPayApiKey;
|
||||
|
||||
static String get _secretKey => secrets.moonPaySecretKey;
|
||||
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
|
||||
final String baseUrl;
|
||||
|
||||
Future<String> getMoonpaySignature(String query) async {
|
||||
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
|
||||
|
||||
final response = await post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': _exchangeHelperApiKey,
|
||||
},
|
||||
body: json.encode({'query': query}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
|
||||
} else {
|
||||
throw Exception(
|
||||
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri> requestMoonPayUrl({
|
||||
required CryptoCurrency currency,
|
||||
required String refundWalletAddress,
|
||||
|
@ -81,16 +103,12 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
'',
|
||||
<String, dynamic>{
|
||||
'apiKey': _apiKey,
|
||||
'defaultBaseCurrencyCode': currency.toString().toLowerCase(),
|
||||
'defaultBaseCurrencyCode': _normalizeCurrency(currency),
|
||||
'refundWalletAddress': refundWalletAddress,
|
||||
}..addAll(customParams),
|
||||
);
|
||||
|
||||
final messageBytes = utf8.encode('?${originalUri.query}');
|
||||
final key = utf8.encode(_secretKey);
|
||||
final hmac = Hmac(sha256, key);
|
||||
final digest = hmac.convert(messageBytes);
|
||||
final signature = base64.encode(digest.bytes);
|
||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
||||
|
||||
if (isTestEnvironment) {
|
||||
return originalUri;
|
||||
|
@ -134,6 +152,14 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
if (currency == CryptoCurrency.maticpoly) {
|
||||
return "MATIC_POLYGON";
|
||||
}
|
||||
|
||||
return currency.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
class MoonPayBuyProvider extends BuyProvider {
|
||||
|
|