9
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
10
import 'package:cw_core/cake_hive.dart';
11
import 'package:cw_core/mweb_utxo.dart';
12
+import 'package:cw_core/node.dart';
13
import 'package:cw_mweb/mwebd.pbgrpc.dart';
14
import 'package:fixnum/fixnum.dart';
15
import 'package:bip39/bip39.dart' as bip39;
48
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
49
import 'package:pointycastle/ecc/api.dart';
50
import 'package:pointycastle/ecc/curves/secp256k1.dart';
51
+import 'package:shared_preferences/shared_preferences.dart';
52
53
part 'litecoin_wallet.g.dart';
54
87
alwaysScan: alwaysScan,
88
) {
89
if (seedBytes != null) {
88
- mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
89
- "m/1000'") as Bip32Slip10Secp256k1;
90
+ mwebHd =
91
+ Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
92
mwebEnabled = alwaysScan ?? false;
93
} else {
94
mwebHd = null;
289
await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020);
290
}
291
292
+ @action
293
+ @override
294
+ Future<void> connectToNode({required Node node}) async {
295
+ await super.connectToNode(node: node);
296
+
297
+ final prefs = await SharedPreferences.getInstance();
298
+ final mwebNodeUri = prefs.getString("mwebNodeUri") ?? "ltc-electrum.cakewallet.com:9333";
299
+ await CwMweb.setNodeUriOverride(mwebNodeUri);
300
+ }
301
+
302
@action
303
@override
304
Future<void> startSync() async {
361
return;
362
}
363
364
+ // update the current chain tip so that confirmation calculations are accurate:
365
+ currentChainTip = nodeHeight;
366
+
367
final resp = await CwMweb.status(StatusRequest());
368
369
try {
376
} else if (resp.mwebUtxosHeight < nodeHeight) {
377
mwebSyncStatus = SyncingSyncStatus(1, 0.999);
378
} else {
379
+ bool confirmationsUpdated = false;
380
if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
381
await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
382
await checkMwebUtxosSpent();
383
// update the confirmations for each transaction:
368
- for (final transaction in transactionHistory.transactions.values) {
369
- if (transaction.isPending) continue;
370
- int txHeight = transaction.height ?? resp.mwebUtxosHeight;
371
- final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
372
- if (transaction.confirmations == confirmations) continue;
373
- if (transaction.confirmations == 0) {
374
- updateBalance();
384
+ for (final tx in transactionHistory.transactions.values) {
385
+ if (tx.height == null || tx.height == 0) {
386
+ // update with first confirmation on next block since it hasn't been confirmed yet:
387
+ tx.height = resp.mwebUtxosHeight;
388
+ continue;
389
}
376
- transaction.confirmations = confirmations;
377
- transactionHistory.addOne(transaction);
390
+
391
+ final confirmations = (resp.mwebUtxosHeight - tx.height!) + 1;
392
+
393
+ // if the confirmations haven't changed, skip updating:
394
+ if (tx.confirmations == confirmations) continue;
395
+
396
+
397
+ // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin):
398
+ if (confirmations >= 2 &&
399
+ tx.direction == TransactionDirection.outgoing &&
400
+ tx.unspents != null) {
401
+ for (var coin in tx.unspents!) {
402
+ final utxo = mwebUtxosBox.get(coin.address);
403
+ if (utxo != null) {
404
+ print("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
405
+ await mwebUtxosBox.delete(coin.address);
406
+ }
407
+ }
408
+ }
409
+
410
+ tx.confirmations = confirmations;
411
+ tx.isPending = false;
412
+ transactionHistory.addOne(tx);
413
+ confirmationsUpdated = true;
414
+ }
415
+ if (confirmationsUpdated) {
416
+ await transactionHistory.save();
417
+ await updateTransactions();
418
}
379
- await transactionHistory.save();
419
}
420
421
// prevent unnecessary reaction triggers:
540
outputAddresses: [utxo.outputId],
541
isReplaced: false,
542
);
504
- }
505
-
506
- // don't update the confirmations if the tx is updated by electrum:
507
- if (tx.confirmations == 0 || utxo.height != 0) {
508
- tx.height = utxo.height;
509
- tx.isPending = utxo.height == 0;
510
- tx.confirmations = confirmations;
543
+ } else {
544
+ if (tx.confirmations != confirmations || tx.height != utxo.height) {
545
+ tx.height = utxo.height;
546
+ tx.confirmations = confirmations;
547
+ tx.isPending = utxo.height == 0;
548
+ }
549
}
550
551
bool isNew = transactionHistory.transactions[tx.id] == null;
595
if (responseStream == null) {
596
throw Exception("failed to get utxos stream!");
597
}
560
- _utxoStream = responseStream.listen((Utxo sUtxo) async {
561
- // we're processing utxos, so our balance could still be innacurate:
562
- if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
563
- mwebSyncStatus = SyncronizingSyncStatus();
564
- processingUtxos = true;
565
- _processingTimer?.cancel();
566
- _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
567
- processingUtxos = false;
568
- timer.cancel();
569
- });
570
- }
598
+ _utxoStream = responseStream.listen(
599
+ (Utxo sUtxo) async {
600
+ // we're processing utxos, so our balance could still be innacurate:
601
+ if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
602
+ mwebSyncStatus = SyncronizingSyncStatus();
603
+ processingUtxos = true;
604
+ _processingTimer?.cancel();
605
+ _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
606
+ processingUtxos = false;
607
+ timer.cancel();
608
+ });
609
+ }
610
572
- final utxo = MwebUtxo(
573
- address: sUtxo.address,
574
- blockTime: sUtxo.blockTime,
575
- height: sUtxo.height,
576
- outputId: sUtxo.outputId,
577
- value: sUtxo.value.toInt(),
578
- );
611
+ final utxo = MwebUtxo(
612
+ address: sUtxo.address,
613
+ blockTime: sUtxo.blockTime,
614
+ height: sUtxo.height,
615
+ outputId: sUtxo.outputId,
616
+ value: sUtxo.value.toInt(),
617
+ );
618
580
- if (mwebUtxosBox.containsKey(utxo.outputId)) {
581
- // we've already stored this utxo, skip it:
582
- // but do update the utxo height if it's somehow different:
583
- final existingUtxo = mwebUtxosBox.get(utxo.outputId);
584
- if (existingUtxo!.height != utxo.height) {
585
- print(
586
- "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
587
- existingUtxo.height = utxo.height;
588
- await mwebUtxosBox.put(utxo.outputId, existingUtxo);
619
+ if (mwebUtxosBox.containsKey(utxo.outputId)) {
620
+ // we've already stored this utxo, skip it:
621
+ // but do update the utxo height if it's somehow different:
622
+ final existingUtxo = mwebUtxosBox.get(utxo.outputId);
623
+ if (existingUtxo!.height != utxo.height) {
624
+ print(
625
+ "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}");
626
+ existingUtxo.height = utxo.height;
627
+ await mwebUtxosBox.put(utxo.outputId, existingUtxo);
628
+ }
629
+ return;
630
}
590
- return;
591
- }
631
593
- await updateUnspent();
594
- await updateBalance();
632
+ await updateUnspent();
633
+ await updateBalance();
634
596
- final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
635
+ final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
636
598
- // don't process utxos with addresses that are not in the mwebAddrs list:
599
- if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
600
- return;
601
- }
637
+ // don't process utxos with addresses that are not in the mwebAddrs list:
638
+ if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
639
+ return;
640
+ }
641
603
- await mwebUtxosBox.put(utxo.outputId, utxo);
642
+ await mwebUtxosBox.put(utxo.outputId, utxo);
643
605
- await handleIncoming(utxo);
606
- });
644
+ await handleIncoming(utxo);
645
+ },
646
+ onError: (error) {
647
+ print("error in utxo stream: $error");
648
+ mwebSyncStatus = FailedSyncStatus(error: error.toString());
649
+ },
650
+ cancelOnError: true,
651
+ );
652
+ }
653
+
654
+ Future<void> deleteSpentUtxos() async {
655
+ print("deleteSpentUtxos() called!");
656
+ final chainHeight = await electrumClient.getCurrentBlockChainTip();
657
+ final status = await CwMweb.status(StatusRequest());
658
+ if (chainHeight == null || status.blockHeaderHeight != chainHeight) return;
659
+ if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced
660
+
661
+ // delete any spent utxos with >= 2 confirmations:
662
+ final spentOutputIds = mwebUtxosBox.values
663
+ .where((utxo) => utxo.spent && (chainHeight - utxo.height) >= 2)
664
+ .map((utxo) => utxo.outputId)
665
+ .toList();
666
+
667
+ if (spentOutputIds.isEmpty) return;
668
+
669
+ final resp = await CwMweb.spent(SpentRequest(outputId: spentOutputIds));
670
+ final spent = resp.outputId;
671
+ if (spent.isEmpty) return;
672
+
673
+ for (final outputId in spent) {
674
+ await mwebUtxosBox.delete(outputId);
675
+ }
676
}
677
678
Future<void> checkMwebUtxosSpent() async {
679
+ print("checkMwebUtxosSpent() called!");
680
if (!mwebEnabled) {
681
return;
682
}
690
updatedAny = await isConfirmed(tx) || updatedAny;
691
}
692
693
+ await deleteSpentUtxos();
694
+
695
// get output ids of all the mweb utxos that have > 0 height:
624
- final outputIds =
625
- mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList();
696
+ final outputIds = mwebUtxosBox.values
697
+ .where((utxo) => utxo.height > 0 && !utxo.spent)
698
+ .map((utxo) => utxo.outputId)
699
+ .toList();
700
701
final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
702
final spent = resp.outputId;
629
- if (spent.isEmpty) {
630
- return;
631
- }
703
+ if (spent.isEmpty) return;
704
705
final status = await CwMweb.status(StatusRequest());
706
final height = await electrumClient.getCurrentBlockChainTip();
811
mwebUtxosBox.keys.forEach((dynamic oId) {
812
final String outputId = oId as String;
813
final utxo = mwebUtxosBox.get(outputId);
742
- if (utxo == null) {
814
+ if (utxo == null || utxo.spent) {
815
return;
816
}
817
if (utxo.address.isEmpty) {
861
int unconfirmedMweb = 0;
862
try {
863
mwebUtxosBox.values.forEach((utxo) {
792
- if (utxo.height > 0) {
864
+ bool isConfirmed = utxo.height > 0;
865
+
866
+ print(
867
+ "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}");
868
+
869
+ if (isConfirmed) {
870
confirmedMweb += utxo.value.toInt();
794
- } else {
871
+ }
872
+
873
+ if (isConfirmed && utxo.spent) {
874
+ unconfirmedMweb -= utxo.value.toInt();
875
+ }
876
+
877
+ if (!isConfirmed && !utxo.spent) {
878
unconfirmedMweb += utxo.value.toInt();
879
}
880
});
798
- if (unconfirmedMweb > 0) {
799
- unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
800
- }
881
} catch (_) {}
882
883
for (var addressRecord in walletAddresses.allAddresses) {
909
// update the txCount for each address using the tx history, since we can't rely on mwebd
910
// to have an accurate count, we should just keep it in sync with what we know from the tx history:
911
for (final tx in transactionHistory.transactions.values) {
832
- // if (tx.isPending) continue;
912
if (tx.inputAddresses == null || tx.outputAddresses == null) {
913
continue;
914
}
987
// https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
988
final preOutputSum =
989
outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
911
- final fee = utxos.sumOfUtxosValue() - preOutputSum;
990
+ var fee = utxos.sumOfUtxosValue() - preOutputSum;
991
+
992
+ // determines if the fee is correct:
993
+ BigInt _sumOutputAmounts(List<TxOutput> outputs) {
994
+ BigInt sum = BigInt.zero;
995
+ for (final e in outputs) {
996
+ sum += e.amount;
997
+ }
998
+ return sum;
999
+ }
1000
+
1001
+ final sum1 = _sumOutputAmounts(outputs.map((e) => e.toOutput).toList()) + fee;
1002
+ final sum2 = utxos.sumOfUtxosValue();
1003
+ if (sum1 != sum2) {
1004
+ print("@@@@@ WE HAD TO ADJUST THE FEE! @@@@@@@@");
1005
+ final diff = sum2 - sum1;
1006
+ // add the difference to the fee (abs value):
1007
+ fee += diff.abs();
1008
+ }
1009
+
1010
final txb =
1011
BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
1012
final resp = await CwMweb.create(CreateRequest(
1047
1048
if (!mwebEnabled) {
1049
tx.changeAddressOverride =
952
- (await (walletAddresses as LitecoinWalletAddresses)
953
- .getChangeAddress(isPegIn: false))
1050
+ (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false))
1051
.address;
1052
return tx;
1053
}
1066
1067
bool hasMwebInput = false;
1068
bool hasMwebOutput = false;
1069
+ bool hasRegularOutput = false;
1070
1071
for (final output in transactionCredentials.outputs) {
974
- if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) {
1072
+ final address = output.address.toLowerCase();
1073
+ final extractedAddress = output.extractedAddress?.toLowerCase();
1074
+
1075
+ if (address.contains("mweb")) {
1076
hasMwebOutput = true;
976
- break;
1077
}
978
- if (output.address.toLowerCase().contains("mweb")) {
979
- hasMwebOutput = true;
980
- break;
1078
+ if (!address.contains("mweb")) {
1079
+ hasRegularOutput = true;
1080
+ }
1081
+ if (extractedAddress != null && extractedAddress.isNotEmpty) {
1082
+ if (extractedAddress.contains("mweb")) {
1083
+ hasMwebOutput = true;
1084
+ }
1085
+ if (!extractedAddress.contains("mweb")) {
1086
+ hasRegularOutput = true;
1087
+ }
1088
}
1089
}
1090
1096
}
1097
1098
bool isPegIn = !hasMwebInput && hasMwebOutput;
1099
+ bool isPegOut = hasMwebInput && hasRegularOutput;
1100
bool isRegular = !hasMwebInput && !hasMwebOutput;
993
- tx.changeAddressOverride =
994
- (await (walletAddresses as LitecoinWalletAddresses)
995
- .getChangeAddress(isPegIn: isPegIn || isRegular))
996
- .address;
1101
+ tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
1102
+ .getChangeAddress(isPegIn: isPegIn || isRegular))
1103
+ .address;
1104
if (!hasMwebInput && !hasMwebOutput) {
1105
tx.isMweb = false;
1106
return tx;
1153
final addresses = <String>{};
1154
transaction.inputAddresses?.forEach((id) async {
1155
final utxo = mwebUtxosBox.get(id);
1049
- await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
1156
+ // await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
1157
if (utxo == null) return;
1158
+ // mark utxo as spent so we add it to the unconfirmed balance (as negative):
1159
+ utxo.spent = true;
1160
+ await mwebUtxosBox.put(id, utxo);
1161
final addressRecord = walletAddresses.allAddresses
1162
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
1163
if (!addresses.contains(utxo.address)) {
1166
addressRecord.balance -= utxo.value.toInt();
1167
});
1168
transaction.inputAddresses?.addAll(addresses);
1059
-
1169
+ print("isPegIn: $isPegIn, isPegOut: $isPegOut");
1170
+ transaction.additionalInfo["isPegIn"] = isPegIn;
1171
+ transaction.additionalInfo["isPegOut"] = isPegOut;
1172
transactionHistory.addOne(transaction);
1173
await updateUnspent();
1174
await updateBalance();
1352
@override
1353
void setLedgerConnection(LedgerConnection connection) {
1354
_ledgerConnection = connection;
1243
- _litecoinLedgerApp =
1244
- LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
1355
+ _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!,
1356
+ derivationPath: walletInfo.derivationInfo!.derivationPath!);
1357
}
1358
1359
@override
1389
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
1390
}
1391
1280
-
1392
final rawHex = await _litecoinLedgerApp!.createTransaction(
1282
- inputs: readyInputs,
1283
- outputs: outputs
1284
- .map((e) => TransactionOutput.fromBigInt(
1285
- (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
1286
- .toList(),
1287
- changePath: changePath,
1288
- sigHashType: 0x01,
1289
- additionals: ["bech32"],
1290
- isSegWit: true,
1291
- useTrustedInputForSegwit: true
1292
- );
1393
+ inputs: readyInputs,
1394
+ outputs: outputs
1395
+ .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value,
1396
+ Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
1397
+ .toList(),
1398
+ changePath: changePath,
1399
+ sigHashType: 0x01,
1400
+ additionals: ["bech32"],
1401
+ isSegWit: true,
1402
+ useTrustedInputForSegwit: true);
1403
1404
return BtcTransaction.fromRaw(rawHex);
1405
}