Merge branch 'main' into CW-685-passphrase-support-for-monero-wownero-wallets
22
.github/workflows/pr_test_build_android.yml
vendored
|
@ -13,6 +13,9 @@ on:
|
|||
jobs:
|
||||
PR_test_build:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [29]
|
||||
env:
|
||||
STORE_PASS: test@cake_wallet
|
||||
KEY_PASS: test@cake_wallet
|
||||
|
@ -93,6 +96,25 @@ jobs:
|
|||
cd /opt/android/cake_wallet
|
||||
flutter pub get
|
||||
|
||||
|
||||
- name: Install go and gomobile
|
||||
run: |
|
||||
# install go > 1.23:
|
||||
wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: Build mwebd
|
||||
run: |
|
||||
# paths are reset after each step, so we need to set them again:
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
export PATH=$PATH:~/go/bin
|
||||
cd /opt/android/cake_wallet/scripts/android/
|
||||
./build_mwebd.sh --dont-install
|
||||
|
||||
- name: Generate KeyStore
|
||||
run: |
|
||||
cd /opt/android/cake_wallet/android/app
|
||||
|
|
19
.github/workflows/pr_test_build_linux.yml
vendored
|
@ -89,6 +89,25 @@ jobs:
|
|||
cd /opt/android/cake_wallet
|
||||
flutter pub get
|
||||
|
||||
- name: Install go and gomobile
|
||||
run: |
|
||||
# install go > 1.23:
|
||||
wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: Build mwebd
|
||||
run: |
|
||||
# paths are reset after each step, so we need to set them again:
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
export PATH=$PATH:~/go/bin
|
||||
# build mwebd:
|
||||
cd /opt/android/cake_wallet/scripts/android/
|
||||
./build_mwebd.sh --dont-install
|
||||
|
||||
- name: Generate localization
|
||||
run: |
|
||||
cd /opt/android/cake_wallet
|
||||
|
|
3
.gitignore
vendored
|
@ -171,6 +171,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
|
|||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
|
||||
macos/Runner/Configs/AppInfo.xcconfig
|
||||
|
||||
|
||||
integration_test/playground.dart
|
||||
|
||||
# Monero.dart (Monero_C)
|
||||
scripts/monero_c
|
||||
# iOS generated framework bin
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
include: package:lints/recommended.yaml
|
||||
|
||||
|
||||
analyzer:
|
||||
exclude: [
|
||||
build/**,
|
||||
|
@ -83,4 +84,4 @@ linter:
|
|||
# - unawaited_futures
|
||||
# - unnecessary_getters_setters
|
||||
# - unrelated_type_equality_checks
|
||||
# - valid_regexps
|
||||
# - valid_regexps
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
-
|
||||
uri: bitcoincash.stackwallet.com:50002
|
||||
is_default: true
|
||||
is_default: true
|
||||
useSSL: true
|
||||
-
|
||||
uri: bch.aftrek.org:50002
|
||||
useSSL: true
|
||||
-
|
||||
uri: node.minisatoshi.cash:50002
|
||||
useSSL: true
|
||||
|
|
|
@ -6,3 +6,6 @@
|
|||
isDefault: true
|
||||
-
|
||||
uri: electrs.cakewallet.com:50001
|
||||
-
|
||||
uri: fulcrum.sethforprivacy.com:50002
|
||||
useSSL: true
|
||||
|
|
BIN
assets/images/cards.png
Normal file
After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 692 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 845 B After Width: | Height: | Size: 975 B |
Before Width: | Height: | Size: 695 B After Width: | Height: | Size: 366 B |
BIN
assets/images/mweb_logo.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/images/nanogpt.png
Normal file
After Width: | Height: | Size: 505 KiB |
BIN
assets/images/ton_icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/wallet_group_bright.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/images/wallet_group_dark.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
assets/images/wallet_group_light.png
Normal file
After Width: | Height: | Size: 7 KiB |
|
@ -4,4 +4,7 @@
|
|||
useSSL: true
|
||||
-
|
||||
uri: api.mainnet-beta.solana.com:443
|
||||
useSSL: true
|
||||
-
|
||||
uri: solana-rpc.publicnode.com:443
|
||||
useSSL: true
|
|
@ -1,2 +1,4 @@
|
|||
Enhance auto-address generation for Monero
|
||||
Bug fixes and enhancements
|
||||
Monero enhancements for sending and address generation
|
||||
StealthEx
|
||||
LetsExchange
|
||||
Visual enhancements and bug fixes
|
|
@ -1,4 +1,9 @@
|
|||
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
|
||||
Add Litecoin MWEB
|
||||
Wallet groups (same seed, multiple wallets)
|
||||
Silent Payments enhancements
|
||||
Monero enhancements for sending and address generation
|
||||
StealthEx
|
||||
LetsExchange
|
||||
Replace-By-Fee improvements
|
||||
ERC20 tokens potential scam detection
|
||||
Visual enhancements and bug fixes
|
|
@ -1,14 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
|
||||
|
||||
List<int> addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) {
|
||||
try {
|
||||
if (network == bitcoin.BitcoinCashNetwork.mainnet) {
|
||||
return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes();
|
||||
}
|
||||
return bitcoin.addressToOutputScript(address: address, network: network);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
return Uint8List(0);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart' as sh;
|
||||
|
||||
abstract class BaseBitcoinAddressRecord {
|
||||
BaseBitcoinAddressRecord(
|
||||
|
@ -65,8 +64,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
|||
required super.type,
|
||||
String? scriptHash,
|
||||
required super.network,
|
||||
}) : scriptHash =
|
||||
scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null);
|
||||
}) : scriptHash = scriptHash ??
|
||||
(network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null);
|
||||
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
@ -92,7 +91,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
|||
|
||||
String getScriptHash(BasedUtxoNetwork network) {
|
||||
if (scriptHash != null) return scriptHash!;
|
||||
scriptHash = sh.scriptHash(address, network: network);
|
||||
scriptHash = BitcoinAddressUtils.scriptHash(address, network: network);
|
||||
return scriptHash!;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:cw_core/sec_random_native.dart';
|
|||
import 'package:cw_core/utils/text_normalizer.dart';
|
||||
|
||||
const segwit = '100';
|
||||
const mweb = 'eb';
|
||||
final wordlist = englishWordlist;
|
||||
|
||||
double logBase(num x, num base) => log(x) / log(base);
|
||||
|
@ -125,7 +126,7 @@ Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
|
|||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
||||
bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el);
|
||||
bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit, mweb]).any((el) => el);
|
||||
|
||||
bool validateMnemonic(String mnemonic, {String prefix = segwit}) {
|
||||
try {
|
||||
|
|
|
@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption {
|
|||
static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)');
|
||||
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
|
||||
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
|
||||
static const mweb = BitcoinReceivePageOption._('MWEB');
|
||||
|
||||
static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
|
||||
|
||||
|
@ -27,6 +28,11 @@ class BitcoinReceivePageOption implements ReceivePageOption {
|
|||
BitcoinReceivePageOption.p2pkh
|
||||
];
|
||||
|
||||
static const allLitecoin = [
|
||||
BitcoinReceivePageOption.p2wpkh,
|
||||
BitcoinReceivePageOption.mweb,
|
||||
];
|
||||
|
||||
BitcoinAddressType toType() {
|
||||
switch (this) {
|
||||
case BitcoinReceivePageOption.p2tr:
|
||||
|
@ -39,6 +45,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
|
|||
return P2shAddressType.p2wpkhInP2sh;
|
||||
case BitcoinReceivePageOption.silent_payments:
|
||||
return SilentPaymentsAddresType.p2sp;
|
||||
case BitcoinReceivePageOption.mweb:
|
||||
return SegwitAddresType.mweb;
|
||||
case BitcoinReceivePageOption.p2wpkh:
|
||||
default:
|
||||
return SegwitAddresType.p2wpkh;
|
||||
|
@ -51,6 +59,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
|
|||
return BitcoinReceivePageOption.p2tr;
|
||||
case SegwitAddresType.p2wsh:
|
||||
return BitcoinReceivePageOption.p2wsh;
|
||||
case SegwitAddresType.mweb:
|
||||
return BitcoinReceivePageOption.mweb;
|
||||
case P2pkhAddressType.p2pkh:
|
||||
return BitcoinReceivePageOption.p2pkh;
|
||||
case P2shAddressType.p2wpkhInP2sh:
|
||||
|
|
|
@ -87,7 +87,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority {
|
|||
}
|
||||
|
||||
@override
|
||||
String get units => 'Latoshi';
|
||||
String get units => 'Litoshi';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
|
@ -10,12 +10,17 @@ class BitcoinNewWalletCredentials extends WalletCredentials {
|
|||
DerivationType? derivationType,
|
||||
String? derivationPath,
|
||||
String? passphrase,
|
||||
this.mnemonic,
|
||||
String? parentAddress,
|
||||
}) : super(
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
passphrase: passphrase,
|
||||
parentAddress: parentAddress,
|
||||
);
|
||||
|
||||
final String? mnemonic;
|
||||
}
|
||||
|
||||
class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
|
|
|
@ -41,7 +41,7 @@ class BitcoinWalletService extends WalletService<
|
|||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||
mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:io';
|
|||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
|
@ -48,6 +47,7 @@ class ElectrumClient {
|
|||
final Map<String, SocketTask> _tasks;
|
||||
Map<String, SocketTask> get tasks => _tasks;
|
||||
final Map<String, String> _errors;
|
||||
ConnectionStatus _connectionStatus = ConnectionStatus.disconnected;
|
||||
bool _isConnected;
|
||||
Timer? _aliveTimer;
|
||||
String unterminatedString;
|
||||
|
@ -57,11 +57,13 @@ class ElectrumClient {
|
|||
|
||||
Future<void> connectToUri(Uri uri, {bool? useSSL}) async {
|
||||
this.uri = uri;
|
||||
this.useSSL = useSSL;
|
||||
await connect(host: uri.host, port: uri.port, useSSL: useSSL);
|
||||
if (useSSL != null) {
|
||||
this.useSSL = useSSL;
|
||||
}
|
||||
await connect(host: uri.host, port: uri.port);
|
||||
}
|
||||
|
||||
Future<void> connect({required String host, required int port, bool? useSSL}) async {
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
_setConnectionStatus(ConnectionStatus.connecting);
|
||||
|
||||
try {
|
||||
|
@ -80,15 +82,26 @@ class ElectrumClient {
|
|||
onBadCertificate: (_) => true,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
} catch (e) {
|
||||
if (e is HandshakeException) {
|
||||
useSSL = !(useSSL ?? false);
|
||||
}
|
||||
|
||||
if (_connectionStatus != ConnectionStatus.connecting) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket == null) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
if (_connectionStatus != ConnectionStatus.connecting) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_setConnectionStatus(ConnectionStatus.connected);
|
||||
|
||||
socket!.listen(
|
||||
|
@ -103,7 +116,7 @@ class ElectrumClient {
|
|||
_parseResponse(message);
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
print("socket.listen: $e");
|
||||
}
|
||||
},
|
||||
onError: (Object error) {
|
||||
|
@ -112,14 +125,15 @@ class ElectrumClient {
|
|||
unterminatedString = '';
|
||||
},
|
||||
onDone: () {
|
||||
print("SOCKET CLOSED!!!!!");
|
||||
unterminatedString = '';
|
||||
try {
|
||||
if (host == socket?.address.host) {
|
||||
socket?.destroy();
|
||||
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||
socket?.destroy();
|
||||
}
|
||||
} catch(e) {
|
||||
print(e.toString());
|
||||
} catch (e) {
|
||||
print("onDone: $e");
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
|
@ -217,25 +231,6 @@ class ElectrumClient {
|
|||
return [];
|
||||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
|
||||
String address, BasedUtxoNetwork network) =>
|
||||
call(
|
||||
method: 'blockchain.scripthash.listunspent',
|
||||
params: [scriptHash(address, network: network)]).then((dynamic result) {
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) {
|
||||
if (val is Map<String, dynamic>) {
|
||||
val['address'] = address;
|
||||
return val;
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) =>
|
||||
call(method: 'blockchain.scripthash.listunspent', params: [scriptHash])
|
||||
.then((dynamic result) {
|
||||
|
@ -272,16 +267,12 @@ class ElectrumClient {
|
|||
try {
|
||||
final result = await callWithTimeout(
|
||||
method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000);
|
||||
if (result is Map<String, dynamic>) {
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
} on RequestFailedTimeoutException catch (_) {
|
||||
return <String, dynamic>{};
|
||||
} catch (e) {
|
||||
print("getTransaction: ${e.toString()}");
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTransactionVerbose({required String hash}) =>
|
||||
|
@ -326,9 +317,8 @@ class ElectrumClient {
|
|||
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
|
||||
|
||||
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) {
|
||||
_id += 1;
|
||||
return subscribe<Object>(
|
||||
id: 'blockchain.tweaks.subscribe:${height + count}',
|
||||
id: 'blockchain.tweaks.subscribe',
|
||||
method: 'blockchain.tweaks.subscribe',
|
||||
params: [height, count, false],
|
||||
);
|
||||
|
@ -432,7 +422,7 @@ class ElectrumClient {
|
|||
BehaviorSubject<T>? subscribe<T>(
|
||||
{required String id, required String method, List<Object> params = const []}) {
|
||||
try {
|
||||
if (socket == null) {
|
||||
if (socket == null || !isConnected) {
|
||||
return null;
|
||||
}
|
||||
final subscription = BehaviorSubject<T>();
|
||||
|
@ -448,7 +438,7 @@ class ElectrumClient {
|
|||
|
||||
Future<dynamic> call(
|
||||
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
|
||||
if (socket == null) {
|
||||
if (socket == null || !isConnected) {
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
|
@ -464,7 +454,7 @@ class ElectrumClient {
|
|||
Future<dynamic> callWithTimeout(
|
||||
{required String method, List<Object> params = const [], int timeout = 5000}) async {
|
||||
try {
|
||||
if (socket == null) {
|
||||
if (socket == null || !isConnected) {
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
|
@ -517,6 +507,12 @@ class ElectrumClient {
|
|||
|
||||
void _methodHandler({required String method, required Map<String, dynamic> request}) {
|
||||
switch (method) {
|
||||
case 'blockchain.headers.subscribe':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
final id = 'blockchain.headers.subscribe';
|
||||
|
||||
_tasks[id]?.subject?.add(params.last);
|
||||
break;
|
||||
case 'blockchain.scripthash.subscribe':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
final scripthash = params.first as String?;
|
||||
|
@ -539,6 +535,7 @@ class ElectrumClient {
|
|||
|
||||
void _setConnectionStatus(ConnectionStatus status) {
|
||||
onConnectionStatusChange?.call(status);
|
||||
_connectionStatus = status;
|
||||
_isConnected = status == ConnectionStatus.connected;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,14 @@ class ElectrumBalance extends Balance {
|
|||
required this.confirmed,
|
||||
required this.unconfirmed,
|
||||
required this.frozen,
|
||||
}) : super(confirmed, unconfirmed);
|
||||
this.secondConfirmed = 0,
|
||||
this.secondUnconfirmed = 0,
|
||||
}) : super(
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
secondAvailable: secondConfirmed,
|
||||
secondAdditional: secondUnconfirmed,
|
||||
);
|
||||
|
||||
static ElectrumBalance? fromJSON(String? jsonSource) {
|
||||
if (jsonSource == null) {
|
||||
|
@ -25,9 +32,12 @@ class ElectrumBalance extends Balance {
|
|||
int confirmed;
|
||||
int unconfirmed;
|
||||
final int frozen;
|
||||
int secondConfirmed = 0;
|
||||
int secondUnconfirmed = 0;
|
||||
|
||||
@override
|
||||
String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
|
||||
String get formattedAvailableBalance =>
|
||||
bitcoinAmountToString(amount: confirmed - frozen);
|
||||
|
||||
@override
|
||||
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
|
||||
|
@ -38,6 +48,21 @@ class ElectrumBalance extends Balance {
|
|||
return frozenFormatted == '0.0' ? '' : frozenFormatted;
|
||||
}
|
||||
|
||||
String toJSON() =>
|
||||
json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
|
||||
@override
|
||||
String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed);
|
||||
|
||||
@override
|
||||
String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed);
|
||||
|
||||
@override
|
||||
String get formattedFullAvailableBalance =>
|
||||
bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'confirmed': confirmed,
|
||||
'unconfirmed': unconfirmed,
|
||||
'frozen': frozen,
|
||||
'secondConfirmed': secondConfirmed,
|
||||
'secondUnconfirmed': secondUnconfirmed
|
||||
});
|
||||
}
|
||||
|
|
|
@ -76,8 +76,13 @@ abstract class ElectrumTransactionHistoryBase
|
|||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, dynamic>) {
|
||||
final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type);
|
||||
_update(tx);
|
||||
// removing transactions with invalid date
|
||||
if (val['date'] == 1168650000) {
|
||||
transactions.remove(entry.key);
|
||||
} else {
|
||||
final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type);
|
||||
_update(tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -23,20 +23,25 @@ class ElectrumTransactionBundle {
|
|||
|
||||
class ElectrumTransactionInfo extends TransactionInfo {
|
||||
List<BitcoinSilentPaymentsUnspent>? unspents;
|
||||
bool isReceivedSilentPayment;
|
||||
|
||||
ElectrumTransactionInfo(this.type,
|
||||
{required String id,
|
||||
int? height,
|
||||
required int amount,
|
||||
int? fee,
|
||||
List<String>? inputAddresses,
|
||||
List<String>? outputAddresses,
|
||||
required TransactionDirection direction,
|
||||
required bool isPending,
|
||||
required DateTime date,
|
||||
required int confirmations,
|
||||
String? to,
|
||||
this.unspents}) {
|
||||
ElectrumTransactionInfo(
|
||||
this.type, {
|
||||
required String id,
|
||||
int? height,
|
||||
required int amount,
|
||||
int? fee,
|
||||
List<String>? inputAddresses,
|
||||
List<String>? outputAddresses,
|
||||
required TransactionDirection direction,
|
||||
required bool isPending,
|
||||
bool isReplaced = false,
|
||||
required DateTime date,
|
||||
required int confirmations,
|
||||
String? to,
|
||||
this.unspents,
|
||||
this.isReceivedSilentPayment = false,
|
||||
}) {
|
||||
this.id = id;
|
||||
this.height = height;
|
||||
this.amount = amount;
|
||||
|
@ -46,6 +51,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
this.direction = direction;
|
||||
this.date = date;
|
||||
this.isPending = isPending;
|
||||
this.isReplaced = isReplaced;
|
||||
this.confirmations = confirmations;
|
||||
this.to = to;
|
||||
}
|
||||
|
@ -94,6 +100,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
id: id,
|
||||
height: height,
|
||||
isPending: false,
|
||||
isReplaced: false,
|
||||
fee: fee,
|
||||
direction: direction,
|
||||
amount: amount,
|
||||
|
@ -169,6 +176,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
id: bundle.originalTransaction.txId(),
|
||||
height: height,
|
||||
isPending: bundle.confirmations == 0,
|
||||
isReplaced: false,
|
||||
inputAddresses: inputAddresses,
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee,
|
||||
|
@ -192,6 +200,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
isReplaced: data['isReplaced'] as bool? ?? false,
|
||||
confirmations: data['confirmations'] as int,
|
||||
inputAddresses:
|
||||
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
|
||||
|
@ -202,6 +211,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
.map((unspent) =>
|
||||
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
|
||||
.toList(),
|
||||
isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -233,6 +243,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
direction: direction,
|
||||
date: date,
|
||||
isPending: isPending,
|
||||
isReplaced: isReplaced ?? false,
|
||||
inputAddresses: inputAddresses,
|
||||
outputAddresses: outputAddresses,
|
||||
confirmations: info.confirmations);
|
||||
|
@ -246,16 +257,18 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
m['direction'] = direction.index;
|
||||
m['date'] = date.millisecondsSinceEpoch;
|
||||
m['isPending'] = isPending;
|
||||
m['isReplaced'] = isReplaced;
|
||||
m['confirmations'] = confirmations;
|
||||
m['fee'] = fee;
|
||||
m['to'] = to;
|
||||
m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? [];
|
||||
m['inputAddresses'] = inputAddresses;
|
||||
m['outputAddresses'] = outputAddresses;
|
||||
m['isReceivedSilentPayment'] = isReceivedSilentPayment;
|
||||
return m;
|
||||
}
|
||||
|
||||
String toString() {
|
||||
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)';
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
@ -24,7 +23,6 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
|||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
|
@ -51,8 +49,6 @@ part 'electrum_wallet.g.dart';
|
|||
|
||||
class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
|
||||
|
||||
const int TWEAKS_COUNT = 25;
|
||||
|
||||
abstract class ElectrumWalletBase
|
||||
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
|
||||
with Store, WalletKeysFile {
|
||||
|
@ -115,11 +111,18 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
if (seedBytes != null) {
|
||||
return currency == CryptoCurrency.bch
|
||||
? bitcoinCashHDWallet(seedBytes)
|
||||
: Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
|
||||
switch (currency) {
|
||||
case CryptoCurrency.btc:
|
||||
case CryptoCurrency.ltc:
|
||||
case CryptoCurrency.tbtc:
|
||||
return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
|
||||
_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
|
||||
as Bip32Slip10Secp256k1;
|
||||
case CryptoCurrency.bch:
|
||||
return bitcoinCashHDWallet(seedBytes);
|
||||
default:
|
||||
throw Exception("Unsupported currency");
|
||||
}
|
||||
}
|
||||
|
||||
return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
|
||||
|
@ -163,15 +166,20 @@ abstract class ElectrumWalletBase
|
|||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
Set<String> get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
||||
Set<String> get addressesSet => walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.map((addr) => addr.address)
|
||||
.toSet();
|
||||
|
||||
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
|
||||
.map((addr) => scriptHash(addr.address, network: network))
|
||||
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
|
||||
.map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network))
|
||||
.toList();
|
||||
|
||||
List<String> get publicScriptHashes => walletAddresses.allAddresses
|
||||
.where((addr) => !addr.isHidden)
|
||||
.map((addr) => scriptHash(addr.address, network: network))
|
||||
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
|
||||
.map((addr) => addr.getScriptHash(network))
|
||||
.toList();
|
||||
|
||||
String get xpub => accountHD.publicKey.toExtended;
|
||||
|
@ -212,7 +220,7 @@ abstract class ElectrumWalletBase
|
|||
silentPaymentsScanningActive = active;
|
||||
|
||||
if (active) {
|
||||
syncStatus = StartingScanSyncStatus();
|
||||
syncStatus = AttemptingScanSyncStatus();
|
||||
|
||||
final tip = await getUpdatedChainTip();
|
||||
|
||||
|
@ -277,6 +285,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
void Function(FlutterErrorDetails)? _onError;
|
||||
Timer? _autoSaveTimer;
|
||||
StreamSubscription<dynamic>? _receiveStream;
|
||||
Timer? _updateFeeRateTimer;
|
||||
static const int _autoSaveInterval = 1;
|
||||
|
||||
|
@ -290,12 +299,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
@action
|
||||
Future<void> _setListeners(
|
||||
int height, {
|
||||
int? chainTipParam,
|
||||
bool? doSingleScan,
|
||||
bool? usingSupportedNode,
|
||||
}) async {
|
||||
Future<void> _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async {
|
||||
final chainTip = chainTipParam ?? await getUpdatedChainTip();
|
||||
|
||||
if (chainTip == height) {
|
||||
|
@ -303,7 +307,7 @@ abstract class ElectrumWalletBase
|
|||
return;
|
||||
}
|
||||
|
||||
syncStatus = StartingScanSyncStatus();
|
||||
syncStatus = AttemptingScanSyncStatus();
|
||||
|
||||
if (_isolate != null) {
|
||||
final runningIsolate = await _isolate!;
|
||||
|
@ -332,7 +336,8 @@ abstract class ElectrumWalletBase
|
|||
isSingleScan: doSingleScan ?? false,
|
||||
));
|
||||
|
||||
await for (var message in receivePort) {
|
||||
_receiveStream?.cancel();
|
||||
_receiveStream = receivePort.listen((var message) async {
|
||||
if (message is Map<String, ElectrumTransactionInfo>) {
|
||||
for (final map in message.entries) {
|
||||
final txid = map.key;
|
||||
|
@ -395,10 +400,16 @@ abstract class ElectrumWalletBase
|
|||
nodeSupportsSilentPayments = false;
|
||||
}
|
||||
|
||||
syncStatus = message.syncStatus;
|
||||
if (message.syncStatus is SyncingSyncStatus) {
|
||||
var status = message.syncStatus as SyncingSyncStatus;
|
||||
syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
|
||||
} else {
|
||||
syncStatus = message.syncStatus;
|
||||
}
|
||||
|
||||
await walletInfo.updateRestoreHeight(message.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) {
|
||||
|
@ -438,9 +449,9 @@ abstract class ElectrumWalletBase
|
|||
await _setInitialHeight();
|
||||
}
|
||||
|
||||
await _subscribeForUpdates();
|
||||
|
||||
await subscribeForUpdates();
|
||||
await updateTransactions();
|
||||
|
||||
await updateAllUnspents();
|
||||
await updateBalance();
|
||||
await updateFeeRates();
|
||||
|
@ -469,8 +480,14 @@ abstract class ElectrumWalletBase
|
|||
|
||||
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;
|
||||
int mediumFee = result['hourFee']?.toInt() ?? 0;
|
||||
int fastFee = result['fastestFee']?.toInt() ?? 0;
|
||||
if (slowFee == mediumFee) {
|
||||
mediumFee++;
|
||||
}
|
||||
while (fastFee <= mediumFee) {
|
||||
fastFee++;
|
||||
}
|
||||
_feeRates = [slowFee, mediumFee, fastFee];
|
||||
return;
|
||||
} catch (_) {}
|
||||
|
@ -545,12 +562,14 @@ abstract class ElectrumWalletBase
|
|||
try {
|
||||
syncStatus = ConnectingSyncStatus();
|
||||
|
||||
await _receiveStream?.cancel();
|
||||
await electrumClient.close();
|
||||
|
||||
electrumClient.onConnectionStatusChange = _onConnectionStatusChange;
|
||||
|
||||
await electrumClient.connectToUri(node.uri, useSSL: node.useSSL);
|
||||
} catch (e) {
|
||||
} catch (e, stacktrace) {
|
||||
print(stacktrace);
|
||||
print(e.toString());
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
|
@ -592,7 +611,7 @@ abstract class ElectrumWalletBase
|
|||
allInputsAmount += utx.value;
|
||||
leftAmount = leftAmount - utx.value;
|
||||
|
||||
final address = addressTypeFromStr(utx.address, network);
|
||||
final address = RegexUtils.addressTypeFromStr(utx.address, network);
|
||||
ECPrivate? privkey;
|
||||
bool? isSilentPayment = false;
|
||||
|
||||
|
@ -689,26 +708,15 @@ abstract class ElectrumWalletBase
|
|||
paysToSilentPayment: hasSilentPayment,
|
||||
);
|
||||
|
||||
int estimatedSize;
|
||||
if (network is BitcoinCashNetwork) {
|
||||
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
network: network as BitcoinCashNetwork,
|
||||
memo: memo,
|
||||
);
|
||||
} else {
|
||||
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
vinOutpoints: utxoDetails.vinOutpoints,
|
||||
);
|
||||
}
|
||||
|
||||
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
|
||||
int fee = await calcFee(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
feeRate: feeRate,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
vinOutpoints: utxoDetails.vinOutpoints,
|
||||
);
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionNoFeeException();
|
||||
|
@ -795,33 +803,26 @@ abstract class ElectrumWalletBase
|
|||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
final address = addressTypeFromStr(changeAddress, network);
|
||||
final changeAddress = await walletAddresses.getChangeAddress(
|
||||
outputs: outputs,
|
||||
utxoDetails: utxoDetails,
|
||||
);
|
||||
final address = RegexUtils.addressTypeFromStr(changeAddress, network);
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(amountLeftForChangeAndFee),
|
||||
isChange: true,
|
||||
));
|
||||
|
||||
int estimatedSize;
|
||||
if (network is BitcoinCashNetwork) {
|
||||
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
network: network as BitcoinCashNetwork,
|
||||
memo: memo,
|
||||
);
|
||||
} else {
|
||||
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
vinOutpoints: utxoDetails.vinOutpoints,
|
||||
);
|
||||
}
|
||||
|
||||
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
|
||||
int fee = await calcFee(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
feeRate: feeRate,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
vinOutpoints: utxoDetails.vinOutpoints,
|
||||
);
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionNoFeeException();
|
||||
|
@ -831,10 +832,16 @@ abstract class ElectrumWalletBase
|
|||
final lastOutput = outputs.last;
|
||||
final amountLeftForChange = amountLeftForChangeAndFee - fee;
|
||||
|
||||
print(amountLeftForChangeAndFee);
|
||||
|
||||
if (!_isBelowDust(amountLeftForChange)) {
|
||||
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
|
||||
outputs[outputs.length - 1] =
|
||||
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange));
|
||||
outputs[outputs.length - 1] = BitcoinOutput(
|
||||
address: lastOutput.address,
|
||||
value: BigInt.from(amountLeftForChange),
|
||||
isSilentPayment: lastOutput.isSilentPayment,
|
||||
isChange: true,
|
||||
);
|
||||
} else {
|
||||
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
|
||||
outputs.removeLast();
|
||||
|
@ -876,7 +883,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
final totalAmount = amount + fee;
|
||||
|
||||
if (totalAmount > balance[currency]!.confirmed) {
|
||||
if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
|
@ -911,6 +918,37 @@ abstract class ElectrumWalletBase
|
|||
);
|
||||
}
|
||||
|
||||
Future<int> calcFee({
|
||||
required List<UtxoWithAddress> utxos,
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required BasedUtxoNetwork network,
|
||||
String? memo,
|
||||
required int feeRate,
|
||||
List<ECPrivateInfo>? inputPrivKeyInfos,
|
||||
List<Outpoint>? vinOutpoints,
|
||||
}) async {
|
||||
int estimatedSize;
|
||||
if (network is BitcoinCashNetwork) {
|
||||
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
);
|
||||
} else {
|
||||
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
inputPrivKeyInfos: inputPrivKeyInfos,
|
||||
vinOutpoints: vinOutpoints,
|
||||
);
|
||||
}
|
||||
|
||||
return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
try {
|
||||
|
@ -938,18 +976,27 @@ abstract class ElectrumWalletBase
|
|||
|
||||
credentialsAmount += outputAmount;
|
||||
|
||||
final address =
|
||||
addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
|
||||
final address = RegexUtils.addressTypeFromStr(
|
||||
out.isParsedAddress ? out.extractedAddress! : out.address, network);
|
||||
final isSilentPayment = address is SilentPaymentAddress;
|
||||
|
||||
if (address is SilentPaymentAddress) {
|
||||
if (isSilentPayment) {
|
||||
hasSilentPayment = true;
|
||||
}
|
||||
|
||||
if (sendAll) {
|
||||
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(0),
|
||||
isSilentPayment: isSilentPayment,
|
||||
));
|
||||
} else {
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(outputAmount),
|
||||
isSilentPayment: isSilentPayment,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1089,7 +1136,8 @@ abstract class ElectrumWalletBase
|
|||
});
|
||||
}
|
||||
|
||||
unspentCoins.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
|
||||
unspentCoins
|
||||
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
|
||||
|
||||
await updateBalance();
|
||||
});
|
||||
|
@ -1126,6 +1174,8 @@ abstract class ElectrumWalletBase
|
|||
'derivationPath': walletInfo.derivationInfo?.derivationPath,
|
||||
'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(),
|
||||
'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(),
|
||||
'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(),
|
||||
'alwaysScan': alwaysScan,
|
||||
});
|
||||
|
||||
int feeRate(TransactionPriority priority) {
|
||||
|
@ -1237,19 +1287,15 @@ abstract class ElectrumWalletBase
|
|||
|
||||
@action
|
||||
@override
|
||||
Future<void> rescan({
|
||||
required int height,
|
||||
int? chainTip,
|
||||
ScanData? scanData,
|
||||
bool? doSingleScan,
|
||||
}) async {
|
||||
Future<void> rescan({required int height, bool? doSingleScan}) async {
|
||||
silentPaymentsScanningActive = true;
|
||||
_setListeners(height, doSingleScan: doSingleScan);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
Future<void> close({required bool shouldCleanup}) async {
|
||||
try {
|
||||
await _receiveStream?.cancel();
|
||||
await electrumClient.close();
|
||||
} catch (_) {}
|
||||
_autoSaveTimer?.cancel();
|
||||
|
@ -1269,77 +1315,70 @@ abstract class ElectrumWalletBase
|
|||
});
|
||||
}
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses.map((address) async {
|
||||
// Set the balance of all non-silent payment addresses to 0 before updating
|
||||
walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.forEach((addr) {
|
||||
if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
|
||||
});
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses
|
||||
.where((element) => element.type != SegwitAddresType.mweb)
|
||||
.map((address) async {
|
||||
updatedUnspentCoins.addAll(await fetchUnspent(address));
|
||||
}));
|
||||
|
||||
unspentCoins = updatedUnspentCoins;
|
||||
|
||||
if (unspentCoinsInfo.isEmpty) {
|
||||
unspentCoins.forEach((coin) => _addCoinInfo(coin));
|
||||
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
|
||||
unspentCoins.forEach((coin) => addCoinInfo(coin));
|
||||
return;
|
||||
}
|
||||
|
||||
if (unspentCoins.isNotEmpty) {
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where((element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.vout == coin.vout);
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
|
||||
coin.isFrozen = coinInfo.isFrozen;
|
||||
coin.isSending = coinInfo.isSending;
|
||||
coin.note = coinInfo.note;
|
||||
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
coin.bitcoinAddressRecord.balance += coinInfo.value;
|
||||
} else {
|
||||
_addCoinInfo(coin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await updateCoins(unspentCoins);
|
||||
await _refreshUnspentCoinsInfo();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateUnspents(BitcoinAddressRecord address) async {
|
||||
final newUnspentCoins = await fetchUnspent(address);
|
||||
|
||||
if (newUnspentCoins.isNotEmpty) {
|
||||
unspentCoins.addAll(newUnspentCoins);
|
||||
|
||||
newUnspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where(
|
||||
(element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.vout == coin.vout,
|
||||
);
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
|
||||
coin.isFrozen = coinInfo.isFrozen;
|
||||
coin.isSending = coinInfo.isSending;
|
||||
coin.note = coinInfo.note;
|
||||
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
coin.bitcoinAddressRecord.balance += coinInfo.value;
|
||||
} else {
|
||||
_addCoinInfo(coin);
|
||||
}
|
||||
});
|
||||
Future<void> updateCoins(List<BitcoinUnspent> newUnspentCoins) async {
|
||||
if (newUnspentCoins.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
newUnspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where(
|
||||
(element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.vout == coin.vout,
|
||||
);
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
|
||||
coin.isFrozen = coinInfo.isFrozen;
|
||||
coin.isSending = coinInfo.isSending;
|
||||
coin.note = coinInfo.note;
|
||||
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
coin.bitcoinAddressRecord.balance += coinInfo.value;
|
||||
} else {
|
||||
addCoinInfo(coin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateUnspentsForAddress(BitcoinAddressRecord address) async {
|
||||
final newUnspentCoins = await fetchUnspent(address);
|
||||
await updateCoins(newUnspentCoins);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
|
||||
final unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
|
||||
|
||||
List<Map<String, dynamic>> unspents = [];
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
|
||||
|
||||
await Future.wait(unspents.map((unspent) async {
|
||||
try {
|
||||
final coin = BitcoinUnspent.fromJSON(address, unspent);
|
||||
|
@ -1355,7 +1394,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
@action
|
||||
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
|
||||
Future<void> addCoinInfo(BitcoinUnspent coin) async {
|
||||
final newInfo = UnspentCoinsInfo(
|
||||
walletId: id,
|
||||
hash: coin.hash,
|
||||
|
@ -1397,14 +1436,16 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
|
||||
int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();
|
||||
|
||||
Future<String?> 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;
|
||||
if (bundle.confirmations > 0) return null;
|
||||
return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1460,7 +1501,7 @@ abstract class ElectrumWalletBase
|
|||
final addressRecord =
|
||||
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
|
||||
|
||||
final btcAddress = addressTypeFromStr(addressRecord.address, network);
|
||||
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: addressRecord.index,
|
||||
|
@ -1501,7 +1542,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
final address = addressFromOutputScript(out.scriptPubKey, network);
|
||||
final btcAddress = addressTypeFromStr(address, network);
|
||||
final btcAddress = RegexUtils.addressTypeFromStr(address, network);
|
||||
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
|
||||
}
|
||||
|
||||
|
@ -1586,6 +1627,13 @@ abstract class ElectrumWalletBase
|
|||
hasChange: changeOutputs.isNotEmpty,
|
||||
feeRate: newFee.toString(),
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.id == hash) {
|
||||
tx.isReplaced = true;
|
||||
tx.isPending = false;
|
||||
transactionHistory.addOne(tx);
|
||||
}
|
||||
});
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
});
|
||||
|
@ -1597,8 +1645,6 @@ abstract class ElectrumWalletBase
|
|||
Future<ElectrumTransactionBundle> getTransactionExpanded(
|
||||
{required String hash, int? height}) async {
|
||||
String transactionHex;
|
||||
// TODO: time is not always available, and calculating it from height is not always accurate.
|
||||
// Add settings to choose API provider and use and http server instead of electrum for this.
|
||||
int? time;
|
||||
int? confirmations;
|
||||
|
||||
|
@ -1606,6 +1652,31 @@ abstract class ElectrumWalletBase
|
|||
|
||||
if (verboseTransaction.isEmpty) {
|
||||
transactionHex = await electrumClient.getTransactionHex(hash: hash);
|
||||
|
||||
if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) {
|
||||
try {
|
||||
final blockHash = await http.get(
|
||||
Uri.parse(
|
||||
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
|
||||
),
|
||||
);
|
||||
|
||||
if (blockHash.statusCode == 200 &&
|
||||
blockHash.body.isNotEmpty &&
|
||||
jsonDecode(blockHash.body) != null) {
|
||||
final blockResponse = await http.get(
|
||||
Uri.parse(
|
||||
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
|
||||
),
|
||||
);
|
||||
if (blockResponse.statusCode == 200 &&
|
||||
blockResponse.body.isNotEmpty &&
|
||||
jsonDecode(blockResponse.body)['timestamp'] != null) {
|
||||
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} else {
|
||||
transactionHex = verboseTransaction['hex'] as String;
|
||||
time = verboseTransaction['time'] as int?;
|
||||
|
@ -1613,7 +1684,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
if (height != null) {
|
||||
if (time == null) {
|
||||
if (time == null && height > 0) {
|
||||
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
|
||||
}
|
||||
|
||||
|
@ -1676,12 +1747,14 @@ abstract class ElectrumWalletBase
|
|||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
|
||||
if (type == WalletType.bitcoin) {
|
||||
await Future.wait(ADDRESS_TYPES
|
||||
await Future.wait(BITCOIN_ADDRESS_TYPES
|
||||
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||
} else if (type == WalletType.bitcoinCash) {
|
||||
await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh);
|
||||
await Future.wait(BITCOIN_CASH_ADDRESS_TYPES
|
||||
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||
} else if (type == WalletType.litecoin) {
|
||||
await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh);
|
||||
await Future.wait(LITECOIN_ADDRESS_TYPES
|
||||
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||
}
|
||||
|
||||
transactionHistory.transactions.values.forEach((tx) async {
|
||||
|
@ -1715,7 +1788,8 @@ abstract class ElectrumWalletBase
|
|||
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
|
||||
final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true);
|
||||
final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false);
|
||||
|
||||
walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
|
||||
await walletAddresses.saveAddressesInBox();
|
||||
await Future.wait(addressesByType.map((addressRecord) async {
|
||||
final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
|
||||
|
||||
|
@ -1738,7 +1812,7 @@ abstract class ElectrumWalletBase
|
|||
matchedAddresses.toList(),
|
||||
addressRecord.isHidden,
|
||||
(address) async {
|
||||
await _subscribeForUpdates();
|
||||
await subscribeForUpdates();
|
||||
return _fetchAddressHistory(address, await getCurrentChainTip())
|
||||
.then((history) => history.isNotEmpty ? address.address : null);
|
||||
},
|
||||
|
@ -1757,6 +1831,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
|
||||
BitcoinAddressRecord addressRecord, int? currentHeight) async {
|
||||
String txid = "";
|
||||
|
||||
try {
|
||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||
|
||||
|
@ -1766,7 +1842,7 @@ abstract class ElectrumWalletBase
|
|||
addressRecord.setAsUsed();
|
||||
|
||||
await Future.wait(history.map((transaction) async {
|
||||
final txid = transaction['tx_hash'] as String;
|
||||
txid = transaction['tx_hash'] as String;
|
||||
final height = transaction['height'] as int;
|
||||
final storedTx = transactionHistory.transactions[txid];
|
||||
|
||||
|
@ -1797,13 +1873,18 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
return historiesWithDetails;
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
} catch (e, stacktrace) {
|
||||
_onError?.call(FlutterErrorDetails(
|
||||
exception: "$txid - $e",
|
||||
stack: stacktrace,
|
||||
library: this.runtimeType.toString(),
|
||||
));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTransactions() async {
|
||||
print("updateTransactions() called!");
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
return;
|
||||
|
@ -1827,24 +1908,28 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _subscribeForUpdates() async {
|
||||
Future<void> subscribeForUpdates() async {
|
||||
final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
|
||||
(address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)),
|
||||
(address) =>
|
||||
!_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) &&
|
||||
address.type != SegwitAddresType.mweb,
|
||||
);
|
||||
|
||||
await Future.wait(unsubscribedScriptHashes.map((address) async {
|
||||
final sh = address.getScriptHash(network);
|
||||
await _scripthashesUpdateSubject[sh]?.close();
|
||||
if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) {
|
||||
await _scripthashesUpdateSubject[sh]?.close();
|
||||
}
|
||||
_scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh);
|
||||
_scripthashesUpdateSubject[sh]?.listen((event) async {
|
||||
try {
|
||||
await updateUnspents(address);
|
||||
await updateUnspentsForAddress(address);
|
||||
|
||||
await updateBalance();
|
||||
|
||||
await _fetchAddressHistory(address, await getCurrentChainTip());
|
||||
} catch (e, s) {
|
||||
print(e.toString());
|
||||
print("sub error: $e");
|
||||
_onError?.call(FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: s,
|
||||
|
@ -1855,12 +1940,14 @@ abstract class ElectrumWalletBase
|
|||
}));
|
||||
}
|
||||
|
||||
Future<ElectrumBalance> _fetchBalances() async {
|
||||
final addresses = walletAddresses.allAddresses.toList();
|
||||
Future<ElectrumBalance> fetchBalances() async {
|
||||
final addresses = walletAddresses.allAddresses
|
||||
.where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
|
||||
.toList();
|
||||
final balanceFutures = <Future<Map<String, dynamic>>>[];
|
||||
for (var i = 0; i < addresses.length; i++) {
|
||||
final addressRecord = addresses[i];
|
||||
final sh = scriptHash(addressRecord.address, network: network);
|
||||
final sh = addressRecord.getScriptHash(network);
|
||||
final balanceFuture = electrumClient.getBalance(sh);
|
||||
balanceFutures.add(balanceFuture);
|
||||
}
|
||||
|
@ -1869,6 +1956,18 @@ abstract class ElectrumWalletBase
|
|||
var totalConfirmed = 0;
|
||||
var totalUnconfirmed = 0;
|
||||
|
||||
unspentCoinsInfo.values.forEach((info) {
|
||||
unspentCoins.forEach((element) {
|
||||
if (element.hash == info.hash &&
|
||||
element.vout == info.vout &&
|
||||
info.isFrozen &&
|
||||
element.bitcoinAddressRecord.address == info.address &&
|
||||
element.value == info.value) {
|
||||
totalFrozen += element.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (hasSilentPaymentsScanning) {
|
||||
// Add values from unspent coins that are not fetched by the address list
|
||||
// i.e. scanned silent payments
|
||||
|
@ -1894,32 +1993,25 @@ abstract class ElectrumWalletBase
|
|||
totalConfirmed += confirmed;
|
||||
totalUnconfirmed += unconfirmed;
|
||||
|
||||
addressRecord.balance = confirmed + unconfirmed;
|
||||
if (confirmed > 0 || unconfirmed > 0) {
|
||||
addressRecord.setAsUsed();
|
||||
}
|
||||
}
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen);
|
||||
confirmed: totalConfirmed,
|
||||
unconfirmed: totalUnconfirmed,
|
||||
frozen: totalFrozen,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateBalance() async {
|
||||
balance[currency] = await _fetchBalances();
|
||||
print("updateBalance() called!");
|
||||
balance[currency] = await fetchBalances();
|
||||
await save();
|
||||
}
|
||||
|
||||
String getChangeAddress() {
|
||||
const minCountOfHiddenAddresses = 5;
|
||||
final random = Random();
|
||||
var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList();
|
||||
|
||||
if (addresses.length < minCountOfHiddenAddresses) {
|
||||
addresses = walletAddresses.allAddresses.toList();
|
||||
}
|
||||
|
||||
return addresses[random.nextInt(addresses.length)].address;
|
||||
}
|
||||
|
||||
@override
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
|
||||
|
||||
|
@ -1968,7 +2060,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
List<int> possibleRecoverIds = [0, 1];
|
||||
|
||||
final baseAddress = addressTypeFromStr(address, network);
|
||||
final baseAddress = RegexUtils.addressTypeFromStr(address, network);
|
||||
|
||||
for (int recoveryId in possibleRecoverIds) {
|
||||
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
||||
|
@ -2061,7 +2153,8 @@ abstract class ElectrumWalletBase
|
|||
_isTryingToConnect = true;
|
||||
|
||||
Timer(Duration(seconds: 5), () {
|
||||
if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) {
|
||||
if (this.syncStatus is NotConnectedSyncStatus ||
|
||||
this.syncStatus is LostConnectionSyncStatus) {
|
||||
this.electrumClient.connectToUri(
|
||||
node!.uri,
|
||||
useSSL: node!.useSSL ?? false,
|
||||
|
@ -2192,21 +2285,22 @@ Future<void> startRefresh(ScanData scanData) async {
|
|||
|
||||
BehaviorSubject<Object>? tweaksSubscription = null;
|
||||
|
||||
final syncingStatus = scanData.isSingleScan
|
||||
? SyncingSyncStatus(1, 0)
|
||||
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
|
||||
|
||||
// Initial status UI update, send how many blocks left to scan
|
||||
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
|
||||
|
||||
final electrumClient = scanData.electrumClient;
|
||||
await electrumClient.connectToUri(
|
||||
scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
|
||||
useSSL: scanData.node?.useSSL ?? false,
|
||||
);
|
||||
|
||||
int getCountPerRequest(int syncHeight) {
|
||||
if (scanData.isSingleScan) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final amountLeft = scanData.chainTip - syncHeight + 1;
|
||||
return amountLeft;
|
||||
}
|
||||
|
||||
if (tweaksSubscription == null) {
|
||||
final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT;
|
||||
final receiver = Receiver(
|
||||
scanData.silentAddress.b_scan.toHex(),
|
||||
scanData.silentAddress.B_spend.toHex(),
|
||||
|
@ -2215,16 +2309,45 @@ Future<void> startRefresh(ScanData scanData) async {
|
|||
scanData.labelIndexes.length,
|
||||
);
|
||||
|
||||
tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count);
|
||||
tweaksSubscription?.listen((t) async {
|
||||
final tweaks = t as Map<String, dynamic>;
|
||||
// Initial status UI update, send how many blocks in total to scan
|
||||
final initialCount = getCountPerRequest(syncHeight);
|
||||
scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
|
||||
|
||||
if (tweaks["message"] != null) {
|
||||
tweaksSubscription = await electrumClient.tweaksSubscribe(
|
||||
height: syncHeight,
|
||||
count: initialCount,
|
||||
);
|
||||
|
||||
Future<void> listenFn(t) async {
|
||||
final tweaks = t as Map<String, dynamic>;
|
||||
final msg = tweaks["message"];
|
||||
// success or error msg
|
||||
final noData = msg != null;
|
||||
|
||||
if (noData) {
|
||||
// re-subscribe to continue receiving messages, starting from the next unscanned height
|
||||
electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count);
|
||||
final nextHeight = syncHeight + 1;
|
||||
final nextCount = getCountPerRequest(nextHeight);
|
||||
|
||||
if (nextCount > 0) {
|
||||
tweaksSubscription?.close();
|
||||
|
||||
final nextTweaksSubscription = electrumClient.tweaksSubscribe(
|
||||
height: nextHeight,
|
||||
count: nextCount,
|
||||
);
|
||||
nextTweaksSubscription?.listen(listenFn);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Continuous status UI update, send how many blocks left to scan
|
||||
final syncingStatus = scanData.isSingleScan
|
||||
? SyncingSyncStatus(1, 0)
|
||||
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
|
||||
scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
|
||||
|
||||
final blockHeight = tweaks.keys.first;
|
||||
final tweakHeight = int.parse(blockHeight);
|
||||
|
||||
|
@ -2259,11 +2382,13 @@ Future<void> startRefresh(ScanData scanData) async {
|
|||
fee: 0,
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: false,
|
||||
isReplaced: false,
|
||||
date: scanData.network == BitcoinNetwork.mainnet
|
||||
? getDateByBitcoinHeight(tweakHeight)
|
||||
: DateTime.now(),
|
||||
confirmations: scanData.chainTip - tweakHeight + 1,
|
||||
unspents: [],
|
||||
isReceivedSilentPayment: true,
|
||||
);
|
||||
|
||||
addToWallet.forEach((label, value) {
|
||||
|
@ -2318,16 +2443,6 @@ Future<void> startRefresh(ScanData scanData) async {
|
|||
} catch (_) {}
|
||||
|
||||
syncHeight = tweakHeight;
|
||||
scanData.sendPort.send(
|
||||
SyncResponse(
|
||||
syncHeight,
|
||||
SyncingSyncStatus.fromHeightValues(
|
||||
scanData.chainTip,
|
||||
initialSyncHeight,
|
||||
syncHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
|
||||
if (tweakHeight >= scanData.chainTip)
|
||||
|
@ -2343,7 +2458,9 @@ Future<void> startRefresh(ScanData scanData) async {
|
|||
await tweaksSubscription!.close();
|
||||
await electrumClient.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tweaksSubscription?.listen(listenFn);
|
||||
}
|
||||
|
||||
if (tweaksSubscription == null) {
|
||||
|
@ -2373,6 +2490,8 @@ class EstimatedTxResult {
|
|||
final int fee;
|
||||
final int amount;
|
||||
final bool spendsSilentPayment;
|
||||
|
||||
// final bool sendsToSilentPayment;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
final String? memo;
|
||||
|
@ -2386,31 +2505,6 @@ class PublicKeyWithDerivationPath {
|
|||
final String publicKey;
|
||||
}
|
||||
|
||||
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
|
||||
if (network is BitcoinCashNetwork) {
|
||||
if (!address.startsWith("bitcoincash:") &&
|
||||
(address.startsWith("q") || address.startsWith("p"))) {
|
||||
address = "bitcoincash:$address";
|
||||
}
|
||||
|
||||
return BitcoinCashAddress(address).baseAddress;
|
||||
}
|
||||
|
||||
if (P2pkhAddress.regex.hasMatch(address)) {
|
||||
return P2pkhAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2shAddress.regex.hasMatch(address)) {
|
||||
return P2shAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2wshAddress.regex.hasMatch(address)) {
|
||||
return P2wshAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2trAddress.regex.hasMatch(address)) {
|
||||
return P2trAddress.fromAddress(address: address, network: network);
|
||||
} else if (SilentPaymentAddress.regex.hasMatch(address)) {
|
||||
return SilentPaymentAddress.fromAddress(address);
|
||||
} else {
|
||||
return P2wpkhAddress.fromAddress(address: address, network: network);
|
||||
}
|
||||
}
|
||||
|
||||
BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
|
||||
if (type is P2pkhAddress) {
|
||||
return P2pkhAddressType.p2pkh;
|
||||
|
@ -2420,6 +2514,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
|
|||
return SegwitAddresType.p2wsh;
|
||||
} else if (type is P2trAddress) {
|
||||
return SegwitAddresType.p2tr;
|
||||
} else if (type is MwebAddress) {
|
||||
return SegwitAddresType.mweb;
|
||||
} else if (type is SilentPaymentsAddresType) {
|
||||
return SilentPaymentsAddresType.p2sp;
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
@ -10,7 +13,7 @@ part 'electrum_wallet_addresses.g.dart';
|
|||
|
||||
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
||||
|
||||
const List<BitcoinAddressType> ADDRESS_TYPES = [
|
||||
const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
|
||||
SegwitAddresType.p2wpkh,
|
||||
P2pkhAddressType.p2pkh,
|
||||
SegwitAddresType.p2tr,
|
||||
|
@ -18,6 +21,15 @@ const List<BitcoinAddressType> ADDRESS_TYPES = [
|
|||
P2shAddressType.p2wpkhInP2sh,
|
||||
];
|
||||
|
||||
const List<BitcoinAddressType> LITECOIN_ADDRESS_TYPES = [
|
||||
SegwitAddresType.p2wpkh,
|
||||
SegwitAddresType.mweb,
|
||||
];
|
||||
|
||||
const List<BitcoinAddressType> BITCOIN_CASH_ADDRESS_TYPES = [
|
||||
P2pkhAddressType.p2pkh,
|
||||
];
|
||||
|
||||
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
ElectrumWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
|
@ -29,6 +41,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
Map<String, int>? initialChangeAddressIndex,
|
||||
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
|
||||
int initialSilentAddressIndex = 0,
|
||||
List<BitcoinAddressRecord>? initialMwebAddresses,
|
||||
Bip32Slip10Secp256k1? masterHd,
|
||||
BitcoinAddressType? initialAddressPageType,
|
||||
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
|
@ -49,6 +62,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
|
||||
(initialSilentAddresses ?? []).toSet()),
|
||||
currentSilentAddressIndex = initialSilentAddressIndex,
|
||||
mwebAddresses =
|
||||
ObservableList<BitcoinAddressRecord>.of((initialMwebAddresses ?? []).toSet()),
|
||||
super(walletInfo) {
|
||||
if (masterHd != null) {
|
||||
silentAddress = SilentPaymentOwner.fromPrivateKeys(
|
||||
|
@ -87,10 +102,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
static const gap = 20;
|
||||
|
||||
final ObservableList<BitcoinAddressRecord> _addresses;
|
||||
late ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
|
||||
final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
|
||||
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
||||
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
||||
// TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
|
||||
final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
|
||||
// TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
|
||||
final ObservableList<BitcoinAddressRecord> mwebAddresses;
|
||||
final BasedUtxoNetwork network;
|
||||
final Bip32Slip10Secp256k1 mainHd;
|
||||
final Bip32Slip10Secp256k1 sideHd;
|
||||
|
@ -149,6 +167,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@override
|
||||
set address(String addr) {
|
||||
if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) {
|
||||
return;
|
||||
}
|
||||
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||
final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
|
||||
|
||||
|
@ -160,12 +181,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
|
||||
try {
|
||||
final addressRecord = _addresses.firstWhere(
|
||||
(addressRecord) => addressRecord.address == addr,
|
||||
);
|
||||
|
||||
previousAddressRecord = addressRecord;
|
||||
receiveAddresses.remove(addressRecord);
|
||||
receiveAddresses.insert(0, addressRecord);
|
||||
} catch (e) {
|
||||
print("ElectrumWalletAddressBase: set address ($addr): $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -213,7 +239,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
if (walletInfo.type == WalletType.bitcoinCash) {
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
} else if (walletInfo.type == WalletType.litecoin) {
|
||||
await _generateInitialAddresses();
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await _generateInitialAddresses(type: SegwitAddresType.mweb);
|
||||
}
|
||||
} else if (walletInfo.type == WalletType.bitcoin) {
|
||||
await _generateInitialAddresses();
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
|
@ -221,6 +250,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
|
||||
}
|
||||
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
|
@ -237,7 +267,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
}
|
||||
|
||||
@action
|
||||
Future<String> getChangeAddress() async {
|
||||
Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
|
||||
updateChangeAddresses();
|
||||
|
||||
if (changeAddresses.isEmpty) {
|
||||
|
@ -317,12 +347,110 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
return address;
|
||||
}
|
||||
|
||||
String getAddress(
|
||||
{required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType}) =>
|
||||
String getAddress({
|
||||
required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType,
|
||||
}) =>
|
||||
'';
|
||||
|
||||
Future<String> getAddressAsync({
|
||||
required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType,
|
||||
}) async =>
|
||||
getAddress(index: index, hd: hd, addressType: addressType);
|
||||
|
||||
void addBitcoinAddressTypes() {
|
||||
final lastP2wpkh = _addresses
|
||||
.where((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
|
||||
.toList()
|
||||
.last;
|
||||
if (lastP2wpkh.address != address) {
|
||||
addressesMap[lastP2wpkh.address] = 'P2WPKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WPKH';
|
||||
}
|
||||
|
||||
final lastP2pkh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
|
||||
if (lastP2pkh.address != address) {
|
||||
addressesMap[lastP2pkh.address] = 'P2PKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2PKH';
|
||||
}
|
||||
|
||||
final lastP2sh = _addresses.firstWhere((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
|
||||
if (lastP2sh.address != address) {
|
||||
addressesMap[lastP2sh.address] = 'P2SH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2SH';
|
||||
}
|
||||
|
||||
final lastP2tr = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
|
||||
if (lastP2tr.address != address) {
|
||||
addressesMap[lastP2tr.address] = 'P2TR';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2TR';
|
||||
}
|
||||
|
||||
final lastP2wsh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
|
||||
if (lastP2wsh.address != address) {
|
||||
addressesMap[lastP2wsh.address] = 'P2WSH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WSH';
|
||||
}
|
||||
|
||||
silentAddresses.forEach((addressRecord) {
|
||||
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addressRecord.address != address) {
|
||||
addressesMap[addressRecord.address] = addressRecord.name.isEmpty
|
||||
? "Silent Payments"
|
||||
: "Silent Payments - " + addressRecord.name;
|
||||
} else {
|
||||
addressesMap[address] = 'Active - Silent Payments';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void addLitecoinAddressTypes() {
|
||||
final lastP2wpkh = _addresses
|
||||
.where((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
|
||||
.toList()
|
||||
.last;
|
||||
if (lastP2wpkh.address != address) {
|
||||
addressesMap[lastP2wpkh.address] = 'P2WPKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WPKH';
|
||||
}
|
||||
|
||||
final lastMweb = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb));
|
||||
if (lastMweb.address != address) {
|
||||
addressesMap[lastMweb.address] = 'MWEB';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - MWEB';
|
||||
}
|
||||
}
|
||||
|
||||
void addBitcoinCashAddressTypes() {
|
||||
final lastP2pkh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
|
||||
if (lastP2pkh.address != address) {
|
||||
addressesMap[lastP2pkh.address] = 'P2PKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2PKH';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAddressesInBox() async {
|
||||
try {
|
||||
|
@ -334,63 +462,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
allAddressesMap[addressRecord.address] = addressRecord.name;
|
||||
});
|
||||
|
||||
final lastP2wpkh = _addresses
|
||||
.where((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
|
||||
.toList()
|
||||
.last;
|
||||
if (lastP2wpkh.address != address) {
|
||||
addressesMap[lastP2wpkh.address] = 'P2WPKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WPKH';
|
||||
switch (walletInfo.type) {
|
||||
case WalletType.bitcoin:
|
||||
addBitcoinAddressTypes();
|
||||
break;
|
||||
case WalletType.litecoin:
|
||||
addLitecoinAddressTypes();
|
||||
break;
|
||||
case WalletType.bitcoinCash:
|
||||
addBitcoinCashAddressTypes();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
final lastP2pkh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
|
||||
if (lastP2pkh.address != address) {
|
||||
addressesMap[lastP2pkh.address] = 'P2PKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2PKH';
|
||||
}
|
||||
|
||||
final lastP2sh = _addresses.firstWhere((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
|
||||
if (lastP2sh.address != address) {
|
||||
addressesMap[lastP2sh.address] = 'P2SH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2SH';
|
||||
}
|
||||
|
||||
final lastP2tr = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
|
||||
if (lastP2tr.address != address) {
|
||||
addressesMap[lastP2tr.address] = 'P2TR';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2TR';
|
||||
}
|
||||
|
||||
final lastP2wsh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
|
||||
if (lastP2wsh.address != address) {
|
||||
addressesMap[lastP2wsh.address] = 'P2WSH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WSH';
|
||||
}
|
||||
|
||||
silentAddresses.forEach((addressRecord) {
|
||||
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addressRecord.address != address) {
|
||||
addressesMap[addressRecord.address] = addressRecord.name.isEmpty
|
||||
? "Silent Payments"
|
||||
: "Silent Payments - " + addressRecord.name;
|
||||
} else {
|
||||
addressesMap[address] = 'Active - Silent Payments';
|
||||
}
|
||||
});
|
||||
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
|
@ -410,6 +495,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
foundAddress = addressRecord;
|
||||
}
|
||||
});
|
||||
mwebAddresses.forEach((addressRecord) {
|
||||
if (addressRecord.address == address) {
|
||||
foundAddress = addressRecord;
|
||||
}
|
||||
});
|
||||
|
||||
if (foundAddress != null) {
|
||||
foundAddress!.setNewName(label);
|
||||
|
@ -510,7 +600,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
for (var i = startIndex; i < count + startIndex; i++) {
|
||||
final address = BitcoinAddressRecord(
|
||||
getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
|
||||
await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
|
||||
index: i,
|
||||
isHidden: isHidden,
|
||||
type: type ?? addressPageType,
|
||||
|
@ -540,15 +630,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
@action
|
||||
void addMwebAddresses(Iterable<BitcoinAddressRecord> addresses) {
|
||||
final addressesSet = this.mwebAddresses.toSet();
|
||||
addressesSet.addAll(addresses);
|
||||
this.mwebAddresses.clear();
|
||||
this.mwebAddresses.addAll(addressesSet);
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
void _validateAddresses() {
|
||||
_addresses.forEach((element) {
|
||||
_addresses.forEach((element) async {
|
||||
if (element.type == SegwitAddresType.mweb) {
|
||||
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
|
||||
return;
|
||||
}
|
||||
if (!element.isHidden &&
|
||||
element.address !=
|
||||
getAddress(index: element.index, hd: mainHd, addressType: element.type)) {
|
||||
await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
|
||||
element.isHidden = true;
|
||||
} else if (element.isHidden &&
|
||||
element.address !=
|
||||
getAddress(index: element.index, hd: sideHd, addressType: element.type)) {
|
||||
await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
|
||||
element.isHidden = false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -23,6 +23,8 @@ class ElectrumWalletSnapshot {
|
|||
required this.addressPageType,
|
||||
required this.silentAddresses,
|
||||
required this.silentAddressIndex,
|
||||
required this.mwebAddresses,
|
||||
required this.alwaysScan,
|
||||
this.passphrase,
|
||||
this.derivationType,
|
||||
this.derivationPath,
|
||||
|
@ -44,6 +46,9 @@ class ElectrumWalletSnapshot {
|
|||
|
||||
List<BitcoinAddressRecord> addresses;
|
||||
List<BitcoinSilentPaymentAddressRecord> silentAddresses;
|
||||
List<BitcoinAddressRecord> mwebAddresses;
|
||||
bool alwaysScan;
|
||||
|
||||
ElectrumBalance balance;
|
||||
Map<String, int> regularAddressIndex;
|
||||
Map<String, int> changeAddressIndex;
|
||||
|
@ -51,15 +56,16 @@ class ElectrumWalletSnapshot {
|
|||
DerivationType? derivationType;
|
||||
String? derivationPath;
|
||||
|
||||
static Future<ElectrumWalletSnapshot> load(
|
||||
EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async {
|
||||
static Future<ElectrumWalletSnapshot> load(EncryptionFileUtils encryptionFileUtils, String name,
|
||||
WalletType type, String password, BasedUtxoNetwork network) async {
|
||||
final path = await pathForWallet(name: name, type: type);
|
||||
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final addressesTmp = data['addresses'] as List? ?? <Object>[];
|
||||
final mnemonic = data['mnemonic'] as String?;
|
||||
final xpub = data['xpub'] as String?;
|
||||
final passphrase = data['passphrase'] as String? ?? '';
|
||||
|
||||
final addressesTmp = data['addresses'] as List? ?? <Object>[];
|
||||
final addresses = addressesTmp
|
||||
.whereType<String>()
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
|
||||
|
@ -71,6 +77,14 @@ class ElectrumWalletSnapshot {
|
|||
.map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network))
|
||||
.toList();
|
||||
|
||||
final mwebAddressTmp = data['mweb_addresses'] as List? ?? <Object>[];
|
||||
final mwebAddresses = mwebAddressTmp
|
||||
.whereType<String>()
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
|
||||
.toList();
|
||||
|
||||
final alwaysScan = data['alwaysScan'] as bool? ?? false;
|
||||
|
||||
final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ??
|
||||
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
||||
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
|
@ -113,6 +127,8 @@ class ElectrumWalletSnapshot {
|
|||
derivationPath: derivationPath,
|
||||
silentAddresses: silentAddresses,
|
||||
silentAddressIndex: silentAddressIndex,
|
||||
mwebAddresses: mwebAddresses,
|
||||
alwaysScan: alwaysScan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ import 'package:cw_core/exceptions.dart';
|
|||
|
||||
class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException {
|
||||
BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "BitcoinTransactionWrongBalanceException: $amount, $currency";
|
||||
}
|
||||
}
|
||||
|
||||
class BitcoinTransactionNoInputsException extends TransactionNoInputsException {}
|
||||
|
@ -13,10 +18,20 @@ class BitcoinTransactionNoDustException extends TransactionNoDustException {}
|
|||
|
||||
class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException {
|
||||
BitcoinTransactionNoDustOnChangeException(super.max, super.min);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "BitcoinTransactionNoDustOnChangeException: max: $max, min: $min";
|
||||
}
|
||||
}
|
||||
|
||||
class BitcoinTransactionCommitFailed extends TransactionCommitFailed {
|
||||
BitcoinTransactionCommitFailed({super.errorMessage});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return errorMessage??"unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {}
|
||||
|
@ -30,4 +45,6 @@ class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailed
|
|||
|
||||
class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {}
|
||||
|
||||
class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedLessThanMin {}
|
||||
|
||||
class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {}
|
||||
|
|
|
@ -1,15 +1,31 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:convert/convert.dart' as convert;
|
||||
import 'dart:math';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/mweb_utxo.dart';
|
||||
import 'package:cw_mweb/mwebd.pbgrpc.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
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/bitcoin_unspent.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/utils.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/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
|
@ -19,8 +35,11 @@ import 'package:cw_core/transaction_priority.dart';
|
|||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_keys_file.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_mweb/cw_mweb.dart';
|
||||
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
|
||||
import 'package:pointycastle/ecc/api.dart';
|
||||
import 'package:pointycastle/ecc/curves/secp256k1.dart';
|
||||
|
@ -40,34 +59,54 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
List<BitcoinAddressRecord>? initialMwebAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
int? initialMwebHeight,
|
||||
bool? alwaysScan,
|
||||
}) : super(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
network: LitecoinNetwork.mainnet,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase,
|
||||
currency: CryptoCurrency.ltc) {
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
network: LitecoinNetwork.mainnet,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
currency: CryptoCurrency.ltc,
|
||||
alwaysScan: alwaysScan,
|
||||
) {
|
||||
mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
|
||||
mwebEnabled = alwaysScan ?? false;
|
||||
walletAddresses = LitecoinWalletAddresses(
|
||||
walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
initialMwebAddresses: initialMwebAddresses,
|
||||
mainHd: hd,
|
||||
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
|
||||
network: network,
|
||||
mwebHd: mwebHd,
|
||||
mwebEnabled: mwebEnabled,
|
||||
);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
}
|
||||
late final Bip32Slip10Secp256k1 mwebHd;
|
||||
late final Box<MwebUtxo> mwebUtxosBox;
|
||||
Timer? _syncTimer;
|
||||
Timer? _feeRatesTimer;
|
||||
Timer? _processingTimer;
|
||||
StreamSubscription<Utxo>? _utxoStream;
|
||||
late bool mwebEnabled;
|
||||
bool processingUtxos = false;
|
||||
|
||||
List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
|
||||
List<int> get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;
|
||||
|
||||
static Future<LitecoinWallet> create(
|
||||
{required String mnemonic,
|
||||
|
@ -78,6 +117,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
List<BitcoinAddressRecord>? initialMwebAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex}) async {
|
||||
|
@ -101,6 +141,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialMwebAddresses: initialMwebAddresses,
|
||||
initialBalance: initialBalance,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase,
|
||||
|
@ -111,12 +152,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
);
|
||||
}
|
||||
|
||||
static Future<LitecoinWallet> open(
|
||||
{required String name,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
required EncryptionFileUtils encryptionFileUtils}) async {
|
||||
static Future<LitecoinWallet> open({
|
||||
required String name,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
required bool alwaysScan,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
}) async {
|
||||
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
|
||||
|
||||
ElectrumWalletSnapshot? snp = null;
|
||||
|
@ -178,6 +221,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp?.addresses,
|
||||
initialMwebAddresses: snp?.mwebAddresses,
|
||||
initialBalance: snp?.balance,
|
||||
seedBytes: seedBytes!,
|
||||
passphrase: passphrase,
|
||||
|
@ -185,6 +229,565 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
initialRegularAddressIndex: snp?.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp?.changeAddressIndex,
|
||||
addressPageType: snp?.addressPageType,
|
||||
alwaysScan: snp?.alwaysScan,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> waitForMwebAddresses() async {
|
||||
print("waitForMwebAddresses() called!");
|
||||
// ensure that we have the full 1000 mweb addresses generated before continuing:
|
||||
// should no longer be needed, but leaving here just in case
|
||||
await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020);
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
print("startSync() called!");
|
||||
if (syncStatus is SyncronizingSyncStatus) {
|
||||
return;
|
||||
}
|
||||
print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
|
||||
_syncTimer?.cancel();
|
||||
try {
|
||||
syncStatus = SyncronizingSyncStatus();
|
||||
await subscribeForUpdates();
|
||||
updateFeeRates();
|
||||
_feeRatesTimer?.cancel();
|
||||
_feeRatesTimer =
|
||||
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
|
||||
|
||||
if (!mwebEnabled) {
|
||||
try {
|
||||
// in case we're switching from a litecoin wallet that had mweb enabled
|
||||
CwMweb.stop();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await updateAllUnspents();
|
||||
await updateTransactions();
|
||||
await updateBalance();
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e, s) {
|
||||
print(e);
|
||||
print(s);
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForMwebAddresses();
|
||||
await processMwebUtxos();
|
||||
await updateTransactions();
|
||||
await updateUnspent();
|
||||
await updateBalance();
|
||||
} catch (e) {
|
||||
print("failed to start mweb sync: $e");
|
||||
syncStatus = FailedSyncStatus(error: "failed to start");
|
||||
return;
|
||||
}
|
||||
|
||||
_syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async {
|
||||
if (syncStatus is FailedSyncStatus) return;
|
||||
|
||||
final nodeHeight =
|
||||
await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
|
||||
|
||||
if (nodeHeight == 0) {
|
||||
// we aren't connected to the ltc node yet
|
||||
if (syncStatus is! NotConnectedSyncStatus) {
|
||||
syncStatus = FailedSyncStatus(error: "Failed to connect to Litecoin node");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final resp = await CwMweb.status(StatusRequest());
|
||||
|
||||
try {
|
||||
if (resp.blockHeaderHeight < nodeHeight) {
|
||||
int h = resp.blockHeaderHeight;
|
||||
syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
|
||||
} else if (resp.mwebHeaderHeight < nodeHeight) {
|
||||
int h = resp.mwebHeaderHeight;
|
||||
syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
|
||||
} else if (resp.mwebUtxosHeight < nodeHeight) {
|
||||
syncStatus = SyncingSyncStatus(1, 0.999);
|
||||
} else {
|
||||
if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
|
||||
await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
|
||||
await checkMwebUtxosSpent();
|
||||
// update the confirmations for each transaction:
|
||||
for (final transaction in transactionHistory.transactions.values) {
|
||||
if (transaction.isPending) continue;
|
||||
int txHeight = transaction.height ?? resp.mwebUtxosHeight;
|
||||
final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
|
||||
if (transaction.confirmations == confirmations) continue;
|
||||
transaction.confirmations = confirmations;
|
||||
transactionHistory.addOne(transaction);
|
||||
}
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
// prevent unnecessary reaction triggers:
|
||||
if (syncStatus is! SyncedSyncStatus) {
|
||||
// mwebd is synced, but we could still be processing incoming utxos:
|
||||
if (!processingUtxos) {
|
||||
syncStatus = SyncedSyncStatus();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
print("error syncing: $e");
|
||||
syncStatus = FailedSyncStatus(error: e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> stopSync() async {
|
||||
print("stopSync() called!");
|
||||
_syncTimer?.cancel();
|
||||
_utxoStream?.cancel();
|
||||
_feeRatesTimer?.cancel();
|
||||
await CwMweb.stop();
|
||||
print("stopped syncing!");
|
||||
}
|
||||
|
||||
Future<void> initMwebUtxosBox() async {
|
||||
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
|
||||
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(boxName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
// rename the hive box:
|
||||
final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
|
||||
final oldBox = await CakeHive.openBox<MwebUtxo>(oldBoxName);
|
||||
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
|
||||
for (final key in oldBox.keys) {
|
||||
await mwebUtxosBox.put(key, oldBox.get(key)!);
|
||||
}
|
||||
oldBox.deleteFromDisk();
|
||||
|
||||
await super.renameWalletFiles(newWalletName);
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> rescan({
|
||||
required int height,
|
||||
int? chainTip,
|
||||
ScanData? scanData,
|
||||
bool? doSingleScan,
|
||||
bool? usingElectrs,
|
||||
}) async {
|
||||
_syncTimer?.cancel();
|
||||
int oldHeight = walletInfo.restoreHeight;
|
||||
await walletInfo.updateRestoreHeight(height);
|
||||
|
||||
// go through mwebUtxos and clear any that are above the new restore height:
|
||||
if (height == 0) {
|
||||
await mwebUtxosBox.clear();
|
||||
transactionHistory.clear();
|
||||
} else {
|
||||
for (final utxo in mwebUtxosBox.values) {
|
||||
if (utxo.height > height) {
|
||||
await mwebUtxosBox.delete(utxo.outputId);
|
||||
}
|
||||
}
|
||||
// TODO: remove transactions that are above the new restore height!
|
||||
}
|
||||
|
||||
// reset coin balances and txCount to 0:
|
||||
unspentCoins.forEach((coin) {
|
||||
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
coin.bitcoinAddressRecord.balance = 0;
|
||||
coin.bitcoinAddressRecord.txCount = 0;
|
||||
});
|
||||
|
||||
for (var addressRecord in walletAddresses.allAddresses) {
|
||||
addressRecord.balance = 0;
|
||||
addressRecord.txCount = 0;
|
||||
}
|
||||
|
||||
await startSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
await super.init();
|
||||
await initMwebUtxosBox();
|
||||
}
|
||||
|
||||
Future<void> handleIncoming(MwebUtxo utxo) async {
|
||||
print("handleIncoming() called!");
|
||||
final status = await CwMweb.status(StatusRequest());
|
||||
var date = DateTime.now();
|
||||
var confirmations = 0;
|
||||
if (utxo.height > 0) {
|
||||
date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000);
|
||||
confirmations = status.blockHeaderHeight - utxo.height + 1;
|
||||
}
|
||||
var tx = transactionHistory.transactions.values
|
||||
.firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false);
|
||||
|
||||
if (tx == null) {
|
||||
tx = ElectrumTransactionInfo(
|
||||
WalletType.litecoin,
|
||||
id: utxo.outputId,
|
||||
height: utxo.height,
|
||||
amount: utxo.value.toInt(),
|
||||
fee: 0,
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: utxo.height == 0,
|
||||
date: date,
|
||||
confirmations: confirmations,
|
||||
inputAddresses: [],
|
||||
outputAddresses: [utxo.outputId],
|
||||
isReplaced: false,
|
||||
);
|
||||
}
|
||||
|
||||
// don't update the confirmations if the tx is updated by electrum:
|
||||
if (tx.confirmations == 0 || utxo.height != 0) {
|
||||
tx.height = utxo.height;
|
||||
tx.isPending = utxo.height == 0;
|
||||
tx.confirmations = confirmations;
|
||||
}
|
||||
|
||||
bool isNew = transactionHistory.transactions[tx.id] == null;
|
||||
|
||||
if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) {
|
||||
tx.outputAddresses?.add(utxo.address);
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
final addressRecord = walletAddresses.allAddresses
|
||||
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
|
||||
if (addressRecord == null) {
|
||||
print("we don't have this address in the wallet! ${utxo.address}");
|
||||
return;
|
||||
}
|
||||
|
||||
// update the txCount:
|
||||
addressRecord.txCount++;
|
||||
addressRecord.balance += utxo.value.toInt();
|
||||
addressRecord.setAsUsed();
|
||||
}
|
||||
|
||||
transactionHistory.addOne(tx);
|
||||
|
||||
if (isNew) {
|
||||
// update the unconfirmed balance when a new tx is added:
|
||||
// we do this after adding the tx to the history so that sub address balances are updated correctly
|
||||
// (since that calculation is based on the tx history)
|
||||
await updateBalance();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> processMwebUtxos() async {
|
||||
print("processMwebUtxos() called!");
|
||||
if (!mwebEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
int restoreHeight = walletInfo.restoreHeight;
|
||||
print("SCANNING FROM HEIGHT: $restoreHeight");
|
||||
final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight);
|
||||
|
||||
// process new utxos as they come in:
|
||||
await _utxoStream?.cancel();
|
||||
ResponseStream<Utxo>? responseStream = await CwMweb.utxos(req);
|
||||
if (responseStream == null) {
|
||||
throw Exception("failed to get utxos stream!");
|
||||
}
|
||||
_utxoStream = responseStream.listen((Utxo sUtxo) async {
|
||||
// we're processing utxos, so our balance could still be innacurate:
|
||||
if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) {
|
||||
syncStatus = SyncronizingSyncStatus();
|
||||
processingUtxos = true;
|
||||
_processingTimer?.cancel();
|
||||
_processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||
processingUtxos = false;
|
||||
timer.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
final utxo = MwebUtxo(
|
||||
address: sUtxo.address,
|
||||
blockTime: sUtxo.blockTime,
|
||||
height: sUtxo.height,
|
||||
outputId: sUtxo.outputId,
|
||||
value: sUtxo.value.toInt(),
|
||||
);
|
||||
|
||||
// if (mwebUtxosBox.containsKey(utxo.outputId)) {
|
||||
// // we've already stored this utxo, skip it:
|
||||
// return;
|
||||
// }
|
||||
|
||||
await updateUnspent();
|
||||
await updateBalance();
|
||||
|
||||
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
|
||||
|
||||
// don't process utxos with addresses that are not in the mwebAddrs list:
|
||||
if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await mwebUtxosBox.put(utxo.outputId, utxo);
|
||||
|
||||
await handleIncoming(utxo);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> checkMwebUtxosSpent() async {
|
||||
if (!mwebEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingOutgoingTransactions = transactionHistory.transactions.values
|
||||
.where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending);
|
||||
|
||||
// check if any of the pending outgoing transactions are now confirmed:
|
||||
bool updatedAny = false;
|
||||
for (final tx in pendingOutgoingTransactions) {
|
||||
updatedAny = await isConfirmed(tx) || updatedAny;
|
||||
}
|
||||
|
||||
// get output ids of all the mweb utxos that have > 0 height:
|
||||
final outputIds =
|
||||
mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList();
|
||||
|
||||
final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
|
||||
final spent = resp.outputId;
|
||||
if (spent.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final status = await CwMweb.status(StatusRequest());
|
||||
final height = await electrumClient.getCurrentBlockChainTip();
|
||||
if (height == null || status.blockHeaderHeight != height) return;
|
||||
if (status.mwebUtxosHeight != height) return; // we aren't synced
|
||||
|
||||
int amount = 0;
|
||||
Set<String> inputAddresses = {};
|
||||
var output = convert.AccumulatorSink<Digest>();
|
||||
var input = sha256.startChunkedConversion(output);
|
||||
|
||||
for (final outputId in spent) {
|
||||
final utxo = mwebUtxosBox.get(outputId);
|
||||
await mwebUtxosBox.delete(outputId);
|
||||
if (utxo == null) continue;
|
||||
final addressRecord = walletAddresses.allAddresses
|
||||
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
|
||||
if (!inputAddresses.contains(utxo.address)) {
|
||||
addressRecord.txCount++;
|
||||
}
|
||||
addressRecord.balance -= utxo.value.toInt();
|
||||
amount += utxo.value.toInt();
|
||||
inputAddresses.add(utxo.address);
|
||||
input.add(hex.decode(outputId));
|
||||
}
|
||||
|
||||
if (inputAddresses.isEmpty) return;
|
||||
input.close();
|
||||
var digest = output.events.single;
|
||||
final tx = ElectrumTransactionInfo(
|
||||
WalletType.litecoin,
|
||||
id: digest.toString(),
|
||||
height: height,
|
||||
amount: amount,
|
||||
fee: 0,
|
||||
direction: TransactionDirection.outgoing,
|
||||
isPending: false,
|
||||
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
|
||||
confirmations: 1,
|
||||
inputAddresses: inputAddresses.toList(),
|
||||
outputAddresses: [],
|
||||
isReplaced: false,
|
||||
);
|
||||
|
||||
transactionHistory.addOne(tx);
|
||||
await transactionHistory.save();
|
||||
|
||||
if (updatedAny) {
|
||||
await updateBalance();
|
||||
}
|
||||
}
|
||||
|
||||
// checks if a pending transaction is now confirmed, and updates the tx info accordingly:
|
||||
Future<bool> isConfirmed(ElectrumTransactionInfo tx) async {
|
||||
if (!mwebEnabled) return false;
|
||||
if (!tx.isPending) return false;
|
||||
|
||||
final outputId = <String>[], target = <String>{};
|
||||
final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch;
|
||||
final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? [];
|
||||
final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? [];
|
||||
outputId.addAll(spendingOutputIds);
|
||||
outputId.addAll(payingToOutputIds);
|
||||
target.addAll(spendingOutputIds);
|
||||
|
||||
for (final outputId in payingToOutputIds) {
|
||||
final spendingTx = transactionHistory.transactions.values
|
||||
.firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false);
|
||||
if (spendingTx != null && !spendingTx.isPending) {
|
||||
target.add(outputId);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final resp = await CwMweb.spent(SpentRequest(outputId: outputId));
|
||||
if (!setEquals(resp.outputId.toSet(), target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final status = await CwMweb.status(StatusRequest());
|
||||
tx.height = status.mwebUtxosHeight;
|
||||
tx.confirmations = 1;
|
||||
tx.isPending = false;
|
||||
await transactionHistory.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> updateUnspent() async {
|
||||
print("updateUnspent() called!");
|
||||
await checkMwebUtxosSpent();
|
||||
await updateAllUnspents();
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<void> updateAllUnspents() async {
|
||||
// get ltc unspents:
|
||||
await super.updateAllUnspents();
|
||||
|
||||
if (!mwebEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add the mweb unspents to the list:
|
||||
List<BitcoinUnspent> mwebUnspentCoins = [];
|
||||
// update mweb unspents:
|
||||
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
|
||||
mwebUtxosBox.keys.forEach((dynamic oId) {
|
||||
final String outputId = oId as String;
|
||||
final utxo = mwebUtxosBox.get(outputId);
|
||||
if (utxo == null) {
|
||||
return;
|
||||
}
|
||||
if (utxo.address.isEmpty) {
|
||||
// not sure if a bug or a special case but we definitely ignore these
|
||||
return;
|
||||
}
|
||||
final addressRecord = walletAddresses.allAddresses
|
||||
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
|
||||
|
||||
if (addressRecord == null) {
|
||||
print("utxo contains an address that is not in the wallet: ${utxo.address}");
|
||||
return;
|
||||
}
|
||||
final unspent = BitcoinUnspent(
|
||||
addressRecord,
|
||||
outputId,
|
||||
utxo.value.toInt(),
|
||||
mwebAddrs.indexOf(utxo.address),
|
||||
);
|
||||
if (unspent.vout == 0) {
|
||||
unspent.isChange = true;
|
||||
}
|
||||
mwebUnspentCoins.add(unspent);
|
||||
});
|
||||
unspentCoins.addAll(mwebUnspentCoins);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ElectrumBalance> fetchBalances() async {
|
||||
final balance = await super.fetchBalances();
|
||||
if (!mwebEnabled) {
|
||||
return balance;
|
||||
}
|
||||
|
||||
// update unspent balances:
|
||||
await updateUnspent();
|
||||
|
||||
int confirmed = balance.confirmed;
|
||||
int unconfirmed = balance.unconfirmed;
|
||||
int confirmedMweb = 0;
|
||||
int unconfirmedMweb = 0;
|
||||
try {
|
||||
mwebUtxosBox.values.forEach((utxo) {
|
||||
if (utxo.height > 0) {
|
||||
confirmedMweb += utxo.value.toInt();
|
||||
} else {
|
||||
unconfirmedMweb += utxo.value.toInt();
|
||||
}
|
||||
});
|
||||
if (unconfirmedMweb > 0) {
|
||||
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
for (var addressRecord in walletAddresses.allAddresses) {
|
||||
addressRecord.balance = 0;
|
||||
addressRecord.txCount = 0;
|
||||
}
|
||||
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where(
|
||||
(element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.vout == coin.vout,
|
||||
);
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
|
||||
coin.isFrozen = coinInfo.isFrozen;
|
||||
coin.isSending = coinInfo.isSending;
|
||||
coin.note = coinInfo.note;
|
||||
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
|
||||
coin.bitcoinAddressRecord.balance += coinInfo.value;
|
||||
} else {
|
||||
super.addCoinInfo(coin);
|
||||
}
|
||||
});
|
||||
|
||||
// update the txCount for each address using the tx history, since we can't rely on mwebd
|
||||
// to have an accurate count, we should just keep it in sync with what we know from the tx history:
|
||||
for (final tx in transactionHistory.transactions.values) {
|
||||
// if (tx.isPending) continue;
|
||||
if (tx.inputAddresses == null || tx.outputAddresses == null) {
|
||||
continue;
|
||||
}
|
||||
final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
|
||||
for (final address in txAddresses) {
|
||||
final addressRecord = walletAddresses.allAddresses
|
||||
.firstWhereOrNull((addressRecord) => addressRecord.address == address);
|
||||
if (addressRecord == null) {
|
||||
continue;
|
||||
}
|
||||
addressRecord.txCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: confirmed,
|
||||
unconfirmed: unconfirmed,
|
||||
frozen: balance.frozen,
|
||||
secondConfirmed: confirmedMweb,
|
||||
secondUnconfirmed: unconfirmedMweb,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -204,6 +807,229 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> calcFee({
|
||||
required List<UtxoWithAddress> utxos,
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required BasedUtxoNetwork network,
|
||||
String? memo,
|
||||
required int feeRate,
|
||||
List<ECPrivateInfo>? inputPrivKeyInfos,
|
||||
List<Outpoint>? vinOutpoints,
|
||||
}) async {
|
||||
final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
|
||||
final paysToMweb = outputs
|
||||
.any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
|
||||
if (!spendsMweb && !paysToMweb) {
|
||||
return await super.calcFee(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
feeRate: feeRate,
|
||||
inputPrivKeyInfos: inputPrivKeyInfos,
|
||||
vinOutpoints: vinOutpoints,
|
||||
);
|
||||
}
|
||||
|
||||
if (!mwebEnabled) {
|
||||
throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
|
||||
}
|
||||
|
||||
if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) {
|
||||
outputs = [
|
||||
BitcoinScriptOutput(
|
||||
script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue())
|
||||
];
|
||||
}
|
||||
|
||||
// https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
|
||||
final preOutputSum =
|
||||
outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
|
||||
final fee = utxos.sumOfUtxosValue() - preOutputSum;
|
||||
final txb =
|
||||
BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
|
||||
final resp = await CwMweb.create(CreateRequest(
|
||||
rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(),
|
||||
scanSecret: scanSecret,
|
||||
spendSecret: spendSecret,
|
||||
feeRatePerKb: Int64(feeRate * 1000),
|
||||
dryRun: true));
|
||||
final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
|
||||
final posUtxos = utxos
|
||||
.where((utxo) => tx.inputs
|
||||
.any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout))
|
||||
.toList();
|
||||
final posOutputSum = tx.outputs.fold<int>(0, (acc, output) => acc + output.amount.toInt());
|
||||
final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue();
|
||||
final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt());
|
||||
var feeIncrease = posOutputSum - expectedPegin;
|
||||
if (expectedPegin > 0 && fee == BigInt.zero) {
|
||||
feeIncrease += await super.calcFee(
|
||||
utxos: posUtxos,
|
||||
outputs: tx.outputs
|
||||
.map((output) =>
|
||||
BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount))
|
||||
.toList(),
|
||||
network: network,
|
||||
memo: memo,
|
||||
feeRate: feeRate) +
|
||||
feeRate * 41;
|
||||
}
|
||||
return fee.toInt() + feeIncrease;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
try {
|
||||
var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction;
|
||||
tx.isMweb = mwebEnabled;
|
||||
|
||||
if (!mwebEnabled) {
|
||||
return tx;
|
||||
}
|
||||
await waitForMwebAddresses();
|
||||
|
||||
final resp = await CwMweb.create(CreateRequest(
|
||||
rawTx: hex.decode(tx.hex),
|
||||
scanSecret: scanSecret,
|
||||
spendSecret: spendSecret,
|
||||
feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000,
|
||||
));
|
||||
final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
|
||||
|
||||
// check if the transaction doesn't contain any mweb inputs or outputs:
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
|
||||
bool hasMwebInput = false;
|
||||
bool hasMwebOutput = false;
|
||||
|
||||
for (final output in transactionCredentials.outputs) {
|
||||
if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) {
|
||||
hasMwebOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) {
|
||||
hasMwebInput = true;
|
||||
}
|
||||
|
||||
if (!hasMwebInput && !hasMwebOutput) {
|
||||
return tx;
|
||||
}
|
||||
|
||||
// check if any of the inputs of this transaction are hog-ex:
|
||||
// this list is only non-mweb inputs:
|
||||
tx2.inputs.forEach((txInput) {
|
||||
bool isHogEx = true;
|
||||
|
||||
final utxo = unspentCoins
|
||||
.firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex);
|
||||
|
||||
// TODO: detect actual hog-ex inputs
|
||||
|
||||
if (!isHogEx) {
|
||||
return;
|
||||
}
|
||||
|
||||
int confirmations = utxo.confirmations ?? 0;
|
||||
if (confirmations < 6) {
|
||||
throw Exception(
|
||||
"A transaction input has less than 6 confirmations, please try again later.");
|
||||
}
|
||||
});
|
||||
|
||||
tx.hexOverride = tx2
|
||||
.copyWith(
|
||||
witnesses: tx2.inputs.asMap().entries.map((e) {
|
||||
final utxo = unspentCoins
|
||||
.firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
|
||||
final key = generateECPrivate(
|
||||
hd: utxo.bitcoinAddressRecord.isHidden
|
||||
? walletAddresses.sideHd
|
||||
: walletAddresses.mainHd,
|
||||
index: utxo.bitcoinAddressRecord.index,
|
||||
network: network);
|
||||
final digest = tx2.getTransactionSegwitDigit(
|
||||
txInIndex: e.key,
|
||||
script: key.getPublic().toP2pkhAddress().toScriptPubKey(),
|
||||
amount: BigInt.from(utxo.value),
|
||||
);
|
||||
return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]);
|
||||
}).toList())
|
||||
.toHex();
|
||||
tx.outputAddresses = resp.outputId;
|
||||
|
||||
return tx
|
||||
..addListener((transaction) async {
|
||||
final addresses = <String>{};
|
||||
transaction.inputAddresses?.forEach((id) async {
|
||||
final utxo = mwebUtxosBox.get(id);
|
||||
// await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent
|
||||
if (utxo == null) return;
|
||||
final addressRecord = walletAddresses.allAddresses
|
||||
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
|
||||
if (!addresses.contains(utxo.address)) {
|
||||
addresses.add(utxo.address);
|
||||
}
|
||||
addressRecord.balance -= utxo.value.toInt();
|
||||
});
|
||||
transaction.inputAddresses?.addAll(addresses);
|
||||
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateUnspent();
|
||||
await updateBalance();
|
||||
});
|
||||
} catch (e, s) {
|
||||
print(e);
|
||||
print(s);
|
||||
if (e.toString().contains("commit failed")) {
|
||||
throw Exception("Transaction commit failed (no peers responded), please try again.");
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
await super.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close({required bool shouldCleanup}) async {
|
||||
_utxoStream?.cancel();
|
||||
_feeRatesTimer?.cancel();
|
||||
_syncTimer?.cancel();
|
||||
_processingTimer?.cancel();
|
||||
if (shouldCleanup) {
|
||||
try {
|
||||
await stopSync();
|
||||
} catch (_) {}
|
||||
}
|
||||
await super.close(shouldCleanup: shouldCleanup);
|
||||
}
|
||||
|
||||
Future<void> setMwebEnabled(bool enabled) async {
|
||||
if (mwebEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
alwaysScan = enabled;
|
||||
mwebEnabled = enabled;
|
||||
(walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled;
|
||||
await save();
|
||||
try {
|
||||
await stopSync();
|
||||
} catch (_) {}
|
||||
await startSync();
|
||||
}
|
||||
|
||||
Future<StatusResponse> getStatusRequest() async {
|
||||
final resp = await CwMweb.status(StatusRequest());
|
||||
return resp;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> signMessage(String message, {String? address = null}) async {
|
||||
final index = address != null
|
||||
|
@ -301,7 +1127,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
|
||||
List<int> possibleRecoverIds = [0, 1];
|
||||
|
||||
final baseAddress = addressTypeFromStr(address, network);
|
||||
final baseAddress = RegexUtils.addressTypeFromStr(address, network);
|
||||
|
||||
for (int recoveryId in possibleRecoverIds) {
|
||||
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_mweb/cw_mweb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'litecoin_wallet_addresses.g.dart';
|
||||
|
@ -15,15 +23,167 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
|
|||
required super.mainHd,
|
||||
required super.sideHd,
|
||||
required super.network,
|
||||
required this.mwebHd,
|
||||
required this.mwebEnabled,
|
||||
super.initialAddresses,
|
||||
super.initialMwebAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
}) : super(walletInfo);
|
||||
}) : super(walletInfo) {
|
||||
for (int i = 0; i < mwebAddresses.length; i++) {
|
||||
mwebAddrs.add(mwebAddresses[i].address);
|
||||
}
|
||||
print("initialized with ${mwebAddrs.length} mweb addresses");
|
||||
}
|
||||
|
||||
final Bip32Slip10Secp256k1 mwebHd;
|
||||
bool mwebEnabled;
|
||||
int mwebTopUpIndex = 1000;
|
||||
List<String> mwebAddrs = [];
|
||||
bool generating = false;
|
||||
|
||||
List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
|
||||
|
||||
List<int> get spendPubkey =>
|
||||
mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed;
|
||||
|
||||
@override
|
||||
String getAddress(
|
||||
{required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType}) =>
|
||||
generateP2WPKHAddress(hd: hd, index: index, network: network);
|
||||
Future<void> init() async {
|
||||
await initMwebAddresses();
|
||||
await super.init();
|
||||
}
|
||||
|
||||
@computed
|
||||
@override
|
||||
List<BitcoinAddressRecord> get allAddresses {
|
||||
return List.from(super.allAddresses)..addAll(mwebAddresses);
|
||||
}
|
||||
|
||||
Future<void> ensureMwebAddressUpToIndexExists(int index) async {
|
||||
if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Uint8List scan = Uint8List.fromList(scanSecret);
|
||||
Uint8List spend = Uint8List.fromList(spendPubkey);
|
||||
|
||||
if (index < mwebAddresses.length && index < mwebAddrs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (generating) {
|
||||
print("generating.....");
|
||||
// this function was called multiple times in multiple places:
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
print("Generating MWEB addresses up to index $index");
|
||||
generating = true;
|
||||
try {
|
||||
while (mwebAddrs.length <= (index + 1)) {
|
||||
final addresses =
|
||||
await CwMweb.addresses(scan, spend, mwebAddrs.length, mwebAddrs.length + 50);
|
||||
print("generated up to index ${mwebAddrs.length}");
|
||||
// sleep for a bit to avoid making the main thread unresponsive:
|
||||
await Future.delayed(Duration(milliseconds: 200));
|
||||
mwebAddrs.addAll(addresses!);
|
||||
}
|
||||
} catch (_) {}
|
||||
generating = false;
|
||||
print("Done generating MWEB addresses len: ${mwebAddrs.length}");
|
||||
|
||||
// ensure mweb addresses are up to date:
|
||||
if (mwebAddresses.length < mwebAddrs.length) {
|
||||
List<BitcoinAddressRecord> addressRecords = mwebAddrs
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => BitcoinAddressRecord(
|
||||
e.value,
|
||||
index: e.key,
|
||||
type: SegwitAddresType.mweb,
|
||||
network: network,
|
||||
))
|
||||
.toList();
|
||||
addMwebAddresses(addressRecords);
|
||||
print("set ${addressRecords.length} mweb addresses");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initMwebAddresses() async {
|
||||
if (mwebAddrs.length < 1000) {
|
||||
await ensureMwebAddressUpToIndexExists(20);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String getAddress({
|
||||
required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType,
|
||||
}) {
|
||||
if (addressType == SegwitAddresType.mweb) {
|
||||
return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
|
||||
}
|
||||
return generateP2WPKHAddress(hd: hd, index: index, network: network);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getAddressAsync({
|
||||
required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType,
|
||||
}) async {
|
||||
if (addressType == SegwitAddresType.mweb) {
|
||||
await ensureMwebAddressUpToIndexExists(index);
|
||||
}
|
||||
return getAddress(index: index, hd: hd, addressType: addressType);
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
|
||||
// use regular change address on peg in, otherwise use mweb for change address:
|
||||
|
||||
if (!mwebEnabled) {
|
||||
return super.getChangeAddress();
|
||||
}
|
||||
|
||||
if (outputs != null && utxoDetails != null) {
|
||||
// check if this is a PEGIN:
|
||||
bool outputsToMweb = false;
|
||||
bool comesFromMweb = false;
|
||||
|
||||
for (var i = 0; i < outputs.length; i++) {
|
||||
// TODO: probably not the best way to tell if this is an mweb address
|
||||
// (but it doesn't contain the "mweb" text at this stage)
|
||||
if (outputs[i].address.toAddress(network).length > 110) {
|
||||
outputsToMweb = true;
|
||||
}
|
||||
}
|
||||
// TODO: this doesn't respect coin control because it doesn't know which available inputs are selected
|
||||
utxoDetails.availableInputs.forEach((element) {
|
||||
if (element.address.contains("mweb")) {
|
||||
comesFromMweb = true;
|
||||
}
|
||||
});
|
||||
|
||||
bool isPegIn = !comesFromMweb && outputsToMweb;
|
||||
if (isPegIn && mwebEnabled) {
|
||||
return super.getChangeAddress();
|
||||
}
|
||||
|
||||
// use regular change address if it's not an mweb tx:
|
||||
if (!comesFromMweb && !outputsToMweb) {
|
||||
return super.getChangeAddress();
|
||||
}
|
||||
}
|
||||
|
||||
if (mwebEnabled) {
|
||||
await ensureMwebAddressUpToIndexExists(1);
|
||||
return mwebAddrs[0];
|
||||
}
|
||||
|
||||
return super.getChangeAddress();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:io';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
|
||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
|
||||
import 'package:cw_bitcoin/litecoin_wallet.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
|
@ -14,16 +14,19 @@ import 'package:cw_core/wallet_info.dart';
|
|||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class LitecoinWalletService extends WalletService<
|
||||
BitcoinNewWalletCredentials,
|
||||
BitcoinRestoreWalletFromSeedCredentials,
|
||||
BitcoinRestoreWalletFromWIFCredentials,
|
||||
BitcoinNewWalletCredentials> {
|
||||
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
|
||||
LitecoinWalletService(
|
||||
this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
|
||||
final bool alwaysScan;
|
||||
final bool isDirect;
|
||||
|
||||
@override
|
||||
|
@ -32,11 +35,11 @@ class LitecoinWalletService extends WalletService<
|
|||
@override
|
||||
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final String mnemonic;
|
||||
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
switch (credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
mnemonic = await MnemonicBip39.generate(strength: strength);
|
||||
mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
|
@ -64,6 +67,7 @@ class LitecoinWalletService extends WalletService<
|
|||
|
||||
@override
|
||||
Future<LitecoinWallet> openWallet(String name, String password) async {
|
||||
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
|
||||
|
||||
|
@ -73,6 +77,7 @@ class LitecoinWalletService extends WalletService<
|
|||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
alwaysScan: alwaysScan,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.init();
|
||||
|
@ -85,6 +90,7 @@ class LitecoinWalletService extends WalletService<
|
|||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
alwaysScan: alwaysScan,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
await wallet.init();
|
||||
|
@ -98,6 +104,23 @@ class LitecoinWalletService extends WalletService<
|
|||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
|
||||
await walletInfoSource.delete(walletInfo.key);
|
||||
|
||||
// if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd:
|
||||
if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) {
|
||||
final appDirPath = (await getApplicationSupportDirectory()).path;
|
||||
File neturinoDb = File('$appDirPath/neutrino.db');
|
||||
File blockHeaders = File('$appDirPath/block_headers.bin');
|
||||
File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin');
|
||||
if (neturinoDb.existsSync()) {
|
||||
neturinoDb.deleteSync();
|
||||
}
|
||||
if (blockHeaders.existsSync()) {
|
||||
blockHeaders.deleteSync();
|
||||
}
|
||||
if (regFilterHeaders.existsSync()) {
|
||||
regFilterHeaders.deleteSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -109,6 +132,7 @@ class LitecoinWalletService extends WalletService<
|
|||
name: currentName,
|
||||
walletInfo: currentWalletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
alwaysScan: alwaysScan,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import 'package:grpc/grpc.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_mweb/cw_mweb.dart';
|
||||
import 'package:cw_mweb/mwebd.pb.dart';
|
||||
|
||||
class PendingBitcoinTransaction with PendingTransaction {
|
||||
PendingBitcoinTransaction(
|
||||
|
@ -19,6 +23,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
required this.hasChange,
|
||||
this.isSendAll = false,
|
||||
this.hasTaprootInputs = false,
|
||||
this.isMweb = false,
|
||||
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
final WalletType type;
|
||||
|
@ -28,15 +33,19 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
final int fee;
|
||||
final String feeRate;
|
||||
final BasedUtxoNetwork? network;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
final bool hasChange;
|
||||
final bool hasTaprootInputs;
|
||||
bool isMweb;
|
||||
String? idOverride;
|
||||
String? hexOverride;
|
||||
List<String>? outputAddresses;
|
||||
|
||||
@override
|
||||
String get id => _tx.txId();
|
||||
String get id => idOverride ?? _tx.txId();
|
||||
|
||||
@override
|
||||
String get hex => _tx.serialize();
|
||||
String get hex => hexOverride ?? _tx.serialize();
|
||||
|
||||
@override
|
||||
String get amountFormatted => bitcoinAmountToString(amount: amount);
|
||||
|
@ -47,10 +56,22 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
@override
|
||||
int? get outputCount => _tx.outputs.length;
|
||||
|
||||
List<TxOutput> get outputs => _tx.outputs;
|
||||
|
||||
bool get hasSilentPayment => _tx.hasSilentPayment;
|
||||
|
||||
PendingChange? get change {
|
||||
try {
|
||||
final change = _tx.outputs.firstWhere((out) => out.isChange);
|
||||
return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
Future<void> _commit() async {
|
||||
int? callId;
|
||||
|
||||
final result = await electrumClient.broadcastTransaction(
|
||||
|
@ -78,11 +99,36 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
throw BitcoinTransactionCommitFailedBIP68Final();
|
||||
}
|
||||
|
||||
if (error.contains("min fee not met")) {
|
||||
throw BitcoinTransactionCommitFailedLessThanMin();
|
||||
}
|
||||
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
}
|
||||
|
||||
throw BitcoinTransactionCommitFailed();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ltcCommit() async {
|
||||
try {
|
||||
final stub = await CwMweb.stub();
|
||||
final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
|
||||
idOverride = resp.txid;
|
||||
} on GrpcError catch (e) {
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: e.message);
|
||||
} catch (e) {
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
if (isMweb) {
|
||||
await _ltcCommit();
|
||||
} else {
|
||||
await _commit();
|
||||
}
|
||||
|
||||
_listeners.forEach((listener) => listener(transactionInfo()));
|
||||
}
|
||||
|
@ -97,6 +143,9 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
direction: TransactionDirection.outgoing,
|
||||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
isReplaced: false,
|
||||
confirmations: 0,
|
||||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee);
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/address_to_output_script.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
|
||||
|
||||
String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) {
|
||||
final outputScript = addressToOutputScript(address, network);
|
||||
final parts = sha256.convert(outputScript).toString().split('');
|
||||
var res = '';
|
||||
|
||||
for (var i = parts.length - 1; i >= 0; i--) {
|
||||
final char = parts[i];
|
||||
i--;
|
||||
final nextChar = parts[i];
|
||||
res += nextChar;
|
||||
res += char;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
|
@ -17,6 +17,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.10"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -29,10 +37,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda"
|
||||
sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.3"
|
||||
version: "1.5.5"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -41,6 +49,15 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
bech32:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192"
|
||||
url: "https://github.com/cake-tech/bech32.git"
|
||||
source: git
|
||||
version: "0.2.2"
|
||||
bip32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -70,8 +87,8 @@ packages:
|
|||
dependency: "direct overridden"
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v5
|
||||
resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380
|
||||
ref: cake-update-v8
|
||||
resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2
|
||||
url: "https://github.com/cake-tech/bitcoin_base"
|
||||
source: git
|
||||
version: "4.7.0"
|
||||
|
@ -260,6 +277,13 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
cw_mweb:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../cw_mweb"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -293,13 +317,13 @@ packages:
|
|||
source: hosted
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.0"
|
||||
ffigen:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -379,6 +403,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
googleapis_auth:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: googleapis_auth
|
||||
sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -387,6 +419,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
grpc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: grpc
|
||||
sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.4"
|
||||
hex:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -419,6 +459,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
http2:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http2
|
||||
sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -553,10 +601,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
mobx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -662,13 +710,13 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.1"
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -678,13 +726,13 @@ packages:
|
|||
source: hosted
|
||||
version: "1.5.1"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "3.1.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -713,10 +761,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
|
||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
reactive_ble_mobile:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -855,7 +903,7 @@ packages:
|
|||
description:
|
||||
path: "."
|
||||
ref: "sp_v4.0.0"
|
||||
resolved-ref: "9b04f4b0af80dd7dae9274b496a53c23dcc80ea5"
|
||||
resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2
|
||||
url: "https://github.com/cake-tech/sp_scanner"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
|
|
@ -34,11 +34,16 @@ dependencies:
|
|||
ledger_bitcoin:
|
||||
git:
|
||||
url: https://github.com/cake-tech/ledger-bitcoin
|
||||
cw_mweb:
|
||||
path: ../cw_mweb
|
||||
grpc: ^3.2.4
|
||||
sp_scanner:
|
||||
git:
|
||||
url: https://github.com/cake-tech/sp_scanner
|
||||
ref: sp_v4.0.0
|
||||
|
||||
bech32:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bech32.git
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -54,10 +59,13 @@ dependency_overrides:
|
|||
url: https://github.com/cake-tech/ledger-flutter.git
|
||||
ref: cake-v3
|
||||
watcher: ^1.1.0
|
||||
protobuf: ^3.1.0
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v5
|
||||
ref: cake-update-v8
|
||||
pointycastle: 3.7.4
|
||||
ffi: 2.1.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
|
@ -83,7 +83,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await MnemonicBip39.toSeed(mnemonic, passphrase: passphrase),
|
||||
seedBytes: MnemonicBip39.toSeed(mnemonic, passphrase: passphrase),
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
|
|
|
@ -2,9 +2,21 @@ 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, String? passphrase})
|
||||
: super(name: name, walletInfo: walletInfo, password: password, passphrase: passphrase);
|
||||
BitcoinCashNewWalletCredentials({
|
||||
required String name,
|
||||
WalletInfo? walletInfo,
|
||||
String? password,
|
||||
String? passphrase,
|
||||
this.mnemonic,
|
||||
String? parentAddress,
|
||||
}) : super(
|
||||
name: name,
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
passphrase: passphrase,
|
||||
parentAddress: parentAddress
|
||||
);
|
||||
final String? mnemonic;
|
||||
}
|
||||
|
||||
class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
|
|
|
@ -36,7 +36,7 @@ class BitcoinCashWalletService extends WalletService<
|
|||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final wallet = await BitcoinCashWalletBase.create(
|
||||
mnemonic: await MnemonicBip39.generate(strength: strength),
|
||||
mnemonic: credentials.mnemonic ?? MnemonicBip39.generate(strength: strength),
|
||||
password: credentials.password!,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
|
|
|
@ -82,5 +82,7 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
confirmations: 0,
|
||||
fee: fee);
|
||||
fee: fee,
|
||||
isReplaced: false,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ dependency_overrides:
|
|||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v5
|
||||
ref: cake-update-v8
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
abstract class Balance {
|
||||
const Balance(this.available, this.additional);
|
||||
const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional});
|
||||
|
||||
final int available;
|
||||
|
||||
final int additional;
|
||||
|
||||
final int? secondAvailable;
|
||||
final int? secondAdditional;
|
||||
|
||||
String get formattedAvailableBalance;
|
||||
|
||||
String get formattedAdditionalBalance;
|
||||
|
||||
String get formattedUnAvailableBalance => '';
|
||||
String get formattedSecondAvailableBalance => '';
|
||||
String get formattedSecondAdditionalBalance => '';
|
||||
String get formattedFullAvailableBalance => formattedAvailableBalance;
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
CryptoCurrency.usdcTrc20,
|
||||
CryptoCurrency.tbtc,
|
||||
CryptoCurrency.wow,
|
||||
CryptoCurrency.ton,
|
||||
];
|
||||
|
||||
static const havenCurrencies = [
|
||||
|
@ -174,11 +175,11 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8);
|
||||
static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8);
|
||||
|
||||
static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8);
|
||||
static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8);
|
||||
static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18);
|
||||
static const maticpoly = CryptoCurrency(title: 'MATIC', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18);
|
||||
static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18);
|
||||
static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18);
|
||||
static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18);
|
||||
static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24);
|
||||
|
@ -215,14 +216,15 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18);
|
||||
static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18);
|
||||
static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29);
|
||||
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
|
||||
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
|
||||
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
|
||||
static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8);
|
||||
static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
|
||||
static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8);
|
||||
static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11);
|
||||
static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8);
|
||||
|
||||
|
||||
static final Map<int, CryptoCurrency> _rawCurrencyMap =
|
||||
|
|
|
@ -24,6 +24,11 @@ class TransactionCommitFailed implements Exception {
|
|||
final String? errorMessage;
|
||||
|
||||
TransactionCommitFailed({this.errorMessage});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return errorMessage??"unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionCommitFailedDustChange implements Exception {}
|
||||
|
@ -36,4 +41,6 @@ class TransactionCommitFailedVoutNegative implements Exception {}
|
|||
|
||||
class TransactionCommitFailedBIP68Final implements Exception {}
|
||||
|
||||
class TransactionCommitFailedLessThanMin implements Exception {}
|
||||
|
||||
class TransactionInputNotSupported implements Exception {}
|
||||
|
|
|
@ -267,6 +267,16 @@ const bitcoinDates = {
|
|||
"2023-01": 769810,
|
||||
};
|
||||
|
||||
Future<int> getBitcoinHeightByDateAPI({required DateTime date}) async {
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
"http://mempool.cakewallet.com:8999/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}",
|
||||
),
|
||||
);
|
||||
|
||||
return jsonDecode(response.body)['height'] as int;
|
||||
}
|
||||
|
||||
int getBitcoinHeightByDate({required DateTime date}) {
|
||||
String dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}';
|
||||
final closestKey = bitcoinDates.keys
|
||||
|
@ -300,6 +310,11 @@ DateTime getDateByBitcoinHeight(int height) {
|
|||
return estimatedDate;
|
||||
}
|
||||
|
||||
int getLtcHeightByDate({required DateTime date}) {
|
||||
// TODO: use the proxy layer to get the height with a binary search of blocked header heights
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: enhance all of this global const lists
|
||||
const wowDates = {
|
||||
"2023-12": 583048,
|
||||
|
@ -377,4 +392,3 @@ int getWowneroHeightByDate({required DateTime date}) {
|
|||
|
||||
return wowDates[closestKey] ?? 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,3 +18,4 @@ const SPL_TOKEN_TYPE_ID = 16;
|
|||
const DERIVATION_INFO_TYPE_ID = 17;
|
||||
const TRON_TOKEN_TYPE_ID = 18;
|
||||
const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
|
||||
const MWEB_UTXO_TYPE_ID = 20;
|
33
cw_core/lib/mweb_utxo.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'mweb_utxo.g.dart';
|
||||
|
||||
@HiveType(typeId: MWEB_UTXO_TYPE_ID)
|
||||
class MwebUtxo extends HiveObject {
|
||||
MwebUtxo({
|
||||
required this.height,
|
||||
required this.value,
|
||||
required this.address,
|
||||
required this.outputId,
|
||||
required this.blockTime,
|
||||
});
|
||||
|
||||
static const typeId = MWEB_UTXO_TYPE_ID;
|
||||
static const boxName = 'MwebUtxo';
|
||||
|
||||
@HiveField(0)
|
||||
int height;
|
||||
|
||||
@HiveField(1)
|
||||
int value;
|
||||
|
||||
@HiveField(2)
|
||||
String address;
|
||||
|
||||
@HiveField(3)
|
||||
String outputId;
|
||||
|
||||
@HiveField(4)
|
||||
int blockTime;
|
||||
}
|
|
@ -239,12 +239,15 @@ class Node extends HiveObject with Keyable {
|
|||
// you try to communicate with it
|
||||
Future<bool> requestElectrumServer() async {
|
||||
try {
|
||||
final Socket socket;
|
||||
if (useSSL == true) {
|
||||
await SecureSocket.connect(uri.host, uri.port,
|
||||
socket = await SecureSocket.connect(uri.host, uri.port,
|
||||
timeout: Duration(seconds: 5), onBadCertificate: (_) => true);
|
||||
} else {
|
||||
await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5));
|
||||
socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5));
|
||||
}
|
||||
|
||||
socket.destroy();
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
class PendingChange {
|
||||
final String address;
|
||||
final String amount;
|
||||
|
||||
PendingChange(this.address, this.amount);
|
||||
}
|
||||
|
||||
mixin PendingTransaction {
|
||||
String get id;
|
||||
String get amountFormatted;
|
||||
|
@ -5,6 +12,7 @@ mixin PendingTransaction {
|
|||
String? feeRate;
|
||||
String get hex;
|
||||
int? get outputCount => null;
|
||||
PendingChange? change;
|
||||
|
||||
Future<void> commit();
|
||||
}
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
class Subaddress {
|
||||
Subaddress({required this.id, required this.address, required this.label});
|
||||
Subaddress({
|
||||
required this.id,
|
||||
required this.address,
|
||||
required this.label,
|
||||
this.balance = null,
|
||||
this.txCount = null,
|
||||
});
|
||||
|
||||
Subaddress.fromMap(Map<String, Object?> map)
|
||||
: this.id = map['id'] == null ? 0 : int.parse(map['id'] as String),
|
||||
this.address = (map['address'] ?? '') as String,
|
||||
this.label = (map['label'] ?? '') as String;
|
||||
this.label = (map['label'] ?? '') as String,
|
||||
this.balance = (map['balance'] ?? '') as String?,
|
||||
this.txCount = (map['txCount'] ?? '') as int?;
|
||||
|
||||
final int id;
|
||||
final String address;
|
||||
final String label;
|
||||
final String? balance;
|
||||
final int? txCount;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ abstract class SyncStatus {
|
|||
}
|
||||
|
||||
class StartingScanSyncStatus extends SyncStatus {
|
||||
StartingScanSyncStatus(this.beginHeight);
|
||||
|
||||
final int beginHeight;
|
||||
@override
|
||||
double progress() => 0.0;
|
||||
}
|
||||
|
@ -59,7 +62,18 @@ class AttemptingSyncStatus extends SyncStatus {
|
|||
double progress() => 0.0;
|
||||
}
|
||||
|
||||
class FailedSyncStatus extends NotConnectedSyncStatus {}
|
||||
class AttemptingScanSyncStatus extends SyncStatus {
|
||||
@override
|
||||
double progress() => 0.0;
|
||||
}
|
||||
|
||||
class FailedSyncStatus extends NotConnectedSyncStatus {
|
||||
String? error;
|
||||
FailedSyncStatus({this.error});
|
||||
|
||||
@override
|
||||
String toString() => error ?? super.toString();
|
||||
}
|
||||
|
||||
class ConnectingSyncStatus extends SyncStatus {
|
||||
@override
|
||||
|
|
|
@ -18,6 +18,7 @@ abstract class TransactionInfo extends Object with Keyable {
|
|||
String? to;
|
||||
String? from;
|
||||
String? evmSignatureName;
|
||||
bool? isReplaced;
|
||||
List<String>? inputAddresses;
|
||||
List<String>? outputAddresses;
|
||||
|
||||
|
|
|
@ -1,26 +1,58 @@
|
|||
import 'package:cw_core/address_info.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
abstract class WalletAddresses {
|
||||
WalletAddresses(this.walletInfo)
|
||||
: addressesMap = {},
|
||||
allAddressesMap = {},
|
||||
addressInfos = {};
|
||||
addressInfos = {},
|
||||
usedAddresses = {},
|
||||
hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {},
|
||||
manualAddresses = walletInfo.manualAddresses?.toSet() ?? {};
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
|
||||
String get address;
|
||||
|
||||
String get latestAddress {
|
||||
if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) {
|
||||
if (addressesMap.keys.length == 0) return address;
|
||||
return addressesMap[addressesMap.keys.last] ?? address;
|
||||
}
|
||||
return _localAddress ?? address;
|
||||
}
|
||||
|
||||
String? get primaryAddress => null;
|
||||
|
||||
set address(String address);
|
||||
String? _localAddress;
|
||||
|
||||
set address(String address) => _localAddress = address;
|
||||
|
||||
String get addressForExchange => address;
|
||||
|
||||
Map<String, String> addressesMap;
|
||||
Map<String, String> allAddressesMap;
|
||||
|
||||
Map<String, String> get usableAddressesMap {
|
||||
final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map
|
||||
tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key));
|
||||
return tmp;
|
||||
}
|
||||
|
||||
Map<String, String> get usableAllAddressesMap {
|
||||
final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map
|
||||
tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key));
|
||||
return tmp;
|
||||
}
|
||||
|
||||
Map<int, List<AddressInfo>> addressInfos;
|
||||
|
||||
Set<String> usedAddresses = {};
|
||||
Set<String> usedAddresses;
|
||||
|
||||
Set<String> hiddenAddresses;
|
||||
|
||||
Set<String> manualAddresses;
|
||||
|
||||
Future<void> init();
|
||||
|
||||
|
@ -32,6 +64,8 @@ abstract class WalletAddresses {
|
|||
walletInfo.addresses = addressesMap;
|
||||
walletInfo.addressInfos = addressInfos;
|
||||
walletInfo.usedAddresses = usedAddresses.toList();
|
||||
walletInfo.hiddenAddresses = hiddenAddresses.toList();
|
||||
walletInfo.manualAddresses = manualAddresses.toList();
|
||||
|
||||
if (walletInfo.isInBox) {
|
||||
await walletInfo.save();
|
||||
|
|
|
@ -67,6 +67,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
|
||||
Future<void> startSync();
|
||||
|
||||
Future<void> stopSync() async {}
|
||||
|
||||
Future<PendingTransaction> createTransaction(Object credentials);
|
||||
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount);
|
||||
|
@ -81,7 +83,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
|
||||
Future<void> rescan({required int height});
|
||||
|
||||
void close();
|
||||
Future<void> close({required bool shouldCleanup});
|
||||
|
||||
Future<void> changePassword(String password);
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ abstract class WalletCredentials {
|
|||
this.passphrase,
|
||||
this.derivationInfo,
|
||||
this.hardwareWalletType,
|
||||
this.parentAddress,
|
||||
}) {
|
||||
if (this.walletInfo != null && derivationInfo != null) {
|
||||
this.walletInfo!.derivationInfo = derivationInfo;
|
||||
|
@ -18,6 +19,7 @@ abstract class WalletCredentials {
|
|||
|
||||
final String name;
|
||||
final int? height;
|
||||
String? parentAddress;
|
||||
int? seedPhraseLength;
|
||||
String? password;
|
||||
String? passphrase;
|
||||
|
|
|
@ -80,6 +80,7 @@ class WalletInfo extends HiveObject {
|
|||
this.showIntroCakePayCard,
|
||||
this.derivationInfo,
|
||||
this.hardwareWalletType,
|
||||
this.parentAddress,
|
||||
) : _yatLastUsedAddressController = StreamController<String>.broadcast();
|
||||
|
||||
factory WalletInfo.external({
|
||||
|
@ -97,6 +98,7 @@ class WalletInfo extends HiveObject {
|
|||
String yatLastUsedAddressRaw = '',
|
||||
DerivationInfo? derivationInfo,
|
||||
HardwareWalletType? hardwareWalletType,
|
||||
String? parentAddress,
|
||||
}) {
|
||||
return WalletInfo(
|
||||
id,
|
||||
|
@ -113,6 +115,7 @@ class WalletInfo extends HiveObject {
|
|||
showIntroCakePayCard,
|
||||
derivationInfo,
|
||||
hardwareWalletType,
|
||||
parentAddress,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -184,6 +187,18 @@ class WalletInfo extends HiveObject {
|
|||
@HiveField(21)
|
||||
HardwareWalletType? hardwareWalletType;
|
||||
|
||||
@HiveField(22)
|
||||
String? parentAddress;
|
||||
|
||||
@HiveField(23)
|
||||
List<String>? hiddenAddresses;
|
||||
|
||||
@HiveField(24)
|
||||
List<String>? manualAddresses;
|
||||
|
||||
|
||||
|
||||
|
||||
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
|
||||
|
||||
set yatLastUsedAddress(String address) {
|
||||
|
|
|
@ -27,6 +27,7 @@ class EthereumWallet extends EVMChainWallet {
|
|||
super.initialBalance,
|
||||
super.privateKey,
|
||||
required super.encryptionFileUtils,
|
||||
super.passphrase,
|
||||
}) : super(nativeCurrency: CryptoCurrency.eth);
|
||||
|
||||
@override
|
||||
|
@ -142,7 +143,7 @@ class EthereumWallet extends EVMChainWallet {
|
|||
if (!hasKeysFile) rethrow;
|
||||
}
|
||||
|
||||
final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ??
|
||||
final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ??
|
||||
EVMChainERC20Balance(BigInt.zero);
|
||||
|
||||
final WalletKeysData keysData;
|
||||
|
@ -150,8 +151,9 @@ class EthereumWallet extends EVMChainWallet {
|
|||
if (!hasKeysFile) {
|
||||
final mnemonic = data!['mnemonic'] as String?;
|
||||
final privateKey = data['private_key'] as String?;
|
||||
final passphrase = data['passphrase'] as String?;
|
||||
|
||||
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey);
|
||||
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
|
||||
} else {
|
||||
keysData = await WalletKeysFile.readKeysFile(
|
||||
name,
|
||||
|
@ -166,6 +168,7 @@ class EthereumWallet extends EVMChainWallet {
|
|||
password: password,
|
||||
mnemonic: keysData.mnemonic,
|
||||
privateKey: keysData.privateKey,
|
||||
passphrase: keysData.passphrase,
|
||||
initialBalance: balance,
|
||||
client: EthereumClient(),
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
|
|
|
@ -21,12 +21,13 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
Future<EthereumWallet> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final mnemonic = bip39.generateMnemonic(strength: strength);
|
||||
final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength);
|
||||
|
||||
final wallet = EthereumWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
passphrase: credentials.passphrase,
|
||||
client: client,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
|
@ -144,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
|||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
passphrase: credentials.passphrase,
|
||||
client: client,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
|
|
|
@ -70,6 +70,7 @@ abstract class EVMChainWalletBase
|
|||
required String password,
|
||||
EVMChainERC20Balance? initialBalance,
|
||||
required this.encryptionFileUtils,
|
||||
this.passphrase,
|
||||
}) : syncStatus = const NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_mnemonic = mnemonic,
|
||||
|
@ -178,6 +179,7 @@ abstract class EVMChainWalletBase
|
|||
mnemonic: _mnemonic,
|
||||
privateKey: _hexPrivateKey,
|
||||
password: _password,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
walletAddresses.address = _evmChainPrivateKey.address.hexEip55;
|
||||
}
|
||||
|
@ -262,7 +264,7 @@ abstract class EVMChainWalletBase
|
|||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
Future<void> close({required bool shouldCleanup}) async {
|
||||
_client.stop();
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
_updateFeesTimer?.cancel();
|
||||
|
@ -545,6 +547,7 @@ abstract class EVMChainWalletBase
|
|||
'mnemonic': _mnemonic,
|
||||
'private_key': privateKey,
|
||||
'balance': balance[currency]!.toJSON(),
|
||||
'passphrase': passphrase,
|
||||
});
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
|
@ -574,15 +577,19 @@ abstract class EVMChainWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<EthPrivateKey> getPrivateKey(
|
||||
{String? mnemonic, String? privateKey, required String password}) async {
|
||||
Future<EthPrivateKey> getPrivateKey({
|
||||
String? mnemonic,
|
||||
String? privateKey,
|
||||
required String password,
|
||||
String? passphrase,
|
||||
}) async {
|
||||
assert(mnemonic != null || privateKey != null);
|
||||
|
||||
if (privateKey != null) {
|
||||
return EthPrivateKey.fromHex(privateKey);
|
||||
}
|
||||
|
||||
final seed = bip39.mnemonicToSeed(mnemonic!);
|
||||
final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? '');
|
||||
|
||||
final root = bip32.BIP32.fromSeed(seed);
|
||||
|
||||
|
@ -716,4 +723,7 @@ abstract class EVMChainWalletBase
|
|||
|
||||
@override
|
||||
String get password => _password;
|
||||
|
||||
@override
|
||||
final String? passphrase;
|
||||
}
|
||||
|
|
|
@ -3,17 +3,26 @@ import 'package:cw_core/wallet_credentials.dart';
|
|||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class EVMChainNewWalletCredentials extends WalletCredentials {
|
||||
EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password})
|
||||
: super(name: name, walletInfo: walletInfo, password: password);
|
||||
EVMChainNewWalletCredentials({
|
||||
required super.name,
|
||||
super.walletInfo,
|
||||
super.password,
|
||||
super.parentAddress,
|
||||
this.mnemonic,
|
||||
super.passphrase,
|
||||
});
|
||||
|
||||
final String? mnemonic;
|
||||
}
|
||||
|
||||
class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
EVMChainRestoreWalletFromSeedCredentials({
|
||||
required String name,
|
||||
required String password,
|
||||
required super.name,
|
||||
required super.password,
|
||||
required this.mnemonic,
|
||||
WalletInfo? walletInfo,
|
||||
}) : super(name: name, password: password, walletInfo: walletInfo);
|
||||
super.walletInfo,
|
||||
super.passphrase,
|
||||
});
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ android {
|
|||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,9 @@ class SetupWalletException implements Exception {
|
|||
SetupWalletException({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -106,7 +106,7 @@ abstract class HavenWalletBase
|
|||
Future<void>? updateBalance() => null;
|
||||
|
||||
@override
|
||||
void close() {
|
||||
Future<void> close({required bool shouldCleanup}) async {
|
||||
_listener?.stop();
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_autoSaveTimer?.cancel();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:cw_core/wallet_addresses_with_account.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/account.dart';
|
||||
import 'package:cw_haven/api/wallet.dart';
|
||||
import 'package:cw_haven/haven_account_list.dart';
|
||||
import 'package:cw_haven/haven_subaddress_list.dart';
|
||||
import 'package:cw_core/subaddress.dart';
|
||||
|
@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
|
|||
@override
|
||||
Future<void> init() async {
|
||||
accountList.update();
|
||||
account = accountList.accounts.first;
|
||||
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
|
||||
updateSubaddressList(accountIndex: account?.id ?? 0);
|
||||
await updateAddressesInBox();
|
||||
}
|
||||
|
@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
|
|||
|
||||
void updateSubaddressList({required int accountIndex}) {
|
||||
subaddressList.update(accountIndex: accountIndex);
|
||||
subaddress = subaddressList.subaddresses.first;
|
||||
address = subaddress!.address;
|
||||
address = subaddressList.subaddresses.isNotEmpty
|
||||
? subaddressList.subaddresses.first.address
|
||||
: getAddress();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -116,7 +116,7 @@ class HavenWalletService extends WalletService<
|
|||
|
||||
if (!isValid) {
|
||||
await restoreOrResetWalletFiles(name);
|
||||
wallet.close();
|
||||
wallet.close(shouldCleanup: false);
|
||||
return openWallet(name, password);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,4 +2,7 @@ class SetupWalletException implements Exception {
|
|||
SetupWalletException({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -2,4 +2,7 @@ class WalletRestoreFromKeysException implements Exception {
|
|||
WalletRestoreFromKeysException({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -2,4 +2,7 @@ class WalletRestoreFromSeedException implements Exception {
|
|||
WalletRestoreFromSeedException({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import 'package:cw_monero/api/account_list.dart';
|
||||
import 'package:cw_monero/api/transaction_history.dart';
|
||||
import 'package:cw_monero/api/wallet.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
|
||||
|
@ -14,6 +15,10 @@ class SubaddressInfoMetadata {
|
|||
|
||||
SubaddressInfoMetadata? subaddress = null;
|
||||
|
||||
String getRawLabel({required int accountIndex, required int addressIndex}) {
|
||||
return monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
}
|
||||
|
||||
void refreshSubaddresses({required int accountIndex}) {
|
||||
try {
|
||||
isUpdating = true;
|
||||
|
@ -29,31 +34,94 @@ class Subaddress {
|
|||
Subaddress({
|
||||
required this.addressIndex,
|
||||
required this.accountIndex,
|
||||
required this.received,
|
||||
required this.txCount,
|
||||
});
|
||||
String get address => monero.Wallet_address(
|
||||
wptr!,
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndex,
|
||||
);
|
||||
late String address = getAddress(
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndex,
|
||||
);
|
||||
final int addressIndex;
|
||||
final int accountIndex;
|
||||
String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
final int received;
|
||||
final int txCount;
|
||||
String get label {
|
||||
final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen
|
||||
return "#$addressIndex ${localLabel}".trim();
|
||||
}
|
||||
}
|
||||
|
||||
class TinyTransactionDetails {
|
||||
TinyTransactionDetails({
|
||||
required this.address,
|
||||
required this.amount,
|
||||
});
|
||||
final List<String> address;
|
||||
final int amount;
|
||||
}
|
||||
|
||||
int lastWptr = 0;
|
||||
int lastTxCount = 0;
|
||||
List<TinyTransactionDetails> ttDetails = [];
|
||||
|
||||
List<Subaddress> getAllSubaddresses() {
|
||||
txhistory = monero.Wallet_history(wptr!);
|
||||
final txCount = monero.TransactionHistory_count(txhistory!);
|
||||
if (lastTxCount != txCount && lastWptr != wptr!.address) {
|
||||
final List<TinyTransactionDetails> newttDetails = [];
|
||||
lastTxCount = txCount;
|
||||
lastWptr = wptr!.address;
|
||||
for (var i = 0; i < txCount; i++) {
|
||||
final tx = monero.TransactionHistory_transaction(txhistory!, index: i);
|
||||
if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue;
|
||||
final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(",");
|
||||
final account = monero.TransactionInfo_subaddrAccount(tx);
|
||||
newttDetails.add(TinyTransactionDetails(
|
||||
address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
|
||||
amount: monero.TransactionInfo_amount(tx),
|
||||
));
|
||||
}
|
||||
ttDetails.clear();
|
||||
ttDetails.addAll(newttDetails);
|
||||
}
|
||||
final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
|
||||
final list = List.generate(size, (index) {
|
||||
final ttDetailsLocal = ttDetails.where((element) {
|
||||
final address = getAddress(
|
||||
accountIndex: subaddress!.accountIndex,
|
||||
addressIndex: index,
|
||||
);
|
||||
if (element.address.contains(address)) return true;
|
||||
return false;
|
||||
}).toList();
|
||||
int received = 0;
|
||||
for (var i = 0; i < ttDetailsLocal.length; i++) {
|
||||
received += ttDetailsLocal[i].amount;
|
||||
}
|
||||
return Subaddress(
|
||||
accountIndex: subaddress!.accountIndex,
|
||||
addressIndex: index,
|
||||
received: received,
|
||||
txCount: ttDetailsLocal.length,
|
||||
);
|
||||
}).reversed.toList();
|
||||
if (list.length == 0) {
|
||||
list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0));
|
||||
list.add(
|
||||
Subaddress(
|
||||
addressIndex: subaddress!.accountIndex,
|
||||
accountIndex: 0,
|
||||
received: 0,
|
||||
txCount: 0,
|
||||
));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
int numSubaddresses(int subaccountIndex) {
|
||||
return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex);
|
||||
}
|
||||
|
||||
void addSubaddressSync({required int accountIndex, required String label}) {
|
||||
monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label);
|
||||
refreshSubaddresses(accountIndex: accountIndex);
|
||||
|
|
|
@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart';
|
|||
import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
|
||||
import 'package:cw_monero/api/monero_output.dart';
|
||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||
import 'package:cw_monero/api/wallet.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
|
||||
String getTxKey(String txId) {
|
||||
return monero.Wallet_getTxKey(wptr!, txid: txId);
|
||||
}
|
||||
|
||||
final txHistoryMutex = Mutex();
|
||||
monero.TransactionHistory? txhistory;
|
||||
|
||||
void refreshTransactions() {
|
||||
bool isRefreshingTx = false;
|
||||
Future<void> refreshTransactions() async {
|
||||
if (isRefreshingTx == true) return;
|
||||
isRefreshingTx = true;
|
||||
txhistory ??= monero.Wallet_history(wptr!);
|
||||
monero.TransactionHistory_refresh(txhistory!);
|
||||
final ptr = txhistory!.address;
|
||||
await txHistoryMutex.acquire();
|
||||
await Isolate.run(() {
|
||||
monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
|
||||
});
|
||||
txHistoryMutex.release();
|
||||
isRefreshingTx = false;
|
||||
}
|
||||
|
||||
int countOfTransactions() => monero.TransactionHistory_count(txhistory!);
|
||||
|
||||
List<Transaction> getAllTransactions() {
|
||||
Future<List<Transaction>> getAllTransactions() async {
|
||||
List<Transaction> dummyTxs = [];
|
||||
|
||||
|
||||
await txHistoryMutex.acquire();
|
||||
txhistory ??= monero.Wallet_history(wptr!);
|
||||
monero.TransactionHistory_refresh(txhistory!);
|
||||
int size = countOfTransactions();
|
||||
final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index)));
|
||||
|
||||
txHistoryMutex.release();
|
||||
final accts = monero.Wallet_numSubaddressAccounts(wptr!);
|
||||
for (var i = 0; i < accts; i++) {
|
||||
final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i);
|
||||
|
@ -45,6 +55,8 @@ List<Transaction> getAllTransactions() {
|
|||
confirmations: 0,
|
||||
blockheight: 0,
|
||||
accountIndex: i,
|
||||
addressIndex: 0,
|
||||
addressIndexList: [0],
|
||||
paymentId: "",
|
||||
amount: fullBalance - availBalance,
|
||||
isSpend: false,
|
||||
|
@ -251,19 +263,28 @@ Future<PendingTransactionDescription> createTransactionMultDest(
|
|||
|
||||
class Transaction {
|
||||
final String displayLabel;
|
||||
String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0);
|
||||
late final String address = monero.Wallet_address(
|
||||
late final String subaddressLabel = monero.Wallet_getSubaddressLabel(
|
||||
wptr!,
|
||||
accountIndex: 0,
|
||||
addressIndex: 0,
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndex,
|
||||
);
|
||||
late final String address = getAddress(
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndex,
|
||||
);
|
||||
late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
|
||||
getAddress(
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndexList[index],
|
||||
));
|
||||
final String description;
|
||||
final int fee;
|
||||
final int confirmations;
|
||||
late final bool isPending = confirmations < 10;
|
||||
final int blockheight;
|
||||
final int addressIndex = 0;
|
||||
final int addressIndex;
|
||||
final int accountIndex;
|
||||
final List<int> addressIndexList;
|
||||
final String paymentId;
|
||||
final int amount;
|
||||
final bool isSpend;
|
||||
|
@ -309,6 +330,8 @@ class Transaction {
|
|||
amount = monero.TransactionInfo_amount(txInfo),
|
||||
paymentId = monero.TransactionInfo_paymentId(txInfo),
|
||||
accountIndex = monero.TransactionInfo_subaddrAccount(txInfo),
|
||||
addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
|
||||
addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
|
||||
blockheight = monero.TransactionInfo_blockHeight(txInfo),
|
||||
confirmations = monero.TransactionInfo_confirmations(txInfo),
|
||||
fee = monero.TransactionInfo_fee(txInfo),
|
||||
|
@ -319,27 +342,9 @@ class Transaction {
|
|||
final txKey = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo));
|
||||
final status = monero.Wallet_status(wptr!);
|
||||
if (status != 0) {
|
||||
return monero.Wallet_errorString(wptr!);
|
||||
return "";
|
||||
}
|
||||
return breakTxKey(txKey);
|
||||
}
|
||||
|
||||
static String breakTxKey(String input) {
|
||||
final x = 64;
|
||||
StringBuffer buffer = StringBuffer();
|
||||
|
||||
for (int i = 0; i < input.length; i += x) {
|
||||
int endIndex = i + x;
|
||||
if (endIndex > input.length) {
|
||||
endIndex = input.length;
|
||||
}
|
||||
buffer.write(input.substring(i, endIndex));
|
||||
if (endIndex != input.length) {
|
||||
buffer.write('\n\n');
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
return txKey;
|
||||
}
|
||||
|
||||
Transaction.dummy({
|
||||
|
@ -349,6 +354,8 @@ class Transaction {
|
|||
required this.confirmations,
|
||||
required this.blockheight,
|
||||
required this.accountIndex,
|
||||
required this.addressIndexList,
|
||||
required this.addressIndex,
|
||||
required this.paymentId,
|
||||
required this.amount,
|
||||
required this.isSpend,
|
||||
|
|
|
@ -82,9 +82,20 @@ String getPassphrase() {
|
|||
return monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase");
|
||||
}
|
||||
|
||||
String getAddress({int accountIndex = 0, int addressIndex = 0}) =>
|
||||
monero.Wallet_address(wptr!,
|
||||
Map<int, Map<int, Map<int, String>>> addressCache = {};
|
||||
|
||||
String getAddress({int accountIndex = 0, int addressIndex = 0}) {
|
||||
// print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}");
|
||||
while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
|
||||
print("adding subaddress");
|
||||
monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
|
||||
}
|
||||
addressCache[wptr!.address] ??= {};
|
||||
addressCache[wptr!.address]![accountIndex] ??= {};
|
||||
addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!,
|
||||
accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
|
||||
}
|
||||
|
||||
int getFullBalance({int accountIndex = 0}) =>
|
||||
monero.Wallet_balance(wptr!, accountIndex: accountIndex);
|
||||
|
|
|
@ -130,7 +130,7 @@ void restoreWalletFromKeysSync(
|
|||
int nettype = 0,
|
||||
int restoreHeight = 0}) {
|
||||
txhistory = null;
|
||||
final newWptr = spendKey != ""
|
||||
var newWptr = (spendKey != "")
|
||||
? monero.WalletManager_createDeterministicWalletFromSpendKey(
|
||||
wmPtr,
|
||||
path: path,
|
||||
|
@ -156,6 +156,32 @@ void restoreWalletFromKeysSync(
|
|||
message: monero.Wallet_errorString(newWptr));
|
||||
}
|
||||
|
||||
// CW-712 - Try to restore deterministic wallet first, if the view key doesn't
|
||||
// match the view key provided
|
||||
if (spendKey != "") {
|
||||
final viewKeyRestored = monero.Wallet_secretViewKey(newWptr);
|
||||
if (viewKey != viewKeyRestored && viewKey != "") {
|
||||
monero.WalletManager_closeWallet(wmPtr, newWptr, false);
|
||||
File(path).deleteSync();
|
||||
File(path+".keys").deleteSync();
|
||||
newWptr = monero.WalletManager_createWalletFromKeys(
|
||||
wmPtr,
|
||||
path: path,
|
||||
password: password,
|
||||
restoreHeight: restoreHeight,
|
||||
addressString: address,
|
||||
viewKeyString: viewKey,
|
||||
spendKeyString: spendKey,
|
||||
nettype: 0,
|
||||
);
|
||||
final status = monero.Wallet_status(newWptr);
|
||||
if (status != 0) {
|
||||
throw WalletRestoreFromKeysException(
|
||||
message: monero.Wallet_errorString(newWptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wptr = newWptr;
|
||||
|
||||
openedWalletsByPath[path] = wptr!;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:cw_core/subaddress.dart';
|
||||
import 'package:cw_monero/api/coins_info.dart';
|
||||
import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list;
|
||||
import 'package:cw_monero/api/wallet.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
|
@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store {
|
|||
final address = s.address;
|
||||
final label = s.label;
|
||||
final id = s.addressIndex;
|
||||
final hasDefaultAddressName =
|
||||
label.toLowerCase() == 'Primary account'.toLowerCase() ||
|
||||
label.toLowerCase() == 'Untitled account'.toLowerCase();
|
||||
final isPrimaryAddress = id == 0 && hasDefaultAddressName;
|
||||
return Subaddress(
|
||||
id: id,
|
||||
address: address,
|
||||
label: isPrimaryAddress
|
||||
? 'Primary address'
|
||||
: hasDefaultAddressName
|
||||
? ''
|
||||
: label);
|
||||
balance: (s.received/1e12).toStringAsFixed(6),
|
||||
txCount: s.txCount,
|
||||
label: label);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store {
|
|||
required List<String> usedAddresses,
|
||||
}) async {
|
||||
_usedAddresses.addAll(usedAddresses);
|
||||
final _all = _usedAddresses.toSet().toList();
|
||||
_usedAddresses.clear();
|
||||
_usedAddresses.addAll(_all);
|
||||
if (_isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store {
|
|||
Future<List<Subaddress>> _getAllUnusedAddresses(
|
||||
{required int accountIndex, required String label}) async {
|
||||
final allAddresses = subaddress_list.getAllSubaddresses();
|
||||
if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) {
|
||||
// first because addresses come in reversed order.
|
||||
if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) {
|
||||
final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label);
|
||||
if (!isAddressUnused) {
|
||||
return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label);
|
||||
|
@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store {
|
|||
return Subaddress(
|
||||
id: id,
|
||||
address: address,
|
||||
balance: (s.received/1e12).toStringAsFixed(6),
|
||||
txCount: s.txCount,
|
||||
label: id == 0 &&
|
||||
label.toLowerCase() == 'Primary account'.toLowerCase()
|
||||
? 'Primary address'
|
||||
: label);
|
||||
})
|
||||
.toList();
|
||||
}).toList().reversed.toList();
|
||||
}
|
||||
|
||||
Future<bool> _newSubaddress({required int accountIndex, required String label}) async {
|
||||
|
|
|
@ -59,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
}),
|
||||
_isTransactionUpdating = false,
|
||||
_hasSyncAfterStartup = false,
|
||||
isEnabledAutoGenerateSubaddress = false,
|
||||
isEnabledAutoGenerateSubaddress = true,
|
||||
_password = password,
|
||||
syncStatus = NotConnectedSyncStatus(),
|
||||
unspentCoins = [],
|
||||
|
@ -86,6 +86,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
|
||||
_updateSubAddress(enabled, account: walletAddresses.account);
|
||||
});
|
||||
_onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) {
|
||||
_updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account);
|
||||
});
|
||||
}
|
||||
|
||||
static const int _autoSaveInterval = 30;
|
||||
|
@ -131,6 +134,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
|
||||
monero_wallet.SyncListener? _listener;
|
||||
ReactionDisposer? _onAccountChangeReaction;
|
||||
ReactionDisposer? _onTxHistoryChangeReaction;
|
||||
bool _isTransactionUpdating;
|
||||
bool _hasSyncAfterStartup;
|
||||
Timer? _autoSaveTimer;
|
||||
|
@ -161,15 +165,18 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
|
||||
_autoSaveTimer = Timer.periodic(
|
||||
Duration(seconds: _autoSaveInterval), (_) async => await save());
|
||||
// update transaction details after restore
|
||||
walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? updateBalance() => null;
|
||||
|
||||
@override
|
||||
void close() async {
|
||||
Future<void> close({required bool shouldCleanup}) async {
|
||||
_listener?.stop();
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_onTxHistoryChangeReaction?.reaction.dispose();
|
||||
_autoSaveTimer?.cancel();
|
||||
}
|
||||
|
||||
|
@ -581,7 +588,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
@override
|
||||
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
|
||||
transaction_history.refreshTransactions();
|
||||
return _getAllTransactionsOfAccount(walletAddresses.account?.id)
|
||||
return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
|
||||
.fold<Map<String, MoneroTransactionInfo>>(
|
||||
<String, MoneroTransactionInfo>{},
|
||||
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
|
||||
|
@ -597,8 +604,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
transactionHistory.clear();
|
||||
final transactions = await fetchTransactions();
|
||||
transactionHistory.clear();
|
||||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
|
@ -611,9 +618,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
String getSubaddressLabel(int accountIndex, int addressIndex) =>
|
||||
monero_wallet.getSubaddressLabel(accountIndex, addressIndex);
|
||||
|
||||
List<MoneroTransactionInfo> _getAllTransactionsOfAccount(int? accountIndex) =>
|
||||
transaction_history
|
||||
.getAllTransactions()
|
||||
Future<List<MoneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
|
||||
(await transaction_history
|
||||
.getAllTransactions())
|
||||
.map(
|
||||
(row) => MoneroTransactionInfo(
|
||||
row.hash,
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart';
|
|||
import 'package:cw_core/subaddress.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list;
|
||||
import 'package:cw_monero/api/transaction_history.dart';
|
||||
import 'package:cw_monero/api/wallet.dart';
|
||||
import 'package:cw_monero/monero_account_list.dart';
|
||||
import 'package:cw_monero/monero_subaddress_list.dart';
|
||||
|
@ -27,6 +29,30 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
|
|||
@observable
|
||||
String address;
|
||||
|
||||
@override
|
||||
String get latestAddress {
|
||||
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
|
||||
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
while (hiddenAddresses.contains(address)) {
|
||||
addressIndex++;
|
||||
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
subaddressList.update(accountIndex: account?.id??0);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
@override
|
||||
String get addressForExchange {
|
||||
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
|
||||
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) {
|
||||
addressIndex++;
|
||||
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
subaddressList.update(accountIndex: account?.id??0);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
@observable
|
||||
Account? account;
|
||||
|
||||
|
@ -37,10 +63,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
MoneroAccountList accountList;
|
||||
|
||||
Set<String> usedAddresses = Set();
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
accountList.update();
|
||||
account = accountList.accounts.first;
|
||||
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
|
||||
updateSubaddressList(accountIndex: account?.id ?? 0);
|
||||
await updateAddressesInBox();
|
||||
}
|
||||
|
@ -89,8 +117,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
void updateSubaddressList({required int accountIndex}) {
|
||||
subaddressList.update(accountIndex: accountIndex);
|
||||
subaddress = subaddressList.subaddresses.first;
|
||||
address = subaddress!.address;
|
||||
address = subaddressList.subaddresses.isNotEmpty
|
||||
? subaddressList.subaddresses.first.address
|
||||
: getAddress();
|
||||
}
|
||||
|
||||
Future<void> updateUsedSubaddress() async {
|
||||
|
@ -109,7 +138,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
|
|||
accountIndex: accountIndex,
|
||||
defaultLabel: defaultLabel,
|
||||
usedAddresses: usedAddresses.toList());
|
||||
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last;
|
||||
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
|
||||
if (num.tryParse(subaddress!.balance??'0') != 0) {
|
||||
getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1);
|
||||
}
|
||||
address = subaddress!.address;
|
||||
}
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ class MoneroWalletService extends WalletService<
|
|||
|
||||
if (!isValid) {
|
||||
await restoreOrResetWalletFiles(name);
|
||||
wallet.close();
|
||||
wallet.close(shouldCleanup: false);
|
||||
return openWallet(name, password);
|
||||
}
|
||||
|
||||
|
|
|
@ -463,8 +463,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "impls/monero.dart"
|
||||
ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7"
|
||||
resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7"
|
||||
ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b"
|
||||
resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b"
|
||||
url: "https://github.com/mrcyjanek/monero_c"
|
||||
source: git
|
||||
version: "0.0.0"
|
||||
|
|
|
@ -25,7 +25,7 @@ dependencies:
|
|||
monero:
|
||||
git:
|
||||
url: https://github.com/mrcyjanek/monero_c
|
||||
ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash
|
||||
ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
|
||||
path: impls/monero.dart
|
||||
mutex: ^3.1.0
|
||||
|
||||
|
|
30
cw_mweb/.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
36
cw_mweb/.metadata
Normal file
|
@ -0,0 +1,36 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
- platform: android
|
||||
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
- platform: ios
|
||||
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
- platform: macos
|
||||
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
3
cw_mweb/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
cw_mweb/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
15
cw_mweb/README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# cw_mweb
|
||||
|
||||
A new Flutter plugin project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter
|
||||
[plug-in package](https://flutter.dev/developing-packages/),
|
||||
a specialized package that includes platform-specific implementation code for
|
||||
Android and/or iOS.
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
4
cw_mweb/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
10
cw_mweb/android/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/libs
|
||||
.cxx
|
76
cw_mweb/android/build.gradle
Normal file
|
@ -0,0 +1,76 @@
|
|||
group 'com.cakewallet.mweb'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs project(':cw_mweb').file('libs')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
test.java.srcDirs += 'src/test/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-test'
|
||||
testImplementation 'org.mockito:mockito-core:5.0.0'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation (name: 'mwebd', ext: 'aar')
|
||||
}
|
1
cw_mweb/android/settings.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
rootProject.name = 'cw_mweb'
|
3
cw_mweb/android/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.cakewallet.mweb">
|
||||
</manifest>
|
|
@ -0,0 +1,66 @@
|
|||
package com.cakewallet.mweb
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
import mwebd.Mwebd
|
||||
import mwebd.Server
|
||||
|
||||
/** CwMwebPlugin */
|
||||
class CwMwebPlugin: FlutterPlugin, MethodCallHandler {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
/// when the Flutter Engine is detached from the Activity
|
||||
private lateinit var channel : MethodChannel
|
||||
private var server: Server? = null
|
||||
private var port: Long? = null
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
if (call.method == "start") {
|
||||
server?.stop()
|
||||
val dataDir = call.argument("dataDir") ?: ""
|
||||
val nodeUri = call.argument("nodeUri") ?: ""
|
||||
server = server ?: Mwebd.newServer("", dataDir, nodeUri)
|
||||
port = server?.start(0)
|
||||
result.success(port)
|
||||
} else if (call.method == "stop") {
|
||||
server?.stop()
|
||||
server = null
|
||||
port = null
|
||||
result.success(null)
|
||||
} else if (call.method == "address") {
|
||||
// val scanSecret: ByteArray = call.argument<ByteArray>("scanSecret") ?: ByteArray(0)
|
||||
// val spendPub: ByteArray = call.argument<ByteArray>("spendPub") ?: ByteArray(0)
|
||||
// val index: Int = call.argument<Int>("index") ?: 0
|
||||
// val res = Mwebd.address(scanSecret, spendPub, index)
|
||||
// result.success(res)
|
||||
} else if (call.method == "addresses") {
|
||||
val scanSecret: ByteArray = call.argument<ByteArray>("scanSecret") ?: ByteArray(0)
|
||||
val spendPub: ByteArray = call.argument<ByteArray>("spendPub") ?: ByteArray(0)
|
||||
val fromIndex: Int = call.argument<Int>("fromIndex") ?: 0
|
||||
val toIndex: Int = call.argument<Int>("toIndex") ?: 0
|
||||
val res = Mwebd.addresses(scanSecret, spendPub, fromIndex, toIndex)
|
||||
result.success(res)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
server?.stop()
|
||||
server = null
|
||||
port = null
|
||||
}
|
||||
}
|
38
cw_mweb/ios/.gitignore
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
.idea/
|
||||
.vagrant/
|
||||
.sconsign.dblite
|
||||
.svn/
|
||||
|
||||
.DS_Store
|
||||
*.swp
|
||||
profile
|
||||
|
||||
DerivedData/
|
||||
build/
|
||||
GeneratedPluginRegistrant.h
|
||||
GeneratedPluginRegistrant.m
|
||||
|
||||
.generated/
|
||||
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
|
||||
!default.pbxuser
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.perspectivev3
|
||||
|
||||
xcuserdata
|
||||
|
||||
*.moved-aside
|
||||
|
||||
*.pyc
|
||||
*sync/
|
||||
Icon?
|
||||
.tags*
|
||||
|
||||
/Flutter/Generated.xcconfig
|
||||
/Flutter/ephemeral/
|
||||
/Flutter/flutter_export_environment.sh
|
0
cw_mweb/ios/Assets/.gitkeep
Normal file
100
cw_mweb/ios/Classes/CwMwebPlugin.swift
Normal file
|
@ -0,0 +1,100 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
import Mwebd
|
||||
|
||||
public class CwMwebPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger())
|
||||
let instance = CwMwebPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
private static var server: MwebdServer?
|
||||
private static var port: Int = 0
|
||||
private static var dataDir: String?
|
||||
private static var nodeUri: String?
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "getPlatformVersion":
|
||||
result("iOS " + UIDevice.current.systemVersion)
|
||||
break
|
||||
case "start":
|
||||
stopServer()
|
||||
let args = call.arguments as? [String: String]
|
||||
let dataDir = args?["dataDir"]
|
||||
let nodeUri = args?["nodeUri"]
|
||||
CwMwebPlugin.dataDir = dataDir
|
||||
CwMwebPlugin.nodeUri = nodeUri
|
||||
startServer(result: result)
|
||||
break
|
||||
case "stop":
|
||||
stopServer()
|
||||
result(nil)
|
||||
break
|
||||
// case "address":
|
||||
// let args = call.arguments as! [String: Any]
|
||||
// let scanSecret = args["scanSecret"] as! FlutterStandardTypedData
|
||||
// let spendPub = args["spendPub"] as! FlutterStandardTypedData
|
||||
// let index = args["index"] as! Int32
|
||||
|
||||
// let scanSecretData = scanSecret.data
|
||||
// let spendPubData = spendPub.data
|
||||
// result(MwebdAddress(scanSecretData, spendPubData, index))
|
||||
// break
|
||||
case "addresses":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let scanSecret = args["scanSecret"] as! FlutterStandardTypedData
|
||||
let spendPub = args["spendPub"] as! FlutterStandardTypedData
|
||||
let fromIndex = args["fromIndex"] as! Int32
|
||||
let toIndex = args["toIndex"] as! Int32
|
||||
|
||||
let scanSecretData = scanSecret.data
|
||||
let spendPubData = spendPub.data
|
||||
result(MwebdAddresses(scanSecretData, spendPubData, fromIndex, toIndex))
|
||||
break
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func startServer(result: @escaping FlutterResult) {
|
||||
if CwMwebPlugin.server == nil {
|
||||
var error: NSError?
|
||||
CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, CwMwebPlugin.nodeUri, &error)
|
||||
|
||||
if let server = CwMwebPlugin.server {
|
||||
do {
|
||||
print("Starting server...")
|
||||
try server.start(0, ret0_: &CwMwebPlugin.port)
|
||||
print("Server started successfully on port: \(CwMwebPlugin.port)")
|
||||
result(CwMwebPlugin.port)
|
||||
} catch let startError as NSError {
|
||||
print("Server Start Error: \(startError.localizedDescription)")
|
||||
result(FlutterError(code: "Server Start Error", message: startError.localizedDescription, details: nil))
|
||||
}
|
||||
} else if let error = error {
|
||||
print("Server Creation Error: \(error.localizedDescription)")
|
||||
result(FlutterError(code: "Server Creation Error", message: error.localizedDescription, details: nil))
|
||||
} else {
|
||||
print("Unknown Error: Failed to create server")
|
||||
result(FlutterError(code: "Unknown Error", message: "Failed to create server", details: nil))
|
||||
}
|
||||
} else {
|
||||
print("Server already running on port: \(CwMwebPlugin.port)")
|
||||
result(CwMwebPlugin.port)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopServer() {
|
||||
print("Stopping server")
|
||||
CwMwebPlugin.server?.stop()
|
||||
CwMwebPlugin.server = nil
|
||||
CwMwebPlugin.port = 0
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopServer()
|
||||
}
|
||||
}
|