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
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: "17.x"
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
- name: Configure placeholder git details
|
||||
run: |
|
||||
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
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: "17.x"
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
- name: Configure placeholder git details
|
||||
run: |
|
||||
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.
|
||||
|
||||
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 digit localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
|
||||
5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
|
||||
|
||||
6. Add the new language code to `tool/utils/translation/translation_constants.dart`
|
||||
|
||||
## Add a new fiat currency
|
||||
|
||||
|
|
|
@ -10,7 +10,18 @@ analyzer:
|
|||
lib/generated/*.dart,
|
||||
cw_monero/ios/External/**,
|
||||
cw_shared_external/**,
|
||||
shared_external/**]
|
||||
shared_external/**,
|
||||
lib/bitcoin/cw_bitcoin.dart,
|
||||
lib/bitcoin_cash/cw_bitcoin_cash.dart,
|
||||
lib/ethereum/cw_ethereum.dart,
|
||||
lib/haven/cw_haven.dart,
|
||||
lib/monero/cw_monero.dart,
|
||||
lib/nano/cw_nano.dart,
|
||||
lib/polygon/cw_polygon.dart,
|
||||
lib/solana/cw_solana.dart,
|
||||
lib/tron/cw_tron.dart,
|
||||
lib/wownero/cw_wownero.dart,
|
||||
]
|
||||
language:
|
||||
strict-casts: true
|
||||
strict-raw-types: true
|
||||
|
|
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 |
|
@ -2,3 +2,6 @@
|
|||
uri: rpc.ankr.com
|
||||
is_default: true
|
||||
useSSL: true
|
||||
-
|
||||
uri: api.mainnet-beta.solana.com:443
|
||||
useSSL: true
|
|
@ -1,4 +1,2 @@
|
|||
Monero synchronization improvements
|
||||
Enhance error handling
|
||||
UI enhancements
|
||||
Bug fixes
|
||||
Enhance auto-address generation for Monero
|
||||
Bug fixes and enhancements
|
|
@ -1,6 +1,4 @@
|
|||
Wallets enhancements
|
||||
Monero synchronization improvements
|
||||
Improve wallet backups
|
||||
Enhance error handling
|
||||
UI enhancements
|
||||
Bug fixes
|
||||
Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page)
|
||||
Improve fee calculation for Bitcoin to protect against overpaying or underpaying
|
||||
Enhance auto-address generation for Monero
|
||||
Bug fixes and enhancements
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cryptography/cryptography.dart' as cryptography;
|
||||
import 'package:cw_core/sec_random_native.dart';
|
||||
|
@ -59,11 +60,7 @@ void maskBytes(Uint8List bytes, int bits) {
|
|||
}
|
||||
}
|
||||
|
||||
String bufferToBin(Uint8List data) {
|
||||
final q1 = data.map((e) => e.toRadixString(2).padLeft(8, '0'));
|
||||
final q2 = q1.join('');
|
||||
return q2;
|
||||
}
|
||||
String bufferToBin(Uint8List data) => data.map((e) => e.toRadixString(2).padLeft(8, '0')).join('');
|
||||
|
||||
String encode(Uint8List data) {
|
||||
final dataBitLen = data.length * 8;
|
||||
|
@ -112,17 +109,18 @@ Future<bool> checkIfMnemonicIsElectrum2(String mnemonic) async {
|
|||
Future<String> getMnemonicHash(String mnemonic) async {
|
||||
final hmacSha512 = Hmac(sha512, utf8.encode('Seed version'));
|
||||
final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic)));
|
||||
final hx = digest.toString();
|
||||
return hx;
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
Future<Uint8List> mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async {
|
||||
Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
|
||||
{String prefix = segwit, String passphrase = ''}) async {
|
||||
final pbkdf2 =
|
||||
cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512);
|
||||
final text = normalizeText(mnemonic);
|
||||
// pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce)
|
||||
final passphraseBytes = utf8.encode(normalizeText(passphrase));
|
||||
final key = await pbkdf2.deriveKey(
|
||||
secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits);
|
||||
secretKey: cryptography.SecretKey(text.codeUnits),
|
||||
nonce: [...'electrum'.codeUnits, ...passphraseBytes]);
|
||||
final bytes = await key.extractBytes();
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
|
|
@ -7,5 +7,6 @@ class MnemonicBip39 {
|
|||
static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength);
|
||||
|
||||
/// Create root seed from mnemonic
|
||||
static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic);
|
||||
static Uint8List toSeed(String mnemonic, {String? passphrase}) =>
|
||||
bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
|
||||
}
|
|
@ -115,7 +115,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
}
|
||||
return BitcoinWallet(
|
||||
|
@ -195,7 +195,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
if (mnemonic != null) {
|
||||
switch (walletInfo.derivationInfo!.derivationType) {
|
||||
case DerivationType.electrum:
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
case DerivationType.bip39:
|
||||
default:
|
||||
|
|
|
@ -3,16 +3,18 @@ import 'package:cw_core/wallet_credentials.dart';
|
|||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class BitcoinNewWalletCredentials extends WalletCredentials {
|
||||
BitcoinNewWalletCredentials(
|
||||
{required String name,
|
||||
BitcoinNewWalletCredentials({
|
||||
required String name,
|
||||
WalletInfo? walletInfo,
|
||||
String? password,
|
||||
DerivationType? derivationType,
|
||||
String? derivationPath})
|
||||
: super(
|
||||
String? derivationPath,
|
||||
String? passphrase,
|
||||
}) : super(
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
|
@ -35,8 +36,21 @@ class BitcoinWalletService extends WalletService<
|
|||
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
|
||||
credentials.walletInfo?.network = network.value;
|
||||
|
||||
final String mnemonic;
|
||||
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
mnemonic = await generateElectrumMnemonic();
|
||||
break;
|
||||
}
|
||||
|
||||
final wallet = await BitcoinWalletBase.create(
|
||||
mnemonic: await generateElectrumMnemonic(),
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
passphrase: credentials.passphrase,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
|
|
|
@ -66,6 +66,7 @@ class ElectrumClient {
|
|||
|
||||
try {
|
||||
await socket?.close();
|
||||
socket = null;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
|
@ -90,7 +91,8 @@ class ElectrumClient {
|
|||
}
|
||||
_setConnectionStatus(ConnectionStatus.connected);
|
||||
|
||||
socket!.listen((Uint8List event) {
|
||||
socket!.listen(
|
||||
(Uint8List event) {
|
||||
try {
|
||||
final msg = utf8.decode(event.toList());
|
||||
final messagesList = msg.split("\n");
|
||||
|
@ -103,20 +105,21 @@ class ElectrumClient {
|
|||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
}, onError: (Object error) {
|
||||
},
|
||||
onError: (Object error) {
|
||||
final errorMsg = error.toString();
|
||||
print(errorMsg);
|
||||
unterminatedString = '';
|
||||
|
||||
final currentHost = socket?.address.host;
|
||||
final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} ");
|
||||
|
||||
if (currentHost != null && isErrorForCurrentHost)
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
}, onDone: () {
|
||||
},
|
||||
onDone: () {
|
||||
unterminatedString = '';
|
||||
if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected);
|
||||
});
|
||||
if (host == socket?.address.host) {
|
||||
socket = null;
|
||||
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
keepAlive();
|
||||
}
|
||||
|
@ -426,7 +429,6 @@ class ElectrumClient {
|
|||
{required String id, required String method, List<Object> params = const []}) {
|
||||
try {
|
||||
if (socket == null) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
return null;
|
||||
}
|
||||
final subscription = BehaviorSubject<T>();
|
||||
|
@ -443,7 +445,6 @@ class ElectrumClient {
|
|||
Future<dynamic> call(
|
||||
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
|
||||
if (socket == null) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
|
@ -457,10 +458,9 @@ class ElectrumClient {
|
|||
}
|
||||
|
||||
Future<dynamic> callWithTimeout(
|
||||
{required String method, List<Object> params = const [], int timeout = 4000}) async {
|
||||
{required String method, List<Object> params = const [], int timeout = 5000}) async {
|
||||
try {
|
||||
if (socket == null) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
|
|
|
@ -109,5 +109,4 @@ Map<DerivationType, List<DerivationInfo>> electrum_derivations = {
|
|||
],
|
||||
};
|
||||
|
||||
|
||||
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:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
|
@ -7,10 +9,12 @@ import 'package:cw_core/transaction_direction.dart';
|
|||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
|
||||
class ElectrumTransactionBundle {
|
||||
ElectrumTransactionBundle(this.originalTransaction,
|
||||
{required this.ins, required this.confirmations, this.time});
|
||||
|
||||
final BtcTransaction originalTransaction;
|
||||
final List<BtcTransaction> ins;
|
||||
final int? time;
|
||||
|
@ -125,7 +129,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
for (final out in bundle.originalTransaction.outputs) {
|
||||
totalOutAmount += out.amount.toInt();
|
||||
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
||||
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
|
||||
if (address.isNotEmpty) outputAddresses.add(address);
|
||||
|
||||
// Check if the script contains OP_RETURN
|
||||
final script = out.scriptPubKey.script;
|
||||
if (script.contains('OP_RETURN')) {
|
||||
final index = script.indexOf('OP_RETURN');
|
||||
if (index + 1 <= script.length) {
|
||||
try {
|
||||
final opReturnData = script[index + 1].toString();
|
||||
final decodedString = utf8.decode(HEX.decode(opReturnData));
|
||||
outputAddresses.add('OP_RETURN:$decodedString');
|
||||
} catch (_) {
|
||||
outputAddresses.add('OP_RETURN:');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addressExists) {
|
||||
receivedAmounts.add(out.amount.toInt());
|
||||
|
@ -235,6 +256,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
}
|
||||
|
||||
String toString() {
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'dart:isolate';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:collection/collection.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:rxdart/subjects.dart';
|
||||
import 'package:sp_scanner/sp_scanner.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
part 'electrum_wallet.g.dart';
|
||||
|
||||
|
@ -89,7 +92,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
: {}),
|
||||
this.unspentCoinsInfo = unspentCoinsInfo,
|
||||
this.isTestnet = network == BitcoinNetwork.testnet,
|
||||
this.isTestnet = !network.isMainnet,
|
||||
this._mnemonic = mnemonic,
|
||||
super(walletInfo) {
|
||||
this.electrumClient = electrumClient ?? ElectrumClient();
|
||||
|
@ -101,6 +104,8 @@ abstract class ElectrumWalletBase
|
|||
);
|
||||
|
||||
reaction((_) => syncStatus, _syncStatusReaction);
|
||||
|
||||
sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
|
||||
|
@ -144,7 +149,11 @@ abstract class ElectrumWalletBase
|
|||
|
||||
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
|
||||
|
||||
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
|
||||
|
||||
final EncryptionFileUtils encryptionFileUtils;
|
||||
|
||||
@override
|
||||
final String? passphrase;
|
||||
|
||||
@override
|
||||
|
@ -191,7 +200,7 @@ abstract class ElectrumWalletBase
|
|||
BasedUtxoNetwork network;
|
||||
|
||||
@override
|
||||
bool? isTestnet;
|
||||
bool isTestnet;
|
||||
|
||||
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
|
||||
|
||||
|
@ -202,6 +211,13 @@ abstract class ElectrumWalletBase
|
|||
|
||||
bool _isTryingToConnect = false;
|
||||
|
||||
Completer<SharedPreferences> sharedPrefs = Completer();
|
||||
|
||||
Future<bool> checkIfMempoolAPIIsEnabled() async {
|
||||
bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true;
|
||||
return isMempoolAPIEnabled;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> setSilentPaymentsScanning(bool active) async {
|
||||
silentPaymentsScanningActive = active;
|
||||
|
@ -227,10 +243,7 @@ abstract class ElectrumWalletBase
|
|||
if (electrumClient.isConnected) {
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} else {
|
||||
if (electrumClient.uri != null) {
|
||||
await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL);
|
||||
startSync();
|
||||
}
|
||||
syncStatus = NotConnectedSyncStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +288,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
void Function(FlutterErrorDetails)? _onError;
|
||||
Timer? _autoSaveTimer;
|
||||
static const int _autoSaveInterval = 30;
|
||||
Timer? _updateFeeRateTimer;
|
||||
static const int _autoSaveInterval = 1;
|
||||
|
||||
Future<void> init() async {
|
||||
await walletAddresses.init();
|
||||
|
@ -283,7 +297,7 @@ abstract class ElectrumWalletBase
|
|||
await save();
|
||||
|
||||
_autoSaveTimer =
|
||||
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
|
||||
Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save());
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -425,6 +439,10 @@ abstract class ElectrumWalletBase
|
|||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
if (syncStatus is SyncronizingSyncStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncStatus = SyncronizingSyncStatus();
|
||||
|
||||
if (hasSilentPaymentsScanning) {
|
||||
|
@ -436,6 +454,10 @@ abstract class ElectrumWalletBase
|
|||
await updateTransactions();
|
||||
await updateAllUnspents();
|
||||
await updateBalance();
|
||||
await updateFeeRates();
|
||||
|
||||
_updateFeeRateTimer ??=
|
||||
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
|
||||
|
||||
if (alwaysScan == true) {
|
||||
_setListeners(walletInfo.restoreHeight);
|
||||
|
@ -451,9 +473,25 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
Future<void> updateFeeRates() async {
|
||||
if (await checkIfMempoolAPIIsEnabled()) {
|
||||
try {
|
||||
final response =
|
||||
await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended"));
|
||||
|
||||
final result = json.decode(response.body) as Map<String, num>;
|
||||
final slowFee = result['economyFee']?.toInt() ?? 0;
|
||||
final mediumFee = result['hourFee']?.toInt() ?? 0;
|
||||
final fastFee = result['fastestFee']?.toInt() ?? 0;
|
||||
_feeRates = [slowFee, mediumFee, fastFee];
|
||||
return;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final feeRates = await electrumClient.feeRates(network: network);
|
||||
if (feeRates != [0, 0, 0]) {
|
||||
_feeRates = feeRates;
|
||||
} else if (isTestnet) {
|
||||
_feeRates = [1, 1, 1];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,7 +640,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
final derivationPath =
|
||||
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
|
||||
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}"
|
||||
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
|
||||
"/${utx.bitcoinAddressRecord.index}";
|
||||
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
|
||||
|
@ -1019,7 +1057,7 @@ abstract class ElectrumWalletBase
|
|||
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
|
||||
error += "\nNo private keys generated.";
|
||||
} else {
|
||||
error += "\nAddress: ${utxo.ownerDetails.address.toAddress()}";
|
||||
error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";
|
||||
|
||||
key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) {
|
||||
final elemPubkey = element.privkey.getPublic().toHex();
|
||||
|
@ -1235,6 +1273,7 @@ abstract class ElectrumWalletBase
|
|||
await electrumClient.close();
|
||||
} catch (_) {}
|
||||
_autoSaveTimer?.cancel();
|
||||
_updateFeeRateTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -1378,26 +1417,15 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> canReplaceByFee(String hash) async {
|
||||
final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
|
||||
|
||||
final String? transactionHex;
|
||||
int confirmations = 0;
|
||||
|
||||
if (verboseTransaction.isEmpty) {
|
||||
transactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
} else {
|
||||
confirmations = verboseTransaction['confirmations'] as int? ?? 0;
|
||||
transactionHex = verboseTransaction['hex'] as String?;
|
||||
}
|
||||
|
||||
if (confirmations > 0) return false;
|
||||
|
||||
if (transactionHex == null) {
|
||||
Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
|
||||
try {
|
||||
final bundle = await getTransactionExpanded(hash: tx.txHash);
|
||||
_updateInputsAndOutputs(tx, bundle);
|
||||
if (bundle.confirmations > 0) return false;
|
||||
return bundle.originalTransaction.canReplaceByFee;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
|
||||
}
|
||||
|
||||
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
|
||||
|
@ -1438,6 +1466,7 @@ abstract class ElectrumWalletBase
|
|||
List<ECPrivate> privateKeys = [];
|
||||
|
||||
var allInputsAmount = 0;
|
||||
String? memo;
|
||||
|
||||
// Add inputs
|
||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
|
@ -1473,53 +1502,82 @@ abstract class ElectrumWalletBase
|
|||
);
|
||||
}
|
||||
|
||||
int totalOutAmount = bundle.originalTransaction.outputs
|
||||
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
|
||||
|
||||
var currentFee = allInputsAmount - totalOutAmount;
|
||||
int remainingFee = newFee - currentFee;
|
||||
|
||||
// Create a list of available outputs
|
||||
final outputs = <BitcoinOutput>[];
|
||||
for (final out in bundle.originalTransaction.outputs) {
|
||||
// Check if the script contains OP_RETURN
|
||||
final script = out.scriptPubKey.script;
|
||||
if (script.contains('OP_RETURN') && memo == null) {
|
||||
final index = script.indexOf('OP_RETURN');
|
||||
if (index + 1 <= script.length) {
|
||||
try {
|
||||
final opReturnData = script[index + 1].toString();
|
||||
memo = utf8.decode(HEX.decode(opReturnData));
|
||||
continue;
|
||||
} catch (_) {
|
||||
throw Exception('Cannot decode OP_RETURN data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add outputs and deduct the fees from it
|
||||
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
|
||||
final out = bundle.originalTransaction.outputs[i];
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final btcAddress = addressTypeFromStr(address, network);
|
||||
|
||||
int newAmount;
|
||||
if (out.amount.toInt() >= remainingFee) {
|
||||
newAmount = out.amount.toInt() - remainingFee;
|
||||
remainingFee = 0;
|
||||
|
||||
// if new amount of output is less than dust amount, then don't add this output as well
|
||||
if (newAmount <= _dustAmount) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
remainingFee -= out.amount.toInt();
|
||||
continue;
|
||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
|
||||
}
|
||||
|
||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
|
||||
// Calculate the total amount and fees
|
||||
int totalOutAmount =
|
||||
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
|
||||
int currentFee = allInputsAmount - totalOutAmount;
|
||||
int remainingFee = newFee - currentFee;
|
||||
|
||||
if (remainingFee <= 0) {
|
||||
throw Exception("New fee must be higher than the current fee.");
|
||||
}
|
||||
|
||||
// Deduct Remaining Fee from Main Outputs
|
||||
if (remainingFee > 0) {
|
||||
for (int i = outputs.length - 1; i >= 0; i--) {
|
||||
int outputAmount = outputs[i].value.toInt();
|
||||
|
||||
if (outputAmount > _dustAmount) {
|
||||
int deduction = (outputAmount - _dustAmount >= remainingFee)
|
||||
? remainingFee
|
||||
: outputAmount - _dustAmount;
|
||||
outputs[i] = BitcoinOutput(
|
||||
address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
|
||||
remainingFee -= deduction;
|
||||
|
||||
if (remainingFee <= 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final check if the remaining fee couldn't be deducted
|
||||
if (remainingFee > 0) {
|
||||
throw Exception("Not enough funds to cover the fee.");
|
||||
}
|
||||
|
||||
// Identify all change outputs
|
||||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
||||
final List<BitcoinOutput> changeOutputs = outputs
|
||||
.where((output) => changeAddresses
|
||||
.any((element) => element.address == output.address.toAddress(network)))
|
||||
.toList();
|
||||
|
||||
// look for a change address in the outputs
|
||||
final changeOutput = outputs.firstWhereOrNull((output) =>
|
||||
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
|
||||
int totalChangeAmount =
|
||||
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
|
||||
|
||||
// deduct the change amount from the output amount
|
||||
if (changeOutput != null) {
|
||||
totalOutAmount -= changeOutput.value.toInt();
|
||||
}
|
||||
// The final amount that the receiver will receive
|
||||
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
|
||||
|
||||
final txb = BitcoinTransactionBuilder(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
fee: BigInt.from(newFee),
|
||||
network: network,
|
||||
memo: memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
enableRBF: true,
|
||||
);
|
||||
|
||||
|
@ -1542,10 +1600,10 @@ abstract class ElectrumWalletBase
|
|||
transaction,
|
||||
type,
|
||||
electrumClient: electrumClient,
|
||||
amount: totalOutAmount,
|
||||
amount: sendingAmount,
|
||||
fee: newFee,
|
||||
network: network,
|
||||
hasChange: changeOutput != null,
|
||||
hasChange: changeOutputs.isNotEmpty,
|
||||
feeRate: newFee.toString(),
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
|
@ -1891,18 +1949,78 @@ abstract class ElectrumWalletBase
|
|||
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||
: null;
|
||||
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
||||
final priv = ECPrivate.fromWif(
|
||||
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
|
||||
netVersion: network.wifNetVer,
|
||||
);
|
||||
return priv.signMessage(StringUtils.encode(message));
|
||||
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
|
||||
|
||||
String messagePrefix = '\x18Bitcoin Signed Message:\n';
|
||||
final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
|
||||
final decodedSig = hex.decode(hexEncoded);
|
||||
return base64Encode(decodedSig);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||
if (address == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<int> sigDecodedBytes = [];
|
||||
|
||||
if (signature.endsWith('=')) {
|
||||
sigDecodedBytes = base64.decode(signature);
|
||||
} else {
|
||||
sigDecodedBytes = hex.decode(signature);
|
||||
}
|
||||
|
||||
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
|
||||
throw ArgumentException(
|
||||
"signature must be 64 bytes without recover-id or 65 bytes with recover-id");
|
||||
}
|
||||
|
||||
String messagePrefix = '\x18Bitcoin Signed Message:\n';
|
||||
final messageHash = QuickCrypto.sha256Hash(
|
||||
BitcoinSignerUtils.magicMessage(utf8.encode(message), messagePrefix));
|
||||
|
||||
List<int> correctSignature =
|
||||
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
|
||||
List<int> rBytes = correctSignature.sublist(0, 32);
|
||||
List<int> sBytes = correctSignature.sublist(32);
|
||||
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
|
||||
|
||||
List<int> possibleRecoverIds = [0, 1];
|
||||
|
||||
final baseAddress = addressTypeFromStr(address, network);
|
||||
|
||||
for (int recoveryId in possibleRecoverIds) {
|
||||
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
||||
|
||||
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
|
||||
|
||||
String? recoveredAddress;
|
||||
|
||||
if (baseAddress is P2pkAddress) {
|
||||
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
|
||||
} else if (baseAddress is P2pkhAddress) {
|
||||
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
|
||||
} else if (baseAddress is P2wshAddress) {
|
||||
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
|
||||
} else if (baseAddress is P2wpkhAddress) {
|
||||
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
|
||||
}
|
||||
|
||||
if (recoveredAddress == address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _setInitialHeight() async {
|
||||
if (_chainTipUpdateSubject != null) return;
|
||||
|
||||
_currentChainTip = await getUpdatedChainTip();
|
||||
|
||||
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
|
||||
await getUpdatedChainTip();
|
||||
await walletInfo.updateRestoreHeight(_currentChainTip!);
|
||||
}
|
||||
|
||||
|
@ -1941,13 +2059,6 @@ abstract class ElectrumWalletBase
|
|||
break;
|
||||
case ConnectionStatus.failed:
|
||||
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;
|
||||
case ConnectionStatus.connecting:
|
||||
syncStatus = ConnectingSyncStatus();
|
||||
|
@ -1957,7 +2068,11 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
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
|
||||
_scripthashesUpdateSubject = {};
|
||||
|
||||
|
@ -1965,8 +2080,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
_isTryingToConnect = true;
|
||||
|
||||
Future.delayed(Duration(seconds: 10), () {
|
||||
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) {
|
||||
Timer(Duration(seconds: 10), () {
|
||||
if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) {
|
||||
this.electrumClient.connectToUri(
|
||||
node!.uri,
|
||||
useSSL: node!.useSSL ?? false,
|
||||
|
@ -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 {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/electrum_derivations.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
|
@ -21,6 +24,9 @@ import 'package:hive/hive.dart';
|
|||
import 'package:ledger_flutter/ledger_flutter.dart';
|
||||
import 'package:ledger_litecoin/ledger_litecoin.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';
|
||||
|
||||
|
@ -35,6 +41,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
Uint8List? seedBytes,
|
||||
String? mnemonic,
|
||||
String? xpub,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
|
@ -51,6 +58,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase,
|
||||
currency: CryptoCurrency.ltc) {
|
||||
walletAddresses = LitecoinWalletAddresses(
|
||||
walletInfo,
|
||||
|
@ -89,7 +97,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic);
|
||||
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
||||
break;
|
||||
}
|
||||
return LitecoinWallet(
|
||||
|
@ -100,6 +108,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase,
|
||||
seedBytes: seedBytes,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
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(
|
||||
mnemonic: keysData.mnemonic,
|
||||
xpub: keysData.xPub,
|
||||
|
@ -151,7 +185,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp?.addresses,
|
||||
initialBalance: snp?.balance,
|
||||
seedBytes: keysData.mnemonic != null ? await mnemonicToSeedBytes(keysData.mnemonic!) : null,
|
||||
seedBytes: seedBytes,
|
||||
passphrase: passphrase,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||
|
@ -175,6 +210,129 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address = null}) async {
|
||||
final index = address != null
|
||||
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||
: null;
|
||||
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
||||
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
|
||||
|
||||
final privateKey = ECDSAPrivateKey.fromBytes(
|
||||
priv.toBytes(),
|
||||
Curves.generatorSecp256k1,
|
||||
);
|
||||
|
||||
final signature =
|
||||
signLitecoinMessage(utf8.encode(message), privateKey: privateKey, bipPrive: priv.prive);
|
||||
|
||||
return base64Encode(signature);
|
||||
}
|
||||
|
||||
List<int> _magicPrefix(List<int> message, List<int> messagePrefix) {
|
||||
final encodeLength = IntUtils.encodeVarint(message.length);
|
||||
|
||||
return [...messagePrefix, ...encodeLength, ...message];
|
||||
}
|
||||
|
||||
List<int> signLitecoinMessage(List<int> message,
|
||||
{required ECDSAPrivateKey privateKey, required Bip32PrivateKey bipPrive}) {
|
||||
String messagePrefix = '\x19Litecoin Signed Message:\n';
|
||||
final messageHash = QuickCrypto.sha256Hash(magicMessage(message, messagePrefix));
|
||||
final signingKey = EcdsaSigningKey(privateKey);
|
||||
ECDSASignature ecdsaSign =
|
||||
signingKey.signDigestDeterminstic(digest: messageHash, hashFunc: () => SHA256());
|
||||
final n = Curves.generatorSecp256k1.order! >> 1;
|
||||
BigInt newS;
|
||||
if (ecdsaSign.s.compareTo(n) > 0) {
|
||||
newS = Curves.generatorSecp256k1.order! - ecdsaSign.s;
|
||||
} else {
|
||||
newS = ecdsaSign.s;
|
||||
}
|
||||
final rawSig = ECDSASignature(ecdsaSign.r, newS);
|
||||
final rawSigBytes = rawSig.toBytes(BitcoinSignerUtils.baselen);
|
||||
|
||||
final pub = bipPrive.publicKey;
|
||||
final ECDomainParameters curve = ECCurve_secp256k1();
|
||||
final point = curve.curve.decodePoint(pub.point.toBytes());
|
||||
|
||||
final rawSigEc = ECSignature(rawSig.r, rawSig.s);
|
||||
|
||||
final recId = SignUtils.findRecoveryId(
|
||||
SignUtils.getHexString(messageHash, offset: 0, length: messageHash.length),
|
||||
rawSigEc,
|
||||
Uint8List.fromList(pub.uncompressed),
|
||||
);
|
||||
|
||||
final v = recId + 27 + (point!.isCompressed ? 4 : 0);
|
||||
|
||||
final combined = Uint8List.fromList([v, ...rawSigBytes]);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
List<int> magicMessage(List<int> message, String messagePrefix) {
|
||||
final prefixBytes = StringUtils.encode(messagePrefix);
|
||||
final magic = _magicPrefix(message, prefixBytes);
|
||||
return QuickCrypto.sha256Hash(magic);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
||||
if (address == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<int> sigDecodedBytes = [];
|
||||
|
||||
if (signature.endsWith('=')) {
|
||||
sigDecodedBytes = base64.decode(signature);
|
||||
} else {
|
||||
sigDecodedBytes = hex.decode(signature);
|
||||
}
|
||||
|
||||
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
|
||||
throw ArgumentException(
|
||||
"litecoin signature must be 64 bytes without recover-id or 65 bytes with recover-id");
|
||||
}
|
||||
|
||||
String messagePrefix = '\x19Litecoin Signed Message:\n';
|
||||
final messageHash = QuickCrypto.sha256Hash(magicMessage(utf8.encode(message), messagePrefix));
|
||||
|
||||
List<int> correctSignature =
|
||||
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
|
||||
List<int> rBytes = correctSignature.sublist(0, 32);
|
||||
List<int> sBytes = correctSignature.sublist(32);
|
||||
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
|
||||
|
||||
List<int> possibleRecoverIds = [0, 1];
|
||||
|
||||
final baseAddress = addressTypeFromStr(address, network);
|
||||
|
||||
for (int recoveryId in possibleRecoverIds) {
|
||||
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
||||
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
|
||||
|
||||
String? recoveredAddress;
|
||||
|
||||
if (baseAddress is P2pkAddress) {
|
||||
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
|
||||
} else if (baseAddress is P2pkhAddress) {
|
||||
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
|
||||
} else if (baseAddress is P2wshAddress) {
|
||||
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
|
||||
} else if (baseAddress is P2wpkhAddress) {
|
||||
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
|
||||
}
|
||||
|
||||
if (recoveredAddress == address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Ledger? _ledger;
|
||||
LedgerDevice? _ledgerDevice;
|
||||
LitecoinLedgerApp? _litecoinLedgerApp;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
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/unspent_coins_info.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
@ -31,8 +32,21 @@ class LitecoinWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final String mnemonic;
|
||||
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
mnemonic = await generateElectrumMnemonic();
|
||||
break;
|
||||
}
|
||||
|
||||
final wallet = await LitecoinWalletBase.create(
|
||||
mnemonic: await generateElectrumMnemonic(),
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
passphrase: credentials.passphrase,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
|
|
|
@ -67,11 +67,11 @@ packages:
|
|||
source: git
|
||||
version: "1.0.1"
|
||||
bitcoin_base:
|
||||
dependency: "direct main"
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v4
|
||||
resolved-ref: "574486bfcdbbaf978dcd006b46fc8716f880da29"
|
||||
ref: cake-update-v5
|
||||
resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380
|
||||
url: "https://github.com/cake-tech/bitcoin_base"
|
||||
source: git
|
||||
version: "4.7.0"
|
||||
|
@ -350,6 +350,11 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -745,6 +750,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -953,4 +1014,4 @@ packages:
|
|||
version: "2.2.1"
|
||||
sdks:
|
||||
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
|
||||
flutter_mobx: ^2.0.6+1
|
||||
intl: ^0.18.0
|
||||
shared_preferences: ^2.0.15
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
bitbox:
|
||||
|
@ -25,10 +26,6 @@ dependencies:
|
|||
ref: Add-Support-For-OP-Return-data
|
||||
rxdart: ^0.27.5
|
||||
cryptography: ^2.0.5
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v4
|
||||
blockchain_utils:
|
||||
git:
|
||||
url: https://github.com/cake-tech/blockchain_utils
|
||||
|
@ -60,6 +57,10 @@ dependency_overrides:
|
|||
url: https://github.com/cake-tech/ledger-flutter.git
|
||||
ref: cake-v3
|
||||
watcher: ^1.1.0
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v5
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
|
@ -3,5 +3,4 @@ export 'bitcoin_cash_wallet_addresses.dart';
|
|||
export 'bitcoin_cash_wallet_creation_credentials.dart';
|
||||
export 'bitcoin_cash_wallet_service.dart';
|
||||
export 'exceptions/exceptions.dart';
|
||||
export 'mnemonic.dart';
|
||||
export 'bitcoin_cash_address_utils.dart';
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:bitcoin_base/bitcoin_base.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_mnemonics_bip39.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/unspent_coins_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 Uint8List seedBytes,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
BitcoinAddressType? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
|
@ -45,7 +47,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.bch,
|
||||
encryptionFileUtils: encryptionFileUtils) {
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase) {
|
||||
walletAddresses = BitcoinCashWalletAddresses(
|
||||
walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
|
@ -67,6 +70,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
|
@ -79,11 +83,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await MnemonicBip39.toSeed(mnemonic),
|
||||
seedBytes: await MnemonicBip39.toSeed(mnemonic, passphrase: passphrase),
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
addressPageType: P2pkhAddressType.p2pkh,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -150,11 +155,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
}
|
||||
}).toList(),
|
||||
initialBalance: snp?.balance,
|
||||
seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!),
|
||||
seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!, passphrase: keysData.passphrase),
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||
addressPageType: P2pkhAddressType.p2pkh,
|
||||
passphrase: keysData.passphrase,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -202,11 +208,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address = null}) async {
|
||||
final index = address != null
|
||||
? walletAddresses.allAddresses
|
||||
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))
|
||||
.index
|
||||
int? index;
|
||||
try {
|
||||
index = address != null
|
||||
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
||||
: null;
|
||||
} catch (_) {}
|
||||
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
||||
final priv = ECPrivate.fromWif(
|
||||
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
|
||||
|
|
|
@ -2,17 +2,19 @@ import 'package:cw_core/wallet_credentials.dart';
|
|||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class BitcoinCashNewWalletCredentials extends WalletCredentials {
|
||||
BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password})
|
||||
: super(name: name, walletInfo: walletInfo, password: password);
|
||||
BitcoinCashNewWalletCredentials(
|
||||
{required String name, WalletInfo? walletInfo, String? password, String? passphrase})
|
||||
: super(name: name, walletInfo: walletInfo, password: password, passphrase: passphrase);
|
||||
}
|
||||
|
||||
class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
BitcoinCashRestoreWalletFromSeedCredentials(
|
||||
{required String name,
|
||||
BitcoinCashRestoreWalletFromSeedCredentials({
|
||||
required String name,
|
||||
required String password,
|
||||
required this.mnemonic,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
WalletInfo? walletInfo,
|
||||
String? passphrase,
|
||||
}) : super(name: name, password: password, walletInfo: walletInfo, passphrase: passphrase);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
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_core/encryption_file_utils.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_service.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
class BitcoinCashWalletService extends WalletService<
|
||||
|
@ -40,6 +41,7 @@ class BitcoinCashWalletService extends WalletService<
|
|||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
passphrase: credentials.passphrase,
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
|
@ -130,7 +132,9 @@ class BitcoinCashWalletService extends WalletService<
|
|||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
passphrase: credentials.passphrase
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
|
|
|
@ -25,10 +25,6 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||
ref: Add-Support-For-OP-Return-data
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v4
|
||||
blockchain_utils:
|
||||
git:
|
||||
url: https://github.com/cake-tech/blockchain_utils
|
||||
|
@ -43,6 +39,10 @@ dev_dependencies:
|
|||
|
||||
dependency_overrides:
|
||||
watcher: ^1.1.0
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v5
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
|
@ -46,6 +46,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
|
||||
String? get hexSeed => null;
|
||||
|
||||
String? get passphrase => null;
|
||||
|
||||
Object get keys;
|
||||
|
||||
WalletAddresses get walletAddresses;
|
||||
|
@ -69,7 +71,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount);
|
||||
|
||||
|
||||
// void fetchTransactionsAsync(
|
||||
// void Function(TransactionType transaction) onTransactionLoaded,
|
||||
// {void Function() onFinished});
|
||||
|
@ -92,7 +93,9 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
|
||||
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:web3dart/crypto.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:eth_sig_util/eth_sig_util.dart';
|
||||
|
||||
import 'evm_chain_transaction_info.dart';
|
||||
import 'evm_erc20_balance.dart';
|
||||
|
@ -112,6 +113,8 @@ abstract class EVMChainWalletBase
|
|||
int? gasBaseFee = 0;
|
||||
int estimatedGasUnits = 0;
|
||||
|
||||
Timer? _updateFeesTimer;
|
||||
|
||||
bool _isTransactionUpdating;
|
||||
|
||||
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
||||
|
@ -262,6 +265,7 @@ abstract class EVMChainWalletBase
|
|||
void close() {
|
||||
_client.stop();
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
_updateFeesTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -296,7 +300,7 @@ abstract class EVMChainWalletBase
|
|||
|
||||
await _updateEstimatedGasFeeParams();
|
||||
|
||||
Timer.periodic(const Duration(seconds: 10), (timer) async {
|
||||
_updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
await _updateEstimatedGasFeeParams();
|
||||
});
|
||||
|
||||
|
@ -692,8 +696,21 @@ abstract class EVMChainWalletBase
|
|||
}
|
||||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address}) async =>
|
||||
bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
|
||||
Future<String> signMessage(String message, {String? address}) async {
|
||||
return bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
|
||||
if (address == null) {
|
||||
return false;
|
||||
}
|
||||
final recoveredAddress = EthSigUtil.recoverPersonalSignature(
|
||||
message: ascii.encode(message),
|
||||
signature: signature,
|
||||
);
|
||||
return recoveredAddress.toUpperCase() == address.toUpperCase();
|
||||
}
|
||||
|
||||
Web3Client? getWeb3Client() => _client.getWeb3Client();
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
web3dart: ^2.7.1
|
||||
eth_sig_util: ^0.0.9
|
||||
erc20: ^1.0.1
|
||||
bip39: ^1.0.6
|
||||
bip32: ^2.0.0
|
||||
hex: ^0.2.0
|
||||
|
|
|
@ -10,10 +10,8 @@ import 'package:cw_haven/haven_transaction_info.dart';
|
|||
import 'package:cw_haven/haven_wallet_addresses.dart';
|
||||
import 'package:cw_core/monero_wallet_utils.dart';
|
||||
import 'package:cw_haven/api/structs/pending_transaction.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history;
|
||||
//import 'package:cw_haven/wallet.dart';
|
||||
import 'package:cw_haven/api/wallet.dart' as haven_wallet;
|
||||
import 'package:cw_haven/api/transaction_history.dart' as transaction_history;
|
||||
import 'package:cw_haven/api/monero_output.dart';
|
||||
|
@ -123,7 +121,8 @@ abstract class HavenWalletBase
|
|||
login: node.login,
|
||||
password: node.password,
|
||||
useSSL: node.useSSL ?? false,
|
||||
isLightWallet: false, // FIXME: hardcoded value
|
||||
isLightWallet: false,
|
||||
// FIXME: hardcoded value
|
||||
socksProxyAddress: node.socksProxyAddress);
|
||||
|
||||
haven_wallet.setTrustedDaemon(node.trusted);
|
||||
|
@ -419,4 +418,12 @@ abstract class HavenWalletBase
|
|||
|
||||
@override
|
||||
String get password => _password;
|
||||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address = null}) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<bool> verifyMessage(String message, String signature, {String? address = null}) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
|
|
@ -45,12 +45,23 @@ String getSeed() {
|
|||
|
||||
String getSeedLegacy(String? language) {
|
||||
var legacy = monero.Wallet_seed(wptr!, seedOffset: '');
|
||||
switch (language) {
|
||||
case "Chinese (Traditional)": language = "Chinese (simplified)"; break;
|
||||
case "Chinese (Simplified)": language = "Chinese (simplified)"; break;
|
||||
case "Korean": language = "English"; break;
|
||||
case "Czech": language = "English"; break;
|
||||
case "Japanese": language = "English"; break;
|
||||
}
|
||||
if (monero.Wallet_status(wptr!) != 0) {
|
||||
monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
||||
legacy = monero.Wallet_seed(wptr!, seedOffset: '');
|
||||
}
|
||||
if (monero.Wallet_status(wptr!) != 0) {
|
||||
return monero.Wallet_errorString(wptr!);
|
||||
final err = monero.Wallet_errorString(wptr!);
|
||||
if (legacy.isNotEmpty) {
|
||||
return "$err\n\n$legacy";
|
||||
}
|
||||
return err;
|
||||
}
|
||||
return legacy;
|
||||
}
|
||||
|
@ -305,3 +316,7 @@ Future<bool> trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!);
|
|||
String signMessage(String message, {String address = ""}) {
|
||||
return monero.Wallet_signMessage(wptr!, message: message, address: address);
|
||||
}
|
||||
|
||||
bool verifyMessage(String message, String address, String signature) {
|
||||
return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
|
||||
}
|
|
@ -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_utils.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
@ -783,4 +781,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
final useAddress = address ?? "";
|
||||
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_type.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';
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:polyseed/polyseed.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
|
@ -119,8 +117,7 @@ class MoneroWalletService extends WalletService<
|
|||
}
|
||||
|
||||
@override
|
||||
Future<MoneroWallet> openWallet(String name, String password) async {
|
||||
MoneroWallet? wallet;
|
||||
Future<MoneroWallet> openWallet(String name, String password, {bool? retryOnFailure}) async {
|
||||
try {
|
||||
final path = await pathForWallet(name: name, type: getType());
|
||||
|
||||
|
@ -147,46 +144,15 @@ class MoneroWalletService extends WalletService<
|
|||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
} catch (e, s) {
|
||||
} catch (e) {
|
||||
// TODO: Implement Exception for wallet list service.
|
||||
|
||||
final bool isBadAlloc = e.toString().contains('bad_alloc') ||
|
||||
(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) {
|
||||
if (retryOnFailure == false) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
await restoreOrResetWalletFiles(name);
|
||||
return openWallet(name, password);
|
||||
return openWallet(name, password, retryOnFailure: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -295,10 +295,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: hashlib
|
||||
sha256: "5037d3b8c36384c03a728543ae67d962a56970c5432a50862279fe68ee4c8411"
|
||||
sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
version: "1.19.2"
|
||||
hashlib_codecs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -576,10 +576,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: polyseed
|
||||
sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217
|
||||
sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
version: "0.0.6"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -19,7 +19,7 @@ dependencies:
|
|||
flutter_mobx: ^2.0.6+1
|
||||
intl: ^0.18.0
|
||||
encrypt: ^5.0.1
|
||||
polyseed: ^0.0.5
|
||||
polyseed: ^0.0.6
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
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 '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_nano/nano_balance.dart';
|
||||
import 'package:cw_nano/nano_transaction_model.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:nanoutil/nanoutil.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({
|
||||
required String privateKey,
|
||||
required String repAddress,
|
||||
|
@ -135,8 +156,8 @@ class NanoClient {
|
|||
};
|
||||
|
||||
// sign the change block:
|
||||
final String hash = NanoBlocks.computeStateHash(
|
||||
NanoAccountType.NANO,
|
||||
final String hash = NanoSignatures.computeStateHash(
|
||||
NanoBasedCurrency.NANO,
|
||||
changeBlock["account"]!,
|
||||
changeBlock["previous"]!,
|
||||
changeBlock["representative"]!,
|
||||
|
@ -248,7 +269,7 @@ class NanoClient {
|
|||
}
|
||||
final String representative = infoResponse.representative;
|
||||
// link = destination address:
|
||||
final String link = NanoAccounts.extractPublicKey(destinationAddress);
|
||||
final String link = NanoDerivations.addressToPublicKey(destinationAddress);
|
||||
final String linkAsAccount = destinationAddress;
|
||||
|
||||
// construct the send block:
|
||||
|
@ -262,8 +283,8 @@ class NanoClient {
|
|||
};
|
||||
|
||||
// sign the send block:
|
||||
final String hash = NanoBlocks.computeStateHash(
|
||||
NanoAccountType.NANO,
|
||||
final String hash = NanoSignatures.computeStateHash(
|
||||
NanoBasedCurrency.NANO,
|
||||
sendBlock["account"]!,
|
||||
sendBlock["previous"]!,
|
||||
sendBlock["representative"]!,
|
||||
|
@ -285,7 +306,6 @@ class NanoClient {
|
|||
|
||||
Future<void> receiveBlock({
|
||||
required String blockHash,
|
||||
required String source,
|
||||
required String amountRaw,
|
||||
required String destinationAddress,
|
||||
required String privateKey,
|
||||
|
@ -310,15 +330,56 @@ class NanoClient {
|
|||
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:
|
||||
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 balanceAfterTx = currentBalance + txAmount;
|
||||
|
||||
// link = send block hash:
|
||||
final String link = blockHash;
|
||||
// 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:
|
||||
Map<String, String> receiveBlock = {
|
||||
|
@ -332,8 +393,8 @@ class NanoClient {
|
|||
};
|
||||
|
||||
// sign the receive block:
|
||||
final String hash = NanoBlocks.computeStateHash(
|
||||
NanoAccountType.NANO,
|
||||
final String hash = NanoSignatures.computeStateHash(
|
||||
NanoBasedCurrency.NANO,
|
||||
receiveBlock["account"]!,
|
||||
receiveBlock["previous"]!,
|
||||
receiveBlock["representative"]!,
|
||||
|
@ -345,7 +406,7 @@ class NanoClient {
|
|||
// get PoW for the receive block:
|
||||
String? work;
|
||||
if (openBlock) {
|
||||
work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress));
|
||||
work = await requestWork(NanoDerivations.addressToPublicKey(destinationAddress));
|
||||
} else {
|
||||
work = await requestWork(frontier);
|
||||
}
|
||||
|
@ -409,10 +470,8 @@ class NanoClient {
|
|||
for (final blockHash in blocks.keys) {
|
||||
final block = blocks[blockHash];
|
||||
final String amountRaw = block["amount"] as String;
|
||||
final String source = block["source"] as String;
|
||||
await receiveBlock(
|
||||
blockHash: blockHash,
|
||||
source: source,
|
||||
amountRaw: amountRaw,
|
||||
privateKey: privateKey,
|
||||
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/pending_nano_transaction.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
part 'nano_wallet.g.dart';
|
||||
|
@ -107,7 +106,6 @@ abstract class NanoWalletBase
|
|||
if (_derivationType == DerivationType.unknown) {
|
||||
_derivationType = DerivationType.nano;
|
||||
}
|
||||
final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
|
||||
|
||||
// our "mnemonic" is actually a hex form seed:
|
||||
if (!_mnemonic.contains(' ')) {
|
||||
|
@ -122,8 +120,10 @@ abstract class NanoWalletBase
|
|||
_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(
|
||||
_hexSeed!,
|
||||
index: 0,
|
||||
|
@ -216,8 +216,8 @@ abstract class NanoWalletBase
|
|||
balanceAfterTx: runningBalance,
|
||||
previousHash: previousHash,
|
||||
);
|
||||
previousHash = NanoBlocks.computeStateHash(
|
||||
NanoAccountType.NANO,
|
||||
previousHash = NanoSignatures.computeStateHash(
|
||||
NanoBasedCurrency.NANO,
|
||||
block["account"]!,
|
||||
block["previous"]!,
|
||||
block["representative"]!,
|
||||
|
@ -535,4 +535,17 @@ abstract class NanoWalletBase
|
|||
// Delete old name's dir and files
|
||||
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
|
||||
version: "2.3.0"
|
||||
nanodart:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nanodart
|
||||
sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282"
|
||||
|
@ -524,11 +524,11 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
||||
resolved-ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
||||
ref: c01a9c552917008d8fbc6b540db657031625b04f
|
||||
resolved-ref: c01a9c552917008d8fbc6b540db657031625b04f
|
||||
url: "https://github.com/perishllc/nanoutil.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
version: "1.0.3"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -15,7 +15,6 @@ dependencies:
|
|||
mobx: ^2.0.7+4
|
||||
bip39: ^1.0.6
|
||||
bip32: ^2.0.0
|
||||
nanodart: ^2.0.0
|
||||
decimal: ^2.3.3
|
||||
libcrypto: ^0.2.2
|
||||
ed25519_hd_key: ^2.2.0
|
||||
|
@ -25,7 +24,7 @@ dependencies:
|
|||
nanoutil:
|
||||
git:
|
||||
url: https://github.com/perishllc/nanoutil.git
|
||||
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
||||
ref: c01a9c552917008d8fbc6b540db657031625b04f
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
import 'package:solana/base58.dart';
|
||||
import 'package:solana/metaplex.dart' as metaplex;
|
||||
import 'package:solana/solana.dart';
|
||||
import 'package:solana/src/crypto/ed25519_hd_keypair.dart';
|
||||
import 'package:cryptography/cryptography.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
|
||||
final messageBytes = utf8.encode(message);
|
||||
|
||||
// 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
|
||||
final hex = HEX.encode(signature.bytes);
|
||||
return HEX.encode(utf8.encode(signature)).toUpperCase();
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -580,8 +580,18 @@ abstract class TronWalletBase
|
|||
}
|
||||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address}) async =>
|
||||
_tronPrivateKey.signPersonalMessage(ascii.encode(message));
|
||||
Future<String> signMessage(String message, {String? address}) async {
|
||||
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();
|
||||
|
||||
|
|
|
@ -47,12 +47,23 @@ String getSeed() {
|
|||
|
||||
String getSeedLegacy(String? language) {
|
||||
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) {
|
||||
wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
||||
legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -309,3 +320,7 @@ Future<bool> trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!);
|
|||
String signMessage(String message, {String 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 ?? "";
|
||||
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
|
||||
description:
|
||||
name: hashlib
|
||||
sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96"
|
||||
sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.19.2"
|
||||
hashlib_codecs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hashlib_codecs
|
||||
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
|
||||
sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.5.0"
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -568,10 +568,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: polyseed
|
||||
sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217
|
||||
sha256: "11d4dbee409db053c5e9cd77382b2f5115f43fc2529158a826a96f3ba505d770"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
version: "0.0.6"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|