Merge branch 'main' into zano-pr
BIN
assets/images/flags/abw.png
Normal file
After Width: | Height: | Size: 607 B |
BIN
assets/images/flags/afg.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/images/flags/ago.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/flags/aia.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/flags/and.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/flags/asm.png
Normal file
After Width: | Height: | Size: 503 B |
BIN
assets/images/flags/atf.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/flags/atg.png
Normal file
After Width: | Height: | Size: 963 B |
BIN
assets/images/flags/aut.png
Normal file
After Width: | Height: | Size: 125 B |
BIN
assets/images/flags/aze.png
Normal file
After Width: | Height: | Size: 717 B |
BIN
assets/images/flags/bel.png
Normal file
After Width: | Height: | Size: 127 B |
BIN
assets/images/flags/bes.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
assets/images/flags/bhr.png
Normal file
After Width: | Height: | Size: 567 B |
BIN
assets/images/flags/blz.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/images/flags/bmu.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/flags/bol.png
Normal file
After Width: | Height: | Size: 668 B |
BIN
assets/images/flags/brn.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/flags/btn.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/images/flags/bvt.png
Normal file
After Width: | Height: | Size: 147 B |
BIN
assets/images/flags/bwa.png
Normal file
After Width: | Height: | Size: 134 B |
BIN
assets/images/flags/cck.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/flags/cmr.png
Normal file
After Width: | Height: | Size: 402 B |
BIN
assets/images/flags/cok.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/flags/cpv.png
Normal file
After Width: | Height: | Size: 297 B |
BIN
assets/images/flags/cri.png
Normal file
After Width: | Height: | Size: 136 B |
BIN
assets/images/flags/cuw.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
assets/images/flags/cxr.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/flags/cyp.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/flags/dji.png
Normal file
After Width: | Height: | Size: 387 B |
BIN
assets/images/flags/dma.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/flags/dza.png
Normal file
After Width: | Height: | Size: 518 B |
BIN
assets/images/flags/ecu.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/flags/est.png
Normal file
After Width: | Height: | Size: 130 B |
BIN
assets/images/flags/eth.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/flags/fin.png
Normal file
After Width: | Height: | Size: 131 B |
BIN
assets/images/flags/fji.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/flags/flk.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/images/flags/fro.png
Normal file
After Width: | Height: | Size: 147 B |
BIN
assets/images/flags/fsm.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
assets/images/flags/gab.png
Normal file
After Width: | Height: | Size: 130 B |
BIN
assets/images/flags/geo.png
Normal file
After Width: | Height: | Size: 187 B |
BIN
assets/images/flags/ggi.png
Normal file
After Width: | Height: | Size: 892 B |
BIN
assets/images/flags/ggy.png
Normal file
After Width: | Height: | Size: 192 B |
BIN
assets/images/flags/glp.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/images/flags/gmb.png
Normal file
After Width: | Height: | Size: 278 B |
BIN
assets/images/flags/grc.png
Normal file
After Width: | Height: | Size: 436 B |
BIN
assets/images/flags/grd.png
Normal file
After Width: | Height: | Size: 892 B |
BIN
assets/images/flags/grl.png
Normal file
After Width: | Height: | Size: 407 B |
BIN
assets/images/flags/guf.png
Normal file
After Width: | Height: | Size: 581 B |
BIN
assets/images/flags/gum.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/flags/guy.png
Normal file
After Width: | Height: | Size: 635 B |
BIN
assets/images/flags/hmd.png
Normal file
After Width: | Height: | Size: 780 B |
BIN
assets/images/flags/iot.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/images/flags/irl.png
Normal file
After Width: | Height: | Size: 127 B |
BIN
assets/images/flags/jam.png
Normal file
After Width: | Height: | Size: 526 B |
BIN
assets/images/flags/jey.png
Normal file
After Width: | Height: | Size: 914 B |
BIN
assets/images/flags/jor.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/images/flags/kaz.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/images/flags/ken.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
assets/images/flags/kir.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/images/flags/kwt.png
Normal file
After Width: | Height: | Size: 372 B |
BIN
assets/images/flags/lbn.png
Normal file
After Width: | Height: | Size: 960 B |
BIN
assets/images/flags/lie.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
assets/images/flags/lka.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/images/flags/ltu.png
Normal file
After Width: | Height: | Size: 130 B |
BIN
assets/images/flags/lux.png
Normal file
After Width: | Height: | Size: 130 B |
BIN
assets/images/flags/lva.png
Normal file
After Width: | Height: | Size: 124 B |
BIN
assets/images/flags/mco.png
Normal file
After Width: | Height: | Size: 204 B |
BIN
assets/images/flags/mlt.png
Normal file
After Width: | Height: | Size: 485 B |
BIN
assets/images/flags/mnp.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
assets/images/flags/mrt.png
Normal file
After Width: | Height: | Size: 509 B |
BIN
assets/images/flags/msr.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/flags/mtq.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/flags/mwi.png
Normal file
After Width: | Height: | Size: 754 B |
BIN
assets/images/flags/myt.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
assets/images/flags/ner.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
assets/images/flags/nfk.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/flags/niu.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/flags/omn.png
Normal file
After Width: | Height: | Size: 711 B |
BIN
assets/images/flags/per.png
Normal file
After Width: | Height: | Size: 123 B |
BIN
assets/images/flags/plw.png
Normal file
After Width: | Height: | Size: 608 B |
BIN
assets/images/flags/pri.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/images/flags/pyf.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/flags/qat.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
assets/images/flags/slb.png
Normal file
After Width: | Height: | Size: 403 B |
BIN
assets/images/flags/slv.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
assets/images/flags/svk.png
Normal file
After Width: | Height: | Size: 521 B |
BIN
assets/images/flags/svn.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/flags/tkm.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/images/flags/ton.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
assets/images/flags/tuv.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/flags/ury.png
Normal file
After Width: | Height: | Size: 1,017 B |
BIN
assets/images/flags/vat.png
Normal file
After Width: | Height: | Size: 851 B |
BIN
assets/images/flags/vir.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/flags/vut.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
|
@ -124,6 +124,7 @@ class ElectrumClient {
|
||||||
final errorMsg = error.toString();
|
final errorMsg = error.toString();
|
||||||
print(errorMsg);
|
print(errorMsg);
|
||||||
unterminatedString = '';
|
unterminatedString = '';
|
||||||
|
socket = null;
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
print("SOCKET CLOSED!!!!!");
|
print("SOCKET CLOSED!!!!!");
|
||||||
|
@ -132,6 +133,7 @@ class ElectrumClient {
|
||||||
if (host == socket?.address.host || socket == null) {
|
if (host == socket?.address.host || socket == null) {
|
||||||
_setConnectionStatus(ConnectionStatus.disconnected);
|
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||||
socket?.destroy();
|
socket?.destroy();
|
||||||
|
socket = null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("onDone: $e");
|
print("onDone: $e");
|
||||||
|
|
|
@ -24,9 +24,12 @@ class ElectrumBalance extends Balance {
|
||||||
final decoded = json.decode(jsonSource) as Map;
|
final decoded = json.decode(jsonSource) as Map;
|
||||||
|
|
||||||
return ElectrumBalance(
|
return ElectrumBalance(
|
||||||
confirmed: decoded['confirmed'] as int? ?? 0,
|
confirmed: decoded['confirmed'] as int? ?? 0,
|
||||||
unconfirmed: decoded['unconfirmed'] as int? ?? 0,
|
unconfirmed: decoded['unconfirmed'] as int? ?? 0,
|
||||||
frozen: decoded['frozen'] as int? ?? 0);
|
frozen: decoded['frozen'] as int? ?? 0,
|
||||||
|
secondConfirmed: decoded['secondConfirmed'] as int? ?? 0,
|
||||||
|
secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
int confirmed;
|
int confirmed;
|
||||||
|
@ -36,8 +39,7 @@ class ElectrumBalance extends Balance {
|
||||||
int secondUnconfirmed = 0;
|
int secondUnconfirmed = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get formattedAvailableBalance =>
|
String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
|
||||||
bitcoinAmountToString(amount: confirmed - frozen);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
|
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
String? to,
|
String? to,
|
||||||
this.unspents,
|
this.unspents,
|
||||||
this.isReceivedSilentPayment = false,
|
this.isReceivedSilentPayment = false,
|
||||||
|
Map<String, dynamic>? additionalInfo,
|
||||||
}) {
|
}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
@ -54,6 +55,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
this.isReplaced = isReplaced;
|
this.isReplaced = isReplaced;
|
||||||
this.confirmations = confirmations;
|
this.confirmations = confirmations;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
|
this.additionalInfo = additionalInfo ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
|
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
|
||||||
|
@ -212,6 +214,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
|
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
|
isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
|
||||||
|
additionalInfo: data['additionalInfo'] as Map<String, dynamic>?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,7 +249,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
isReplaced: isReplaced ?? false,
|
isReplaced: isReplaced ?? false,
|
||||||
inputAddresses: inputAddresses,
|
inputAddresses: inputAddresses,
|
||||||
outputAddresses: outputAddresses,
|
outputAddresses: outputAddresses,
|
||||||
confirmations: info.confirmations);
|
confirmations: info.confirmations,
|
||||||
|
additionalInfo: additionalInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
|
@ -265,10 +269,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
m['inputAddresses'] = inputAddresses;
|
m['inputAddresses'] = inputAddresses;
|
||||||
m['outputAddresses'] = outputAddresses;
|
m['outputAddresses'] = outputAddresses;
|
||||||
m['isReceivedSilentPayment'] = isReceivedSilentPayment;
|
m['isReceivedSilentPayment'] = isReceivedSilentPayment;
|
||||||
|
m['additionalInfo'] = additionalInfo;
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
String toString() {
|
String toString() {
|
||||||
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)';
|
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, additionalInfo: $additionalInfo)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_wallet.dart';
|
import 'package:cw_bitcoin/bitcoin_wallet.dart';
|
||||||
|
import 'package:cw_bitcoin/litecoin_wallet.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
@ -52,10 +53,9 @@ part 'electrum_wallet.g.dart';
|
||||||
|
|
||||||
class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
|
class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
|
||||||
|
|
||||||
abstract class ElectrumWalletBase extends WalletBase<
|
abstract class ElectrumWalletBase
|
||||||
ElectrumBalance,
|
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
|
||||||
ElectrumTransactionHistory,
|
with Store, WalletKeysFile {
|
||||||
ElectrumTransactionInfo> with Store, WalletKeysFile {
|
|
||||||
ElectrumWalletBase({
|
ElectrumWalletBase({
|
||||||
required String password,
|
required String password,
|
||||||
required WalletInfo walletInfo,
|
required WalletInfo walletInfo,
|
||||||
|
@ -71,8 +71,8 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
ElectrumBalance? initialBalance,
|
ElectrumBalance? initialBalance,
|
||||||
CryptoCurrency? currency,
|
CryptoCurrency? currency,
|
||||||
this.alwaysScan,
|
this.alwaysScan,
|
||||||
}) : accountHD = getAccountHDWallet(
|
}) : accountHD =
|
||||||
currency, network, seedBytes, xpub, walletInfo.derivationInfo),
|
getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
|
||||||
syncStatus = NotConnectedSyncStatus(),
|
syncStatus = NotConnectedSyncStatus(),
|
||||||
_password = password,
|
_password = password,
|
||||||
_feeRates = <int>[],
|
_feeRates = <int>[],
|
||||||
|
@ -107,12 +107,8 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
sharedPrefs.complete(SharedPreferences.getInstance());
|
sharedPrefs.complete(SharedPreferences.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bip32Slip10Secp256k1 getAccountHDWallet(
|
static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
|
||||||
CryptoCurrency? currency,
|
Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) {
|
||||||
BasedUtxoNetwork network,
|
|
||||||
Uint8List? seedBytes,
|
|
||||||
String? xpub,
|
|
||||||
DerivationInfo? derivationInfo) {
|
|
||||||
if (seedBytes == null && xpub == null) {
|
if (seedBytes == null && xpub == null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
"To create a Wallet you need either a seed or an xpub. This should not happen");
|
"To create a Wallet you need either a seed or an xpub. This should not happen");
|
||||||
|
@ -123,9 +119,8 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
case CryptoCurrency.btc:
|
case CryptoCurrency.btc:
|
||||||
case CryptoCurrency.ltc:
|
case CryptoCurrency.ltc:
|
||||||
case CryptoCurrency.tbtc:
|
case CryptoCurrency.tbtc:
|
||||||
return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network))
|
return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)).derivePath(
|
||||||
.derivePath(_hardenedDerivationPath(
|
_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
|
||||||
derivationInfo?.derivationPath ?? electrum_path))
|
|
||||||
as Bip32Slip10Secp256k1;
|
as Bip32Slip10Secp256k1;
|
||||||
case CryptoCurrency.bch:
|
case CryptoCurrency.bch:
|
||||||
return bitcoinCashHDWallet(seedBytes);
|
return bitcoinCashHDWallet(seedBytes);
|
||||||
|
@ -134,13 +129,11 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Bip32Slip10Secp256k1.fromExtendedKey(
|
return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network));
|
||||||
xpub!, getKeyNetVersion(network));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
|
static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
|
||||||
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'")
|
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
|
||||||
as Bip32Slip10Secp256k1;
|
|
||||||
|
|
||||||
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
|
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
|
||||||
inputsCount * 68 + outputsCounts * 34 + 10;
|
inputsCount * 68 + outputsCounts * 34 + 10;
|
||||||
|
@ -250,7 +243,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tip > walletInfo.restoreHeight) {
|
if (tip > walletInfo.restoreHeight) {
|
||||||
_setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
|
_setListeners(walletInfo.restoreHeight, chainTipParam: currentChainTip);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alwaysScan = false;
|
alwaysScan = false;
|
||||||
|
@ -265,23 +258,23 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _currentChainTip;
|
int? currentChainTip;
|
||||||
|
|
||||||
Future<int> getCurrentChainTip() async {
|
Future<int> getCurrentChainTip() async {
|
||||||
if ((_currentChainTip ?? 0) > 0) {
|
if ((currentChainTip ?? 0) > 0) {
|
||||||
return _currentChainTip!;
|
return currentChainTip!;
|
||||||
}
|
}
|
||||||
_currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0;
|
currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0;
|
||||||
|
|
||||||
return _currentChainTip!;
|
return currentChainTip!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getUpdatedChainTip() async {
|
Future<int> getUpdatedChainTip() async {
|
||||||
final newTip = await electrumClient.getCurrentBlockChainTip();
|
final newTip = await electrumClient.getCurrentBlockChainTip();
|
||||||
if (newTip != null && newTip > (_currentChainTip ?? 0)) {
|
if (newTip != null && newTip > (currentChainTip ?? 0)) {
|
||||||
_currentChainTip = newTip;
|
currentChainTip = newTip;
|
||||||
}
|
}
|
||||||
return _currentChainTip ?? 0;
|
return currentChainTip ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -357,7 +350,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
isSingleScan: doSingleScan ?? false,
|
isSingleScan: doSingleScan ?? false,
|
||||||
));
|
));
|
||||||
|
|
||||||
_receiveStream?.cancel();
|
await _receiveStream?.cancel();
|
||||||
_receiveStream = receivePort.listen((var message) async {
|
_receiveStream = receivePort.listen((var message) async {
|
||||||
if (message is Map<String, ElectrumTransactionInfo>) {
|
if (message is Map<String, ElectrumTransactionInfo>) {
|
||||||
for (final map in message.entries) {
|
for (final map in message.entries) {
|
||||||
|
@ -618,7 +611,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
bool spendsUnconfirmedTX = false;
|
bool spendsUnconfirmedTX = false;
|
||||||
|
|
||||||
int leftAmount = credentialsAmount;
|
int leftAmount = credentialsAmount;
|
||||||
final availableInputs = unspentCoins.where((utx) {
|
var availableInputs = unspentCoins.where((utx) {
|
||||||
if (!utx.isSending || utx.isFrozen) {
|
if (!utx.isSending || utx.isFrozen) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -634,6 +627,9 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
}).toList();
|
}).toList();
|
||||||
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
|
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
|
||||||
|
|
||||||
|
// sort the unconfirmed coins so that mweb coins are first:
|
||||||
|
availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1);
|
||||||
|
|
||||||
for (int i = 0; i < availableInputs.length; i++) {
|
for (int i = 0; i < availableInputs.length; i++) {
|
||||||
final utx = availableInputs[i];
|
final utx = availableInputs[i];
|
||||||
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
|
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
|
||||||
|
@ -652,9 +648,8 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
ECPrivate? privkey;
|
ECPrivate? privkey;
|
||||||
bool? isSilentPayment = false;
|
bool? isSilentPayment = false;
|
||||||
|
|
||||||
final hd = utx.bitcoinAddressRecord.isHidden
|
final hd =
|
||||||
? walletAddresses.sideHd
|
utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
|
||||||
: walletAddresses.mainHd;
|
|
||||||
|
|
||||||
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
|
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
|
||||||
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
|
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
|
||||||
|
@ -1233,8 +1228,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLedgerConnection(ledger.LedgerConnection connection) =>
|
void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError();
|
||||||
throw UnimplementedError();
|
|
||||||
|
|
||||||
Future<BtcTransaction> buildHardwareWalletTransaction({
|
Future<BtcTransaction> buildHardwareWalletTransaction({
|
||||||
required List<BitcoinBaseOutput> outputs,
|
required List<BitcoinBaseOutput> outputs,
|
||||||
|
@ -1593,9 +1587,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
|
|
||||||
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
|
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
|
||||||
final privkey = generateECPrivate(
|
final privkey = generateECPrivate(
|
||||||
hd: addressRecord.isHidden
|
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||||
? walletAddresses.sideHd
|
|
||||||
: walletAddresses.mainHd,
|
|
||||||
index: addressRecord.index,
|
index: addressRecord.index,
|
||||||
network: network);
|
network: network);
|
||||||
|
|
||||||
|
@ -1777,8 +1769,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
|
|
||||||
if (height != null) {
|
if (height != null) {
|
||||||
if (time == null && height > 0) {
|
if (time == null && height > 0) {
|
||||||
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000)
|
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
|
||||||
.round();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmations == null) {
|
if (confirmations == null) {
|
||||||
|
@ -1847,6 +1838,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||||
} else if (type == WalletType.litecoin) {
|
} else if (type == WalletType.litecoin) {
|
||||||
await Future.wait(LITECOIN_ADDRESS_TYPES
|
await Future.wait(LITECOIN_ADDRESS_TYPES
|
||||||
|
.where((type) => type != SegwitAddresType.mweb)
|
||||||
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1958,6 +1950,20 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
|
|
||||||
// Got a new transaction fetched, add it to the transaction history
|
// Got a new transaction fetched, add it to the transaction history
|
||||||
// instead of waiting all to finish, and next time it will be faster
|
// instead of waiting all to finish, and next time it will be faster
|
||||||
|
|
||||||
|
if (this is LitecoinWallet) {
|
||||||
|
// if we have a peg out transaction with the same value
|
||||||
|
// that matches this received transaction, mark it as being from a peg out:
|
||||||
|
for (final tx2 in transactionHistory.transactions.values) {
|
||||||
|
final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs();
|
||||||
|
// this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other
|
||||||
|
if (tx2.additionalInfo["isPegOut"] == true &&
|
||||||
|
tx2.amount == tx.amount &&
|
||||||
|
heightDiff <= 5) {
|
||||||
|
tx.additionalInfo["fromPegOut"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
transactionHistory.addOne(tx);
|
transactionHistory.addOne(tx);
|
||||||
await transactionHistory.save();
|
await transactionHistory.save();
|
||||||
}
|
}
|
||||||
|
@ -1984,18 +1990,28 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
if (_isTransactionUpdating) {
|
if (_isTransactionUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await getCurrentChainTip();
|
currentChainTip = await getUpdatedChainTip();
|
||||||
|
|
||||||
|
bool updated = false;
|
||||||
transactionHistory.transactions.values.forEach((tx) {
|
transactionHistory.transactions.values.forEach((tx) {
|
||||||
if (tx.unspents != null &&
|
if ((tx.height ?? 0) > 0 && (currentChainTip ?? 0) > 0) {
|
||||||
tx.unspents!.isNotEmpty &&
|
var confirmations = currentChainTip! - tx.height! + 1;
|
||||||
tx.height != null &&
|
if (confirmations < 0) {
|
||||||
tx.height! > 0 &&
|
// if our chain tip is outdated then it could lead to negative confirmations so this is just a failsafe:
|
||||||
(_currentChainTip ?? 0) > 0) {
|
confirmations = 0;
|
||||||
tx.confirmations = _currentChainTip! - tx.height! + 1;
|
}
|
||||||
|
if (confirmations != tx.confirmations) {
|
||||||
|
updated = true;
|
||||||
|
tx.confirmations = confirmations;
|
||||||
|
transactionHistory.addOne(tx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
await transactionHistory.save();
|
||||||
|
}
|
||||||
|
|
||||||
_isTransactionUpdating = true;
|
_isTransactionUpdating = true;
|
||||||
await fetchTransactions();
|
await fetchTransactions();
|
||||||
walletAddresses.updateReceiveAddresses();
|
walletAddresses.updateReceiveAddresses();
|
||||||
|
@ -2043,6 +2059,8 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
library: this.runtimeType.toString(),
|
library: this.runtimeType.toString(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}, onError: (e, s) {
|
||||||
|
print("sub_listen error: $e $s");
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -2092,6 +2110,13 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
|
|
||||||
final balances = await Future.wait(balanceFutures);
|
final balances = await Future.wait(balanceFutures);
|
||||||
|
|
||||||
|
if (balances.isNotEmpty && balances.first['confirmed'] == null) {
|
||||||
|
// if we got null balance responses from the server, set our connection status to lost and return our last known balance:
|
||||||
|
print("got null balance responses from the server, setting connection status to lost");
|
||||||
|
syncStatus = LostConnectionSyncStatus();
|
||||||
|
return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < balances.length; i++) {
|
for (var i = 0; i < balances.length; i++) {
|
||||||
final addressRecord = addresses[i];
|
final addressRecord = addresses[i];
|
||||||
final balance = balances[i];
|
final balance = balances[i];
|
||||||
|
@ -2197,10 +2222,10 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
Future<void> _setInitialHeight() async {
|
Future<void> _setInitialHeight() async {
|
||||||
if (_chainTipUpdateSubject != null) return;
|
if (_chainTipUpdateSubject != null) return;
|
||||||
|
|
||||||
_currentChainTip = await getUpdatedChainTip();
|
currentChainTip = await getUpdatedChainTip();
|
||||||
|
|
||||||
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
|
if ((currentChainTip == null || currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
|
||||||
await walletInfo.updateRestoreHeight(_currentChainTip!);
|
await walletInfo.updateRestoreHeight(currentChainTip!);
|
||||||
}
|
}
|
||||||
|
|
||||||
_chainTipUpdateSubject = electrumClient.chainTipSubscribe();
|
_chainTipUpdateSubject = electrumClient.chainTipSubscribe();
|
||||||
|
@ -2209,7 +2234,7 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
final height = int.tryParse(event['height'].toString());
|
final height = int.tryParse(event['height'].toString());
|
||||||
|
|
||||||
if (height != null) {
|
if (height != null) {
|
||||||
_currentChainTip = height;
|
currentChainTip = height;
|
||||||
|
|
||||||
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
|
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
|
||||||
_setListeners(walletInfo.restoreHeight);
|
_setListeners(walletInfo.restoreHeight);
|
||||||
|
@ -2223,7 +2248,6 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
|
|
||||||
@action
|
@action
|
||||||
void _onConnectionStatusChange(ConnectionStatus status) {
|
void _onConnectionStatusChange(ConnectionStatus status) {
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
if (syncStatus is NotConnectedSyncStatus ||
|
if (syncStatus is NotConnectedSyncStatus ||
|
||||||
|
@ -2270,8 +2294,6 @@ abstract class ElectrumWalletBase extends WalletBase<
|
||||||
Timer(Duration(seconds: 5), () {
|
Timer(Duration(seconds: 5), () {
|
||||||
if (this.syncStatus is NotConnectedSyncStatus ||
|
if (this.syncStatus is NotConnectedSyncStatus ||
|
||||||
this.syncStatus is LostConnectionSyncStatus) {
|
this.syncStatus is LostConnectionSyncStatus) {
|
||||||
if (node == null) return;
|
|
||||||
|
|
||||||
this.electrumClient.connectToUri(
|
this.electrumClient.connectToUri(
|
||||||
node!.uri,
|
node!.uri,
|
||||||
useSSL: node!.useSSL ?? false,
|
useSSL: node!.useSSL ?? false,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||||
import 'package:cw_core/cake_hive.dart';
|
import 'package:cw_core/cake_hive.dart';
|
||||||
import 'package:cw_core/mweb_utxo.dart';
|
import 'package:cw_core/mweb_utxo.dart';
|
||||||
|
import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_mweb/mwebd.pbgrpc.dart';
|
import 'package:cw_mweb/mwebd.pbgrpc.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:bip39/bip39.dart' as bip39;
|
import 'package:bip39/bip39.dart' as bip39;
|
||||||
|
@ -47,6 +48,7 @@ import 'package:cw_mweb/cw_mweb.dart';
|
||||||
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
|
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
|
||||||
import 'package:pointycastle/ecc/api.dart';
|
import 'package:pointycastle/ecc/api.dart';
|
||||||
import 'package:pointycastle/ecc/curves/secp256k1.dart';
|
import 'package:pointycastle/ecc/curves/secp256k1.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
part 'litecoin_wallet.g.dart';
|
part 'litecoin_wallet.g.dart';
|
||||||
|
|
||||||
|
@ -85,8 +87,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
alwaysScan: alwaysScan,
|
alwaysScan: alwaysScan,
|
||||||
) {
|
) {
|
||||||
if (seedBytes != null) {
|
if (seedBytes != null) {
|
||||||
mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
|
mwebHd =
|
||||||
"m/1000'") as Bip32Slip10Secp256k1;
|
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
|
||||||
mwebEnabled = alwaysScan ?? false;
|
mwebEnabled = alwaysScan ?? false;
|
||||||
} else {
|
} else {
|
||||||
mwebHd = null;
|
mwebHd = null;
|
||||||
|
@ -287,6 +289,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020);
|
await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
@override
|
||||||
|
Future<void> connectToNode({required Node node}) async {
|
||||||
|
await super.connectToNode(node: node);
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final mwebNodeUri = prefs.getString("mwebNodeUri") ?? "ltc-electrum.cakewallet.com:9333";
|
||||||
|
await CwMweb.setNodeUriOverride(mwebNodeUri);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@override
|
@override
|
||||||
Future<void> startSync() async {
|
Future<void> startSync() async {
|
||||||
|
@ -349,6 +361,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update the current chain tip so that confirmation calculations are accurate:
|
||||||
|
currentChainTip = nodeHeight;
|
||||||
|
|
||||||
final resp = await CwMweb.status(StatusRequest());
|
final resp = await CwMweb.status(StatusRequest());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -361,22 +376,46 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
} else if (resp.mwebUtxosHeight < nodeHeight) {
|
} else if (resp.mwebUtxosHeight < nodeHeight) {
|
||||||
mwebSyncStatus = SyncingSyncStatus(1, 0.999);
|
mwebSyncStatus = SyncingSyncStatus(1, 0.999);
|
||||||
} else {
|
} else {
|
||||||
|
bool confirmationsUpdated = false;
|
||||||
if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
|
if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
|
||||||
await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
|
await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
|
||||||
await checkMwebUtxosSpent();
|
await checkMwebUtxosSpent();
|
||||||
// update the confirmations for each transaction:
|
// update the confirmations for each transaction:
|
||||||
for (final transaction in transactionHistory.transactions.values) {
|
for (final tx in transactionHistory.transactions.values) {
|
||||||
if (transaction.isPending) continue;
|
if (tx.height == null || tx.height == 0) {
|
||||||
int txHeight = transaction.height ?? resp.mwebUtxosHeight;
|
// update with first confirmation on next block since it hasn't been confirmed yet:
|
||||||
final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
|
tx.height = resp.mwebUtxosHeight;
|
||||||
if (transaction.confirmations == confirmations) continue;
|
continue;
|
||||||
if (transaction.confirmations == 0) {
|
|
||||||
updateBalance();
|
|
||||||
}
|
}
|
||||||
transaction.confirmations = confirmations;
|
|
||||||
transactionHistory.addOne(transaction);
|
final confirmations = (resp.mwebUtxosHeight - tx.height!) + 1;
|
||||||
|
|
||||||
|
// if the confirmations haven't changed, skip updating:
|
||||||
|
if (tx.confirmations == confirmations) continue;
|
||||||
|
|
||||||
|
|
||||||
|
// if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin):
|
||||||
|
if (confirmations >= 2 &&
|
||||||
|
tx.direction == TransactionDirection.outgoing &&
|
||||||
|
tx.unspents != null) {
|
||||||
|
for (var coin in tx.unspents!) {
|
||||||
|
final utxo = mwebUtxosBox.get(coin.address);
|
||||||
|
if (utxo != null) {
|
||||||
|
print("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||||
|
await mwebUtxosBox.delete(coin.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.confirmations = confirmations;
|
||||||
|
tx.isPending = false;
|
||||||
|
transactionHistory.addOne(tx);
|
||||||
|
confirmationsUpdated = true;
|
||||||
|
}
|
||||||
|
if (confirmationsUpdated) {
|
||||||
|
await transactionHistory.save();
|
||||||
|
await updateTransactions();
|
||||||
}
|
}
|
||||||
await transactionHistory.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent unnecessary reaction triggers:
|
// prevent unnecessary reaction triggers:
|
||||||
|
@ -501,13 +540,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
outputAddresses: [utxo.outputId],
|
outputAddresses: [utxo.outputId],
|
||||||
isReplaced: false,
|
isReplaced: false,
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
if (tx.confirmations != confirmations || tx.height != utxo.height) {
|
||||||
// don't update the confirmations if the tx is updated by electrum:
|
tx.height = utxo.height;
|
||||||
if (tx.confirmations == 0 || utxo.height != 0) {
|
tx.confirmations = confirmations;
|
||||||
tx.height = utxo.height;
|
tx.isPending = utxo.height == 0;
|
||||||
tx.isPending = utxo.height == 0;
|
}
|
||||||
tx.confirmations = confirmations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isNew = transactionHistory.transactions[tx.id] == null;
|
bool isNew = transactionHistory.transactions[tx.id] == null;
|
||||||
|
@ -557,56 +595,88 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
if (responseStream == null) {
|
if (responseStream == null) {
|
||||||
throw Exception("failed to get utxos stream!");
|
throw Exception("failed to get utxos stream!");
|
||||||
}
|
}
|
||||||
_utxoStream = responseStream.listen((Utxo sUtxo) async {
|
_utxoStream = responseStream.listen(
|
||||||
// we're processing utxos, so our balance could still be innacurate:
|
(Utxo sUtxo) async {
|
||||||
if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
|
// we're processing utxos, so our balance could still be innacurate:
|
||||||
mwebSyncStatus = SyncronizingSyncStatus();
|
if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
|
||||||
processingUtxos = true;
|
mwebSyncStatus = SyncronizingSyncStatus();
|
||||||
_processingTimer?.cancel();
|
processingUtxos = true;
|
||||||
_processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
_processingTimer?.cancel();
|
||||||
processingUtxos = false;
|
_processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||||
timer.cancel();
|
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:
|
|
||||||
// but do update the utxo height if it's somehow different:
|
|
||||||
final existingUtxo = mwebUtxosBox.get(utxo.outputId);
|
|
||||||
if (existingUtxo!.height != utxo.height) {
|
|
||||||
print(
|
|
||||||
"updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
|
|
||||||
existingUtxo.height = utxo.height;
|
|
||||||
await mwebUtxosBox.put(utxo.outputId, existingUtxo);
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUnspent();
|
final utxo = MwebUtxo(
|
||||||
await updateBalance();
|
address: sUtxo.address,
|
||||||
|
blockTime: sUtxo.blockTime,
|
||||||
|
height: sUtxo.height,
|
||||||
|
outputId: sUtxo.outputId,
|
||||||
|
value: sUtxo.value.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
|
if (mwebUtxosBox.containsKey(utxo.outputId)) {
|
||||||
|
// we've already stored this utxo, skip it:
|
||||||
|
// but do update the utxo height if it's somehow different:
|
||||||
|
final existingUtxo = mwebUtxosBox.get(utxo.outputId);
|
||||||
|
if (existingUtxo!.height != utxo.height) {
|
||||||
|
print(
|
||||||
|
"updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
|
||||||
|
existingUtxo.height = utxo.height;
|
||||||
|
await mwebUtxosBox.put(utxo.outputId, existingUtxo);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// don't process utxos with addresses that are not in the mwebAddrs list:
|
await updateUnspent();
|
||||||
if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
|
await updateBalance();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mwebUtxosBox.put(utxo.outputId, utxo);
|
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
|
||||||
|
|
||||||
await handleIncoming(utxo);
|
// 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);
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
print("error in utxo stream: $error");
|
||||||
|
mwebSyncStatus = FailedSyncStatus(error: error.toString());
|
||||||
|
},
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSpentUtxos() async {
|
||||||
|
print("deleteSpentUtxos() called!");
|
||||||
|
final chainHeight = await electrumClient.getCurrentBlockChainTip();
|
||||||
|
final status = await CwMweb.status(StatusRequest());
|
||||||
|
if (chainHeight == null || status.blockHeaderHeight != chainHeight) return;
|
||||||
|
if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced
|
||||||
|
|
||||||
|
// delete any spent utxos with >= 2 confirmations:
|
||||||
|
final spentOutputIds = mwebUtxosBox.values
|
||||||
|
.where((utxo) => utxo.spent && (chainHeight - utxo.height) >= 2)
|
||||||
|
.map((utxo) => utxo.outputId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (spentOutputIds.isEmpty) return;
|
||||||
|
|
||||||
|
final resp = await CwMweb.spent(SpentRequest(outputId: spentOutputIds));
|
||||||
|
final spent = resp.outputId;
|
||||||
|
if (spent.isEmpty) return;
|
||||||
|
|
||||||
|
for (final outputId in spent) {
|
||||||
|
await mwebUtxosBox.delete(outputId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> checkMwebUtxosSpent() async {
|
Future<void> checkMwebUtxosSpent() async {
|
||||||
|
print("checkMwebUtxosSpent() called!");
|
||||||
if (!mwebEnabled) {
|
if (!mwebEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -620,15 +690,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
updatedAny = await isConfirmed(tx) || updatedAny;
|
updatedAny = await isConfirmed(tx) || updatedAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await deleteSpentUtxos();
|
||||||
|
|
||||||
// get output ids of all the mweb utxos that have > 0 height:
|
// get output ids of all the mweb utxos that have > 0 height:
|
||||||
final outputIds =
|
final outputIds = mwebUtxosBox.values
|
||||||
mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList();
|
.where((utxo) => utxo.height > 0 && !utxo.spent)
|
||||||
|
.map((utxo) => utxo.outputId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
|
final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
|
||||||
final spent = resp.outputId;
|
final spent = resp.outputId;
|
||||||
if (spent.isEmpty) {
|
if (spent.isEmpty) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final status = await CwMweb.status(StatusRequest());
|
final status = await CwMweb.status(StatusRequest());
|
||||||
final height = await electrumClient.getCurrentBlockChainTip();
|
final height = await electrumClient.getCurrentBlockChainTip();
|
||||||
|
@ -739,7 +811,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
mwebUtxosBox.keys.forEach((dynamic oId) {
|
mwebUtxosBox.keys.forEach((dynamic oId) {
|
||||||
final String outputId = oId as String;
|
final String outputId = oId as String;
|
||||||
final utxo = mwebUtxosBox.get(outputId);
|
final utxo = mwebUtxosBox.get(outputId);
|
||||||
if (utxo == null) {
|
if (utxo == null || utxo.spent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (utxo.address.isEmpty) {
|
if (utxo.address.isEmpty) {
|
||||||
|
@ -789,15 +861,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
int unconfirmedMweb = 0;
|
int unconfirmedMweb = 0;
|
||||||
try {
|
try {
|
||||||
mwebUtxosBox.values.forEach((utxo) {
|
mwebUtxosBox.values.forEach((utxo) {
|
||||||
if (utxo.height > 0) {
|
bool isConfirmed = utxo.height > 0;
|
||||||
|
|
||||||
|
print(
|
||||||
|
"utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}");
|
||||||
|
|
||||||
|
if (isConfirmed) {
|
||||||
confirmedMweb += utxo.value.toInt();
|
confirmedMweb += utxo.value.toInt();
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (isConfirmed && utxo.spent) {
|
||||||
|
unconfirmedMweb -= utxo.value.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConfirmed && !utxo.spent) {
|
||||||
unconfirmedMweb += utxo.value.toInt();
|
unconfirmedMweb += utxo.value.toInt();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (unconfirmedMweb > 0) {
|
|
||||||
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
for (var addressRecord in walletAddresses.allAddresses) {
|
for (var addressRecord in walletAddresses.allAddresses) {
|
||||||
|
@ -829,7 +909,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
// update the txCount for each address using the tx history, since we can't rely on mwebd
|
// 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:
|
// 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) {
|
for (final tx in transactionHistory.transactions.values) {
|
||||||
// if (tx.isPending) continue;
|
|
||||||
if (tx.inputAddresses == null || tx.outputAddresses == null) {
|
if (tx.inputAddresses == null || tx.outputAddresses == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -908,7 +987,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
// https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
|
// https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
|
||||||
final preOutputSum =
|
final preOutputSum =
|
||||||
outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
|
outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
|
||||||
final fee = utxos.sumOfUtxosValue() - preOutputSum;
|
var fee = utxos.sumOfUtxosValue() - preOutputSum;
|
||||||
|
|
||||||
|
// determines if the fee is correct:
|
||||||
|
BigInt _sumOutputAmounts(List<TxOutput> outputs) {
|
||||||
|
BigInt sum = BigInt.zero;
|
||||||
|
for (final e in outputs) {
|
||||||
|
sum += e.amount;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sum1 = _sumOutputAmounts(outputs.map((e) => e.toOutput).toList()) + fee;
|
||||||
|
final sum2 = utxos.sumOfUtxosValue();
|
||||||
|
if (sum1 != sum2) {
|
||||||
|
print("@@@@@ WE HAD TO ADJUST THE FEE! @@@@@@@@");
|
||||||
|
final diff = sum2 - sum1;
|
||||||
|
// add the difference to the fee (abs value):
|
||||||
|
fee += diff.abs();
|
||||||
|
}
|
||||||
|
|
||||||
final txb =
|
final txb =
|
||||||
BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
|
BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
|
||||||
final resp = await CwMweb.create(CreateRequest(
|
final resp = await CwMweb.create(CreateRequest(
|
||||||
|
@ -949,8 +1047,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
|
|
||||||
if (!mwebEnabled) {
|
if (!mwebEnabled) {
|
||||||
tx.changeAddressOverride =
|
tx.changeAddressOverride =
|
||||||
(await (walletAddresses as LitecoinWalletAddresses)
|
(await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false))
|
||||||
.getChangeAddress(isPegIn: false))
|
|
||||||
.address;
|
.address;
|
||||||
return tx;
|
return tx;
|
||||||
}
|
}
|
||||||
|
@ -969,15 +1066,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
|
|
||||||
bool hasMwebInput = false;
|
bool hasMwebInput = false;
|
||||||
bool hasMwebOutput = false;
|
bool hasMwebOutput = false;
|
||||||
|
bool hasRegularOutput = false;
|
||||||
|
|
||||||
for (final output in transactionCredentials.outputs) {
|
for (final output in transactionCredentials.outputs) {
|
||||||
if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) {
|
final address = output.address.toLowerCase();
|
||||||
|
final extractedAddress = output.extractedAddress?.toLowerCase();
|
||||||
|
|
||||||
|
if (address.contains("mweb")) {
|
||||||
hasMwebOutput = true;
|
hasMwebOutput = true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (output.address.toLowerCase().contains("mweb")) {
|
if (!address.contains("mweb")) {
|
||||||
hasMwebOutput = true;
|
hasRegularOutput = true;
|
||||||
break;
|
}
|
||||||
|
if (extractedAddress != null && extractedAddress.isNotEmpty) {
|
||||||
|
if (extractedAddress.contains("mweb")) {
|
||||||
|
hasMwebOutput = true;
|
||||||
|
}
|
||||||
|
if (!extractedAddress.contains("mweb")) {
|
||||||
|
hasRegularOutput = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -989,11 +1096,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPegIn = !hasMwebInput && hasMwebOutput;
|
bool isPegIn = !hasMwebInput && hasMwebOutput;
|
||||||
|
bool isPegOut = hasMwebInput && hasRegularOutput;
|
||||||
bool isRegular = !hasMwebInput && !hasMwebOutput;
|
bool isRegular = !hasMwebInput && !hasMwebOutput;
|
||||||
tx.changeAddressOverride =
|
tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
|
||||||
(await (walletAddresses as LitecoinWalletAddresses)
|
.getChangeAddress(isPegIn: isPegIn || isRegular))
|
||||||
.getChangeAddress(isPegIn: isPegIn || isRegular))
|
.address;
|
||||||
.address;
|
|
||||||
if (!hasMwebInput && !hasMwebOutput) {
|
if (!hasMwebInput && !hasMwebOutput) {
|
||||||
tx.isMweb = false;
|
tx.isMweb = false;
|
||||||
return tx;
|
return tx;
|
||||||
|
@ -1046,8 +1153,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
final addresses = <String>{};
|
final addresses = <String>{};
|
||||||
transaction.inputAddresses?.forEach((id) async {
|
transaction.inputAddresses?.forEach((id) async {
|
||||||
final utxo = mwebUtxosBox.get(id);
|
final utxo = mwebUtxosBox.get(id);
|
||||||
await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
|
// await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
|
||||||
if (utxo == null) return;
|
if (utxo == null) return;
|
||||||
|
// mark utxo as spent so we add it to the unconfirmed balance (as negative):
|
||||||
|
utxo.spent = true;
|
||||||
|
await mwebUtxosBox.put(id, utxo);
|
||||||
final addressRecord = walletAddresses.allAddresses
|
final addressRecord = walletAddresses.allAddresses
|
||||||
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
|
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
|
||||||
if (!addresses.contains(utxo.address)) {
|
if (!addresses.contains(utxo.address)) {
|
||||||
|
@ -1056,7 +1166,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
addressRecord.balance -= utxo.value.toInt();
|
addressRecord.balance -= utxo.value.toInt();
|
||||||
});
|
});
|
||||||
transaction.inputAddresses?.addAll(addresses);
|
transaction.inputAddresses?.addAll(addresses);
|
||||||
|
print("isPegIn: $isPegIn, isPegOut: $isPegOut");
|
||||||
|
transaction.additionalInfo["isPegIn"] = isPegIn;
|
||||||
|
transaction.additionalInfo["isPegOut"] = isPegOut;
|
||||||
transactionHistory.addOne(transaction);
|
transactionHistory.addOne(transaction);
|
||||||
await updateUnspent();
|
await updateUnspent();
|
||||||
await updateBalance();
|
await updateBalance();
|
||||||
|
@ -1240,8 +1352,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
@override
|
@override
|
||||||
void setLedgerConnection(LedgerConnection connection) {
|
void setLedgerConnection(LedgerConnection connection) {
|
||||||
_ledgerConnection = connection;
|
_ledgerConnection = connection;
|
||||||
_litecoinLedgerApp =
|
_litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!,
|
||||||
LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
|
derivationPath: walletInfo.derivationInfo!.derivationPath!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1277,19 +1389,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
|
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final rawHex = await _litecoinLedgerApp!.createTransaction(
|
final rawHex = await _litecoinLedgerApp!.createTransaction(
|
||||||
inputs: readyInputs,
|
inputs: readyInputs,
|
||||||
outputs: outputs
|
outputs: outputs
|
||||||
.map((e) => TransactionOutput.fromBigInt(
|
.map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value,
|
||||||
(e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
|
Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
|
||||||
.toList(),
|
.toList(),
|
||||||
changePath: changePath,
|
changePath: changePath,
|
||||||
sigHashType: 0x01,
|
sigHashType: 0x01,
|
||||||
additionals: ["bech32"],
|
additionals: ["bech32"],
|
||||||
isSegWit: true,
|
isSegWit: true,
|
||||||
useTrustedInputForSegwit: true
|
useTrustedInputForSegwit: true);
|
||||||
);
|
|
||||||
|
|
||||||
return BtcTransaction.fromRaw(rawHex);
|
return BtcTransaction.fromRaw(rawHex);
|
||||||
}
|
}
|
||||||
|
|