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
This commit is contained in:
Konstantin Ullrich 2024-09-03 18:54:36 +02:00
commit ab165e58a8
No known key found for this signature in database
GPG key ID: E9562A013280F5DB
198 changed files with 3845 additions and 990 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/images/flags/arm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,005 B

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 B

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,005 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,023 B

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,013 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 1,005 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,009 B

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 882 B

View file

@ -1,4 +1,7 @@
-
uri: rpc.ankr.com
is_default: true
useSSL: true
-
uri: api.mainnet-beta.solana.com:443
useSSL: true

View file

@ -1,4 +1,2 @@
Monero synchronization improvements
Enhance error handling
UI enhancements
Bug fixes
Enhance auto-address generation for Monero
Bug fixes and enhancements

View file

@ -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

View file

@ -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);
}

View file

@ -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 ?? '');
}

View file

@ -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:

View file

@ -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,
WalletInfo? walletInfo,
String? password,
DerivationType? derivationType,
String? derivationPath})
: super(
BitcoinNewWalletCredentials({
required String name,
WalletInfo? walletInfo,
String? password,
DerivationType? derivationType,
String? derivationPath,
String? passphrase,
}) : super(
name: name,
walletInfo: walletInfo,
password: password,
passphrase: passphrase,
);
}

View file

@ -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!,

View file

@ -66,6 +66,7 @@ class ElectrumClient {
try {
await socket?.close();
socket = null;
} catch (_) {}
try {
@ -90,33 +91,35 @@ class ElectrumClient {
}
_setConnectionStatus(ConnectionStatus.connected);
socket!.listen((Uint8List event) {
try {
final msg = utf8.decode(event.toList());
final messagesList = msg.split("\n");
for (var message in messagesList) {
if (message.isEmpty) {
continue;
socket!.listen(
(Uint8List event) {
try {
final msg = utf8.decode(event.toList());
final messagesList = msg.split("\n");
for (var message in messagesList) {
if (message.isEmpty) {
continue;
}
_parseResponse(message);
}
_parseResponse(message);
} catch (e) {
print(e.toString());
}
} catch (e) {
print(e.toString());
}
}, 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: () {
unterminatedString = '';
if (host == socket?.address.host) _setConnectionStatus(ConnectionStatus.disconnected);
});
},
onError: (Object error) {
final errorMsg = error.toString();
print(errorMsg);
unterminatedString = '';
},
onDone: () {
unterminatedString = '';
if (host == socket?.address.host) {
socket = null;
_setConnectionStatus(ConnectionStatus.disconnected);
}
},
cancelOnError: true,
);
keepAlive();
}
@ -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>();

View file

@ -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!;

View file

@ -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)';
}
}

View file

@ -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>[];
// Add outputs and deduct the fees from it
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
final out = bundle.originalTransaction.outputs[i];
final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network);
int newAmount;
if (out.amount.toInt() >= remainingFee) {
newAmount = out.amount.toInt() - remainingFee;
remainingFee = 0;
// if new amount of output is less than dust amount, then don't add this output as well
if (newAmount <= _dustAmount) {
continue;
for (final out in bundle.originalTransaction.outputs) {
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN') && memo == null) {
final index = script.indexOf('OP_RETURN');
if (index + 1 <= script.length) {
try {
final opReturnData = script[index + 1].toString();
memo = utf8.decode(HEX.decode(opReturnData));
continue;
} catch (_) {
throw Exception('Cannot decode OP_RETURN data');
}
}
} else {
remainingFee -= out.amount.toInt();
continue;
}
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network);
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
}
// Calculate the total amount and fees
int totalOutAmount =
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
int currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
if (remainingFee <= 0) {
throw Exception("New fee must be higher than the current fee.");
}
// Deduct Remaining Fee from Main Outputs
if (remainingFee > 0) {
for (int i = outputs.length - 1; i >= 0; i--) {
int outputAmount = outputs[i].value.toInt();
if (outputAmount > _dustAmount) {
int deduction = (outputAmount - _dustAmount >= remainingFee)
? remainingFee
: outputAmount - _dustAmount;
outputs[i] = BitcoinOutput(
address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
remainingFee -= deduction;
if (remainingFee <= 0) break;
}
}
}
// Final check if the remaining fee couldn't be deducted
if (remainingFee > 0) {
throw Exception("Not enough funds to cover the fee.");
}
// Identify all change outputs
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
final List<BitcoinOutput> changeOutputs = outputs
.where((output) => changeAddresses
.any((element) => element.address == output.address.toAddress(network)))
.toList();
// look for a change address in the outputs
final changeOutput = outputs.firstWhereOrNull((output) =>
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
int totalChangeAmount =
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
// deduct the change amount from the output amount
if (changeOutput != null) {
totalOutAmount -= changeOutput.value.toInt();
}
// The final amount that the receiver will receive
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
final txb = BitcoinTransactionBuilder(
utxos: utxos,
outputs: outputs,
fee: BigInt.from(newFee),
network: network,
memo: memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
);
@ -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 {

View file

@ -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;

View file

@ -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!,

View file

@ -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"

View file

@ -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

View file

@ -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';

View file

@ -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
: null;
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),

View file

@ -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,
required String password,
required this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
BitcoinCashRestoreWalletFromSeedCredentials({
required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo,
String? passphrase,
}) : super(name: name, password: password, walletInfo: walletInfo, passphrase: passphrase);
final String mnemonic;
}

View file

@ -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<
@ -35,11 +36,12 @@ class BitcoinCashWalletService extends WalletService<
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final wallet = await BitcoinCashWalletBase.create(
mnemonic: await MnemonicBip39.generate(strength: strength),
mnemonic: await MnemonicBip39.generate(strength: strength),
password: credentials.password!,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
passphrase: credentials.passphrase,
);
await wallet.save();
await wallet.init();
@ -54,11 +56,11 @@ class BitcoinCashWalletService extends WalletService<
try {
final wallet = await BitcoinCashWalletBase.open(
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
saveBackup(name);
@ -66,11 +68,11 @@ class BitcoinCashWalletService extends WalletService<
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await BitcoinCashWalletBase.open(
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
return wallet;
@ -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;

View file

@ -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

View file

@ -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;
}

View file

@ -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();
});
@ -500,7 +504,7 @@ abstract class EVMChainWalletBase
}
final methodSignature =
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null;
return methodSignatureToType[methodSignature];
}
@ -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();

View file

@ -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

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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:

View file

@ -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:

View 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,
);
}
}

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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:

View file

@ -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

View file

@ -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;

View file

@ -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();

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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:

Some files were not shown because too many files have changed in this diff Show more