Merge branch 'refs/heads/main' into CW-679-Add-Ledger-Litecoin-Support
# Conflicts: # cw_bitcoin/lib/electrum_wallet.dart # cw_bitcoin/lib/litecoin_wallet.dart # cw_bitcoin/lib/litecoin_wallet_service.dart
5
.github/workflows/cache_dependencies.yml
vendored
|
@ -23,9 +23,10 @@ jobs:
|
||||||
docker-images: true
|
docker-images: true
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: "17.x"
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
- name: Configure placeholder git details
|
- name: Configure placeholder git details
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "CI@cakewallet.com"
|
git config --global user.email "CI@cakewallet.com"
|
||||||
|
|
5
.github/workflows/pr_test_build_android.yml
vendored
|
@ -39,9 +39,10 @@ jobs:
|
||||||
docker-images: true
|
docker-images: true
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: "17.x"
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
- name: Configure placeholder git details
|
- name: Configure placeholder git details
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "CI@cakewallet.com"
|
git config --global user.email "CI@cakewallet.com"
|
||||||
|
|
|
@ -161,7 +161,9 @@ The only parts to be translated, if needed, are the values m and s after the var
|
||||||
|
|
||||||
4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language.
|
4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language.
|
||||||
|
|
||||||
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 digit localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
|
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
|
||||||
|
|
||||||
|
6. Add the new language code to `tool/utils/translation/translation_constants.dart`
|
||||||
|
|
||||||
## Add a new fiat currency
|
## Add a new fiat currency
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,18 @@ analyzer:
|
||||||
lib/generated/*.dart,
|
lib/generated/*.dart,
|
||||||
cw_monero/ios/External/**,
|
cw_monero/ios/External/**,
|
||||||
cw_shared_external/**,
|
cw_shared_external/**,
|
||||||
shared_external/**]
|
shared_external/**,
|
||||||
|
lib/bitcoin/cw_bitcoin.dart,
|
||||||
|
lib/bitcoin_cash/cw_bitcoin_cash.dart,
|
||||||
|
lib/ethereum/cw_ethereum.dart,
|
||||||
|
lib/haven/cw_haven.dart,
|
||||||
|
lib/monero/cw_monero.dart,
|
||||||
|
lib/nano/cw_nano.dart,
|
||||||
|
lib/polygon/cw_polygon.dart,
|
||||||
|
lib/solana/cw_solana.dart,
|
||||||
|
lib/tron/cw_tron.dart,
|
||||||
|
lib/wownero/cw_wownero.dart,
|
||||||
|
]
|
||||||
language:
|
language:
|
||||||
strict-casts: true
|
strict-casts: true
|
||||||
strict-raw-types: true
|
strict-raw-types: true
|
||||||
|
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 376 B |
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/flags/arm.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 372 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 692 B |
Before Width: | Height: | Size: 1,005 B After Width: | Height: | Size: 788 B |
Before Width: | Height: | Size: 855 B After Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 753 B |
Before Width: | Height: | Size: 860 B After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 381 B |
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 830 B |
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 553 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1,005 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 432 B After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1,023 B After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 373 B |
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 360 B |
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 994 B |
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 896 B After Width: | Height: | Size: 851 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 849 B |
Before Width: | Height: | Size: 1,013 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 372 B |
Before Width: | Height: | Size: 898 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 366 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 702 B After Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 750 B |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 390 B |
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 384 B |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 373 B After Width: | Height: | Size: 1,005 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 597 B |
Before Width: | Height: | Size: 1,009 B After Width: | Height: | Size: 887 B |
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 882 B |
|
@ -1,4 +1,7 @@
|
||||||
-
|
-
|
||||||
uri: rpc.ankr.com
|
uri: rpc.ankr.com
|
||||||
is_default: true
|
is_default: true
|
||||||
|
useSSL: true
|
||||||
|
-
|
||||||
|
uri: api.mainnet-beta.solana.com:443
|
||||||
useSSL: true
|
useSSL: true
|
|
@ -1,4 +1,2 @@
|
||||||
Monero synchronization improvements
|
Enhance auto-address generation for Monero
|
||||||
Enhance error handling
|
Bug fixes and enhancements
|
||||||
UI enhancements
|
|
||||||
Bug fixes
|
|
|
@ -1,6 +1,4 @@
|
||||||
Wallets enhancements
|
Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page)
|
||||||
Monero synchronization improvements
|
Improve fee calculation for Bitcoin to protect against overpaying or underpaying
|
||||||
Improve wallet backups
|
Enhance auto-address generation for Monero
|
||||||
Enhance error handling
|
Bug fixes and enhancements
|
||||||
UI enhancements
|
|
||||||
Bug fixes
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:cryptography/cryptography.dart' as cryptography;
|
import 'package:cryptography/cryptography.dart' as cryptography;
|
||||||
import 'package:cw_core/sec_random_native.dart';
|
import 'package:cw_core/sec_random_native.dart';
|
||||||
|
@ -59,11 +60,7 @@ void maskBytes(Uint8List bytes, int bits) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String bufferToBin(Uint8List data) {
|
String bufferToBin(Uint8List data) => data.map((e) => e.toRadixString(2).padLeft(8, '0')).join('');
|
||||||
final q1 = data.map((e) => e.toRadixString(2).padLeft(8, '0'));
|
|
||||||
final q2 = q1.join('');
|
|
||||||
return q2;
|
|
||||||
}
|
|
||||||
|
|
||||||
String encode(Uint8List data) {
|
String encode(Uint8List data) {
|
||||||
final dataBitLen = data.length * 8;
|
final dataBitLen = data.length * 8;
|
||||||
|
@ -112,17 +109,18 @@ Future<bool> checkIfMnemonicIsElectrum2(String mnemonic) async {
|
||||||
Future<String> getMnemonicHash(String mnemonic) async {
|
Future<String> getMnemonicHash(String mnemonic) async {
|
||||||
final hmacSha512 = Hmac(sha512, utf8.encode('Seed version'));
|
final hmacSha512 = Hmac(sha512, utf8.encode('Seed version'));
|
||||||
final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic)));
|
final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic)));
|
||||||
final hx = digest.toString();
|
return digest.toString();
|
||||||
return hx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async {
|
Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
|
||||||
|
{String prefix = segwit, String passphrase = ''}) async {
|
||||||
final pbkdf2 =
|
final pbkdf2 =
|
||||||
cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512);
|
cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512);
|
||||||
final text = normalizeText(mnemonic);
|
final text = normalizeText(mnemonic);
|
||||||
// pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce)
|
final passphraseBytes = utf8.encode(normalizeText(passphrase));
|
||||||
final key = await pbkdf2.deriveKey(
|
final key = await pbkdf2.deriveKey(
|
||||||
secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits);
|
secretKey: cryptography.SecretKey(text.codeUnits),
|
||||||
|
nonce: [...'electrum'.codeUnits, ...passphraseBytes]);
|
||||||
final bytes = await key.extractBytes();
|
final bytes = await key.extractBytes();
|
||||||
return Uint8List.fromList(bytes);
|
return Uint8List.fromList(bytes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,6 @@ class MnemonicBip39 {
|
||||||
static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength);
|
static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength);
|
||||||
|
|
||||||
/// Create root seed from mnemonic
|
/// Create root seed from mnemonic
|
||||||
static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic);
|
static Uint8List toSeed(String mnemonic, {String? passphrase}) =>
|
||||||
|
bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
|
||||||
}
|
}
|
|
@ -115,7 +115,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
break;
|
break;
|
||||||
case DerivationType.electrum:
|
case DerivationType.electrum:
|
||||||
default:
|
default:
|
||||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return BitcoinWallet(
|
return BitcoinWallet(
|
||||||
|
@ -195,7 +195,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
if (mnemonic != null) {
|
if (mnemonic != null) {
|
||||||
switch (walletInfo.derivationInfo!.derivationType) {
|
switch (walletInfo.derivationInfo!.derivationType) {
|
||||||
case DerivationType.electrum:
|
case DerivationType.electrum:
|
||||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||||
break;
|
break;
|
||||||
case DerivationType.bip39:
|
case DerivationType.bip39:
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -3,16 +3,18 @@ import 'package:cw_core/wallet_credentials.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
|
||||||
class BitcoinNewWalletCredentials extends WalletCredentials {
|
class BitcoinNewWalletCredentials extends WalletCredentials {
|
||||||
BitcoinNewWalletCredentials(
|
BitcoinNewWalletCredentials({
|
||||||
{required String name,
|
required String name,
|
||||||
WalletInfo? walletInfo,
|
WalletInfo? walletInfo,
|
||||||
String? password,
|
String? password,
|
||||||
DerivationType? derivationType,
|
DerivationType? derivationType,
|
||||||
String? derivationPath})
|
String? derivationPath,
|
||||||
: super(
|
String? passphrase,
|
||||||
|
}) : super(
|
||||||
name: name,
|
name: name,
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
password: password,
|
password: password,
|
||||||
|
passphrase: passphrase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||||
|
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
||||||
import 'package:cw_core/encryption_file_utils.dart';
|
import 'package:cw_core/encryption_file_utils.dart';
|
||||||
|
@ -35,8 +36,21 @@ class BitcoinWalletService extends WalletService<
|
||||||
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
||||||
credentials.walletInfo?.network = network.value;
|
credentials.walletInfo?.network = network.value;
|
||||||
|
|
||||||
|
final String mnemonic;
|
||||||
|
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||||
|
case DerivationType.bip39:
|
||||||
|
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||||
|
|
||||||
|
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||||
|
break;
|
||||||
|
case DerivationType.electrum:
|
||||||
|
default:
|
||||||
|
mnemonic = await generateElectrumMnemonic();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
final wallet = await BitcoinWalletBase.create(
|
final wallet = await BitcoinWalletBase.create(
|
||||||
mnemonic: await generateElectrumMnemonic(),
|
mnemonic: mnemonic,
|
||||||
password: credentials.password!,
|
password: credentials.password!,
|
||||||
passphrase: credentials.passphrase,
|
passphrase: credentials.passphrase,
|
||||||
walletInfo: credentials.walletInfo!,
|
walletInfo: credentials.walletInfo!,
|
||||||
|
|
|
@ -66,6 +66,7 @@ class ElectrumClient {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await socket?.close();
|
await socket?.close();
|
||||||
|
socket = null;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -90,33 +91,35 @@ class ElectrumClient {
|
||||||
}
|
}
|
||||||
_setConnectionStatus(ConnectionStatus.connected);
|
_setConnectionStatus(ConnectionStatus.connected);
|
||||||
|
|
||||||
socket!.listen((Uint8List event) {
|
socket!.listen(
|
||||||
try {
|
(Uint8List event) {
|
||||||
final msg = utf8.decode(event.toList());
|
try {
|
||||||
final messagesList = msg.split("\n");
|
final msg = utf8.decode(event.toList());
|
||||||
for (var message in messagesList) {
|
final messagesList = msg.split("\n");
|
||||||
if (message.isEmpty) {
|
for (var message in messagesList) {
|
||||||
continue;
|
if (message.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_parseResponse(message);
|
||||||
}
|
}
|
||||||
_parseResponse(message);
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
},
|
||||||
print(e.toString());
|
onError: (Object error) {
|
||||||
}
|
final errorMsg = error.toString();
|
||||||
}, onError: (Object error) {
|
print(errorMsg);
|
||||||
final errorMsg = error.toString();
|
unterminatedString = '';
|
||||||
print(errorMsg);
|
},
|
||||||
unterminatedString = '';
|
onDone: () {
|
||||||
|
unterminatedString = '';
|
||||||
final currentHost = socket?.address.host;
|
if (host == socket?.address.host) {
|
||||||
final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} ");
|
socket = null;
|
||||||
|
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||||
if (currentHost != null && isErrorForCurrentHost)
|
}
|
||||||
_setConnectionStatus(ConnectionStatus.failed);
|
},
|
||||||
}, onDone: () {
|
cancelOnError: true,
|
||||||
unterminatedString = '';
|
);
|
||||||
if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected);
|
|
||||||
});
|
|
||||||
|
|
||||||
keepAlive();
|
keepAlive();
|
||||||
}
|
}
|
||||||
|
@ -426,7 +429,6 @@ class ElectrumClient {
|
||||||
{required String id, required String method, List<Object> params = const []}) {
|
{required String id, required String method, List<Object> params = const []}) {
|
||||||
try {
|
try {
|
||||||
if (socket == null) {
|
if (socket == null) {
|
||||||
_setConnectionStatus(ConnectionStatus.failed);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final subscription = BehaviorSubject<T>();
|
final subscription = BehaviorSubject<T>();
|
||||||
|
@ -443,7 +445,6 @@ class ElectrumClient {
|
||||||
Future<dynamic> call(
|
Future<dynamic> call(
|
||||||
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
|
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
|
||||||
if (socket == null) {
|
if (socket == null) {
|
||||||
_setConnectionStatus(ConnectionStatus.failed);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final completer = Completer<dynamic>();
|
final completer = Completer<dynamic>();
|
||||||
|
@ -457,10 +458,9 @@ class ElectrumClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> callWithTimeout(
|
Future<dynamic> callWithTimeout(
|
||||||
{required String method, List<Object> params = const [], int timeout = 4000}) async {
|
{required String method, List<Object> params = const [], int timeout = 5000}) async {
|
||||||
try {
|
try {
|
||||||
if (socket == null) {
|
if (socket == null) {
|
||||||
_setConnectionStatus(ConnectionStatus.failed);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final completer = Completer<dynamic>();
|
final completer = Completer<dynamic>();
|
||||||
|
|
|
@ -109,5 +109,4 @@ Map<DerivationType, List<DerivationInfo>> electrum_derivations = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!;
|
||||||
String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!;
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:cw_bitcoin/address_from_output.dart';
|
import 'package:cw_bitcoin/address_from_output.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||||
|
@ -7,10 +9,12 @@ import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/transaction_info.dart';
|
import 'package:cw_core/transaction_info.dart';
|
||||||
import 'package:cw_core/format_amount.dart';
|
import 'package:cw_core/format_amount.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
|
||||||
class ElectrumTransactionBundle {
|
class ElectrumTransactionBundle {
|
||||||
ElectrumTransactionBundle(this.originalTransaction,
|
ElectrumTransactionBundle(this.originalTransaction,
|
||||||
{required this.ins, required this.confirmations, this.time});
|
{required this.ins, required this.confirmations, this.time});
|
||||||
|
|
||||||
final BtcTransaction originalTransaction;
|
final BtcTransaction originalTransaction;
|
||||||
final List<BtcTransaction> ins;
|
final List<BtcTransaction> ins;
|
||||||
final int? time;
|
final int? time;
|
||||||
|
@ -125,7 +129,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
for (final out in bundle.originalTransaction.outputs) {
|
for (final out in bundle.originalTransaction.outputs) {
|
||||||
totalOutAmount += out.amount.toInt();
|
totalOutAmount += out.amount.toInt();
|
||||||
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
||||||
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));
|
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||||
|
|
||||||
|
if (address.isNotEmpty) outputAddresses.add(address);
|
||||||
|
|
||||||
|
// Check if the script contains OP_RETURN
|
||||||
|
final script = out.scriptPubKey.script;
|
||||||
|
if (script.contains('OP_RETURN')) {
|
||||||
|
final index = script.indexOf('OP_RETURN');
|
||||||
|
if (index + 1 <= script.length) {
|
||||||
|
try {
|
||||||
|
final opReturnData = script[index + 1].toString();
|
||||||
|
final decodedString = utf8.decode(HEX.decode(opReturnData));
|
||||||
|
outputAddresses.add('OP_RETURN:$decodedString');
|
||||||
|
} catch (_) {
|
||||||
|
outputAddresses.add('OP_RETURN:');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (addressExists) {
|
if (addressExists) {
|
||||||
receivedAmounts.add(out.amount.toInt());
|
receivedAmounts.add(out.amount.toInt());
|
||||||
|
@ -235,6 +256,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
|
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'dart:isolate';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:cw_bitcoin/address_from_output.dart';
|
import 'package:cw_bitcoin/address_from_output.dart';
|
||||||
|
@ -44,6 +45,8 @@ import 'package:ledger_flutter/ledger_flutter.dart' as ledger;
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:rxdart/subjects.dart';
|
import 'package:rxdart/subjects.dart';
|
||||||
import 'package:sp_scanner/sp_scanner.dart';
|
import 'package:sp_scanner/sp_scanner.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
part 'electrum_wallet.g.dart';
|
part 'electrum_wallet.g.dart';
|
||||||
|
|
||||||
|
@ -89,7 +92,7 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
this.unspentCoinsInfo = unspentCoinsInfo,
|
this.unspentCoinsInfo = unspentCoinsInfo,
|
||||||
this.isTestnet = network == BitcoinNetwork.testnet,
|
this.isTestnet = !network.isMainnet,
|
||||||
this._mnemonic = mnemonic,
|
this._mnemonic = mnemonic,
|
||||||
super(walletInfo) {
|
super(walletInfo) {
|
||||||
this.electrumClient = electrumClient ?? ElectrumClient();
|
this.electrumClient = electrumClient ?? ElectrumClient();
|
||||||
|
@ -101,6 +104,8 @@ abstract class ElectrumWalletBase
|
||||||
);
|
);
|
||||||
|
|
||||||
reaction((_) => syncStatus, _syncStatusReaction);
|
reaction((_) => syncStatus, _syncStatusReaction);
|
||||||
|
|
||||||
|
sharedPrefs.complete(SharedPreferences.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
|
static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
|
||||||
|
@ -144,7 +149,11 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
|
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
|
||||||
|
|
||||||
|
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
|
||||||
|
|
||||||
final EncryptionFileUtils encryptionFileUtils;
|
final EncryptionFileUtils encryptionFileUtils;
|
||||||
|
|
||||||
|
@override
|
||||||
final String? passphrase;
|
final String? passphrase;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -191,7 +200,7 @@ abstract class ElectrumWalletBase
|
||||||
BasedUtxoNetwork network;
|
BasedUtxoNetwork network;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool? isTestnet;
|
bool isTestnet;
|
||||||
|
|
||||||
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
|
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
|
||||||
|
|
||||||
|
@ -202,6 +211,13 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
bool _isTryingToConnect = false;
|
bool _isTryingToConnect = false;
|
||||||
|
|
||||||
|
Completer<SharedPreferences> sharedPrefs = Completer();
|
||||||
|
|
||||||
|
Future<bool> checkIfMempoolAPIIsEnabled() async {
|
||||||
|
bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true;
|
||||||
|
return isMempoolAPIEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> setSilentPaymentsScanning(bool active) async {
|
Future<void> setSilentPaymentsScanning(bool active) async {
|
||||||
silentPaymentsScanningActive = active;
|
silentPaymentsScanningActive = active;
|
||||||
|
@ -227,10 +243,7 @@ abstract class ElectrumWalletBase
|
||||||
if (electrumClient.isConnected) {
|
if (electrumClient.isConnected) {
|
||||||
syncStatus = SyncedSyncStatus();
|
syncStatus = SyncedSyncStatus();
|
||||||
} else {
|
} else {
|
||||||
if (electrumClient.uri != null) {
|
syncStatus = NotConnectedSyncStatus();
|
||||||
await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL);
|
|
||||||
startSync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,7 +288,8 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
void Function(FlutterErrorDetails)? _onError;
|
void Function(FlutterErrorDetails)? _onError;
|
||||||
Timer? _autoSaveTimer;
|
Timer? _autoSaveTimer;
|
||||||
static const int _autoSaveInterval = 30;
|
Timer? _updateFeeRateTimer;
|
||||||
|
static const int _autoSaveInterval = 1;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await walletAddresses.init();
|
await walletAddresses.init();
|
||||||
|
@ -283,7 +297,7 @@ abstract class ElectrumWalletBase
|
||||||
await save();
|
await save();
|
||||||
|
|
||||||
_autoSaveTimer =
|
_autoSaveTimer =
|
||||||
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
|
Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save());
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -425,6 +439,10 @@ abstract class ElectrumWalletBase
|
||||||
@override
|
@override
|
||||||
Future<void> startSync() async {
|
Future<void> startSync() async {
|
||||||
try {
|
try {
|
||||||
|
if (syncStatus is SyncronizingSyncStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
syncStatus = SyncronizingSyncStatus();
|
syncStatus = SyncronizingSyncStatus();
|
||||||
|
|
||||||
if (hasSilentPaymentsScanning) {
|
if (hasSilentPaymentsScanning) {
|
||||||
|
@ -436,6 +454,10 @@ abstract class ElectrumWalletBase
|
||||||
await updateTransactions();
|
await updateTransactions();
|
||||||
await updateAllUnspents();
|
await updateAllUnspents();
|
||||||
await updateBalance();
|
await updateBalance();
|
||||||
|
await updateFeeRates();
|
||||||
|
|
||||||
|
_updateFeeRateTimer ??=
|
||||||
|
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
|
||||||
|
|
||||||
if (alwaysScan == true) {
|
if (alwaysScan == true) {
|
||||||
_setListeners(walletInfo.restoreHeight);
|
_setListeners(walletInfo.restoreHeight);
|
||||||
|
@ -451,9 +473,25 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> updateFeeRates() async {
|
Future<void> updateFeeRates() async {
|
||||||
|
if (await checkIfMempoolAPIIsEnabled()) {
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended"));
|
||||||
|
|
||||||
|
final result = json.decode(response.body) as Map<String, num>;
|
||||||
|
final slowFee = result['economyFee']?.toInt() ?? 0;
|
||||||
|
final mediumFee = result['hourFee']?.toInt() ?? 0;
|
||||||
|
final fastFee = result['fastestFee']?.toInt() ?? 0;
|
||||||
|
_feeRates = [slowFee, mediumFee, fastFee];
|
||||||
|
return;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
final feeRates = await electrumClient.feeRates(network: network);
|
final feeRates = await electrumClient.feeRates(network: network);
|
||||||
if (feeRates != [0, 0, 0]) {
|
if (feeRates != [0, 0, 0]) {
|
||||||
_feeRates = feeRates;
|
_feeRates = feeRates;
|
||||||
|
} else if (isTestnet) {
|
||||||
|
_feeRates = [1, 1, 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -602,7 +640,7 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
final derivationPath =
|
final derivationPath =
|
||||||
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
|
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}"
|
||||||
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
|
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
|
||||||
"/${utx.bitcoinAddressRecord.index}";
|
"/${utx.bitcoinAddressRecord.index}";
|
||||||
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
|
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
|
||||||
|
@ -1019,7 +1057,7 @@ abstract class ElectrumWalletBase
|
||||||
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
|
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
|
||||||
error += "\nNo private keys generated.";
|
error += "\nNo private keys generated.";
|
||||||
} else {
|
} else {
|
||||||
error += "\nAddress: ${utxo.ownerDetails.address.toAddress()}";
|
error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";
|
||||||
|
|
||||||
key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) {
|
key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) {
|
||||||
final elemPubkey = element.privkey.getPublic().toHex();
|
final elemPubkey = element.privkey.getPublic().toHex();
|
||||||
|
@ -1235,6 +1273,7 @@ abstract class ElectrumWalletBase
|
||||||
await electrumClient.close();
|
await electrumClient.close();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
_autoSaveTimer?.cancel();
|
_autoSaveTimer?.cancel();
|
||||||
|
_updateFeeRateTimer?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -1378,26 +1417,15 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> canReplaceByFee(String hash) async {
|
Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
|
||||||
final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
|
try {
|
||||||
|
final bundle = await getTransactionExpanded(hash: tx.txHash);
|
||||||
final String? transactionHex;
|
_updateInputsAndOutputs(tx, bundle);
|
||||||
int confirmations = 0;
|
if (bundle.confirmations > 0) return false;
|
||||||
|
return bundle.originalTransaction.canReplaceByFee;
|
||||||
if (verboseTransaction.isEmpty) {
|
} catch (e) {
|
||||||
transactionHex = await electrumClient.getTransactionHex(hash: hash);
|
|
||||||
} else {
|
|
||||||
confirmations = verboseTransaction['confirmations'] as int? ?? 0;
|
|
||||||
transactionHex = verboseTransaction['hex'] as String?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (confirmations > 0) return false;
|
|
||||||
|
|
||||||
if (transactionHex == null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
|
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
|
||||||
|
@ -1438,6 +1466,7 @@ abstract class ElectrumWalletBase
|
||||||
List<ECPrivate> privateKeys = [];
|
List<ECPrivate> privateKeys = [];
|
||||||
|
|
||||||
var allInputsAmount = 0;
|
var allInputsAmount = 0;
|
||||||
|
String? memo;
|
||||||
|
|
||||||
// Add inputs
|
// Add inputs
|
||||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||||
|
@ -1473,53 +1502,82 @@ abstract class ElectrumWalletBase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
int totalOutAmount = bundle.originalTransaction.outputs
|
// Create a list of available outputs
|
||||||
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
|
|
||||||
|
|
||||||
var currentFee = allInputsAmount - totalOutAmount;
|
|
||||||
int remainingFee = newFee - currentFee;
|
|
||||||
|
|
||||||
final outputs = <BitcoinOutput>[];
|
final outputs = <BitcoinOutput>[];
|
||||||
|
for (final out in bundle.originalTransaction.outputs) {
|
||||||
// Add outputs and deduct the fees from it
|
// Check if the script contains OP_RETURN
|
||||||
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
|
final script = out.scriptPubKey.script;
|
||||||
final out = bundle.originalTransaction.outputs[i];
|
if (script.contains('OP_RETURN') && memo == null) {
|
||||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
final index = script.indexOf('OP_RETURN');
|
||||||
final btcAddress = addressTypeFromStr(address, network);
|
if (index + 1 <= script.length) {
|
||||||
|
try {
|
||||||
int newAmount;
|
final opReturnData = script[index + 1].toString();
|
||||||
if (out.amount.toInt() >= remainingFee) {
|
memo = utf8.decode(HEX.decode(opReturnData));
|
||||||
newAmount = out.amount.toInt() - remainingFee;
|
continue;
|
||||||
remainingFee = 0;
|
} catch (_) {
|
||||||
|
throw Exception('Cannot decode OP_RETURN data');
|
||||||
// if new amount of output is less than dust amount, then don't add this output as well
|
}
|
||||||
if (newAmount <= _dustAmount) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
remainingFee -= out.amount.toInt();
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
|
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||||
|
final btcAddress = addressTypeFromStr(address, network);
|
||||||
|
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate the total amount and fees
|
||||||
|
int totalOutAmount =
|
||||||
|
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
|
||||||
|
int currentFee = allInputsAmount - totalOutAmount;
|
||||||
|
int remainingFee = newFee - currentFee;
|
||||||
|
|
||||||
|
if (remainingFee <= 0) {
|
||||||
|
throw Exception("New fee must be higher than the current fee.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduct Remaining Fee from Main Outputs
|
||||||
|
if (remainingFee > 0) {
|
||||||
|
for (int i = outputs.length - 1; i >= 0; i--) {
|
||||||
|
int outputAmount = outputs[i].value.toInt();
|
||||||
|
|
||||||
|
if (outputAmount > _dustAmount) {
|
||||||
|
int deduction = (outputAmount - _dustAmount >= remainingFee)
|
||||||
|
? remainingFee
|
||||||
|
: outputAmount - _dustAmount;
|
||||||
|
outputs[i] = BitcoinOutput(
|
||||||
|
address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
|
||||||
|
remainingFee -= deduction;
|
||||||
|
|
||||||
|
if (remainingFee <= 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final check if the remaining fee couldn't be deducted
|
||||||
|
if (remainingFee > 0) {
|
||||||
|
throw Exception("Not enough funds to cover the fee.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify all change outputs
|
||||||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
||||||
|
final List<BitcoinOutput> changeOutputs = outputs
|
||||||
|
.where((output) => changeAddresses
|
||||||
|
.any((element) => element.address == output.address.toAddress(network)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
// look for a change address in the outputs
|
int totalChangeAmount =
|
||||||
final changeOutput = outputs.firstWhereOrNull((output) =>
|
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
|
||||||
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
|
|
||||||
|
|
||||||
// deduct the change amount from the output amount
|
// The final amount that the receiver will receive
|
||||||
if (changeOutput != null) {
|
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
|
||||||
totalOutAmount -= changeOutput.value.toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
final txb = BitcoinTransactionBuilder(
|
final txb = BitcoinTransactionBuilder(
|
||||||
utxos: utxos,
|
utxos: utxos,
|
||||||
outputs: outputs,
|
outputs: outputs,
|
||||||
fee: BigInt.from(newFee),
|
fee: BigInt.from(newFee),
|
||||||
network: network,
|
network: network,
|
||||||
|
memo: memo,
|
||||||
|
outputOrdering: BitcoinOrdering.none,
|
||||||
enableRBF: true,
|
enableRBF: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1542,10 +1600,10 @@ abstract class ElectrumWalletBase
|
||||||
transaction,
|
transaction,
|
||||||
type,
|
type,
|
||||||
electrumClient: electrumClient,
|
electrumClient: electrumClient,
|
||||||
amount: totalOutAmount,
|
amount: sendingAmount,
|
||||||
fee: newFee,
|
fee: newFee,
|
||||||
network: network,
|
network: network,
|
||||||
hasChange: changeOutput != null,
|
hasChange: changeOutputs.isNotEmpty,
|
||||||
feeRate: newFee.toString(),
|
feeRate: newFee.toString(),
|
||||||
)..addListener((transaction) async {
|
)..addListener((transaction) async {
|
||||||
transactionHistory.addOne(transaction);
|
transactionHistory.addOne(transaction);
|
||||||
|
@ -1891,18 +1949,78 @@ abstract class ElectrumWalletBase
|
||||||
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||||
: null;
|
: null;
|
||||||
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
||||||
final priv = ECPrivate.fromWif(
|
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
|
||||||
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
|
|
||||||
netVersion: network.wifNetVer,
|
String messagePrefix = '\x18Bitcoin Signed Message:\n';
|
||||||
);
|
final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
|
||||||
return priv.signMessage(StringUtils.encode(message));
|
final decodedSig = hex.decode(hexEncoded);
|
||||||
|
return base64Encode(decodedSig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> sigDecodedBytes = [];
|
||||||
|
|
||||||
|
if (signature.endsWith('=')) {
|
||||||
|
sigDecodedBytes = base64.decode(signature);
|
||||||
|
} else {
|
||||||
|
sigDecodedBytes = hex.decode(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
|
||||||
|
throw ArgumentException(
|
||||||
|
"signature must be 64 bytes without recover-id or 65 bytes with recover-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
String messagePrefix = '\x18Bitcoin Signed Message:\n';
|
||||||
|
final messageHash = QuickCrypto.sha256Hash(
|
||||||
|
BitcoinSignerUtils.magicMessage(utf8.encode(message), messagePrefix));
|
||||||
|
|
||||||
|
List<int> correctSignature =
|
||||||
|
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
|
||||||
|
List<int> rBytes = correctSignature.sublist(0, 32);
|
||||||
|
List<int> sBytes = correctSignature.sublist(32);
|
||||||
|
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
|
||||||
|
|
||||||
|
List<int> possibleRecoverIds = [0, 1];
|
||||||
|
|
||||||
|
final baseAddress = addressTypeFromStr(address, network);
|
||||||
|
|
||||||
|
for (int recoveryId in possibleRecoverIds) {
|
||||||
|
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
||||||
|
|
||||||
|
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
|
||||||
|
|
||||||
|
String? recoveredAddress;
|
||||||
|
|
||||||
|
if (baseAddress is P2pkAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
|
||||||
|
} else if (baseAddress is P2pkhAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
|
||||||
|
} else if (baseAddress is P2wshAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
|
||||||
|
} else if (baseAddress is P2wpkhAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recoveredAddress == address) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setInitialHeight() async {
|
Future<void> _setInitialHeight() async {
|
||||||
if (_chainTipUpdateSubject != null) return;
|
if (_chainTipUpdateSubject != null) return;
|
||||||
|
|
||||||
|
_currentChainTip = await getUpdatedChainTip();
|
||||||
|
|
||||||
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
|
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
|
||||||
await getUpdatedChainTip();
|
|
||||||
await walletInfo.updateRestoreHeight(_currentChainTip!);
|
await walletInfo.updateRestoreHeight(_currentChainTip!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1941,13 +2059,6 @@ abstract class ElectrumWalletBase
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.failed:
|
case ConnectionStatus.failed:
|
||||||
syncStatus = LostConnectionSyncStatus();
|
syncStatus = LostConnectionSyncStatus();
|
||||||
// wait for 5 seconds and then try to reconnect:
|
|
||||||
Future.delayed(Duration(seconds: 5), () {
|
|
||||||
electrumClient.connectToUri(
|
|
||||||
node!.uri,
|
|
||||||
useSSL: node!.useSSL ?? false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.connecting:
|
case ConnectionStatus.connecting:
|
||||||
syncStatus = ConnectingSyncStatus();
|
syncStatus = ConnectingSyncStatus();
|
||||||
|
@ -1957,7 +2068,11 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncStatusReaction(SyncStatus syncStatus) async {
|
void _syncStatusReaction(SyncStatus syncStatus) async {
|
||||||
if (syncStatus is NotConnectedSyncStatus) {
|
if (syncStatus is SyncingSyncStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) {
|
||||||
// Needs to re-subscribe to all scripthashes when reconnected
|
// Needs to re-subscribe to all scripthashes when reconnected
|
||||||
_scripthashesUpdateSubject = {};
|
_scripthashesUpdateSubject = {};
|
||||||
|
|
||||||
|
@ -1965,8 +2080,8 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
_isTryingToConnect = true;
|
_isTryingToConnect = true;
|
||||||
|
|
||||||
Future.delayed(Duration(seconds: 10), () {
|
Timer(Duration(seconds: 10), () {
|
||||||
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) {
|
if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) {
|
||||||
this.electrumClient.connectToUri(
|
this.electrumClient.connectToUri(
|
||||||
node!.uri,
|
node!.uri,
|
||||||
useSSL: node!.useSSL ?? false,
|
useSSL: node!.useSSL ?? false,
|
||||||
|
@ -1983,6 +2098,54 @@ abstract class ElectrumWalletBase
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) {
|
||||||
|
tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList();
|
||||||
|
|
||||||
|
if (tx.inputAddresses == null ||
|
||||||
|
tx.inputAddresses!.isEmpty ||
|
||||||
|
tx.outputAddresses == null ||
|
||||||
|
tx.outputAddresses!.isEmpty) {
|
||||||
|
List<String> inputAddresses = [];
|
||||||
|
List<String> outputAddresses = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||||
|
final input = bundle.originalTransaction.inputs[i];
|
||||||
|
final inputTransaction = bundle.ins[i];
|
||||||
|
final vout = input.txIndex;
|
||||||
|
final outTransaction = inputTransaction.outputs[vout];
|
||||||
|
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
|
||||||
|
|
||||||
|
if (address.isNotEmpty) inputAddresses.add(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
|
||||||
|
final out = bundle.originalTransaction.outputs[i];
|
||||||
|
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||||
|
|
||||||
|
if (address.isNotEmpty) outputAddresses.add(address);
|
||||||
|
|
||||||
|
// Check if the script contains OP_RETURN
|
||||||
|
final script = out.scriptPubKey.script;
|
||||||
|
if (script.contains('OP_RETURN')) {
|
||||||
|
final index = script.indexOf('OP_RETURN');
|
||||||
|
if (index + 1 <= script.length) {
|
||||||
|
try {
|
||||||
|
final opReturnData = script[index + 1].toString();
|
||||||
|
final decodedString = utf8.decode(HEX.decode(opReturnData));
|
||||||
|
outputAddresses.add('OP_RETURN:$decodedString');
|
||||||
|
} catch (_) {
|
||||||
|
outputAddresses.add('OP_RETURN:');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.inputAddresses = inputAddresses;
|
||||||
|
tx.outputAddresses = outputAddresses;
|
||||||
|
|
||||||
|
transactionHistory.addOne(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanNode {
|
class ScanNode {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:bip39/bip39.dart' as bip39;
|
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
|
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
|
||||||
|
import 'package:bip39/bip39.dart' as bip39;
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||||
|
import 'package:cw_bitcoin/electrum_derivations.dart';
|
||||||
import 'package:cw_core/encryption_file_utils.dart';
|
import 'package:cw_core/encryption_file_utils.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/unspent_coins_info.dart';
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
|
@ -21,6 +24,9 @@ import 'package:hive/hive.dart';
|
||||||
import 'package:ledger_flutter/ledger_flutter.dart';
|
import 'package:ledger_flutter/ledger_flutter.dart';
|
||||||
import 'package:ledger_litecoin/ledger_litecoin.dart';
|
import 'package:ledger_litecoin/ledger_litecoin.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
|
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
|
||||||
|
import 'package:pointycastle/ecc/api.dart';
|
||||||
|
import 'package:pointycastle/ecc/curves/secp256k1.dart';
|
||||||
|
|
||||||
part 'litecoin_wallet.g.dart';
|
part 'litecoin_wallet.g.dart';
|
||||||
|
|
||||||
|
@ -35,6 +41,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
Uint8List? seedBytes,
|
Uint8List? seedBytes,
|
||||||
String? mnemonic,
|
String? mnemonic,
|
||||||
String? xpub,
|
String? xpub,
|
||||||
|
String? passphrase,
|
||||||
String? addressPageType,
|
String? addressPageType,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
List<BitcoinAddressRecord>? initialAddresses,
|
||||||
ElectrumBalance? initialBalance,
|
ElectrumBalance? initialBalance,
|
||||||
|
@ -51,6 +58,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
initialBalance: initialBalance,
|
initialBalance: initialBalance,
|
||||||
seedBytes: seedBytes,
|
seedBytes: seedBytes,
|
||||||
encryptionFileUtils: encryptionFileUtils,
|
encryptionFileUtils: encryptionFileUtils,
|
||||||
|
passphrase: passphrase,
|
||||||
currency: CryptoCurrency.ltc) {
|
currency: CryptoCurrency.ltc) {
|
||||||
walletAddresses = LitecoinWalletAddresses(
|
walletAddresses = LitecoinWalletAddresses(
|
||||||
walletInfo,
|
walletInfo,
|
||||||
|
@ -89,7 +97,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
break;
|
break;
|
||||||
case DerivationType.electrum:
|
case DerivationType.electrum:
|
||||||
default:
|
default:
|
||||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return LitecoinWallet(
|
return LitecoinWallet(
|
||||||
|
@ -100,6 +108,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
initialAddresses: initialAddresses,
|
initialAddresses: initialAddresses,
|
||||||
initialBalance: initialBalance,
|
initialBalance: initialBalance,
|
||||||
encryptionFileUtils: encryptionFileUtils,
|
encryptionFileUtils: encryptionFileUtils,
|
||||||
|
passphrase: passphrase,
|
||||||
seedBytes: seedBytes,
|
seedBytes: seedBytes,
|
||||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||||
|
@ -143,6 +152,31 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
walletInfo.derivationInfo ??= DerivationInfo();
|
||||||
|
|
||||||
|
// set the default if not present:
|
||||||
|
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
|
||||||
|
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
|
||||||
|
|
||||||
|
Uint8List? seedBytes = null;
|
||||||
|
final mnemonic = keysData.mnemonic;
|
||||||
|
final passphrase = keysData.passphrase;
|
||||||
|
|
||||||
|
if (mnemonic != null) {
|
||||||
|
switch (walletInfo.derivationInfo?.derivationType) {
|
||||||
|
case DerivationType.bip39:
|
||||||
|
seedBytes = await bip39.mnemonicToSeed(
|
||||||
|
mnemonic,
|
||||||
|
passphrase: passphrase ?? "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DerivationType.electrum:
|
||||||
|
default:
|
||||||
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return LitecoinWallet(
|
return LitecoinWallet(
|
||||||
mnemonic: keysData.mnemonic,
|
mnemonic: keysData.mnemonic,
|
||||||
xpub: keysData.xPub,
|
xpub: keysData.xPub,
|
||||||
|
@ -151,7 +185,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
unspentCoinsInfo: unspentCoinsInfo,
|
unspentCoinsInfo: unspentCoinsInfo,
|
||||||
initialAddresses: snp?.addresses,
|
initialAddresses: snp?.addresses,
|
||||||
initialBalance: snp?.balance,
|
initialBalance: snp?.balance,
|
||||||
seedBytes: keysData.mnemonic != null ? await mnemonicToSeedBytes(keysData.mnemonic!) : null,
|
seedBytes: seedBytes,
|
||||||
|
passphrase: passphrase,
|
||||||
encryptionFileUtils: encryptionFileUtils,
|
encryptionFileUtils: encryptionFileUtils,
|
||||||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||||
|
@ -175,6 +210,129 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> signMessage(String message, {String? address = null}) async {
|
||||||
|
final index = address != null
|
||||||
|
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||||
|
: null;
|
||||||
|
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
||||||
|
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
|
||||||
|
|
||||||
|
final privateKey = ECDSAPrivateKey.fromBytes(
|
||||||
|
priv.toBytes(),
|
||||||
|
Curves.generatorSecp256k1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final signature =
|
||||||
|
signLitecoinMessage(utf8.encode(message), privateKey: privateKey, bipPrive: priv.prive);
|
||||||
|
|
||||||
|
return base64Encode(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _magicPrefix(List<int> message, List<int> messagePrefix) {
|
||||||
|
final encodeLength = IntUtils.encodeVarint(message.length);
|
||||||
|
|
||||||
|
return [...messagePrefix, ...encodeLength, ...message];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> signLitecoinMessage(List<int> message,
|
||||||
|
{required ECDSAPrivateKey privateKey, required Bip32PrivateKey bipPrive}) {
|
||||||
|
String messagePrefix = '\x19Litecoin Signed Message:\n';
|
||||||
|
final messageHash = QuickCrypto.sha256Hash(magicMessage(message, messagePrefix));
|
||||||
|
final signingKey = EcdsaSigningKey(privateKey);
|
||||||
|
ECDSASignature ecdsaSign =
|
||||||
|
signingKey.signDigestDeterminstic(digest: messageHash, hashFunc: () => SHA256());
|
||||||
|
final n = Curves.generatorSecp256k1.order! >> 1;
|
||||||
|
BigInt newS;
|
||||||
|
if (ecdsaSign.s.compareTo(n) > 0) {
|
||||||
|
newS = Curves.generatorSecp256k1.order! - ecdsaSign.s;
|
||||||
|
} else {
|
||||||
|
newS = ecdsaSign.s;
|
||||||
|
}
|
||||||
|
final rawSig = ECDSASignature(ecdsaSign.r, newS);
|
||||||
|
final rawSigBytes = rawSig.toBytes(BitcoinSignerUtils.baselen);
|
||||||
|
|
||||||
|
final pub = bipPrive.publicKey;
|
||||||
|
final ECDomainParameters curve = ECCurve_secp256k1();
|
||||||
|
final point = curve.curve.decodePoint(pub.point.toBytes());
|
||||||
|
|
||||||
|
final rawSigEc = ECSignature(rawSig.r, rawSig.s);
|
||||||
|
|
||||||
|
final recId = SignUtils.findRecoveryId(
|
||||||
|
SignUtils.getHexString(messageHash, offset: 0, length: messageHash.length),
|
||||||
|
rawSigEc,
|
||||||
|
Uint8List.fromList(pub.uncompressed),
|
||||||
|
);
|
||||||
|
|
||||||
|
final v = recId + 27 + (point!.isCompressed ? 4 : 0);
|
||||||
|
|
||||||
|
final combined = Uint8List.fromList([v, ...rawSigBytes]);
|
||||||
|
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> magicMessage(List<int> message, String messagePrefix) {
|
||||||
|
final prefixBytes = StringUtils.encode(messagePrefix);
|
||||||
|
final magic = _magicPrefix(message, prefixBytes);
|
||||||
|
return QuickCrypto.sha256Hash(magic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> sigDecodedBytes = [];
|
||||||
|
|
||||||
|
if (signature.endsWith('=')) {
|
||||||
|
sigDecodedBytes = base64.decode(signature);
|
||||||
|
} else {
|
||||||
|
sigDecodedBytes = hex.decode(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
|
||||||
|
throw ArgumentException(
|
||||||
|
"litecoin signature must be 64 bytes without recover-id or 65 bytes with recover-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
String messagePrefix = '\x19Litecoin Signed Message:\n';
|
||||||
|
final messageHash = QuickCrypto.sha256Hash(magicMessage(utf8.encode(message), messagePrefix));
|
||||||
|
|
||||||
|
List<int> correctSignature =
|
||||||
|
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
|
||||||
|
List<int> rBytes = correctSignature.sublist(0, 32);
|
||||||
|
List<int> sBytes = correctSignature.sublist(32);
|
||||||
|
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
|
||||||
|
|
||||||
|
List<int> possibleRecoverIds = [0, 1];
|
||||||
|
|
||||||
|
final baseAddress = addressTypeFromStr(address, network);
|
||||||
|
|
||||||
|
for (int recoveryId in possibleRecoverIds) {
|
||||||
|
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
||||||
|
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
|
||||||
|
|
||||||
|
String? recoveredAddress;
|
||||||
|
|
||||||
|
if (baseAddress is P2pkAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
|
||||||
|
} else if (baseAddress is P2pkhAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
|
||||||
|
} else if (baseAddress is P2wshAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
|
||||||
|
} else if (baseAddress is P2wpkhAddress) {
|
||||||
|
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recoveredAddress == address) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Ledger? _ledger;
|
Ledger? _ledger;
|
||||||
LedgerDevice? _ledgerDevice;
|
LedgerDevice? _ledgerDevice;
|
||||||
LitecoinLedgerApp? _litecoinLedgerApp;
|
LitecoinLedgerApp? _litecoinLedgerApp;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
|
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||||
import 'package:cw_core/encryption_file_utils.dart';
|
import 'package:cw_core/encryption_file_utils.dart';
|
||||||
import 'package:cw_core/unspent_coins_info.dart';
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
@ -31,8 +32,21 @@ class LitecoinWalletService extends WalletService<
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||||
|
final String mnemonic;
|
||||||
|
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||||
|
case DerivationType.bip39:
|
||||||
|
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||||
|
|
||||||
|
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||||
|
break;
|
||||||
|
case DerivationType.electrum:
|
||||||
|
default:
|
||||||
|
mnemonic = await generateElectrumMnemonic();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
final wallet = await LitecoinWalletBase.create(
|
final wallet = await LitecoinWalletBase.create(
|
||||||
mnemonic: await generateElectrumMnemonic(),
|
mnemonic: mnemonic,
|
||||||
password: credentials.password!,
|
password: credentials.password!,
|
||||||
passphrase: credentials.passphrase,
|
passphrase: credentials.passphrase,
|
||||||
walletInfo: credentials.walletInfo!,
|
walletInfo: credentials.walletInfo!,
|
||||||
|
|
|
@ -67,11 +67,11 @@ packages:
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
bitcoin_base:
|
bitcoin_base:
|
||||||
dependency: "direct main"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: cake-update-v4
|
ref: cake-update-v5
|
||||||
resolved-ref: "574486bfcdbbaf978dcd006b46fc8716f880da29"
|
resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380
|
||||||
url: "https://github.com/cake-tech/bitcoin_base"
|
url: "https://github.com/cake-tech/bitcoin_base"
|
||||||
source: git
|
source: git
|
||||||
version: "4.7.0"
|
version: "4.7.0"
|
||||||
|
@ -350,6 +350,11 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -745,6 +750,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.27.7"
|
version: "0.27.7"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.2"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -953,4 +1014,4 @@ packages:
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
flutter: ">=3.16.6"
|
flutter: ">=3.19.0"
|
||||||
|
|
|
@ -17,6 +17,7 @@ dependencies:
|
||||||
mobx: ^2.0.7+4
|
mobx: ^2.0.7+4
|
||||||
flutter_mobx: ^2.0.6+1
|
flutter_mobx: ^2.0.6+1
|
||||||
intl: ^0.18.0
|
intl: ^0.18.0
|
||||||
|
shared_preferences: ^2.0.15
|
||||||
cw_core:
|
cw_core:
|
||||||
path: ../cw_core
|
path: ../cw_core
|
||||||
bitbox:
|
bitbox:
|
||||||
|
@ -25,10 +26,6 @@ dependencies:
|
||||||
ref: Add-Support-For-OP-Return-data
|
ref: Add-Support-For-OP-Return-data
|
||||||
rxdart: ^0.27.5
|
rxdart: ^0.27.5
|
||||||
cryptography: ^2.0.5
|
cryptography: ^2.0.5
|
||||||
bitcoin_base:
|
|
||||||
git:
|
|
||||||
url: https://github.com/cake-tech/bitcoin_base
|
|
||||||
ref: cake-update-v4
|
|
||||||
blockchain_utils:
|
blockchain_utils:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/cake-tech/blockchain_utils
|
url: https://github.com/cake-tech/blockchain_utils
|
||||||
|
@ -60,6 +57,10 @@ dependency_overrides:
|
||||||
url: https://github.com/cake-tech/ledger-flutter.git
|
url: https://github.com/cake-tech/ledger-flutter.git
|
||||||
ref: cake-v3
|
ref: cake-v3
|
||||||
watcher: ^1.1.0
|
watcher: ^1.1.0
|
||||||
|
bitcoin_base:
|
||||||
|
git:
|
||||||
|
url: https://github.com/cake-tech/bitcoin_base
|
||||||
|
ref: cake-update-v5
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
|
@ -3,5 +3,4 @@ export 'bitcoin_cash_wallet_addresses.dart';
|
||||||
export 'bitcoin_cash_wallet_creation_credentials.dart';
|
export 'bitcoin_cash_wallet_creation_credentials.dart';
|
||||||
export 'bitcoin_cash_wallet_service.dart';
|
export 'bitcoin_cash_wallet_service.dart';
|
||||||
export 'exceptions/exceptions.dart';
|
export 'exceptions/exceptions.dart';
|
||||||
export 'mnemonic.dart';
|
|
||||||
export 'bitcoin_cash_address_utils.dart';
|
export 'bitcoin_cash_address_utils.dart';
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
import 'package:cw_core/encryption_file_utils.dart';
|
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||||
|
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
import 'package:cw_core/encryption_file_utils.dart';
|
||||||
import 'package:cw_core/transaction_priority.dart';
|
import 'package:cw_core/transaction_priority.dart';
|
||||||
import 'package:cw_core/unspent_coins_info.dart';
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
@ -30,6 +31,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||||
required Uint8List seedBytes,
|
required Uint8List seedBytes,
|
||||||
required EncryptionFileUtils encryptionFileUtils,
|
required EncryptionFileUtils encryptionFileUtils,
|
||||||
|
String? passphrase,
|
||||||
BitcoinAddressType? addressPageType,
|
BitcoinAddressType? addressPageType,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
List<BitcoinAddressRecord>? initialAddresses,
|
||||||
ElectrumBalance? initialBalance,
|
ElectrumBalance? initialBalance,
|
||||||
|
@ -45,7 +47,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||||
initialBalance: initialBalance,
|
initialBalance: initialBalance,
|
||||||
seedBytes: seedBytes,
|
seedBytes: seedBytes,
|
||||||
currency: CryptoCurrency.bch,
|
currency: CryptoCurrency.bch,
|
||||||
encryptionFileUtils: encryptionFileUtils) {
|
encryptionFileUtils: encryptionFileUtils,
|
||||||
|
passphrase: passphrase) {
|
||||||
walletAddresses = BitcoinCashWalletAddresses(
|
walletAddresses = BitcoinCashWalletAddresses(
|
||||||
walletInfo,
|
walletInfo,
|
||||||
initialAddresses: initialAddresses,
|
initialAddresses: initialAddresses,
|
||||||
|
@ -67,6 +70,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||||
required WalletInfo walletInfo,
|
required WalletInfo walletInfo,
|
||||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||||
required EncryptionFileUtils encryptionFileUtils,
|
required EncryptionFileUtils encryptionFileUtils,
|
||||||
|
String? passphrase,
|
||||||
String? addressPageType,
|
String? addressPageType,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
List<BitcoinAddressRecord>? initialAddresses,
|
||||||
ElectrumBalance? initialBalance,
|
ElectrumBalance? initialBalance,
|
||||||
|
@ -79,11 +83,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||||
unspentCoinsInfo: unspentCoinsInfo,
|
unspentCoinsInfo: unspentCoinsInfo,
|
||||||
initialAddresses: initialAddresses,
|
initialAddresses: initialAddresses,
|
||||||
initialBalance: initialBalance,
|
initialBalance: initialBalance,
|
||||||
seedBytes: await MnemonicBip39.toSeed(mnemonic),
|
seedBytes: await MnemonicBip39.toSeed(mnemonic, passphrase: passphrase),
|
||||||
encryptionFileUtils: encryptionFileUtils,
|
encryptionFileUtils: encryptionFileUtils,
|
||||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||||
addressPageType: P2pkhAddressType.p2pkh,
|
addressPageType: P2pkhAddressType.p2pkh,
|
||||||
|
passphrase: passphrase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,11 +155,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||||
}
|
}
|
||||||
}).toList(),
|
}).toList(),
|
||||||
initialBalance: snp?.balance,
|
initialBalance: snp?.balance,
|
||||||
seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!),
|
seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!, passphrase: keysData.passphrase),
|
||||||
encryptionFileUtils: encryptionFileUtils,
|
encryptionFileUtils: encryptionFileUtils,
|
||||||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||||
addressPageType: P2pkhAddressType.p2pkh,
|
addressPageType: P2pkhAddressType.p2pkh,
|
||||||
|
passphrase: keysData.passphrase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,11 +208,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> signMessage(String message, {String? address = null}) async {
|
Future<String> signMessage(String message, {String? address = null}) async {
|
||||||
final index = address != null
|
int? index;
|
||||||
? walletAddresses.allAddresses
|
try {
|
||||||
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))
|
index = address != null
|
||||||
.index
|
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||||
: null;
|
: null;
|
||||||
|
} catch (_) {}
|
||||||
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
||||||
final priv = ECPrivate.fromWif(
|
final priv = ECPrivate.fromWif(
|
||||||
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
|
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
|
||||||
|
|
|
@ -2,17 +2,19 @@ import 'package:cw_core/wallet_credentials.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
|
||||||
class BitcoinCashNewWalletCredentials extends WalletCredentials {
|
class BitcoinCashNewWalletCredentials extends WalletCredentials {
|
||||||
BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password})
|
BitcoinCashNewWalletCredentials(
|
||||||
: super(name: name, walletInfo: walletInfo, password: password);
|
{required String name, WalletInfo? walletInfo, String? password, String? passphrase})
|
||||||
|
: super(name: name, walletInfo: walletInfo, password: password, passphrase: passphrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials {
|
class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||||
BitcoinCashRestoreWalletFromSeedCredentials(
|
BitcoinCashRestoreWalletFromSeedCredentials({
|
||||||
{required String name,
|
required String name,
|
||||||
required String password,
|
required String password,
|
||||||
required this.mnemonic,
|
required this.mnemonic,
|
||||||
WalletInfo? walletInfo})
|
WalletInfo? walletInfo,
|
||||||
: super(name: name, password: password, walletInfo: walletInfo);
|
String? passphrase,
|
||||||
|
}) : super(name: name, password: password, walletInfo: walletInfo, passphrase: passphrase);
|
||||||
|
|
||||||
final String mnemonic;
|
final String mnemonic;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bip39/bip39.dart';
|
import 'package:bip39/bip39.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||||
import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart';
|
import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart';
|
||||||
import 'package:cw_core/encryption_file_utils.dart';
|
import 'package:cw_core/encryption_file_utils.dart';
|
||||||
import 'package:cw_core/pathForWallet.dart';
|
import 'package:cw_core/pathForWallet.dart';
|
||||||
|
@ -9,7 +11,6 @@ import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
import 'package:cw_core/wallet_service.dart';
|
import 'package:cw_core/wallet_service.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
class BitcoinCashWalletService extends WalletService<
|
class BitcoinCashWalletService extends WalletService<
|
||||||
|
@ -35,11 +36,12 @@ class BitcoinCashWalletService extends WalletService<
|
||||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||||
|
|
||||||
final wallet = await BitcoinCashWalletBase.create(
|
final wallet = await BitcoinCashWalletBase.create(
|
||||||
mnemonic: await MnemonicBip39.generate(strength: strength),
|
mnemonic: await MnemonicBip39.generate(strength: strength),
|
||||||
password: credentials.password!,
|
password: credentials.password!,
|
||||||
walletInfo: credentials.walletInfo!,
|
walletInfo: credentials.walletInfo!,
|
||||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||||
|
passphrase: credentials.passphrase,
|
||||||
);
|
);
|
||||||
await wallet.save();
|
await wallet.save();
|
||||||
await wallet.init();
|
await wallet.init();
|
||||||
|
@ -54,11 +56,11 @@ class BitcoinCashWalletService extends WalletService<
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final wallet = await BitcoinCashWalletBase.open(
|
final wallet = await BitcoinCashWalletBase.open(
|
||||||
password: password,
|
password: password,
|
||||||
name: name,
|
name: name,
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||||
);
|
);
|
||||||
await wallet.init();
|
await wallet.init();
|
||||||
saveBackup(name);
|
saveBackup(name);
|
||||||
|
@ -66,11 +68,11 @@ class BitcoinCashWalletService extends WalletService<
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
await restoreWalletFilesFromBackup(name);
|
await restoreWalletFilesFromBackup(name);
|
||||||
final wallet = await BitcoinCashWalletBase.open(
|
final wallet = await BitcoinCashWalletBase.open(
|
||||||
password: password,
|
password: password,
|
||||||
name: name,
|
name: name,
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||||
);
|
);
|
||||||
await wallet.init();
|
await wallet.init();
|
||||||
return wallet;
|
return wallet;
|
||||||
|
@ -130,7 +132,9 @@ class BitcoinCashWalletService extends WalletService<
|
||||||
mnemonic: credentials.mnemonic,
|
mnemonic: credentials.mnemonic,
|
||||||
walletInfo: credentials.walletInfo!,
|
walletInfo: credentials.walletInfo!,
|
||||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||||
|
passphrase: credentials.passphrase
|
||||||
|
);
|
||||||
await wallet.save();
|
await wallet.save();
|
||||||
await wallet.init();
|
await wallet.init();
|
||||||
return wallet;
|
return wallet;
|
||||||
|
|
|
@ -25,10 +25,6 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||||
ref: Add-Support-For-OP-Return-data
|
ref: Add-Support-For-OP-Return-data
|
||||||
bitcoin_base:
|
|
||||||
git:
|
|
||||||
url: https://github.com/cake-tech/bitcoin_base
|
|
||||||
ref: cake-update-v4
|
|
||||||
blockchain_utils:
|
blockchain_utils:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/cake-tech/blockchain_utils
|
url: https://github.com/cake-tech/blockchain_utils
|
||||||
|
@ -43,6 +39,10 @@ dev_dependencies:
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
watcher: ^1.1.0
|
watcher: ^1.1.0
|
||||||
|
bitcoin_base:
|
||||||
|
git:
|
||||||
|
url: https://github.com/cake-tech/bitcoin_base
|
||||||
|
ref: cake-update-v5
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
|
@ -46,6 +46,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||||
|
|
||||||
String? get hexSeed => null;
|
String? get hexSeed => null;
|
||||||
|
|
||||||
|
String? get passphrase => null;
|
||||||
|
|
||||||
Object get keys;
|
Object get keys;
|
||||||
|
|
||||||
WalletAddresses get walletAddresses;
|
WalletAddresses get walletAddresses;
|
||||||
|
@ -69,7 +71,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||||
|
|
||||||
int calculateEstimatedFee(TransactionPriority priority, int? amount);
|
int calculateEstimatedFee(TransactionPriority priority, int? amount);
|
||||||
|
|
||||||
|
|
||||||
// void fetchTransactionsAsync(
|
// void fetchTransactionsAsync(
|
||||||
// void Function(TransactionType transaction) onTransactionLoaded,
|
// void Function(TransactionType transaction) onTransactionLoaded,
|
||||||
// {void Function() onFinished});
|
// {void Function() onFinished});
|
||||||
|
@ -92,7 +93,9 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||||
|
|
||||||
Future<void> renameWalletFiles(String newWalletName);
|
Future<void> renameWalletFiles(String newWalletName);
|
||||||
|
|
||||||
Future<String> signMessage(String message, {String? address = null}) => throw UnimplementedError();
|
Future<String> signMessage(String message, {String? address = null});
|
||||||
|
|
||||||
bool? isTestnet;
|
Future<bool> verifyMessage(String message, String signature, {String? address = null});
|
||||||
|
|
||||||
|
bool isTestnet = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import 'package:mobx/mobx.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:web3dart/crypto.dart';
|
import 'package:web3dart/crypto.dart';
|
||||||
import 'package:web3dart/web3dart.dart';
|
import 'package:web3dart/web3dart.dart';
|
||||||
|
import 'package:eth_sig_util/eth_sig_util.dart';
|
||||||
|
|
||||||
import 'evm_chain_transaction_info.dart';
|
import 'evm_chain_transaction_info.dart';
|
||||||
import 'evm_erc20_balance.dart';
|
import 'evm_erc20_balance.dart';
|
||||||
|
@ -112,6 +113,8 @@ abstract class EVMChainWalletBase
|
||||||
int? gasBaseFee = 0;
|
int? gasBaseFee = 0;
|
||||||
int estimatedGasUnits = 0;
|
int estimatedGasUnits = 0;
|
||||||
|
|
||||||
|
Timer? _updateFeesTimer;
|
||||||
|
|
||||||
bool _isTransactionUpdating;
|
bool _isTransactionUpdating;
|
||||||
|
|
||||||
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
||||||
|
@ -262,6 +265,7 @@ abstract class EVMChainWalletBase
|
||||||
void close() {
|
void close() {
|
||||||
_client.stop();
|
_client.stop();
|
||||||
_transactionsUpdateTimer?.cancel();
|
_transactionsUpdateTimer?.cancel();
|
||||||
|
_updateFeesTimer?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -296,7 +300,7 @@ abstract class EVMChainWalletBase
|
||||||
|
|
||||||
await _updateEstimatedGasFeeParams();
|
await _updateEstimatedGasFeeParams();
|
||||||
|
|
||||||
Timer.periodic(const Duration(seconds: 10), (timer) async {
|
_updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||||
await _updateEstimatedGasFeeParams();
|
await _updateEstimatedGasFeeParams();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -500,7 +504,7 @@ abstract class EVMChainWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
final methodSignature =
|
final methodSignature =
|
||||||
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
|
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
|
||||||
|
|
||||||
return methodSignatureToType[methodSignature];
|
return methodSignatureToType[methodSignature];
|
||||||
}
|
}
|
||||||
|
@ -692,8 +696,21 @@ abstract class EVMChainWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> signMessage(String message, {String? address}) async =>
|
Future<String> signMessage(String message, {String? address}) async {
|
||||||
bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
|
return bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final recoveredAddress = EthSigUtil.recoverPersonalSignature(
|
||||||
|
message: ascii.encode(message),
|
||||||
|
signature: signature,
|
||||||
|
);
|
||||||
|
return recoveredAddress.toUpperCase() == address.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
Web3Client? getWeb3Client() => _client.getWeb3Client();
|
Web3Client? getWeb3Client() => _client.getWeb3Client();
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
web3dart: ^2.7.1
|
web3dart: ^2.7.1
|
||||||
|
eth_sig_util: ^0.0.9
|
||||||
|
erc20: ^1.0.1
|
||||||
bip39: ^1.0.6
|
bip39: ^1.0.6
|
||||||
bip32: ^2.0.0
|
bip32: ^2.0.0
|
||||||
hex: ^0.2.0
|
hex: ^0.2.0
|
||||||
|
|
|
@ -10,10 +10,8 @@ import 'package:cw_haven/haven_transaction_info.dart';
|
||||||
import 'package:cw_haven/haven_wallet_addresses.dart';
|
import 'package:cw_haven/haven_wallet_addresses.dart';
|
||||||
import 'package:cw_core/monero_wallet_utils.dart';
|
import 'package:cw_core/monero_wallet_utils.dart';
|
||||||
import 'package:cw_haven/api/structs/pending_transaction.dart';
|
import 'package:cw_haven/api/structs/pending_transaction.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history;
|
import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history;
|
||||||
//import 'package:cw_haven/wallet.dart';
|
|
||||||
import 'package:cw_haven/api/wallet.dart' as haven_wallet;
|
import 'package:cw_haven/api/wallet.dart' as haven_wallet;
|
||||||
import 'package:cw_haven/api/transaction_history.dart' as transaction_history;
|
import 'package:cw_haven/api/transaction_history.dart' as transaction_history;
|
||||||
import 'package:cw_haven/api/monero_output.dart';
|
import 'package:cw_haven/api/monero_output.dart';
|
||||||
|
@ -123,7 +121,8 @@ abstract class HavenWalletBase
|
||||||
login: node.login,
|
login: node.login,
|
||||||
password: node.password,
|
password: node.password,
|
||||||
useSSL: node.useSSL ?? false,
|
useSSL: node.useSSL ?? false,
|
||||||
isLightWallet: false, // FIXME: hardcoded value
|
isLightWallet: false,
|
||||||
|
// FIXME: hardcoded value
|
||||||
socksProxyAddress: node.socksProxyAddress);
|
socksProxyAddress: node.socksProxyAddress);
|
||||||
|
|
||||||
haven_wallet.setTrustedDaemon(node.trusted);
|
haven_wallet.setTrustedDaemon(node.trusted);
|
||||||
|
@ -419,4 +418,12 @@ abstract class HavenWalletBase
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get password => _password;
|
String get password => _password;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> signMessage(String message, {String? address = null}) =>
|
||||||
|
throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) =>
|
||||||
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,12 +45,23 @@ String getSeed() {
|
||||||
|
|
||||||
String getSeedLegacy(String? language) {
|
String getSeedLegacy(String? language) {
|
||||||
var legacy = monero.Wallet_seed(wptr!, seedOffset: '');
|
var legacy = monero.Wallet_seed(wptr!, seedOffset: '');
|
||||||
|
switch (language) {
|
||||||
|
case "Chinese (Traditional)": language = "Chinese (simplified)"; break;
|
||||||
|
case "Chinese (Simplified)": language = "Chinese (simplified)"; break;
|
||||||
|
case "Korean": language = "English"; break;
|
||||||
|
case "Czech": language = "English"; break;
|
||||||
|
case "Japanese": language = "English"; break;
|
||||||
|
}
|
||||||
if (monero.Wallet_status(wptr!) != 0) {
|
if (monero.Wallet_status(wptr!) != 0) {
|
||||||
monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
||||||
legacy = monero.Wallet_seed(wptr!, seedOffset: '');
|
legacy = monero.Wallet_seed(wptr!, seedOffset: '');
|
||||||
}
|
}
|
||||||
if (monero.Wallet_status(wptr!) != 0) {
|
if (monero.Wallet_status(wptr!) != 0) {
|
||||||
return monero.Wallet_errorString(wptr!);
|
final err = monero.Wallet_errorString(wptr!);
|
||||||
|
if (legacy.isNotEmpty) {
|
||||||
|
return "$err\n\n$legacy";
|
||||||
|
}
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
return legacy;
|
return legacy;
|
||||||
}
|
}
|
||||||
|
@ -305,3 +316,7 @@ Future<bool> trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!);
|
||||||
String signMessage(String message, {String address = ""}) {
|
String signMessage(String message, {String address = ""}) {
|
||||||
return monero.Wallet_signMessage(wptr!, message: message, address: address);
|
return monero.Wallet_signMessage(wptr!, message: message, address: address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool verifyMessage(String message, String address, String signature) {
|
||||||
|
return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
|
||||||
|
}
|
|
@ -13,11 +13,9 @@ import 'package:cw_core/monero_transaction_priority.dart';
|
||||||
import 'package:cw_core/monero_wallet_keys.dart';
|
import 'package:cw_core/monero_wallet_keys.dart';
|
||||||
import 'package:cw_core/monero_wallet_utils.dart';
|
import 'package:cw_core/monero_wallet_utils.dart';
|
||||||
import 'package:cw_core/node.dart';
|
import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_core/pathForWallet.dart';
|
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
import 'package:cw_core/sync_status.dart';
|
import 'package:cw_core/sync_status.dart';
|
||||||
import 'package:cw_core/transaction_direction.dart';
|
import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/transaction_priority.dart';
|
|
||||||
import 'package:cw_core/unspent_coins_info.dart';
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
@ -783,4 +781,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
||||||
final useAddress = address ?? "";
|
final useAddress = address ?? "";
|
||||||
return monero_wallet.signMessage(message, address: useAddress);
|
return monero_wallet.signMessage(message, address: useAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||||
|
if (address == null) return false;
|
||||||
|
|
||||||
|
return monero_wallet.verifyMessage(message, address, signature);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,9 @@ import 'package:cw_core/wallet_info.dart';
|
||||||
import 'package:cw_core/wallet_service.dart';
|
import 'package:cw_core/wallet_service.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:cw_core/get_height_by_date.dart';
|
import 'package:cw_core/get_height_by_date.dart';
|
||||||
import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart';
|
|
||||||
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
|
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
|
||||||
import 'package:cw_monero/api/wallet_manager.dart';
|
import 'package:cw_monero/api/wallet_manager.dart';
|
||||||
import 'package:cw_monero/monero_wallet.dart';
|
import 'package:cw_monero/monero_wallet.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:polyseed/polyseed.dart';
|
import 'package:polyseed/polyseed.dart';
|
||||||
import 'package:monero/monero.dart' as monero;
|
import 'package:monero/monero.dart' as monero;
|
||||||
|
@ -119,8 +117,7 @@ class MoneroWalletService extends WalletService<
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MoneroWallet> openWallet(String name, String password) async {
|
Future<MoneroWallet> openWallet(String name, String password, {bool? retryOnFailure}) async {
|
||||||
MoneroWallet? wallet;
|
|
||||||
try {
|
try {
|
||||||
final path = await pathForWallet(name: name, type: getType());
|
final path = await pathForWallet(name: name, type: getType());
|
||||||
|
|
||||||
|
@ -147,46 +144,15 @@ class MoneroWalletService extends WalletService<
|
||||||
await wallet.init();
|
await wallet.init();
|
||||||
|
|
||||||
return wallet;
|
return wallet;
|
||||||
} catch (e, s) {
|
} catch (e) {
|
||||||
// TODO: Implement Exception for wallet list service.
|
// TODO: Implement Exception for wallet list service.
|
||||||
|
|
||||||
final bool isBadAlloc = e.toString().contains('bad_alloc') ||
|
if (retryOnFailure == false) {
|
||||||
(e is WalletOpeningException &&
|
|
||||||
(e.message == 'std::bad_alloc' || e.message.contains('bad_alloc')));
|
|
||||||
|
|
||||||
final bool doesNotCorrespond = e.toString().contains('does not correspond') ||
|
|
||||||
(e is WalletOpeningException && e.message.contains('does not correspond'));
|
|
||||||
|
|
||||||
final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') ||
|
|
||||||
(e is WalletOpeningException && e.message.contains('basic_string'));
|
|
||||||
|
|
||||||
final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') ||
|
|
||||||
e.toString().contains('input stream error') ||
|
|
||||||
(e is WalletOpeningException &&
|
|
||||||
(e.message.contains('input_stream') || e.message.contains('input stream error')));
|
|
||||||
|
|
||||||
final bool invalidSignature = e.toString().contains('invalid signature') ||
|
|
||||||
(e is WalletOpeningException && e.message.contains('invalid signature'));
|
|
||||||
|
|
||||||
final bool invalidPassword = e.toString().contains('invalid password') ||
|
|
||||||
(e is WalletOpeningException && e.message.contains('invalid password'));
|
|
||||||
|
|
||||||
if (!isBadAlloc &&
|
|
||||||
!doesNotCorrespond &&
|
|
||||||
!isMissingCacheFilesIOS &&
|
|
||||||
!isMissingCacheFilesAndroid &&
|
|
||||||
!invalidSignature &&
|
|
||||||
!invalidPassword &&
|
|
||||||
wallet != null &&
|
|
||||||
wallet.onError != null) {
|
|
||||||
wallet.onError!(FlutterErrorDetails(exception: e, stack: s));
|
|
||||||
}
|
|
||||||
if (invalidPassword) {
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
await restoreOrResetWalletFiles(name);
|
await restoreOrResetWalletFiles(name);
|
||||||
return openWallet(name, password);
|
return openWallet(name, password, retryOnFailure: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -295,10 +295,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hashlib
|
name: hashlib
|
||||||
sha256: "5037d3b8c36384c03a728543ae67d962a56970c5432a50862279fe68ee4c8411"
|
sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.2"
|
||||||
hashlib_codecs:
|
hashlib_codecs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -576,10 +576,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: polyseed
|
name: polyseed
|
||||||
sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217
|
sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.5"
|
version: "0.0.6"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -19,7 +19,7 @@ dependencies:
|
||||||
flutter_mobx: ^2.0.6+1
|
flutter_mobx: ^2.0.6+1
|
||||||
intl: ^0.18.0
|
intl: ^0.18.0
|
||||||
encrypt: ^5.0.1
|
encrypt: ^5.0.1
|
||||||
polyseed: ^0.0.5
|
polyseed: ^0.0.6
|
||||||
cw_core:
|
cw_core:
|
||||||
path: ../cw_core
|
path: ../cw_core
|
||||||
monero:
|
monero:
|
||||||
|
|
37
cw_nano/lib/nano_block_info_response.dart
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
class BlockContentsResponse {
|
||||||
|
String type;
|
||||||
|
String account;
|
||||||
|
String previous;
|
||||||
|
String representative;
|
||||||
|
String balance;
|
||||||
|
String link;
|
||||||
|
String linkAsAccount;
|
||||||
|
String signature;
|
||||||
|
String work;
|
||||||
|
|
||||||
|
BlockContentsResponse({
|
||||||
|
required this.type,
|
||||||
|
required this.account,
|
||||||
|
required this.previous,
|
||||||
|
required this.representative,
|
||||||
|
required this.balance,
|
||||||
|
required this.link,
|
||||||
|
required this.linkAsAccount,
|
||||||
|
required this.signature,
|
||||||
|
required this.work,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BlockContentsResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return BlockContentsResponse(
|
||||||
|
type: json['type'] as String,
|
||||||
|
account: json['account'] as String,
|
||||||
|
previous: json['previous'] as String,
|
||||||
|
representative: json['representative'] as String,
|
||||||
|
balance: json['balance'] as String,
|
||||||
|
link: json['link'] as String,
|
||||||
|
linkAsAccount: json['link_as_account'] as String,
|
||||||
|
signature: json['signature'] as String,
|
||||||
|
work: json['work'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,11 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cw_core/nano_account_info_response.dart';
|
import 'package:cw_core/nano_account_info_response.dart';
|
||||||
|
import 'package:cw_nano/nano_block_info_response.dart';
|
||||||
import 'package:cw_core/n2_node.dart';
|
import 'package:cw_core/n2_node.dart';
|
||||||
import 'package:cw_nano/nano_balance.dart';
|
import 'package:cw_nano/nano_balance.dart';
|
||||||
import 'package:cw_nano/nano_transaction_model.dart';
|
import 'package:cw_nano/nano_transaction_model.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:nanodart/nanodart.dart';
|
|
||||||
import 'package:cw_core/node.dart';
|
import 'package:cw_core/node.dart';
|
||||||
import 'package:nanoutil/nanoutil.dart';
|
import 'package:nanoutil/nanoutil.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
@ -111,6 +111,27 @@ class NanoClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<BlockContentsResponse?> getBlockContents(String block) async {
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
_node!.uri,
|
||||||
|
headers: CAKE_HEADERS,
|
||||||
|
body: jsonEncode(
|
||||||
|
{
|
||||||
|
"action": "block_info",
|
||||||
|
"json_block": "true",
|
||||||
|
"hash": block,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final data = await jsonDecode(response.body);
|
||||||
|
return BlockContentsResponse.fromJson(data["contents"] as Map<String, dynamic>);
|
||||||
|
} catch (e) {
|
||||||
|
print("error while getting block info $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> changeRep({
|
Future<String> changeRep({
|
||||||
required String privateKey,
|
required String privateKey,
|
||||||
required String repAddress,
|
required String repAddress,
|
||||||
|
@ -135,8 +156,8 @@ class NanoClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
// sign the change block:
|
// sign the change block:
|
||||||
final String hash = NanoBlocks.computeStateHash(
|
final String hash = NanoSignatures.computeStateHash(
|
||||||
NanoAccountType.NANO,
|
NanoBasedCurrency.NANO,
|
||||||
changeBlock["account"]!,
|
changeBlock["account"]!,
|
||||||
changeBlock["previous"]!,
|
changeBlock["previous"]!,
|
||||||
changeBlock["representative"]!,
|
changeBlock["representative"]!,
|
||||||
|
@ -248,7 +269,7 @@ class NanoClient {
|
||||||
}
|
}
|
||||||
final String representative = infoResponse.representative;
|
final String representative = infoResponse.representative;
|
||||||
// link = destination address:
|
// link = destination address:
|
||||||
final String link = NanoAccounts.extractPublicKey(destinationAddress);
|
final String link = NanoDerivations.addressToPublicKey(destinationAddress);
|
||||||
final String linkAsAccount = destinationAddress;
|
final String linkAsAccount = destinationAddress;
|
||||||
|
|
||||||
// construct the send block:
|
// construct the send block:
|
||||||
|
@ -262,8 +283,8 @@ class NanoClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
// sign the send block:
|
// sign the send block:
|
||||||
final String hash = NanoBlocks.computeStateHash(
|
final String hash = NanoSignatures.computeStateHash(
|
||||||
NanoAccountType.NANO,
|
NanoBasedCurrency.NANO,
|
||||||
sendBlock["account"]!,
|
sendBlock["account"]!,
|
||||||
sendBlock["previous"]!,
|
sendBlock["previous"]!,
|
||||||
sendBlock["representative"]!,
|
sendBlock["representative"]!,
|
||||||
|
@ -285,7 +306,6 @@ class NanoClient {
|
||||||
|
|
||||||
Future<void> receiveBlock({
|
Future<void> receiveBlock({
|
||||||
required String blockHash,
|
required String blockHash,
|
||||||
required String source,
|
|
||||||
required String amountRaw,
|
required String amountRaw,
|
||||||
required String destinationAddress,
|
required String destinationAddress,
|
||||||
required String privateKey,
|
required String privateKey,
|
||||||
|
@ -310,15 +330,56 @@ class NanoClient {
|
||||||
representative = infoData.representative;
|
representative = infoData.representative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((BigInt.tryParse(amountRaw) ?? BigInt.zero) <= BigInt.zero) {
|
||||||
|
throw Exception("amountRaw must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockContentsResponse? frontierContents;
|
||||||
|
|
||||||
|
if (!openBlock) {
|
||||||
|
// get the block info of the frontier block:
|
||||||
|
frontierContents = await getBlockContents(frontier);
|
||||||
|
|
||||||
|
if (frontierContents == null) {
|
||||||
|
throw Exception("error while getting frontier block info");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String frontierHash = NanoSignatures.computeStateHash(
|
||||||
|
NanoBasedCurrency.NANO,
|
||||||
|
frontierContents.account,
|
||||||
|
frontierContents.previous,
|
||||||
|
frontierContents.representative,
|
||||||
|
BigInt.parse(frontierContents.balance),
|
||||||
|
frontierContents.link,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool valid = await NanoSignatures.verify(
|
||||||
|
frontierHash,
|
||||||
|
frontierContents.signature,
|
||||||
|
destinationAddress,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw Exception(
|
||||||
|
"Frontier block signature is invalid! Potentially malicious block detected!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// first get the account balance:
|
// first get the account balance:
|
||||||
final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance;
|
late BigInt currentBalance;
|
||||||
|
if (!openBlock) {
|
||||||
|
currentBalance = BigInt.parse(frontierContents!.balance);
|
||||||
|
} else {
|
||||||
|
currentBalance = BigInt.zero;
|
||||||
|
}
|
||||||
final BigInt txAmount = BigInt.parse(amountRaw);
|
final BigInt txAmount = BigInt.parse(amountRaw);
|
||||||
final BigInt balanceAfterTx = currentBalance + txAmount;
|
final BigInt balanceAfterTx = currentBalance + txAmount;
|
||||||
|
|
||||||
// link = send block hash:
|
// link = send block hash:
|
||||||
final String link = blockHash;
|
final String link = blockHash;
|
||||||
// this "linkAsAccount" is meaningless:
|
// this "linkAsAccount" is meaningless:
|
||||||
final String linkAsAccount = NanoAccounts.createAccount(NanoAccountType.NANO, blockHash);
|
final String linkAsAccount =
|
||||||
|
NanoDerivations.publicKeyToAddress(blockHash, currency: NanoBasedCurrency.NANO);
|
||||||
|
|
||||||
// construct the receive block:
|
// construct the receive block:
|
||||||
Map<String, String> receiveBlock = {
|
Map<String, String> receiveBlock = {
|
||||||
|
@ -332,8 +393,8 @@ class NanoClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
// sign the receive block:
|
// sign the receive block:
|
||||||
final String hash = NanoBlocks.computeStateHash(
|
final String hash = NanoSignatures.computeStateHash(
|
||||||
NanoAccountType.NANO,
|
NanoBasedCurrency.NANO,
|
||||||
receiveBlock["account"]!,
|
receiveBlock["account"]!,
|
||||||
receiveBlock["previous"]!,
|
receiveBlock["previous"]!,
|
||||||
receiveBlock["representative"]!,
|
receiveBlock["representative"]!,
|
||||||
|
@ -345,7 +406,7 @@ class NanoClient {
|
||||||
// get PoW for the receive block:
|
// get PoW for the receive block:
|
||||||
String? work;
|
String? work;
|
||||||
if (openBlock) {
|
if (openBlock) {
|
||||||
work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress));
|
work = await requestWork(NanoDerivations.addressToPublicKey(destinationAddress));
|
||||||
} else {
|
} else {
|
||||||
work = await requestWork(frontier);
|
work = await requestWork(frontier);
|
||||||
}
|
}
|
||||||
|
@ -409,10 +470,8 @@ class NanoClient {
|
||||||
for (final blockHash in blocks.keys) {
|
for (final blockHash in blocks.keys) {
|
||||||
final block = blocks[blockHash];
|
final block = blocks[blockHash];
|
||||||
final String amountRaw = block["amount"] as String;
|
final String amountRaw = block["amount"] as String;
|
||||||
final String source = block["source"] as String;
|
|
||||||
await receiveBlock(
|
await receiveBlock(
|
||||||
blockHash: blockHash,
|
blockHash: blockHash,
|
||||||
source: source,
|
|
||||||
amountRaw: amountRaw,
|
amountRaw: amountRaw,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
destinationAddress: destinationAddress,
|
destinationAddress: destinationAddress,
|
||||||
|
|
|
@ -27,7 +27,6 @@ import 'package:cw_nano/nano_wallet_addresses.dart';
|
||||||
import 'package:cw_nano/nano_wallet_keys.dart';
|
import 'package:cw_nano/nano_wallet_keys.dart';
|
||||||
import 'package:cw_nano/pending_nano_transaction.dart';
|
import 'package:cw_nano/pending_nano_transaction.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:nanodart/nanodart.dart';
|
|
||||||
import 'package:nanoutil/nanoutil.dart';
|
import 'package:nanoutil/nanoutil.dart';
|
||||||
|
|
||||||
part 'nano_wallet.g.dart';
|
part 'nano_wallet.g.dart';
|
||||||
|
@ -107,7 +106,6 @@ abstract class NanoWalletBase
|
||||||
if (_derivationType == DerivationType.unknown) {
|
if (_derivationType == DerivationType.unknown) {
|
||||||
_derivationType = DerivationType.nano;
|
_derivationType = DerivationType.nano;
|
||||||
}
|
}
|
||||||
final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
|
|
||||||
|
|
||||||
// our "mnemonic" is actually a hex form seed:
|
// our "mnemonic" is actually a hex form seed:
|
||||||
if (!_mnemonic.contains(' ')) {
|
if (!_mnemonic.contains(' ')) {
|
||||||
|
@ -122,8 +120,10 @@ abstract class NanoWalletBase
|
||||||
_hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' '));
|
_hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' '));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NanoDerivationType derivationType =
|
|
||||||
type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD;
|
final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
|
||||||
|
NanoDerivationType derivationType = NanoDerivations.stringToType(type);
|
||||||
|
|
||||||
_privateKey = await NanoDerivations.universalSeedToPrivate(
|
_privateKey = await NanoDerivations.universalSeedToPrivate(
|
||||||
_hexSeed!,
|
_hexSeed!,
|
||||||
index: 0,
|
index: 0,
|
||||||
|
@ -216,8 +216,8 @@ abstract class NanoWalletBase
|
||||||
balanceAfterTx: runningBalance,
|
balanceAfterTx: runningBalance,
|
||||||
previousHash: previousHash,
|
previousHash: previousHash,
|
||||||
);
|
);
|
||||||
previousHash = NanoBlocks.computeStateHash(
|
previousHash = NanoSignatures.computeStateHash(
|
||||||
NanoAccountType.NANO,
|
NanoBasedCurrency.NANO,
|
||||||
block["account"]!,
|
block["account"]!,
|
||||||
block["previous"]!,
|
block["previous"]!,
|
||||||
block["representative"]!,
|
block["representative"]!,
|
||||||
|
@ -535,4 +535,17 @@ abstract class NanoWalletBase
|
||||||
// Delete old name's dir and files
|
// Delete old name's dir and files
|
||||||
await Directory(currentDirPath).delete(recursive: true);
|
await Directory(currentDirPath).delete(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> signMessage(String message, {String? address = null}) async {
|
||||||
|
return NanoSignatures.signMessage(message, privateKey!);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await NanoSignatures.verifyMessage(message, signature, address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,7 +513,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
nanodart:
|
nanodart:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: nanodart
|
name: nanodart
|
||||||
sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282"
|
sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282"
|
||||||
|
@ -524,11 +524,11 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
ref: c01a9c552917008d8fbc6b540db657031625b04f
|
||||||
resolved-ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
resolved-ref: c01a9c552917008d8fbc6b540db657031625b04f
|
||||||
url: "https://github.com/perishllc/nanoutil.git"
|
url: "https://github.com/perishllc/nanoutil.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.0"
|
version: "1.0.3"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -15,7 +15,6 @@ dependencies:
|
||||||
mobx: ^2.0.7+4
|
mobx: ^2.0.7+4
|
||||||
bip39: ^1.0.6
|
bip39: ^1.0.6
|
||||||
bip32: ^2.0.0
|
bip32: ^2.0.0
|
||||||
nanodart: ^2.0.0
|
|
||||||
decimal: ^2.3.3
|
decimal: ^2.3.3
|
||||||
libcrypto: ^0.2.2
|
libcrypto: ^0.2.2
|
||||||
ed25519_hd_key: ^2.2.0
|
ed25519_hd_key: ^2.2.0
|
||||||
|
@ -25,7 +24,7 @@ dependencies:
|
||||||
nanoutil:
|
nanoutil:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/perishllc/nanoutil.git
|
url: https://github.com/perishllc/nanoutil.git
|
||||||
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
ref: c01a9c552917008d8fbc6b540db657031625b04f
|
||||||
cw_core:
|
cw_core:
|
||||||
path: ../cw_core
|
path: ../cw_core
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,8 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solana/base58.dart';
|
import 'package:solana/base58.dart';
|
||||||
import 'package:solana/metaplex.dart' as metaplex;
|
import 'package:solana/metaplex.dart' as metaplex;
|
||||||
import 'package:solana/solana.dart';
|
import 'package:solana/solana.dart';
|
||||||
|
import 'package:solana/src/crypto/ed25519_hd_keypair.dart';
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
|
||||||
part 'solana_wallet.g.dart';
|
part 'solana_wallet.g.dart';
|
||||||
|
|
||||||
|
@ -571,17 +573,59 @@ abstract class SolanaWalletBase
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> signSolanaMessage(String message) async {
|
@override
|
||||||
|
Future<String> signMessage(String message, {String? address}) async {
|
||||||
// Convert the message to bytes
|
// Convert the message to bytes
|
||||||
final messageBytes = utf8.encode(message);
|
final messageBytes = utf8.encode(message);
|
||||||
|
|
||||||
// Sign the message bytes with the wallet's private key
|
// Sign the message bytes with the wallet's private key
|
||||||
final signature = await _walletKeyPair!.sign(messageBytes);
|
final signature = (await _walletKeyPair!.sign(messageBytes)).toString();
|
||||||
|
|
||||||
// Convert the signature to a hexadecimal string
|
return HEX.encode(utf8.encode(signature)).toUpperCase();
|
||||||
final hex = HEX.encode(signature.bytes);
|
}
|
||||||
|
|
||||||
return hex;
|
List<List<int>> bytesFromSigString(String signatureString) {
|
||||||
|
final regex = RegExp(r'Signature\(\[(.+)\], publicKey: (.+)\)');
|
||||||
|
final match = regex.firstMatch(signatureString);
|
||||||
|
|
||||||
|
if (match != null) {
|
||||||
|
final bytesString = match.group(1)!;
|
||||||
|
final base58EncodedPublicKeyString = match.group(2)!;
|
||||||
|
final sigBytes = bytesString.split(', ').map(int.parse).toList();
|
||||||
|
|
||||||
|
List<int> pubKeyBytes = base58decode(base58EncodedPublicKeyString);
|
||||||
|
|
||||||
|
return [sigBytes, pubKeyBytes];
|
||||||
|
} else {
|
||||||
|
throw const FormatException('Invalid Signature string format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
|
||||||
|
String signatureString = utf8.decode(HEX.decode(signature));
|
||||||
|
|
||||||
|
List<List<int>> bytes = bytesFromSigString(signatureString);
|
||||||
|
|
||||||
|
final messageBytes = utf8.encode(message);
|
||||||
|
final sigBytes = bytes[0];
|
||||||
|
final pubKeyBytes = bytes[1];
|
||||||
|
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the address derived from the public key provided matches the one we expect
|
||||||
|
final pub = Ed25519HDPublicKey(pubKeyBytes);
|
||||||
|
if (address != pub.toBase58()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await verifySignature(
|
||||||
|
message: messageBytes,
|
||||||
|
signature: sigBytes,
|
||||||
|
publicKey: Ed25519HDPublicKey(pubKeyBytes),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaClient? get solanaClient => _client.getSolanaClient;
|
SolanaClient? get solanaClient => _client.getSolanaClient;
|
||||||
|
|
|
@ -580,8 +580,18 @@ abstract class TronWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> signMessage(String message, {String? address}) async =>
|
Future<String> signMessage(String message, {String? address}) async {
|
||||||
_tronPrivateKey.signPersonalMessage(ascii.encode(message));
|
return _tronPrivateKey.signPersonalMessage(ascii.encode(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TronPublicKey pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!;
|
||||||
|
return pubKey.toAddress().toString() == address;
|
||||||
|
}
|
||||||
|
|
||||||
String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress();
|
String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress();
|
||||||
|
|
||||||
|
|
|
@ -47,12 +47,23 @@ String getSeed() {
|
||||||
|
|
||||||
String getSeedLegacy(String? language) {
|
String getSeedLegacy(String? language) {
|
||||||
var legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
|
var legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
|
||||||
|
switch (language) {
|
||||||
|
case "Chinese (Traditional)": language = "Chinese (simplified)"; break;
|
||||||
|
case "Chinese (Simplified)": language = "Chinese (simplified)"; break;
|
||||||
|
case "Korean": language = "English"; break;
|
||||||
|
case "Czech": language = "English"; break;
|
||||||
|
case "Japanese": language = "English"; break;
|
||||||
|
}
|
||||||
if (wownero.Wallet_status(wptr!) != 0) {
|
if (wownero.Wallet_status(wptr!) != 0) {
|
||||||
wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
||||||
legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
|
legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
|
||||||
}
|
}
|
||||||
if (wownero.Wallet_status(wptr!) != 0) {
|
if (wownero.Wallet_status(wptr!) != 0) {
|
||||||
return wownero.Wallet_errorString(wptr!);
|
final err = wownero.Wallet_errorString(wptr!);
|
||||||
|
if (legacy.isNotEmpty) {
|
||||||
|
return "$err\n\n$legacy";
|
||||||
|
}
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
return legacy;
|
return legacy;
|
||||||
}
|
}
|
||||||
|
@ -309,3 +320,7 @@ Future<bool> trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!);
|
||||||
String signMessage(String message, {String address = ""}) {
|
String signMessage(String message, {String address = ""}) {
|
||||||
return wownero.Wallet_signMessage(wptr!, message: message, address: address);
|
return wownero.Wallet_signMessage(wptr!, message: message, address: address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool verifyMessage(String message, String address, String signature) {
|
||||||
|
return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
|
||||||
|
}
|
|
@ -743,4 +743,11 @@ abstract class WowneroWalletBase
|
||||||
final useAddress = address ?? "";
|
final useAddress = address ?? "";
|
||||||
return wownero_wallet.signMessage(message, address: useAddress);
|
return wownero_wallet.signMessage(message, address: useAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||||
|
if (address == null) return false;
|
||||||
|
|
||||||
|
return wownero_wallet.verifyMessage(message, address, signature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,18 +295,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hashlib
|
name: hashlib
|
||||||
sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96"
|
sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.19.2"
|
||||||
hashlib_codecs:
|
hashlib_codecs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hashlib_codecs
|
name: hashlib_codecs
|
||||||
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
|
sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.5.0"
|
||||||
hive:
|
hive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -568,10 +568,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: polyseed
|
name: polyseed
|
||||||
sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217
|
sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.5"
|
version: "0.0.6"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|