litecoin mweb support (#1455)

* Fix stub creation * Generate MWEB addresses * Fix mweb address derivation * Use camel-case * Show utxos in tx list * A few fixes * Add spent processing * Update balance * Balance fixes * Update address records * Get rid of debounce hack * Get sending up to the confirmation box * Fee estimation * Stop the daemon if plugin is unloaded * Normal fee for non-mweb txns * Fix fee estimation for send all * Don't hash mweb addresses * More fee fixes * Broadcast mweb * Remove test files * One more * Confirm sent txns * Couple of fixes * Resign inputs after mweb create * Some more fixes * Update balance after sending * Correctly update address records * Update confs * [skip ci] updates * [skip ci] add dep overrides * working * small fix * merge fixes [skip ci] * merge fixes [skip ci] * [skip ci] minor fixes * silent payment fixes [skip ci] * updates [skip ci] * save [skip ci] * use mwebutxos box * [skip ci] lots of fixes, still testing * add rescan from height feature and test workflow build * install go * use sudo * correct package name * move building mweb higher for faster testing * install fixes * install later version of go * go fixes * testing * testing * testing * testing * testing * should workgit add .github/workflows/pr_test_build.yml * ??? * ??? pt.2 * should work, for real this time * fix tx history not persisting + update build_mwebd script * updates * fix some rescan and address gen issues * save [skip ci] * fix unconfirmed balance not updating when receiving * unspent coins / coin control fixes * coin control fixes * address balance and txCount fixes, try/catch electrum call * fix txCount for addresses * save [skip ci] * potential fixes * minor fix * minor fix - 2 * sync status fixes, potential fix for background state issue * workflow and script updates * updates * expirimental optimization * [skip ci] minor enhancements * workflow and script fixes * workflow minor cleanup [skip ci] * minor code cleanup & friendlier error message on failed tx's * balance when sending fix * experimental * more experiments * save * updates * coin control edge cases * remove neutrino.db if no litecoin wallets left after deleting * update translations * updates * minor fix * [skip ci] update translations + minor fixes * state fixes * configure fix * ui updates * translation fixes * [skip ci] addressbook updates * fix popup * fix popup2 * fix litecoin address book * fix ios mwebd build script * fix for building monero.com * minor fix * uncomment fix for state issues * potential mweb sync fix (ios) * remove print [skip ci] * electrum stream potential fix * fix ios build issues [skip ci] * connection reliability updates, update kotlin code to match swift code, minor electrum error handling * dep fixes * minor fix * more merge fixes * bitcoin_flutter removal fixes * [skip ci] fix always scan setting, swift updates * updates * fixes * small fix * small fix * fix * dart:convert != package:convert * change address fixes * update bitcoin_base to fix mweb address program checking * fix ios xcode project [skip ci] * updates * more fixes * more fixes * ensure we don't initialize mweb until we really have to * fix regression * improve mweb reliability * [skip ci] wip adress generation * wip * wip * [skip ci] wip * updates [skip ci] * ios fixes * fix workflows + ios fix * test old mweb version * update go version and mwebd hash * review updates pt.1 * Update cw_bitcoin/lib/litecoin_wallet.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * remove non-litecoin address types regex [skip ci] * more minor fixes * remove duplicate [skip ci] * Update lib/store/settings_store.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * script updates, swap params on createLitecoinWalletService * topup fix * [skip ci] wip * [skip ci] testing * [skip ci] file didn't get saved * more address generation reliability fixes * [skip ci] minor * minor code cleanup * hopefully prevents send issue * [skip ci] wip address changes * [skip ci] save * save mweb addresses, auto-restart sync process if it gets stuck [skip ci] * address generation issues mostly resolved * more performance fixes * [skip ci] * this should maybe be refactored, pt.1 * separate mweb balances, pt.2 * [skip ci] save * add translations [skip ci] * fix sending with mweb amounts * works for simple mweb-mweb case, further testing needed * found an edge case * [skip ci] make failed broadcast error message less serious * minor * capture all grpc errors and much better error handling overall * [skip ci] minor * prevent transactions with < 6 confirmations from being used + hide mweb balances if mweb is off * fix * merge fixes pt.1 [skip ci] * fix mweb tags * fix * [skip ci] fix tag spacing * fix transaction history not showing up * fix mweb crash on non-fully deleted mweb cache, sync status ETA, other connection fixes * [skip ci] minor code cleanup * [skip ci] minor code cleanup * additional cleanup * silent payments eta fixes and updates * revert sync eta changes into separate pr * [skip ci] minor * [skip ci] minor * revert sync status title * review fixes, additional cleanup * [skip ci] minor * [skip ci] minor * [skip ci] minor * trigger build * review fixes, pt.2 * check if still processing utxos before updating sync status [skip ci] * [skip ci] minor * balance fix * minor * minor * [skip ci] minor * [skip ci] fix test net btc * don't use mwebd for non-mweb tx's * [skip ci] minor cleanup * don't show all 1000+ mweb addresses on receive page * minor cleanup + additional logging --------- Co-authored-by: Hector Chu <hectorchu@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>

Matthew Fosse committed Sep 27, 2024 at 19:22 UTC 62e0c2a5928994b74219730291bf4113f23d3625
120 files changed +4304 -361
.github/workflows/pr_test_build_android.yml
+19
@@ -96,6 +96,25 @@ jobs:
96 cd /opt/android/cake_wallet
97 flutter pub get
98
99 +
100 + - name: Install go and gomobile
101 + run: |
102 + # install go > 1.23:
103 + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
104 + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
105 + export PATH=$PATH:/usr/local/go/bin
106 + export PATH=$PATH:~/go/bin
107 + go install golang.org/x/mobile/cmd/gomobile@latest
108 + gomobile init
109 +
110 + - name: Build mwebd
111 + run: |
112 + # paths are reset after each step, so we need to set them again:
113 + export PATH=$PATH:/usr/local/go/bin
114 + export PATH=$PATH:~/go/bin
115 + cd /opt/android/cake_wallet/scripts/android/
116 + ./build_mwebd.sh --dont-install
117 +
118 - name: Generate KeyStore
119 run: |
120 cd /opt/android/cake_wallet/android/app
.github/workflows/pr_test_build_linux.yml
+19
@@ -89,6 +89,25 @@ jobs:
89 cd /opt/android/cake_wallet
90 flutter pub get
91
92 + - name: Install go and gomobile
93 + run: |
94 + # install go > 1.23:
95 + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
96 + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
97 + export PATH=$PATH:/usr/local/go/bin
98 + export PATH=$PATH:~/go/bin
99 + go install golang.org/x/mobile/cmd/gomobile@latest
100 + gomobile init
101 +
102 + - name: Build mwebd
103 + run: |
104 + # paths are reset after each step, so we need to set them again:
105 + export PATH=$PATH:/usr/local/go/bin
106 + export PATH=$PATH:~/go/bin
107 + # build mwebd:
108 + cd /opt/android/cake_wallet/scripts/android/
109 + ./build_mwebd.sh --dont-install
110 +
111 - name: Generate localization
112 run: |
113 cd /opt/android/cake_wallet
assets/images/mweb_logo.png
Binary files /dev/null and b/assets/images/mweb_logo.png differ
cw_bitcoin/lib/bitcoin_mnemonic.dart
+2 -1
@@ -8,6 +8,7 @@ import 'package:cw_core/sec_random_native.dart';
8 import 'package:cw_core/utils/text_normalizer.dart';
9
10 const segwit = '100';
11 +const mweb = 'eb';
12 final wordlist = englishWordlist;
13
14 double logBase(num x, num base) => log(x) / log(base);
@@ -125,7 +126,7 @@ Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
126 return Uint8List.fromList(bytes);
127 }
128
128 -bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el);
129 +bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit, mweb]).any((el) => el);
130
131 bool validateMnemonic(String mnemonic, {String prefix = segwit}) {
132 try {
cw_bitcoin/lib/bitcoin_receive_page_option.dart
+10
@@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption {
7 static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)');
8 static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
9 static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
10 + static const mweb = BitcoinReceivePageOption._('MWEB');
11
12 static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
13
@@ -27,6 +28,11 @@ class BitcoinReceivePageOption implements ReceivePageOption {
28 BitcoinReceivePageOption.p2pkh
29 ];
30
31 + static const allLitecoin = [
32 + BitcoinReceivePageOption.p2wpkh,
33 + BitcoinReceivePageOption.mweb
34 + ];
35 +
36 BitcoinAddressType toType() {
37 switch (this) {
38 case BitcoinReceivePageOption.p2tr:
@@ -39,6 +45,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
45 return P2shAddressType.p2wpkhInP2sh;
46 case BitcoinReceivePageOption.silent_payments:
47 return SilentPaymentsAddresType.p2sp;
48 + case BitcoinReceivePageOption.mweb:
49 + return SegwitAddresType.mweb;
50 case BitcoinReceivePageOption.p2wpkh:
51 default:
52 return SegwitAddresType.p2wpkh;
@@ -51,6 +59,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
59 return BitcoinReceivePageOption.p2tr;
60 case SegwitAddresType.p2wsh:
61 return BitcoinReceivePageOption.p2wsh;
62 + case SegwitAddresType.mweb:
63 + return BitcoinReceivePageOption.mweb;
64 case P2pkhAddressType.p2pkh:
65 return BitcoinReceivePageOption.p2pkh;
66 case P2shAddressType.p2wpkhInP2sh:
cw_bitcoin/lib/bitcoin_transaction_priority.dart
+1 -1
@@ -87,7 +87,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority {
87 }
88
89 @override
90 - String get units => 'Latoshi';
90 + String get units => 'Litoshi';
91
92 @override
93 String toString() {
cw_bitcoin/lib/electrum.dart
+6
@@ -506,6 +506,12 @@ class ElectrumClient {
506
507 void _methodHandler({required String method, required Map<String, dynamic> request}) {
508 switch (method) {
509 + case 'blockchain.headers.subscribe':
510 + final params = request['params'] as List<dynamic>;
511 + final id = 'blockchain.headers.subscribe';
512 +
513 + _tasks[id]?.subject?.add(params.last);
514 + break;
515 case 'blockchain.scripthash.subscribe':
516 final params = request['params'] as List<dynamic>;
517 final scripthash = params.first as String?;
cw_bitcoin/lib/electrum_balance.dart
+29 -4
@@ -7,7 +7,14 @@ class ElectrumBalance extends Balance {
7 required this.confirmed,
8 required this.unconfirmed,
9 required this.frozen,
10 - }) : super(confirmed, unconfirmed);
10 + this.secondConfirmed = 0,
11 + this.secondUnconfirmed = 0,
12 + }) : super(
13 + confirmed,
14 + unconfirmed,
15 + secondAvailable: secondConfirmed,
16 + secondAdditional: secondUnconfirmed,
17 + );
18
19 static ElectrumBalance? fromJSON(String? jsonSource) {
20 if (jsonSource == null) {
@@ -25,9 +32,12 @@ class ElectrumBalance extends Balance {
32 int confirmed;
33 int unconfirmed;
34 final int frozen;
35 + int secondConfirmed = 0;
36 + int secondUnconfirmed = 0;
37
38 @override
30 - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
39 + String get formattedAvailableBalance =>
40 + bitcoinAmountToString(amount: confirmed - frozen);
41
42 @override
43 String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
@@ -38,6 +48,21 @@ class ElectrumBalance extends Balance {
48 return frozenFormatted == '0.0' ? '' : frozenFormatted;
49 }
50
41 - String toJSON() =>
42 - json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
51 + @override
52 + String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed);
53 +
54 + @override
55 + String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed);
56 +
57 + @override
58 + String get formattedFullAvailableBalance =>
59 + bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen);
60 +
61 + String toJSON() => json.encode({
62 + 'confirmed': confirmed,
63 + 'unconfirmed': unconfirmed,
64 + 'frozen': frozen,
65 + 'secondConfirmed': secondConfirmed,
66 + 'secondUnconfirmed': secondUnconfirmed
67 + });
68 }
cw_bitcoin/lib/electrum_wallet.dart
+133 -121
@@ -2,9 +2,9 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 import 'dart:isolate';
5 -import 'dart:math';
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 +import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
8 import 'package:shared_preferences/shared_preferences.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
10 import 'package:blockchain_utils/blockchain_utils.dart';
@@ -23,6 +23,7 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
23 import 'package:cw_bitcoin/electrum_transaction_info.dart';
24 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
25 import 'package:cw_bitcoin/exceptions.dart';
26 +import 'package:cw_bitcoin/litecoin_wallet.dart';
27 import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
28 import 'package:cw_bitcoin/utils.dart';
29 import 'package:cw_core/crypto_currency.dart';
@@ -112,11 +113,18 @@ abstract class ElectrumWalletBase
113 }
114
115 if (seedBytes != null) {
115 - return currency == CryptoCurrency.bch
116 - ? bitcoinCashHDWallet(seedBytes)
117 - : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
116 + switch (currency) {
117 + case CryptoCurrency.btc:
118 + case CryptoCurrency.ltc:
119 + case CryptoCurrency.tbtc:
120 + return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
121 _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
122 as Bip32Slip10Secp256k1;
123 + case CryptoCurrency.bch:
124 + return bitcoinCashHDWallet(seedBytes);
125 + default:
126 + throw Exception("Unsupported currency");
127 + }
128 }
129
130 return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
@@ -163,11 +171,13 @@ abstract class ElectrumWalletBase
171 Set<String> get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet();
172
173 List<String> get scriptHashes => walletAddresses.addressesByReceiveType
174 + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
175 .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network))
176 .toList();
177
178 List<String> get publicScriptHashes => walletAddresses.allAddresses
179 .where((addr) => !addr.isHidden)
180 + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
181 .map((addr) => addr.getScriptHash(network))
182 .toList();
183
@@ -274,6 +284,7 @@ abstract class ElectrumWalletBase
284
285 void Function(FlutterErrorDetails)? _onError;
286 Timer? _autoSaveTimer;
287 + StreamSubscription<dynamic>? _receiveStream;
288 Timer? _updateFeeRateTimer;
289 static const int _autoSaveInterval = 1;
290
@@ -324,7 +335,8 @@ abstract class ElectrumWalletBase
335 isSingleScan: doSingleScan ?? false,
336 ));
337
327 - await for (var message in receivePort) {
338 + _receiveStream?.cancel();
339 + _receiveStream = receivePort.listen((var message) async {
340 if (message is Map<String, ElectrumTransactionInfo>) {
341 for (final map in message.entries) {
342 final txid = map.key;
@@ -387,10 +399,16 @@ abstract class ElectrumWalletBase
399 nodeSupportsSilentPayments = false;
400 }
401
390 - syncStatus = message.syncStatus;
402 + if (message.syncStatus is SyncingSyncStatus) {
403 + var status = message.syncStatus as SyncingSyncStatus;
404 + syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
405 + } else {
406 + syncStatus = message.syncStatus;
407 + }
408 +
409 await walletInfo.updateRestoreHeight(message.height);
410 }
393 - }
411 + });
412 }
413
414 void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) {
@@ -430,9 +448,9 @@ abstract class ElectrumWalletBase
448 await _setInitialHeight();
449 }
450
433 - await _subscribeForUpdates();
434 -
451 + await subscribeForUpdates();
452 await updateTransactions();
453 +
454 await updateAllUnspents();
455 await updateBalance();
456 await updateFeeRates();
@@ -537,6 +555,7 @@ abstract class ElectrumWalletBase
555 try {
556 syncStatus = ConnectingSyncStatus();
557
558 + await _receiveStream?.cancel();
559 await electrumClient.close();
560
561 electrumClient.onConnectionStatusChange = _onConnectionStatusChange;
@@ -682,26 +701,15 @@ abstract class ElectrumWalletBase
701 paysToSilentPayment: hasSilentPayment,
702 );
703
685 - int estimatedSize;
686 - if (network is BitcoinCashNetwork) {
687 - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
688 - utxos: utxoDetails.utxos,
689 - outputs: outputs,
690 - network: network as BitcoinCashNetwork,
691 - memo: memo,
692 - );
693 - } else {
694 - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
695 - utxos: utxoDetails.utxos,
696 - outputs: outputs,
697 - network: network,
698 - memo: memo,
699 - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
700 - vinOutpoints: utxoDetails.vinOutpoints,
701 - );
702 - }
703 -
704 - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
704 + int fee = await calcFee(
705 + utxos: utxoDetails.utxos,
706 + outputs: outputs,
707 + network: network,
708 + memo: memo,
709 + feeRate: feeRate,
710 + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
711 + vinOutpoints: utxoDetails.vinOutpoints,
712 + );
713
714 if (fee == 0) {
715 throw BitcoinTransactionNoFeeException();
@@ -788,7 +796,10 @@ abstract class ElectrumWalletBase
796 throw BitcoinTransactionWrongBalanceException();
797 }
798
791 - final changeAddress = await walletAddresses.getChangeAddress();
799 + final changeAddress = await walletAddresses.getChangeAddress(
800 + outputs: outputs,
801 + utxoDetails: utxoDetails,
802 + );
803 final address = RegexUtils.addressTypeFromStr(changeAddress, network);
804 outputs.add(BitcoinOutput(
805 address: address,
@@ -796,26 +807,13 @@ abstract class ElectrumWalletBase
807 isChange: true,
808 ));
809
799 - int estimatedSize;
800 - if (network is BitcoinCashNetwork) {
801 - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
802 - utxos: utxoDetails.utxos,
803 - outputs: outputs,
804 - network: network as BitcoinCashNetwork,
805 - memo: memo,
806 - );
807 - } else {
808 - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
809 - utxos: utxoDetails.utxos,
810 - outputs: outputs,
811 - network: network,
812 - memo: memo,
813 - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
814 - vinOutpoints: utxoDetails.vinOutpoints,
815 - );
816 - }
817 -
818 - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
810 + int fee = await calcFee(
811 + utxos: utxoDetails.utxos,
812 + outputs: outputs,
813 + network: network,
814 + memo: memo,
815 + feeRate: feeRate,
816 + );
817
818 if (fee == 0) {
819 throw BitcoinTransactionNoFeeException();
@@ -825,6 +823,8 @@ abstract class ElectrumWalletBase
823 final lastOutput = outputs.last;
824 final amountLeftForChange = amountLeftForChangeAndFee - fee;
825
826 + print(amountLeftForChangeAndFee);
827 +
828 if (!_isBelowDust(amountLeftForChange)) {
829 // Here, lastOutput already is change, return the amount left without the fee to the user's address.
830 outputs[outputs.length - 1] = BitcoinOutput(
@@ -874,7 +874,7 @@ abstract class ElectrumWalletBase
874
875 final totalAmount = amount + fee;
876
877 - if (totalAmount > balance[currency]!.confirmed) {
877 + if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) {
878 throw BitcoinTransactionWrongBalanceException();
879 }
880
@@ -909,6 +909,37 @@ abstract class ElectrumWalletBase
909 );
910 }
911
912 + Future<int> calcFee({
913 + required List<UtxoWithAddress> utxos,
914 + required List<BitcoinBaseOutput> outputs,
915 + required BasedUtxoNetwork network,
916 + String? memo,
917 + required int feeRate,
918 + List<ECPrivateInfo>? inputPrivKeyInfos,
919 + List<Outpoint>? vinOutpoints,
920 + }) async {
921 + int estimatedSize;
922 + if (network is BitcoinCashNetwork) {
923 + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
924 + utxos: utxos,
925 + outputs: outputs,
926 + network: network,
927 + memo: memo,
928 + );
929 + } else {
930 + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
931 + utxos: utxos,
932 + outputs: outputs,
933 + network: network,
934 + memo: memo,
935 + inputPrivKeyInfos: inputPrivKeyInfos,
936 + vinOutpoints: vinOutpoints,
937 + );
938 + }
939 +
940 + return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
941 + }
942 +
943 @override
944 Future<PendingTransaction> createTransaction(Object credentials) async {
945 try {
@@ -1134,6 +1165,7 @@ abstract class ElectrumWalletBase
1165 'derivationPath': walletInfo.derivationInfo?.derivationPath,
1166 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(),
1167 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(),
1168 + 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(),
1169 });
1170
1171 int feeRate(TransactionPriority priority) {
@@ -1253,6 +1285,7 @@ abstract class ElectrumWalletBase
1285 @override
1286 Future<void> close() async {
1287 try {
1288 + await _receiveStream?.cancel();
1289 await electrumClient.close();
1290 } catch (_) {}
1291 _autoSaveTimer?.cancel();
@@ -1283,71 +1316,55 @@ abstract class ElectrumWalletBase
1316
1317 unspentCoins = updatedUnspentCoins;
1318
1286 - if (unspentCoinsInfo.isEmpty) {
1287 - unspentCoins.forEach((coin) => _addCoinInfo(coin));
1319 + if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
1320 + unspentCoins.forEach((coin) => addCoinInfo(coin));
1321 return;
1322 }
1323
1291 - if (unspentCoins.isNotEmpty) {
1292 - unspentCoins.forEach((coin) {
1293 - final coinInfoList = unspentCoinsInfo.values.where((element) =>
1324 + await updateCoins(unspentCoins);
1325 + await _refreshUnspentCoinsInfo();
1326 + }
1327 +
1328 + Future<void> updateCoins(List<BitcoinUnspent> newUnspentCoins) async {
1329 + if (newUnspentCoins.isEmpty) {
1330 + return;
1331 + }
1332 +
1333 + newUnspentCoins.forEach((coin) {
1334 + final coinInfoList = unspentCoinsInfo.values.where(
1335 + (element) =>
1336 element.walletId.contains(id) &&
1337 element.hash.contains(coin.hash) &&
1296 - element.vout == coin.vout);
1297 -
1298 - if (coinInfoList.isNotEmpty) {
1299 - final coinInfo = coinInfoList.first;
1338 + element.vout == coin.vout,
1339 + );
1340
1301 - coin.isFrozen = coinInfo.isFrozen;
1302 - coin.isSending = coinInfo.isSending;
1303 - coin.note = coinInfo.note;
1304 - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
1305 - coin.bitcoinAddressRecord.balance += coinInfo.value;
1306 - } else {
1307 - _addCoinInfo(coin);
1308 - }
1309 - });
1310 - }
1341 + if (coinInfoList.isNotEmpty) {
1342 + final coinInfo = coinInfoList.first;
1343
1312 - await _refreshUnspentCoinsInfo();
1344 + coin.isFrozen = coinInfo.isFrozen;
1345 + coin.isSending = coinInfo.isSending;
1346 + coin.note = coinInfo.note;
1347 + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
1348 + coin.bitcoinAddressRecord.balance += coinInfo.value;
1349 + } else {
1350 + addCoinInfo(coin);
1351 + }
1352 + });
1353 }
1354
1355 @action
1316 - Future<void> updateUnspents(BitcoinAddressRecord address) async {
1356 + Future<void> updateUnspentsForAddress(BitcoinAddressRecord address) async {
1357 final newUnspentCoins = await fetchUnspent(address);
1318 -
1319 - if (newUnspentCoins.isNotEmpty) {
1320 - unspentCoins.addAll(newUnspentCoins);
1321 -
1322 - newUnspentCoins.forEach((coin) {
1323 - final coinInfoList = unspentCoinsInfo.values.where(
1324 - (element) =>
1325 - element.walletId.contains(id) &&
1326 - element.hash.contains(coin.hash) &&
1327 - element.vout == coin.vout,
1328 - );
1329 -
1330 - if (coinInfoList.isNotEmpty) {
1331 - final coinInfo = coinInfoList.first;
1332 -
1333 - coin.isFrozen = coinInfo.isFrozen;
1334 - coin.isSending = coinInfo.isSending;
1335 - coin.note = coinInfo.note;
1336 - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
1337 - coin.bitcoinAddressRecord.balance += coinInfo.value;
1338 - } else {
1339 - _addCoinInfo(coin);
1340 - }
1341 - });
1342 - }
1358 + await updateCoins(newUnspentCoins);
1359 }
1360
1361 @action
1362 Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
1347 - final unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
1348 -
1363 + List<Map<String, dynamic>> unspents = [];
1364 List<BitcoinUnspent> updatedUnspentCoins = [];
1365
1366 + unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
1367 +
1368 await Future.wait(unspents.map((unspent) async {
1369 try {
1370 final coin = BitcoinUnspent.fromJSON(address, unspent);
@@ -1363,7 +1380,7 @@ abstract class ElectrumWalletBase
1380 }
1381
1382 @action
1366 - Future<void> _addCoinInfo(BitcoinUnspent coin) async {
1383 + Future<void> addCoinInfo(BitcoinUnspent coin) async {
1384 final newInfo = UnspentCoinsInfo(
1385 walletId: id,
1386 hash: coin.hash,
@@ -1714,12 +1731,14 @@ abstract class ElectrumWalletBase
1731 final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
1732
1733 if (type == WalletType.bitcoin) {
1717 - await Future.wait(ADDRESS_TYPES
1734 + await Future.wait(BITCOIN_ADDRESS_TYPES
1735 .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
1736 } else if (type == WalletType.bitcoinCash) {
1720 - await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh);
1737 + await Future.wait(BITCOIN_CASH_ADDRESS_TYPES
1738 + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
1739 } else if (type == WalletType.litecoin) {
1722 - await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh);
1740 + await Future.wait(LITECOIN_ADDRESS_TYPES
1741 + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
1742 }
1743
1744 transactionHistory.transactions.values.forEach((tx) async {
@@ -1776,7 +1795,7 @@ abstract class ElectrumWalletBase
1795 matchedAddresses.toList(),
1796 addressRecord.isHidden,
1797 (address) async {
1779 - await _subscribeForUpdates();
1798 + await subscribeForUpdates();
1799 return _fetchAddressHistory(address, await getCurrentChainTip())
1800 .then((history) => history.isNotEmpty ? address.address : null);
1801 },
@@ -1865,7 +1884,7 @@ abstract class ElectrumWalletBase
1884 }
1885 }
1886
1868 - Future<void> _subscribeForUpdates() async {
1887 + Future<void> subscribeForUpdates() async {
1888 final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
1889 (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)),
1890 );
@@ -1876,7 +1895,7 @@ abstract class ElectrumWalletBase
1895 _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh);
1896 _scripthashesUpdateSubject[sh]?.listen((event) async {
1897 try {
1879 - await updateUnspents(address);
1898 + await updateUnspentsForAddress(address);
1899
1900 await updateBalance();
1901
@@ -1893,8 +1912,10 @@ abstract class ElectrumWalletBase
1912 }));
1913 }
1914
1896 - Future<ElectrumBalance> _fetchBalances() async {
1897 - final addresses = walletAddresses.allAddresses.toList();
1915 + Future<ElectrumBalance> fetchBalances() async {
1916 + final addresses = walletAddresses.allAddresses
1917 + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
1918 + .toList();
1919 final balanceFutures = <Future<Map<String, dynamic>>>[];
1920 for (var i = 0; i < addresses.length; i++) {
1921 final addressRecord = addresses[i];
@@ -1944,6 +1965,7 @@ abstract class ElectrumWalletBase
1965 totalConfirmed += confirmed;
1966 totalUnconfirmed += unconfirmed;
1967
1968 + addressRecord.balance = confirmed + unconfirmed;
1969 if (confirmed > 0 || unconfirmed > 0) {
1970 addressRecord.setAsUsed();
1971 }
@@ -1957,22 +1979,10 @@ abstract class ElectrumWalletBase
1979 }
1980
1981 Future<void> updateBalance() async {
1960 - balance[currency] = await _fetchBalances();
1982 + balance[currency] = await fetchBalances();
1983 await save();
1984 }
1985
1964 - String getChangeAddress() {
1965 - const minCountOfHiddenAddresses = 5;
1966 - final random = Random();
1967 - var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList();
1968 -
1969 - if (addresses.length < minCountOfHiddenAddresses) {
1970 - addresses = walletAddresses.allAddresses.toList();
1971 - }
1972 -
1973 - return addresses[random.nextInt(addresses.length)].address;
1974 - }
1975 -
1986 @override
1987 void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
1988
@@ -2475,6 +2485,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
2485 return SegwitAddresType.p2wsh;
2486 } else if (type is P2trAddress) {
2487 return SegwitAddresType.p2tr;
2488 + } else if (type is MwebAddress) {
2489 + return SegwitAddresType.mweb;
2490 } else if (type is SilentPaymentsAddresType) {
2491 return SilentPaymentsAddresType.p2sp;
2492 } else {
cw_bitcoin/lib/electrum_wallet_addresses.dart
+155 -66
@@ -1,6 +1,7 @@
1 import 'package:bitcoin_base/bitcoin_base.dart';
2 import 'package:blockchain_utils/blockchain_utils.dart';
3 import 'package:cw_bitcoin/bitcoin_address_record.dart';
4 +import 'package:cw_bitcoin/electrum_wallet.dart';
5 import 'package:cw_core/wallet_addresses.dart';
6 import 'package:cw_core/wallet_info.dart';
7 import 'package:cw_core/wallet_type.dart';
@@ -10,7 +11,7 @@ part 'electrum_wallet_addresses.g.dart';
11
12 class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
13
13 -const List<BitcoinAddressType> ADDRESS_TYPES = [
14 +const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
15 SegwitAddresType.p2wpkh,
16 P2pkhAddressType.p2pkh,
17 SegwitAddresType.p2tr,
@@ -18,6 +19,15 @@ const List<BitcoinAddressType> ADDRESS_TYPES = [
19 P2shAddressType.p2wpkhInP2sh,
20 ];
21
22 +const List<BitcoinAddressType> LITECOIN_ADDRESS_TYPES = [
23 + SegwitAddresType.p2wpkh,
24 + SegwitAddresType.mweb,
25 +];
26 +
27 +const List<BitcoinAddressType> BITCOIN_CASH_ADDRESS_TYPES = [
28 + P2pkhAddressType.p2pkh,
29 +];
30 +
31 abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
32 ElectrumWalletAddressesBase(
33 WalletInfo walletInfo, {
@@ -29,6 +39,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
39 Map<String, int>? initialChangeAddressIndex,
40 List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
41 int initialSilentAddressIndex = 0,
42 + List<BitcoinAddressRecord>? initialMwebAddresses,
43 Bip32Slip10Secp256k1? masterHd,
44 BitcoinAddressType? initialAddressPageType,
45 }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
@@ -49,6 +60,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
60 silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
61 (initialSilentAddresses ?? []).toSet()),
62 currentSilentAddressIndex = initialSilentAddressIndex,
63 + mwebAddresses =
64 + ObservableList<BitcoinAddressRecord>.of((initialMwebAddresses ?? []).toSet()),
65 super(walletInfo) {
66 if (masterHd != null) {
67 silentAddress = SilentPaymentOwner.fromPrivateKeys(
@@ -91,6 +104,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
104 final ObservableList<BitcoinAddressRecord> receiveAddresses;
105 final ObservableList<BitcoinAddressRecord> changeAddresses;
106 final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
107 + final ObservableList<BitcoinAddressRecord> mwebAddresses;
108 final BasedUtxoNetwork network;
109 final Bip32Slip10Secp256k1 mainHd;
110 final Bip32Slip10Secp256k1 sideHd;
@@ -213,7 +227,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
227 if (walletInfo.type == WalletType.bitcoinCash) {
228 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
229 } else if (walletInfo.type == WalletType.litecoin) {
216 - await _generateInitialAddresses();
230 + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
231 + await _generateInitialAddresses(type: SegwitAddresType.mweb);
232 } else if (walletInfo.type == WalletType.bitcoin) {
233 await _generateInitialAddresses();
234 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
@@ -221,6 +236,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
236 await _generateInitialAddresses(type: SegwitAddresType.p2tr);
237 await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
238 }
239 +
240 updateAddressesByMatch();
241 updateReceiveAddresses();
242 updateChangeAddresses();
@@ -237,7 +253,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
253 }
254
255 @action
240 - Future<String> getChangeAddress() async {
256 + Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
257 updateChangeAddresses();
258
259 if (changeAddresses.isEmpty) {
@@ -317,80 +333,135 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
333 return address;
334 }
335
320 - String getAddress(
321 - {required int index,
322 - required Bip32Slip10Secp256k1 hd,
323 - BitcoinAddressType? addressType}) =>
336 + String getAddress({
337 + required int index,
338 + required Bip32Slip10Secp256k1 hd,
339 + BitcoinAddressType? addressType,
340 + }) =>
341 '';
342
326 - @override
327 - Future<void> updateAddressesInBox() async {
328 - try {
329 - addressesMap.clear();
330 - addressesMap[address] = 'Active';
343 + Future<String> getAddressAsync({
344 + required int index,
345 + required Bip32Slip10Secp256k1 hd,
346 + BitcoinAddressType? addressType,
347 + }) async =>
348 + getAddress(index: index, hd: hd, addressType: addressType);
349 +
350 + void addBitcoinAddressTypes() {
351 + final lastP2wpkh = _addresses
352 + .where((addressRecord) =>
353 + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
354 + .toList()
355 + .last;
356 + if (lastP2wpkh.address != address) {
357 + addressesMap[lastP2wpkh.address] = 'P2WPKH';
358 + } else {
359 + addressesMap[address] = 'Active - P2WPKH';
360 + }
361
332 - allAddressesMap.clear();
333 - _addresses.forEach((addressRecord) {
334 - allAddressesMap[addressRecord.address] = addressRecord.name;
335 - });
362 + final lastP2pkh = _addresses.firstWhere(
363 + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
364 + if (lastP2pkh.address != address) {
365 + addressesMap[lastP2pkh.address] = 'P2PKH';
366 + } else {
367 + addressesMap[address] = 'Active - P2PKH';
368 + }
369
337 - final lastP2wpkh = _addresses
338 - .where((addressRecord) =>
339 - _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
340 - .toList()
341 - .last;
342 - if (lastP2wpkh.address != address) {
343 - addressesMap[lastP2wpkh.address] = 'P2WPKH';
344 - } else {
345 - addressesMap[address] = 'Active - P2WPKH';
346 - }
370 + final lastP2sh = _addresses.firstWhere((addressRecord) =>
371 + _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
372 + if (lastP2sh.address != address) {
373 + addressesMap[lastP2sh.address] = 'P2SH';
374 + } else {
375 + addressesMap[address] = 'Active - P2SH';
376 + }
377
348 - final lastP2pkh = _addresses.firstWhere(
349 - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
350 - if (lastP2pkh.address != address) {
351 - addressesMap[lastP2pkh.address] = 'P2PKH';
352 - } else {
353 - addressesMap[address] = 'Active - P2PKH';
354 - }
378 + final lastP2tr = _addresses.firstWhere(
379 + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
380 + if (lastP2tr.address != address) {
381 + addressesMap[lastP2tr.address] = 'P2TR';
382 + } else {
383 + addressesMap[address] = 'Active - P2TR';
384 + }
385
356 - final lastP2sh = _addresses.firstWhere((addressRecord) =>
357 - _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
358 - if (lastP2sh.address != address) {
359 - addressesMap[lastP2sh.address] = 'P2SH';
360 - } else {
361 - addressesMap[address] = 'Active - P2SH';
362 - }
386 + final lastP2wsh = _addresses.firstWhere(
387 + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
388 + if (lastP2wsh.address != address) {
389 + addressesMap[lastP2wsh.address] = 'P2WSH';
390 + } else {
391 + addressesMap[address] = 'Active - P2WSH';
392 + }
393
364 - final lastP2tr = _addresses.firstWhere(
365 - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
366 - if (lastP2tr.address != address) {
367 - addressesMap[lastP2tr.address] = 'P2TR';
368 - } else {
369 - addressesMap[address] = 'Active - P2TR';
394 + silentAddresses.forEach((addressRecord) {
395 + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
396 + return;
397 }
398
372 - final lastP2wsh = _addresses.firstWhere(
373 - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
374 - if (lastP2wsh.address != address) {
375 - addressesMap[lastP2wsh.address] = 'P2WSH';
399 + if (addressRecord.address != address) {
400 + addressesMap[addressRecord.address] = addressRecord.name.isEmpty
401 + ? "Silent Payments"
402 + : "Silent Payments - " + addressRecord.name;
403 } else {
377 - addressesMap[address] = 'Active - P2WSH';
404 + addressesMap[address] = 'Active - Silent Payments';
405 }
406 + });
407 + }
408
380 - silentAddresses.forEach((addressRecord) {
381 - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
382 - return;
383 - }
409 + void addLitecoinAddressTypes() {
410 + final lastP2wpkh = _addresses
411 + .where((addressRecord) =>
412 + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
413 + .toList()
414 + .last;
415 + if (lastP2wpkh.address != address) {
416 + addressesMap[lastP2wpkh.address] = 'P2WPKH';
417 + } else {
418 + addressesMap[address] = 'Active - P2WPKH';
419 + }
420
385 - if (addressRecord.address != address) {
386 - addressesMap[addressRecord.address] = addressRecord.name.isEmpty
387 - ? "Silent Payments"
388 - : "Silent Payments - " + addressRecord.name;
389 - } else {
390 - addressesMap[address] = 'Active - Silent Payments';
391 - }
421 + final lastMweb = _addresses.firstWhere(
422 + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb));
423 + if (lastMweb.address != address) {
424 + addressesMap[lastMweb.address] = 'MWEB';
425 + } else {
426 + addressesMap[address] = 'Active - MWEB';
427 + }
428 + }
429 +
430 + void addBitcoinCashAddressTypes() {
431 + final lastP2pkh = _addresses.firstWhere(
432 + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
433 + if (lastP2pkh.address != address) {
434 + addressesMap[lastP2pkh.address] = 'P2PKH';
435 + } else {
436 + addressesMap[address] = 'Active - P2PKH';
437 + }
438 + }
439 +
440 + @override
441 + Future<void> updateAddressesInBox() async {
442 + try {
443 + addressesMap.clear();
444 + addressesMap[address] = 'Active';
445 +
446 + allAddressesMap.clear();
447 + _addresses.forEach((addressRecord) {
448 + allAddressesMap[addressRecord.address] = addressRecord.name;
449 });
450
451 + switch (walletInfo.type) {
452 + case WalletType.bitcoin:
453 + addBitcoinAddressTypes();
454 + break;
455 + case WalletType.litecoin:
456 + addLitecoinAddressTypes();
457 + break;
458 + case WalletType.bitcoinCash:
459 + addBitcoinCashAddressTypes();
460 + break;
461 + default:
462 + break;
463 + }
464 +
465 await saveAddressesInBox();
466 } catch (e) {
467 print(e.toString());
@@ -410,6 +481,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
481 foundAddress = addressRecord;
482 }
483 });
484 + mwebAddresses.forEach((addressRecord) {
485 + if (addressRecord.address == address) {
486 + foundAddress = addressRecord;
487 + }
488 + });
489
490 if (foundAddress != null) {
491 foundAddress!.setNewName(label);
@@ -510,7 +586,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
586
587 for (var i = startIndex; i < count + startIndex; i++) {
588 final address = BitcoinAddressRecord(
513 - getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
589 + await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
590 index: i,
591 isHidden: isHidden,
592 type: type ?? addressPageType,
@@ -540,15 +616,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
616 updateAddressesByMatch();
617 }
618
619 + @action
620 + void addMwebAddresses(Iterable<BitcoinAddressRecord> addresses) {
621 + final addressesSet = this.mwebAddresses.toSet();
622 + addressesSet.addAll(addresses);
623 + this.mwebAddresses.clear();
624 + this.mwebAddresses.addAll(addressesSet);
625 + updateAddressesByMatch();
626 + }
627 +
628 void _validateAddresses() {
544 - _addresses.forEach((element) {
629 + _addresses.forEach((element) async {
630 + if (element.type == SegwitAddresType.mweb) {
631 + // this would add a ton of startup lag for mweb addresses since we have 1000 of them
632 + return;
633 + }
634 if (!element.isHidden &&
635 element.address !=
547 - getAddress(index: element.index, hd: mainHd, addressType: element.type)) {
636 + await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
637 element.isHidden = true;
638 } else if (element.isHidden &&
639 element.address !=
551 - getAddress(index: element.index, hd: sideHd, addressType: element.type)) {
640 + await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
641 element.isHidden = false;
642 }
643 });
cw_bitcoin/lib/electrum_wallet_snapshot.dart
+12 -1
@@ -23,6 +23,7 @@ class ElectrumWalletSnapshot {
23 required this.addressPageType,
24 required this.silentAddresses,
25 required this.silentAddressIndex,
26 + required this.mwebAddresses,
27 this.passphrase,
28 this.derivationType,
29 this.derivationPath,
@@ -44,6 +45,8 @@ class ElectrumWalletSnapshot {
45
46 List<BitcoinAddressRecord> addresses;
47 List<BitcoinSilentPaymentAddressRecord> silentAddresses;
48 + List<BitcoinAddressRecord> mwebAddresses;
49 +
50 ElectrumBalance balance;
51 Map<String, int> regularAddressIndex;
52 Map<String, int> changeAddressIndex;
@@ -56,10 +59,11 @@ class ElectrumWalletSnapshot {
59 final path = await pathForWallet(name: name, type: type);
60 final jsonSource = await encryptionFileUtils.read(path: path, password: password);
61 final data = json.decode(jsonSource) as Map;
59 - final addressesTmp = data['addresses'] as List? ?? <Object>[];
62 final mnemonic = data['mnemonic'] as String?;
63 final xpub = data['xpub'] as String?;
64 final passphrase = data['passphrase'] as String? ?? '';
65 +
66 + final addressesTmp = data['addresses'] as List? ?? <Object>[];
67 final addresses = addressesTmp
68 .whereType<String>()
69 .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
@@ -71,6 +75,12 @@ class ElectrumWalletSnapshot {
75 .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network))
76 .toList();
77
78 + final mwebAddressTmp = data['mweb_addresses'] as List? ?? <Object>[];
79 + final mwebAddresses = mwebAddressTmp
80 + .whereType<String>()
81 + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
82 + .toList();
83 +
84 final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ??
85 ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
86 var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
@@ -113,6 +123,7 @@ class ElectrumWalletSnapshot {
123 derivationPath: derivationPath,
124 silentAddresses: silentAddresses,
125 silentAddressIndex: silentAddressIndex,
126 + mwebAddresses: mwebAddresses,
127 );
128 }
129 }
cw_bitcoin/lib/litecoin_wallet.dart
+829 -19
@@ -1,15 +1,31 @@
1 +import 'dart:async';
2 import 'dart:convert';
2 -
3 +import 'package:convert/convert.dart' as convert;
4 +import 'dart:math';
5 +import 'package:collection/collection.dart';
6 +import 'package:crypto/crypto.dart';
7 +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
8 +import 'package:cw_core/cake_hive.dart';
9 +import 'package:cw_core/mweb_utxo.dart';
10 +import 'package:cw_mweb/mwebd.pbgrpc.dart';
11 +import 'package:fixnum/fixnum.dart';
12 +import 'package:bip39/bip39.dart' as bip39;
13 import 'package:bitcoin_base/bitcoin_base.dart';
14 import 'package:blockchain_utils/blockchain_utils.dart';
15 import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
6 -import 'package:bip39/bip39.dart' as bip39;
16 import 'package:cw_bitcoin/bitcoin_address_record.dart';
17 import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
18 import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
19 +import 'package:cw_bitcoin/bitcoin_unspent.dart';
20 +import 'package:cw_bitcoin/electrum_transaction_info.dart';
21 +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
22 +import 'package:cw_bitcoin/utils.dart';
23 import 'package:cw_bitcoin/electrum_derivations.dart';
24 import 'package:cw_core/encryption_file_utils.dart';
25 import 'package:cw_core/crypto_currency.dart';
26 +import 'package:cw_core/pending_transaction.dart';
27 +import 'package:cw_core/sync_status.dart';
28 +import 'package:cw_core/transaction_direction.dart';
29 import 'package:cw_core/unspent_coins_info.dart';
30 import 'package:cw_bitcoin/electrum_balance.dart';
31 import 'package:cw_bitcoin/electrum_wallet.dart';
@@ -19,8 +35,11 @@ import 'package:cw_core/transaction_priority.dart';
35 import 'package:cw_core/wallet_info.dart';
36 import 'package:cw_core/wallet_keys_file.dart';
37 import 'package:flutter/foundation.dart';
38 +import 'package:grpc/grpc.dart';
39 import 'package:hive/hive.dart';
40 import 'package:mobx/mobx.dart';
41 +import 'package:cw_core/wallet_type.dart';
42 +import 'package:cw_mweb/cw_mweb.dart';
43 import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
44 import 'package:pointycastle/ecc/api.dart';
45 import 'package:pointycastle/ecc/curves/secp256k1.dart';
@@ -40,34 +59,54 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
59 String? passphrase,
60 String? addressPageType,
61 List<BitcoinAddressRecord>? initialAddresses,
62 + List<BitcoinAddressRecord>? initialMwebAddresses,
63 ElectrumBalance? initialBalance,
64 Map<String, int>? initialRegularAddressIndex,
65 Map<String, int>? initialChangeAddressIndex,
66 + int? initialMwebHeight,
67 + bool? alwaysScan,
68 }) : super(
47 - mnemonic: mnemonic,
48 - password: password,
49 - walletInfo: walletInfo,
50 - unspentCoinsInfo: unspentCoinsInfo,
51 - network: LitecoinNetwork.mainnet,
52 - initialAddresses: initialAddresses,
53 - initialBalance: initialBalance,
54 - seedBytes: seedBytes,
55 - encryptionFileUtils: encryptionFileUtils,
56 - passphrase: passphrase,
57 - currency: CryptoCurrency.ltc) {
69 + mnemonic: mnemonic,
70 + password: password,
71 + walletInfo: walletInfo,
72 + unspentCoinsInfo: unspentCoinsInfo,
73 + network: LitecoinNetwork.mainnet,
74 + initialAddresses: initialAddresses,
75 + initialBalance: initialBalance,
76 + seedBytes: seedBytes,
77 + encryptionFileUtils: encryptionFileUtils,
78 + currency: CryptoCurrency.ltc,
79 + ) {
80 + mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
81 + mwebEnabled = alwaysScan ?? false;
82 walletAddresses = LitecoinWalletAddresses(
83 walletInfo,
84 initialAddresses: initialAddresses,
85 initialRegularAddressIndex: initialRegularAddressIndex,
86 initialChangeAddressIndex: initialChangeAddressIndex,
87 + initialMwebAddresses: initialMwebAddresses,
88 mainHd: hd,
89 sideHd: accountHD.childKey(Bip32KeyIndex(1)),
90 network: network,
91 + mwebHd: mwebHd,
92 + mwebEnabled: mwebEnabled,
93 );
94 autorun((_) {
95 this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
96 });
97 }
98 + late final Bip32Slip10Secp256k1 mwebHd;
99 + late final Box<MwebUtxo> mwebUtxosBox;
100 + Timer? _syncTimer;
101 + Timer? _feeRatesTimer;
102 + Timer? _processingTimer;
103 + StreamSubscription<Utxo>? _utxoStream;
104 + late RpcClient _stub;
105 + late bool mwebEnabled;
106 + bool processingUtxos = false;
107 +
108 + List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
109 + List<int> get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;
110
111 static Future<LitecoinWallet> create(
112 {required String mnemonic,
@@ -78,6 +117,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
117 String? passphrase,
118 String? addressPageType,
119 List<BitcoinAddressRecord>? initialAddresses,
120 + List<BitcoinAddressRecord>? initialMwebAddresses,
121 ElectrumBalance? initialBalance,
122 Map<String, int>? initialRegularAddressIndex,
123 Map<String, int>? initialChangeAddressIndex}) async {
@@ -101,6 +141,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
141 walletInfo: walletInfo,
142 unspentCoinsInfo: unspentCoinsInfo,
143 initialAddresses: initialAddresses,
144 + initialMwebAddresses: initialMwebAddresses,
145 initialBalance: initialBalance,
146 encryptionFileUtils: encryptionFileUtils,
147 passphrase: passphrase,
@@ -111,12 +152,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
152 );
153 }
154
114 - static Future<LitecoinWallet> open(
115 - {required String name,
116 - required WalletInfo walletInfo,
117 - required Box<UnspentCoinsInfo> unspentCoinsInfo,
118 - required String password,
119 - required EncryptionFileUtils encryptionFileUtils}) async {
155 + static Future<LitecoinWallet> open({
156 + required String name,
157 + required WalletInfo walletInfo,
158 + required Box<UnspentCoinsInfo> unspentCoinsInfo,
159 + required String password,
160 + required bool alwaysScan,
161 + required EncryptionFileUtils encryptionFileUtils,
162 + }) async {
163 final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
164
165 ElectrumWalletSnapshot? snp = null;
@@ -178,6 +221,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
221 walletInfo: walletInfo,
222 unspentCoinsInfo: unspentCoinsInfo,
223 initialAddresses: snp?.addresses,
224 + initialMwebAddresses: snp?.mwebAddresses,
225 initialBalance: snp?.balance,
226 seedBytes: seedBytes!,
227 passphrase: passphrase,
@@ -185,6 +229,551 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
229 initialRegularAddressIndex: snp?.regularAddressIndex,
230 initialChangeAddressIndex: snp?.changeAddressIndex,
231 addressPageType: snp?.addressPageType,
232 + alwaysScan: alwaysScan,
233 + );
234 + }
235 +
236 + Future<void> waitForMwebAddresses() async {
237 + // ensure that we have the full 1000 mweb addresses generated before continuing:
238 + // should no longer be needed, but leaving here just in case
239 + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
240 + while (mwebAddrs.length < 1000) {
241 + print("waiting for mweb addresses to finish generating...");
242 + await Future.delayed(const Duration(milliseconds: 1000));
243 + }
244 + }
245 +
246 + @action
247 + @override
248 + Future<void> startSync() async {
249 + if (syncStatus is SyncronizingSyncStatus) {
250 + return;
251 + }
252 + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
253 + try {
254 + syncStatus = SyncronizingSyncStatus();
255 + await subscribeForUpdates();
256 + updateFeeRates();
257 +
258 + _feeRatesTimer?.cancel();
259 + _feeRatesTimer =
260 + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
261 +
262 + if (!mwebEnabled) {
263 + try {
264 + await updateAllUnspents();
265 + await updateTransactions();
266 + await updateBalance();
267 + syncStatus = SyncedSyncStatus();
268 + } catch (e, s) {
269 + print(e);
270 + print(s);
271 + syncStatus = FailedSyncStatus();
272 + }
273 + return;
274 + }
275 +
276 + await waitForMwebAddresses();
277 + await getStub();
278 + await processMwebUtxos();
279 + await updateTransactions();
280 + await updateUnspent();
281 + await updateBalance();
282 + } catch (e) {
283 + print("failed to start mweb sync: $e");
284 + syncStatus = FailedSyncStatus();
285 + return;
286 + }
287 +
288 + _syncTimer?.cancel();
289 + _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async {
290 + if (syncStatus is FailedSyncStatus) return;
291 +
292 + final nodeHeight =
293 + await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
294 + final resp = await CwMweb.status(StatusRequest());
295 + print("resp.mwebUtxosHeight: ${resp.mwebUtxosHeight}");
296 + print("resp.mwebHeaderHeight: ${resp.mwebHeaderHeight}");
297 + print("resp.blockHeaderHeight: ${resp.blockHeaderHeight}");
298 +
299 + if (resp.blockHeaderHeight < nodeHeight) {
300 + int h = resp.blockHeaderHeight;
301 + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
302 + } else if (resp.mwebHeaderHeight < nodeHeight) {
303 + int h = resp.mwebHeaderHeight;
304 + syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
305 + } else if (resp.mwebUtxosHeight < nodeHeight) {
306 + syncStatus = SyncingSyncStatus(1, 0.999);
307 + } else {
308 + if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
309 + await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
310 + await checkMwebUtxosSpent();
311 + // update the confirmations for each transaction:
312 + for (final transaction in transactionHistory.transactions.values) {
313 + if (transaction.isPending) continue;
314 + int txHeight = transaction.height ?? resp.mwebUtxosHeight;
315 + final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
316 + if (transaction.confirmations == confirmations) continue;
317 + transaction.confirmations = confirmations;
318 + transactionHistory.addOne(transaction);
319 + }
320 + await transactionHistory.save();
321 + }
322 +
323 + // prevent unnecessary reaction triggers:
324 + if (syncStatus is! SyncedSyncStatus) {
325 + // mwebd is synced, but we could still be processing incoming utxos:
326 + if (!processingUtxos) {
327 + syncStatus = SyncedSyncStatus();
328 + }
329 + }
330 + return;
331 + }
332 + });
333 + }
334 +
335 + @action
336 + @override
337 + Future<void> stopSync() async {
338 + _syncTimer?.cancel();
339 + _utxoStream?.cancel();
340 + _feeRatesTimer?.cancel();
341 + await CwMweb.stop();
342 + }
343 +
344 + Future<void> initMwebUtxosBox() async {
345 + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
346 +
347 + mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(boxName);
348 + }
349 +
350 + @override
351 + Future<void> renameWalletFiles(String newWalletName) async {
352 + // rename the hive box:
353 + final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
354 + final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
355 +
356 + final oldBox = await CakeHive.openBox<MwebUtxo>(oldBoxName);
357 + mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
358 + for (final key in oldBox.keys) {
359 + await mwebUtxosBox.put(key, oldBox.get(key)!);
360 + }
361 + oldBox.deleteFromDisk();
362 +
363 + await super.renameWalletFiles(newWalletName);
364 + }
365 +
366 + @action
367 + @override
368 + Future<void> rescan({
369 + required int height,
370 + int? chainTip,
371 + ScanData? scanData,
372 + bool? doSingleScan,
373 + bool? usingElectrs,
374 + }) async {
375 + _syncTimer?.cancel();
376 + int oldHeight = walletInfo.restoreHeight;
377 + await walletInfo.updateRestoreHeight(height);
378 +
379 + // go through mwebUtxos and clear any that are above the new restore height:
380 + if (height == 0) {
381 + await mwebUtxosBox.clear();
382 + transactionHistory.clear();
383 + } else {
384 + for (final utxo in mwebUtxosBox.values) {
385 + if (utxo.height > height) {
386 + await mwebUtxosBox.delete(utxo.outputId);
387 + }
388 + }
389 + // TODO: remove transactions that are above the new restore height!
390 + }
391 +
392 + // reset coin balances and txCount to 0:
393 + unspentCoins.forEach((coin) {
394 + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
395 + coin.bitcoinAddressRecord.balance = 0;
396 + coin.bitcoinAddressRecord.txCount = 0;
397 + });
398 +
399 + for (var addressRecord in walletAddresses.allAddresses) {
400 + addressRecord.balance = 0;
401 + addressRecord.txCount = 0;
402 + }
403 +
404 + await startSync();
405 + }
406 +
407 + @override
408 + Future<void> init() async {
409 + await super.init();
410 + await initMwebUtxosBox();
411 + }
412 +
413 + Future<void> handleIncoming(MwebUtxo utxo, RpcClient stub) async {
414 + final status = await stub.status(StatusRequest());
415 + var date = DateTime.now();
416 + var confirmations = 0;
417 + if (utxo.height > 0) {
418 + date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000);
419 + confirmations = status.blockHeaderHeight - utxo.height + 1;
420 + }
421 + var tx = transactionHistory.transactions.values
422 + .firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false);
423 +
424 + if (tx == null) {
425 + tx = ElectrumTransactionInfo(
426 + WalletType.litecoin,
427 + id: utxo.outputId,
428 + height: utxo.height,
429 + amount: utxo.value.toInt(),
430 + fee: 0,
431 + direction: TransactionDirection.incoming,
432 + isPending: utxo.height == 0,
433 + date: date,
434 + confirmations: confirmations,
435 + inputAddresses: [],
436 + outputAddresses: [utxo.outputId],
437 + isReplaced: false,
438 + );
439 + }
440 +
441 + // don't update the confirmations if the tx is updated by electrum:
442 + if (tx.confirmations == 0 || utxo.height != 0) {
443 + tx.height = utxo.height;
444 + tx.isPending = utxo.height == 0;
445 + tx.confirmations = confirmations;
446 + }
447 +
448 + bool isNew = transactionHistory.transactions[tx.id] == null;
449 +
450 + if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) {
451 + tx.outputAddresses?.add(utxo.address);
452 + isNew = true;
453 + }
454 +
455 + if (isNew) {
456 + final addressRecord = walletAddresses.allAddresses
457 + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
458 + if (addressRecord == null) {
459 + print("we don't have this address in the wallet! ${utxo.address}");
460 + return;
461 + }
462 +
463 + // update the txCount:
464 + addressRecord.txCount++;
465 + addressRecord.balance += utxo.value.toInt();
466 + addressRecord.setAsUsed();
467 + }
468 +
469 + transactionHistory.addOne(tx);
470 +
471 + if (isNew) {
472 + // update the unconfirmed balance when a new tx is added:
473 + // we do this after adding the tx to the history so that sub address balances are updated correctly
474 + // (since that calculation is based on the tx history)
475 + await updateBalance();
476 + }
477 + }
478 +
479 + Future<void> processMwebUtxos() async {
480 + if (!mwebEnabled) {
481 + return;
482 + }
483 +
484 + int restoreHeight = walletInfo.restoreHeight;
485 + print("SCANNING FROM HEIGHT: $restoreHeight");
486 + final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight);
487 +
488 + // process new utxos as they come in:
489 + _utxoStream?.cancel();
490 + ResponseStream<Utxo>? responseStream = await CwMweb.utxos(req);
491 + if (responseStream == null) {
492 + throw Exception("failed to get utxos stream!");
493 + }
494 + _utxoStream = responseStream.listen((Utxo sUtxo) async {
495 + // we're processing utxos, so our balance could still be innacurate:
496 + if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) {
497 + syncStatus = SyncronizingSyncStatus();
498 + processingUtxos = true;
499 + _processingTimer?.cancel();
500 + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
501 + processingUtxos = false;
502 + timer.cancel();
503 + });
504 + }
505 +
506 + final utxo = MwebUtxo(
507 + address: sUtxo.address,
508 + blockTime: sUtxo.blockTime,
509 + height: sUtxo.height,
510 + outputId: sUtxo.outputId,
511 + value: sUtxo.value.toInt(),
512 + );
513 +
514 + // if (mwebUtxosBox.containsKey(utxo.outputId)) {
515 + // // we've already stored this utxo, skip it:
516 + // return;
517 + // }
518 +
519 + await updateUnspent();
520 + await updateBalance();
521 +
522 + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
523 +
524 + // don't process utxos with addresses that are not in the mwebAddrs list:
525 + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
526 + return;
527 + }
528 +
529 + await mwebUtxosBox.put(utxo.outputId, utxo);
530 +
531 + await handleIncoming(utxo, _stub);
532 + });
533 + }
534 +
535 + Future<void> checkMwebUtxosSpent() async {
536 + if (!mwebEnabled) {
537 + return;
538 + }
539 +
540 + final pendingOutgoingTransactions = transactionHistory.transactions.values
541 + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending);
542 +
543 + // check if any of the pending outgoing transactions are now confirmed:
544 + bool updatedAny = false;
545 + for (final tx in pendingOutgoingTransactions) {
546 + updatedAny = await isConfirmed(tx) || updatedAny;
547 + }
548 +
549 + // get output ids of all the mweb utxos that have > 0 height:
550 + final outputIds =
551 + mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList();
552 +
553 + final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
554 + final spent = resp.outputId;
555 + if (spent.isEmpty) {
556 + return;
557 + }
558 +
559 + final status = await CwMweb.status(StatusRequest());
560 + final height = await electrumClient.getCurrentBlockChainTip();
561 + if (height == null || status.blockHeaderHeight != height) return;
562 + if (status.mwebUtxosHeight != height) return; // we aren't synced
563 +
564 + int amount = 0;
565 + Set<String> inputAddresses = {};
566 + var output = convert.AccumulatorSink<Digest>();
567 + var input = sha256.startChunkedConversion(output);
568 +
569 + for (final outputId in spent) {
570 + final utxo = mwebUtxosBox.get(outputId);
571 + await mwebUtxosBox.delete(outputId);
572 + if (utxo == null) continue;
573 + final addressRecord = walletAddresses.allAddresses
574 + .firstWhere((addressRecord) => addressRecord.address == utxo.address);
575 + if (!inputAddresses.contains(utxo.address)) {
576 + addressRecord.txCount++;
577 + }
578 + addressRecord.balance -= utxo.value.toInt();
579 + amount += utxo.value.toInt();
580 + inputAddresses.add(utxo.address);
581 + input.add(hex.decode(outputId));
582 + }
583 +
584 + if (inputAddresses.isEmpty) return;
585 + input.close();
586 + var digest = output.events.single;
587 + final tx = ElectrumTransactionInfo(
588 + WalletType.litecoin,
589 + id: digest.toString(),
590 + height: height,
591 + amount: amount,
592 + fee: 0,
593 + direction: TransactionDirection.outgoing,
594 + isPending: false,
595 + date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
596 + confirmations: 1,
597 + inputAddresses: inputAddresses.toList(),
598 + outputAddresses: [],
599 + isReplaced: false,
600 + );
601 +
602 + transactionHistory.addOne(tx);
603 + await transactionHistory.save();
604 +
605 + if (updatedAny) {
606 + await updateBalance();
607 + }
608 + }
609 +
610 + // checks if a pending transaction is now confirmed, and updates the tx info accordingly:
611 + Future<bool> isConfirmed(ElectrumTransactionInfo tx) async {
612 + if (!mwebEnabled) return false;
613 + if (!tx.isPending) return false;
614 +
615 + final outputId = <String>[], target = <String>{};
616 + final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch;
617 + final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? [];
618 + final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? [];
619 + outputId.addAll(spendingOutputIds);
620 + outputId.addAll(payingToOutputIds);
621 + target.addAll(spendingOutputIds);
622 +
623 + for (final outputId in payingToOutputIds) {
624 + final spendingTx = transactionHistory.transactions.values
625 + .firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false);
626 + if (spendingTx != null && !spendingTx.isPending) {
627 + target.add(outputId);
628 + }
629 + }
630 +
631 + if (outputId.isEmpty) {
632 + return false;
633 + }
634 +
635 + final resp = await CwMweb.spent(SpentRequest(outputId: outputId));
636 + if (!setEquals(resp.outputId.toSet(), target)) {
637 + return false;
638 + }
639 +
640 + final status = await CwMweb.status(StatusRequest());
641 + tx.height = status.mwebUtxosHeight;
642 + tx.confirmations = 1;
643 + tx.isPending = false;
644 + await transactionHistory.save();
645 + return true;
646 + }
647 +
648 + Future<void> updateUnspent() async {
649 + await checkMwebUtxosSpent();
650 + await updateAllUnspents();
651 + }
652 +
653 + @override
654 + @action
655 + Future<void> updateAllUnspents() async {
656 + // get ltc unspents:
657 + await super.updateAllUnspents();
658 +
659 + if (!mwebEnabled) {
660 + return;
661 + }
662 + await getStub();
663 +
664 + // add the mweb unspents to the list:
665 + List<BitcoinUnspent> mwebUnspentCoins = [];
666 + // update mweb unspents:
667 + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
668 + mwebUtxosBox.keys.forEach((dynamic oId) {
669 + final String outputId = oId as String;
670 + final utxo = mwebUtxosBox.get(outputId);
671 + if (utxo == null) {
672 + return;
673 + }
674 + if (utxo.address.isEmpty) {
675 + // not sure if a bug or a special case but we definitely ignore these
676 + return;
677 + }
678 + final addressRecord = walletAddresses.allAddresses
679 + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
680 +
681 + if (addressRecord == null) {
682 + print("utxo contains an address that is not in the wallet: ${utxo.address}");
683 + return;
684 + }
685 + final unspent = BitcoinUnspent(
686 + addressRecord,
687 + outputId,
688 + utxo.value.toInt(),
689 + mwebAddrs.indexOf(utxo.address),
690 + );
691 + if (unspent.vout == 0) {
692 + unspent.isChange = true;
693 + }
694 + mwebUnspentCoins.add(unspent);
695 + });
696 + unspentCoins.addAll(mwebUnspentCoins);
697 + }
698 +
699 + @override
700 + Future<ElectrumBalance> fetchBalances() async {
701 + final balance = await super.fetchBalances();
702 + if (!mwebEnabled) {
703 + return balance;
704 + }
705 + await getStub();
706 +
707 + // update unspent balances:
708 + await updateUnspent();
709 +
710 + int confirmed = balance.confirmed;
711 + int unconfirmed = balance.unconfirmed;
712 + int confirmedMweb = 0;
713 + int unconfirmedMweb = 0;
714 + try {
715 + mwebUtxosBox.values.forEach((utxo) {
716 + if (utxo.height > 0) {
717 + confirmedMweb += utxo.value.toInt();
718 + } else {
719 + unconfirmedMweb += utxo.value.toInt();
720 + }
721 + });
722 + if (unconfirmedMweb > 0) {
723 + unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
724 + }
725 + } catch (_) {}
726 +
727 + for (var addressRecord in walletAddresses.allAddresses) {
728 + addressRecord.balance = 0;
729 + addressRecord.txCount = 0;
730 + }
731 +
732 + unspentCoins.forEach((coin) {
733 + final coinInfoList = unspentCoinsInfo.values.where(
734 + (element) =>
735 + element.walletId.contains(id) &&
736 + element.hash.contains(coin.hash) &&
737 + element.vout == coin.vout,
738 + );
739 +
740 + if (coinInfoList.isNotEmpty) {
741 + final coinInfo = coinInfoList.first;
742 +
743 + coin.isFrozen = coinInfo.isFrozen;
744 + coin.isSending = coinInfo.isSending;
745 + coin.note = coinInfo.note;
746 + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
747 + coin.bitcoinAddressRecord.balance += coinInfo.value;
748 + } else {
749 + super.addCoinInfo(coin);
750 + }
751 + });
752 +
753 + // update the txCount for each address using the tx history, since we can't rely on mwebd
754 + // to have an accurate count, we should just keep it in sync with what we know from the tx history:
755 + for (final tx in transactionHistory.transactions.values) {
756 + // if (tx.isPending) continue;
757 + if (tx.inputAddresses == null || tx.outputAddresses == null) {
758 + continue;
759 + }
760 + final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
761 + for (final address in txAddresses) {
762 + final addressRecord = walletAddresses.allAddresses
763 + .firstWhereOrNull((addressRecord) => addressRecord.address == address);
764 + if (addressRecord == null) {
765 + continue;
766 + }
767 + addressRecord.txCount++;
768 + }
769 + }
770 +
771 + return ElectrumBalance(
772 + confirmed: confirmed,
773 + unconfirmed: unconfirmed,
774 + frozen: balance.frozen,
775 + secondConfirmed: confirmedMweb,
776 + secondUnconfirmed: unconfirmedMweb,
777 );
778 }
779
@@ -204,6 +793,227 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
793 return 0;
794 }
795
796 + @override
797 + Future<int> calcFee({
798 + required List<UtxoWithAddress> utxos,
799 + required List<BitcoinBaseOutput> outputs,
800 + required BasedUtxoNetwork network,
801 + String? memo,
802 + required int feeRate,
803 + List<ECPrivateInfo>? inputPrivKeyInfos,
804 + List<Outpoint>? vinOutpoints,
805 + }) async {
806 + final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
807 + final paysToMweb = outputs
808 + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
809 + if (!spendsMweb && !paysToMweb) {
810 + return await super.calcFee(
811 + utxos: utxos,
812 + outputs: outputs,
813 + network: network,
814 + memo: memo,
815 + feeRate: feeRate,
816 + inputPrivKeyInfos: inputPrivKeyInfos,
817 + vinOutpoints: vinOutpoints,
818 + );
819 + }
820 +
821 + if (!mwebEnabled) {
822 + throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
823 + }
824 +
825 + if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) {
826 + outputs = [
827 + BitcoinScriptOutput(
828 + script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue())
829 + ];
830 + }
831 +
832 + // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
833 + final preOutputSum =
834 + outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
835 + final fee = utxos.sumOfUtxosValue() - preOutputSum;
836 + final txb =
837 + BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
838 + final resp = await CwMweb.create(CreateRequest(
839 + rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(),
840 + scanSecret: scanSecret,
841 + spendSecret: spendSecret,
842 + feeRatePerKb: Int64(feeRate * 1000),
843 + dryRun: true));
844 + final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
845 + final posUtxos = utxos
846 + .where((utxo) => tx.inputs
847 + .any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout))
848 + .toList();
849 + final posOutputSum = tx.outputs.fold<int>(0, (acc, output) => acc + output.amount.toInt());
850 + final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue();
851 + final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt());
852 + var feeIncrease = posOutputSum - expectedPegin;
853 + if (expectedPegin > 0 && fee == BigInt.zero) {
854 + feeIncrease += await super.calcFee(
855 + utxos: posUtxos,
856 + outputs: tx.outputs
857 + .map((output) =>
858 + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount))
859 + .toList(),
860 + network: network,
861 + memo: memo,
862 + feeRate: feeRate) +
863 + feeRate * 41;
864 + }
865 + return fee.toInt() + feeIncrease;
866 + }
867 +
868 + @override
869 + Future<PendingTransaction> createTransaction(Object credentials) async {
870 + try {
871 + var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction;
872 + tx.isMweb = mwebEnabled;
873 +
874 + if (!mwebEnabled) {
875 + return tx;
876 + }
877 + await waitForMwebAddresses();
878 + await getStub();
879 +
880 + final resp = await CwMweb.create(CreateRequest(
881 + rawTx: hex.decode(tx.hex),
882 + scanSecret: scanSecret,
883 + spendSecret: spendSecret,
884 + feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000,
885 + ));
886 + final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
887 +
888 + // check if the transaction doesn't contain any mweb inputs or outputs:
889 + final transactionCredentials = credentials as BitcoinTransactionCredentials;
890 +
891 + bool hasMwebInput = false;
892 + bool hasMwebOutput = false;
893 +
894 + for (final output in transactionCredentials.outputs) {
895 + if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) {
896 + hasMwebOutput = true;
897 + break;
898 + }
899 + }
900 +
901 + if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) {
902 + hasMwebInput = true;
903 + }
904 +
905 + if (!hasMwebInput && !hasMwebOutput) {
906 + return tx;
907 + }
908 +
909 + // check if any of the inputs of this transaction are hog-ex:
910 + // this list is only non-mweb inputs:
911 + tx2.inputs.forEach((txInput) {
912 + bool isHogEx = true;
913 +
914 + final utxo = unspentCoins
915 + .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex);
916 +
917 + // TODO: detect actual hog-ex inputs
918 +
919 + if (!isHogEx) {
920 + return;
921 + }
922 +
923 + int confirmations = utxo.confirmations ?? 0;
924 + if (confirmations < 6) {
925 + throw Exception(
926 + "A transaction input has less than 6 confirmations, please try again later.");
927 + }
928 + });
929 +
930 + tx.hexOverride = tx2
931 + .copyWith(
932 + witnesses: tx2.inputs.asMap().entries.map((e) {
933 + final utxo = unspentCoins
934 + .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
935 + final key = generateECPrivate(
936 + hd: utxo.bitcoinAddressRecord.isHidden
937 + ? walletAddresses.sideHd
938 + : walletAddresses.mainHd,
939 + index: utxo.bitcoinAddressRecord.index,
940 + network: network);
941 + final digest = tx2.getTransactionSegwitDigit(
942 + txInIndex: e.key,
943 + script: key.getPublic().toP2pkhAddress().toScriptPubKey(),
944 + amount: BigInt.from(utxo.value),
945 + );
946 + return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]);
947 + }).toList())
948 + .toHex();
949 + tx.outputAddresses = resp.outputId;
950 +
951 + return tx
952 + ..addListener((transaction) async {
953 + final addresses = <String>{};
954 + transaction.inputAddresses?.forEach((id) async {
955 + final utxo = mwebUtxosBox.get(id);
956 + // await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent
957 + if (utxo == null) return;
958 + final addressRecord = walletAddresses.allAddresses
959 + .firstWhere((addressRecord) => addressRecord.address == utxo.address);
960 + if (!addresses.contains(utxo.address)) {
961 + addresses.add(utxo.address);
962 + }
963 + addressRecord.balance -= utxo.value.toInt();
964 + });
965 + transaction.inputAddresses?.addAll(addresses);
966 +
967 + transactionHistory.addOne(transaction);
968 + await updateUnspent();
969 + await updateBalance();
970 + });
971 + } catch (e, s) {
972 + print(e);
973 + print(s);
974 + if (e.toString().contains("commit failed")) {
975 + throw Exception("Transaction commit failed (no peers responded), please try again.");
976 + }
977 + rethrow;
978 + }
979 + }
980 +
981 + @override
982 + Future<void> save() async {
983 + await super.save();
984 + }
985 +
986 + @override
987 + Future<void> close() async {
988 + _utxoStream?.cancel();
989 + _feeRatesTimer?.cancel();
990 + _syncTimer?.cancel();
991 + _processingTimer?.cancel();
992 + await stopSync();
993 + await super.close();
994 + }
995 +
996 + Future<void> setMwebEnabled(bool enabled) async {
997 + if (mwebEnabled == enabled) {
998 + return;
999 + }
1000 +
1001 + mwebEnabled = enabled;
1002 + (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled;
1003 + await stopSync();
1004 + await startSync();
1005 + }
1006 +
1007 + Future<RpcClient> getStub() async {
1008 + _stub = await CwMweb.stub();
1009 + return _stub;
1010 + }
1011 +
1012 + Future<StatusResponse> getStatusRequest() async {
1013 + final resp = await CwMweb.status(StatusRequest());
1014 + return resp;
1015 + }
1016 +
1017 @override
1018 Future<String> signMessage(String message, {String? address = null}) async {
1019 final index = address != null
cw_bitcoin/lib/litecoin_wallet_addresses.dart
+143 -6
@@ -1,8 +1,15 @@
1 +import 'dart:async';
2 +import 'dart:typed_data';
3 +
4 import 'package:bitcoin_base/bitcoin_base.dart';
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 +import 'package:cw_bitcoin/bitcoin_address_record.dart';
7 +import 'package:cw_bitcoin/electrum_wallet.dart';
8 import 'package:cw_bitcoin/utils.dart';
9 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
10 import 'package:cw_core/wallet_info.dart';
11 +import 'package:cw_mweb/cw_mweb.dart';
12 +import 'package:flutter/foundation.dart';
13 import 'package:mobx/mobx.dart';
14
15 part 'litecoin_wallet_addresses.g.dart';
@@ -15,15 +22,145 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
22 required super.mainHd,
23 required super.sideHd,
24 required super.network,
25 + required this.mwebHd,
26 + required this.mwebEnabled,
27 super.initialAddresses,
28 + super.initialMwebAddresses,
29 super.initialRegularAddressIndex,
30 super.initialChangeAddressIndex,
21 - }) : super(walletInfo);
31 + }) : super(walletInfo) {
32 + for (int i = 0; i < mwebAddresses.length; i++) {
33 + mwebAddrs.add(mwebAddresses[i].address);
34 + }
35 + print("initialized with ${mwebAddrs.length} mweb addresses");
36 + }
37 +
38 + final Bip32Slip10Secp256k1 mwebHd;
39 + bool mwebEnabled;
40 + int mwebTopUpIndex = 1000;
41 + List<String> mwebAddrs = [];
42 +
43 + List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
44 + List<int> get spendPubkey =>
45 + mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed;
46 +
47 + @override
48 + Future<void> init() async {
49 + await initMwebAddresses();
50 + await super.init();
51 + }
52 +
53 + @computed
54 + @override
55 + List<BitcoinAddressRecord> get allAddresses {
56 + return List.from(super.allAddresses)..addAll(mwebAddresses);
57 + }
58 +
59 + Future<void> ensureMwebAddressUpToIndexExists(int index) async {
60 + Uint8List scan = Uint8List.fromList(scanSecret);
61 + Uint8List spend = Uint8List.fromList(spendPubkey);
62 + int count = 0;
63 + while (mwebAddrs.length <= (index + 1)) {
64 + final address = await CwMweb.address(scan, spend, mwebAddrs.length);
65 + mwebAddrs.add(address!);
66 + count++;
67 + // sleep for a bit to avoid making the main thread unresponsive:
68 + if (count > 50) {
69 + count = 0;
70 + await Future.delayed(Duration(milliseconds: 100));
71 + }
72 + }
73 + }
74 +
75 + Future<void> initMwebAddresses() async {
76 + if (mwebAddrs.length < 1000) {
77 + print("Generating MWEB addresses...");
78 + await ensureMwebAddressUpToIndexExists(1020);
79 + print("done generating MWEB addresses");
80 + List<BitcoinAddressRecord> addressRecords = mwebAddrs
81 + .asMap()
82 + .entries
83 + .map((e) => BitcoinAddressRecord(
84 + e.value,
85 + index: e.key,
86 + type: SegwitAddresType.mweb,
87 + network: network,
88 + ))
89 + .toList();
90 + addMwebAddresses(addressRecords);
91 + print("added ${addressRecords.length} mweb addresses");
92 + return;
93 + }
94 + }
95 +
96 + @override
97 + String getAddress({
98 + required int index,
99 + required Bip32Slip10Secp256k1 hd,
100 + BitcoinAddressType? addressType,
101 + }) {
102 + if (addressType == SegwitAddresType.mweb) {
103 + return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
104 + }
105 + return generateP2WPKHAddress(hd: hd, index: index, network: network);
106 + }
107 +
108 + @override
109 + Future<String> getAddressAsync({
110 + required int index,
111 + required Bip32Slip10Secp256k1 hd,
112 + BitcoinAddressType? addressType,
113 + }) async {
114 + if (addressType == SegwitAddresType.mweb) {
115 + await ensureMwebAddressUpToIndexExists(index);
116 + }
117 + return getAddress(index: index, hd: hd, addressType: addressType);
118 + }
119
120 + @action
121 @override
24 - String getAddress(
25 - {required int index,
26 - required Bip32Slip10Secp256k1 hd,
27 - BitcoinAddressType? addressType}) =>
28 - generateP2WPKHAddress(hd: hd, index: index, network: network);
122 + Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
123 + // use regular change address on peg in, otherwise use mweb for change address:
124 +
125 + if (!mwebEnabled) {
126 + return super.getChangeAddress();
127 + }
128 +
129 + if (outputs != null && utxoDetails != null) {
130 + // check if this is a PEGIN:
131 + bool outputsToMweb = false;
132 + bool comesFromMweb = false;
133 +
134 + for (var i = 0; i < outputs.length; i++) {
135 + // TODO: probably not the best way to tell if this is an mweb address
136 + // (but it doesn't contain the "mweb" text at this stage)
137 + if (outputs[i].address.toAddress(network).length > 110) {
138 + outputsToMweb = true;
139 + }
140 + }
141 + // TODO: this doesn't respect coin control because it doesn't know which available inputs are selected
142 + utxoDetails.availableInputs.forEach((element) {
143 + if (element.address.contains("mweb")) {
144 + comesFromMweb = true;
145 + }
146 + });
147 +
148 + bool isPegIn = !comesFromMweb && outputsToMweb;
149 + if (isPegIn && mwebEnabled) {
150 + return super.getChangeAddress();
151 + }
152 +
153 + // use regular change address if it's not an mweb tx:
154 + if (!comesFromMweb && !outputsToMweb) {
155 + return super.getChangeAddress();
156 + }
157 + }
158 +
159 + if (mwebEnabled) {
160 + await ensureMwebAddressUpToIndexExists(1);
161 + return mwebAddrs[0];
162 + }
163 +
164 + return super.getChangeAddress();
165 + }
166 }
cw_bitcoin/lib/litecoin_wallet_service.dart
+26 -2
@@ -1,10 +1,10 @@
1 import 'dart:io';
2 import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
3 +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
4 import 'package:cw_core/encryption_file_utils.dart';
5 import 'package:cw_core/unspent_coins_info.dart';
6 import 'package:hive/hive.dart';
7 import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
7 -import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
8 import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
9 import 'package:cw_bitcoin/litecoin_wallet.dart';
10 import 'package:cw_core/wallet_service.dart';
@@ -14,16 +14,19 @@ import 'package:cw_core/wallet_info.dart';
14 import 'package:cw_core/wallet_base.dart';
15 import 'package:collection/collection.dart';
16 import 'package:bip39/bip39.dart' as bip39;
17 +import 'package:path_provider/path_provider.dart';
18
19 class LitecoinWalletService extends WalletService<
20 BitcoinNewWalletCredentials,
21 BitcoinRestoreWalletFromSeedCredentials,
22 BitcoinRestoreWalletFromWIFCredentials,
23 BitcoinNewWalletCredentials> {
23 - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
24 + LitecoinWalletService(
25 + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
26
27 final Box<WalletInfo> walletInfoSource;
28 final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
29 + final bool alwaysScan;
30 final bool isDirect;
31
32 @override
@@ -64,6 +67,7 @@ class LitecoinWalletService extends WalletService<
67
68 @override
69 Future<LitecoinWallet> openWallet(String name, String password) async {
70 +
71 final walletInfo = walletInfoSource.values
72 .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
73
@@ -73,6 +77,7 @@ class LitecoinWalletService extends WalletService<
77 name: name,
78 walletInfo: walletInfo,
79 unspentCoinsInfo: unspentCoinsInfoSource,
80 + alwaysScan: alwaysScan,
81 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
82 );
83 await wallet.init();
@@ -85,6 +90,7 @@ class LitecoinWalletService extends WalletService<
90 name: name,
91 walletInfo: walletInfo,
92 unspentCoinsInfo: unspentCoinsInfoSource,
93 + alwaysScan: alwaysScan,
94 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
95 );
96 await wallet.init();
@@ -98,6 +104,23 @@ class LitecoinWalletService extends WalletService<
104 final walletInfo = walletInfoSource.values
105 .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
106 await walletInfoSource.delete(walletInfo.key);
107 +
108 + // if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd:
109 + if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) {
110 + final appDirPath = (await getApplicationSupportDirectory()).path;
111 + File neturinoDb = File('$appDirPath/neutrino.db');
112 + File blockHeaders = File('$appDirPath/block_headers.bin');
113 + File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin');
114 + if (neturinoDb.existsSync()) {
115 + neturinoDb.deleteSync();
116 + }
117 + if (blockHeaders.existsSync()) {
118 + blockHeaders.deleteSync();
119 + }
120 + if (regFilterHeaders.existsSync()) {
121 + regFilterHeaders.deleteSync();
122 + }
123 + }
124 }
125
126 @override
@@ -109,6 +132,7 @@ class LitecoinWalletService extends WalletService<
132 name: currentName,
133 walletInfo: currentWalletInfo,
134 unspentCoinsInfo: unspentCoinsInfoSource,
135 + alwaysScan: alwaysScan,
136 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
137 );
138
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+34 -5
@@ -1,11 +1,15 @@
1 +import 'package:grpc/grpc.dart';
2 import 'package:cw_bitcoin/exceptions.dart';
3 import 'package:bitcoin_base/bitcoin_base.dart';
4 +import 'package:blockchain_utils/blockchain_utils.dart';
5 import 'package:cw_core/pending_transaction.dart';
6 import 'package:cw_bitcoin/electrum.dart';
7 import 'package:cw_bitcoin/bitcoin_amount_format.dart';
8 import 'package:cw_bitcoin/electrum_transaction_info.dart';
9 import 'package:cw_core/transaction_direction.dart';
10 import 'package:cw_core/wallet_type.dart';
11 +import 'package:cw_mweb/cw_mweb.dart';
12 +import 'package:cw_mweb/mwebd.pb.dart';
13
14 class PendingBitcoinTransaction with PendingTransaction {
15 PendingBitcoinTransaction(
@@ -19,6 +23,7 @@ class PendingBitcoinTransaction with PendingTransaction {
23 required this.hasChange,
24 this.isSendAll = false,
25 this.hasTaprootInputs = false,
26 + this.isMweb = false,
27 }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
28
29 final WalletType type;
@@ -28,15 +33,19 @@ class PendingBitcoinTransaction with PendingTransaction {
33 final int fee;
34 final String feeRate;
35 final BasedUtxoNetwork? network;
31 - final bool hasChange;
36 final bool isSendAll;
37 + final bool hasChange;
38 final bool hasTaprootInputs;
39 + bool isMweb;
40 + String? idOverride;
41 + String? hexOverride;
42 + List<String>? outputAddresses;
43
44 @override
36 - String get id => _tx.txId();
45 + String get id => idOverride ?? _tx.txId();
46
47 @override
39 - String get hex => _tx.serialize();
48 + String get hex => hexOverride ?? _tx.serialize();
49
50 @override
51 String get amountFormatted => bitcoinAmountToString(amount: amount);
@@ -62,8 +71,7 @@ class PendingBitcoinTransaction with PendingTransaction {
71
72 final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
73
65 - @override
66 - Future<void> commit() async {
74 + Future<void> _commit() async {
75 int? callId;
76
77 final result = await electrumClient.broadcastTransaction(
@@ -100,6 +108,25 @@ class PendingBitcoinTransaction with PendingTransaction {
108
109 throw BitcoinTransactionCommitFailed();
110 }
111 + }
112 +
113 + Future<void> _ltcCommit() async {
114 + try {
115 + final stub = await CwMweb.stub();
116 + final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
117 + idOverride = resp.txid;
118 + } on GrpcError catch (e) {
119 + throw BitcoinTransactionCommitFailed(errorMessage: e.message);
120 + }
121 + }
122 +
123 + @override
124 + Future<void> commit() async {
125 + if (isMweb) {
126 + await _ltcCommit();
127 + } else {
128 + await _commit();
129 + }
130
131 _listeners.forEach((listener) => listener(transactionInfo()));
132 }
@@ -116,5 +143,7 @@ class PendingBitcoinTransaction with PendingTransaction {
143 isPending: true,
144 isReplaced: false,
145 confirmations: 0,
146 + inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
147 + outputAddresses: outputAddresses,
148 fee: fee);
149 }
cw_bitcoin/pubspec.lock
+31
@@ -17,6 +17,14 @@ packages:
17 url: "https://pub.dev"
18 source: hosted
19 version: "4.7.0"
20 + archive:
21 + dependency: transitive
22 + description:
23 + name: archive
24 + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
25 + url: "https://pub.dev"
26 + source: hosted
27 + version: "3.4.10"
28 args:
29 dependency: transitive
30 description:
@@ -260,6 +268,13 @@ packages:
268 relative: true
269 source: path
270 version: "0.0.1"
271 + cw_mweb:
272 + dependency: "direct main"
273 + description:
274 + path: "../cw_mweb"
275 + relative: true
276 + source: path
277 + version: "0.0.1"
278 dart_style:
279 dependency: transitive
280 description:
@@ -379,6 +394,14 @@ packages:
394 url: "https://pub.dev"
395 source: hosted
396 version: "2.1.2"
397 + googleapis_auth:
398 + dependency: transitive
399 + description:
400 + name: googleapis_auth
401 + sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da
402 + url: "https://pub.dev"
403 + source: hosted
404 + version: "1.4.1"
405 graphs:
406 dependency: transitive
407 description:
@@ -387,6 +410,14 @@ packages:
410 url: "https://pub.dev"
411 source: hosted
412 version: "2.3.1"
413 + grpc:
414 + dependency: "direct main"
415 + description:
416 + name: grpc
417 + sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40
418 + url: "https://pub.dev"
419 + source: hosted
420 + version: "3.2.4"
421 hex:
422 dependency: transitive
423 description:
cw_bitcoin/pubspec.yaml
+10 -2
@@ -34,11 +34,16 @@ dependencies:
34 ledger_bitcoin:
35 git:
36 url: https://github.com/cake-tech/ledger-bitcoin
37 + cw_mweb:
38 + path: ../cw_mweb
39 + grpc: ^3.2.4
40 sp_scanner:
41 git:
42 url: https://github.com/cake-tech/sp_scanner
43 ref: sp_v4.0.0
41 -
44 + bech32:
45 + git:
46 + url: https://github.com/cake-tech/bech32.git
47
48 dev_dependencies:
49 flutter_test:
@@ -54,10 +59,13 @@ dependency_overrides:
59 url: https://github.com/cake-tech/ledger-flutter.git
60 ref: cake-v3
61 watcher: ^1.1.0
62 + protobuf: ^3.1.0
63 bitcoin_base:
64 git:
65 url: https://github.com/cake-tech/bitcoin_base
60 - ref: cake-update-v7
66 + ref: cake-update-v8
67 + pointycastle: 3.7.4
68 + ffi: 2.1.0
69
70 # For information on the generic Dart part of this file, see the
71 # following page: https://dart.dev/tools/pub/pubspec
cw_bitcoin_cash/pubspec.yaml
+1 -1
@@ -42,7 +42,7 @@ dependency_overrides:
42 bitcoin_base:
43 git:
44 url: https://github.com/cake-tech/bitcoin_base
45 - ref: cake-update-v7
45 + ref: cake-update-v8
46
47 # For information on the generic Dart part of this file, see the
48 # following page: https://dart.dev/tools/pub/pubspec
cw_core/lib/balance.dart
+7 -3
@@ -1,13 +1,17 @@
1 abstract class Balance {
2 - const Balance(this.available, this.additional);
2 + const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional});
3
4 final int available;
5
6 final int additional;
7
8 - String get formattedAvailableBalance;
8 + final int? secondAvailable;
9 + final int? secondAdditional;
10
11 + String get formattedAvailableBalance;
12 String get formattedAdditionalBalance;
11 -
13 String get formattedUnAvailableBalance => '';
14 + String get formattedSecondAvailableBalance => '';
15 + String get formattedSecondAdditionalBalance => '';
16 + String get formattedFullAvailableBalance => formattedAvailableBalance;
17 }
cw_core/lib/get_height_by_date.dart
+5
@@ -310,6 +310,11 @@ DateTime getDateByBitcoinHeight(int height) {
310 return estimatedDate;
311 }
312
313 +int getLtcHeightByDate({required DateTime date}) {
314 + // TODO: use the proxy layer to get the height with a binary search of blocked header heights
315 + return 0;
316 +}
317 +
318 // TODO: enhance all of this global const lists
319 const wowDates = {
320 "2023-12": 583048,
cw_core/lib/hive_type_ids.dart
+1
@@ -18,3 +18,4 @@ const SPL_TOKEN_TYPE_ID = 16;
18 const DERIVATION_INFO_TYPE_ID = 17;
19 const TRON_TOKEN_TYPE_ID = 18;
20 const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
21 +const MWEB_UTXO_TYPE_ID = 20;
\ No newline at end of file
cw_core/lib/mweb_utxo.dart new
+33
@@ -0,0 +1,33 @@
1 +import 'package:cw_core/hive_type_ids.dart';
2 +import 'package:hive/hive.dart';
3 +
4 +part 'mweb_utxo.g.dart';
5 +
6 +@HiveType(typeId: MWEB_UTXO_TYPE_ID)
7 +class MwebUtxo extends HiveObject {
8 + MwebUtxo({
9 + required this.height,
10 + required this.value,
11 + required this.address,
12 + required this.outputId,
13 + required this.blockTime,
14 + });
15 +
16 + static const typeId = MWEB_UTXO_TYPE_ID;
17 + static const boxName = 'MwebUtxo';
18 +
19 + @HiveField(0)
20 + int height;
21 +
22 + @HiveField(1)
23 + int value;
24 +
25 + @HiveField(2)
26 + String address;
27 +
28 + @HiveField(3)
29 + String outputId;
30 +
31 + @HiveField(4)
32 + int blockTime;
33 +}
cw_core/lib/sync_status.dart
+1 -1
@@ -89,4 +89,4 @@ class TimedOutSyncStatus extends NotConnectedSyncStatus {
89 class LostConnectionSyncStatus extends NotConnectedSyncStatus {
90 @override
91 String toString() => 'Reconnecting';
92 -}
92 +}
\ No newline at end of file
cw_core/lib/wallet_base.dart
+2
@@ -67,6 +67,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
67
68 Future<void> startSync();
69
70 + Future<void> stopSync() async {}
71 +
72 Future<PendingTransaction> createTransaction(Object credentials);
73
74 int calculateEstimatedFee(TransactionPriority priority, int? amount);
cw_haven/android/build.gradle
-1
@@ -35,7 +35,6 @@ android {
35 }
36 externalNativeBuild {
37 cmake {
38 - path "CMakeLists.txt"
38 }
39 }
40 }
cw_mweb/.gitignore new
+30
@@ -0,0 +1,30 @@
1 +# Miscellaneous
2 +*.class
3 +*.log
4 +*.pyc
5 +*.swp
6 +.DS_Store
7 +.atom/
8 +.buildlog/
9 +.history
10 +.svn/
11 +migrate_working_dir/
12 +
13 +# IntelliJ related
14 +*.iml
15 +*.ipr
16 +*.iws
17 +.idea/
18 +
19 +# The .vscode folder contains launch configuration and tasks you configure in
20 +# VS Code which you may wish to be included in version control, so this line
21 +# is commented out by default.
22 +#.vscode/
23 +
24 +# Flutter/Dart/Pub related
25 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26 +/pubspec.lock
27 +**/doc/api/
28 +.dart_tool/
29 +.packages
30 +build/
cw_mweb/.metadata new
+36
@@ -0,0 +1,36 @@
1 +# This file tracks properties of this Flutter project.
2 +# Used by Flutter tool to assess capabilities and perform upgrades etc.
3 +#
4 +# This file should be version controlled.
5 +
6 +version:
7 + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
8 + channel: stable
9 +
10 +project_type: plugin
11 +
12 +# Tracks metadata for the flutter migrate command
13 +migration:
14 + platforms:
15 + - platform: root
16 + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
17 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
18 + - platform: android
19 + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
20 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
21 + - platform: ios
22 + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
23 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
24 + - platform: macos
25 + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
26 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
27 +
28 + # User provided section
29 +
30 + # List of Local paths (relative to this file) that should be
31 + # ignored by the migrate tool.
32 + #
33 + # Files that are not part of the templates will be ignored by default.
34 + unmanaged_files:
35 + - 'lib/main.dart'
36 + - 'ios/Runner.xcodeproj/project.pbxproj'
cw_mweb/CHANGELOG.md new
+3
@@ -0,0 +1,3 @@
1 +## 0.0.1
2 +
3 +* TODO: Describe initial release.
cw_mweb/LICENSE new
+1
@@ -0,0 +1 @@
1 +TODO: Add your license here.
cw_mweb/README.md new
+15
@@ -0,0 +1,15 @@
1 +# cw_mweb
2 +
3 +A new Flutter plugin project.
4 +
5 +## Getting Started
6 +
7 +This project is a starting point for a Flutter
8 +[plug-in package](https://flutter.dev/developing-packages/),
9 +a specialized package that includes platform-specific implementation code for
10 +Android and/or iOS.
11 +
12 +For help getting started with Flutter development, view the
13 +[online documentation](https://flutter.dev/docs), which offers tutorials,
14 +samples, guidance on mobile development, and a full API reference.
15 +
cw_mweb/analysis_options.yaml new
+4
@@ -0,0 +1,4 @@
1 +include: package:flutter_lints/flutter.yaml
2 +
3 +# Additional information about this file can be found at
4 +# https://dart.dev/guides/language/analysis-options
cw_mweb/android/.gitignore new
+10
@@ -0,0 +1,10 @@
1 +*.iml
2 +.gradle
3 +/local.properties
4 +/.idea/workspace.xml
5 +/.idea/libraries
6 +.DS_Store
7 +/build
8 +/captures
9 +/libs
10 +.cxx
cw_mweb/android/build.gradle new
+76
@@ -0,0 +1,76 @@
1 +group 'com.cakewallet.mweb'
2 +version '1.0-SNAPSHOT'
3 +
4 +buildscript {
5 + ext.kotlin_version = '1.7.10'
6 + repositories {
7 + google()
8 + mavenCentral()
9 + }
10 +
11 + dependencies {
12 + classpath 'com.android.tools.build:gradle:7.3.0'
13 + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 + }
15 +}
16 +
17 +allprojects {
18 + repositories {
19 + google()
20 + mavenCentral()
21 + }
22 +}
23 +
24 +rootProject.allprojects {
25 + repositories {
26 + flatDir {
27 + dirs project(':cw_mweb').file('libs')
28 + }
29 + }
30 +}
31 +
32 +apply plugin: 'com.android.library'
33 +apply plugin: 'kotlin-android'
34 +
35 +android {
36 + compileSdkVersion 31
37 +
38 + compileOptions {
39 + sourceCompatibility JavaVersion.VERSION_1_8
40 + targetCompatibility JavaVersion.VERSION_1_8
41 + }
42 +
43 + kotlinOptions {
44 + jvmTarget = '1.8'
45 + }
46 +
47 + sourceSets {
48 + main.java.srcDirs += 'src/main/kotlin'
49 + test.java.srcDirs += 'src/test/kotlin'
50 + }
51 +
52 + defaultConfig {
53 + minSdkVersion 16
54 + }
55 +
56 + dependencies {
57 + testImplementation 'org.jetbrains.kotlin:kotlin-test'
58 + testImplementation 'org.mockito:mockito-core:5.0.0'
59 + }
60 +
61 + testOptions {
62 + unitTests.all {
63 + useJUnitPlatform()
64 +
65 + testLogging {
66 + events "passed", "skipped", "failed", "standardOut", "standardError"
67 + outputs.upToDateWhen {false}
68 + showStandardStreams = true
69 + }
70 + }
71 + }
72 +}
73 +
74 +dependencies {
75 + implementation (name: 'mwebd', ext: 'aar')
76 +}
cw_mweb/android/settings.gradle new
+1
@@ -0,0 +1 @@
1 +rootProject.name = 'cw_mweb'
cw_mweb/android/src/main/AndroidManifest.xml new
+3
@@ -0,0 +1,3 @@
1 +<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2 + package="com.cakewallet.mweb">
3 +</manifest>
cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt new
+58
@@ -0,0 +1,58 @@
1 +package com.cakewallet.mweb
2 +
3 +import androidx.annotation.NonNull
4 +
5 +import io.flutter.embedding.engine.plugins.FlutterPlugin
6 +import io.flutter.plugin.common.MethodCall
7 +import io.flutter.plugin.common.MethodChannel
8 +import io.flutter.plugin.common.MethodChannel.MethodCallHandler
9 +import io.flutter.plugin.common.MethodChannel.Result
10 +
11 +import mwebd.Mwebd
12 +import mwebd.Server
13 +
14 +/** CwMwebPlugin */
15 +class CwMwebPlugin: FlutterPlugin, MethodCallHandler {
16 + /// The MethodChannel that will the communication between Flutter and native Android
17 + ///
18 + /// This local reference serves to register the plugin with the Flutter Engine and unregister it
19 + /// when the Flutter Engine is detached from the Activity
20 + private lateinit var channel : MethodChannel
21 + private var server: Server? = null
22 + private var port: Long? = null
23 +
24 + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
25 + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb")
26 + channel.setMethodCallHandler(this)
27 + }
28 +
29 + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
30 + if (call.method == "start") {
31 + server?.stop()
32 + val dataDir = call.argument("dataDir") ?: ""
33 + server = server ?: Mwebd.newServer("", dataDir, "")
34 + port = server?.start(0)
35 + result.success(port)
36 + } else if (call.method == "stop") {
37 + server?.stop()
38 + server = null
39 + port = null
40 + result.success(null)
41 + } else if (call.method == "address") {
42 + val scanSecret: ByteArray = call.argument<ByteArray>("scanSecret") ?: ByteArray(0)
43 + val spendPub: ByteArray = call.argument<ByteArray>("spendPub") ?: ByteArray(0)
44 + val index: Int = call.argument<Int>("index") ?: 0
45 + val res = Mwebd.address(scanSecret, spendPub, index)
46 + result.success(res)
47 + } else {
48 + result.notImplemented()
49 + }
50 + }
51 +
52 + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
53 + channel.setMethodCallHandler(null)
54 + server?.stop()
55 + server = null
56 + port = null
57 + }
58 +}
cw_mweb/ios/.gitignore new
+38
@@ -0,0 +1,38 @@
1 +.idea/
2 +.vagrant/
3 +.sconsign.dblite
4 +.svn/
5 +
6 +.DS_Store
7 +*.swp
8 +profile
9 +
10 +DerivedData/
11 +build/
12 +GeneratedPluginRegistrant.h
13 +GeneratedPluginRegistrant.m
14 +
15 +.generated/
16 +
17 +*.pbxuser
18 +*.mode1v3
19 +*.mode2v3
20 +*.perspectivev3
21 +
22 +!default.pbxuser
23 +!default.mode1v3
24 +!default.mode2v3
25 +!default.perspectivev3
26 +
27 +xcuserdata
28 +
29 +*.moved-aside
30 +
31 +*.pyc
32 +*sync/
33 +Icon?
34 +.tags*
35 +
36 +/Flutter/Generated.xcconfig
37 +/Flutter/ephemeral/
38 +/Flutter/flutter_export_environment.sh
\ No newline at end of file
cw_mweb/ios/Assets/.gitkeep
cw_mweb/ios/Classes/CwMwebPlugin.swift new
+86
@@ -0,0 +1,86 @@
1 +import Flutter
2 +import UIKit
3 +import Mwebd
4 +
5 +public class CwMwebPlugin: NSObject, FlutterPlugin {
6 +public static func register(with registrar: FlutterPluginRegistrar) {
7 + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger())
8 + let instance = CwMwebPlugin()
9 + registrar.addMethodCallDelegate(instance, channel: channel)
10 + }
11 +
12 + private static var server: MwebdServer?
13 + private static var port: Int = 0
14 + private static var dataDir: String?
15 +
16 + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
17 + switch call.method {
18 + case "getPlatformVersion":
19 + result("iOS " + UIDevice.current.systemVersion)
20 + break
21 + case "start":
22 + stopServer()
23 + let args = call.arguments as? [String: String]
24 + let dataDir = args?["dataDir"]
25 + CwMwebPlugin.dataDir = dataDir
26 + startServer(result: result)
27 + break
28 + case "stop":
29 + stopServer()
30 + result(nil)
31 + break
32 + case "address":
33 + let args = call.arguments as! [String: Any]
34 + let scanSecret = args["scanSecret"] as! FlutterStandardTypedData
35 + let spendPub = args["spendPub"] as! FlutterStandardTypedData
36 + let index = args["index"] as! Int32
37 +
38 + let scanSecretData = scanSecret.data
39 + let spendPubData = spendPub.data
40 + result(MwebdAddress(scanSecretData, spendPubData, index))
41 + break
42 + default:
43 + result(FlutterMethodNotImplemented)
44 + break
45 + }
46 + }
47 +
48 + private func startServer(result: @escaping FlutterResult) {
49 + if CwMwebPlugin.server == nil {
50 + var error: NSError?
51 + CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, "", &error)
52 +
53 + if let server = CwMwebPlugin.server {
54 + do {
55 + print("Starting server...")
56 + try server.start(0, ret0_: &CwMwebPlugin.port)
57 + print("Server started successfully on port: \(CwMwebPlugin.port)")
58 + result(CwMwebPlugin.port)
59 + } catch let startError as NSError {
60 + print("Server Start Error: \(startError.localizedDescription)")
61 + result(FlutterError(code: "Server Start Error", message: startError.localizedDescription, details: nil))
62 + }
63 + } else if let error = error {
64 + print("Server Creation Error: \(error.localizedDescription)")
65 + result(FlutterError(code: "Server Creation Error", message: error.localizedDescription, details: nil))
66 + } else {
67 + print("Unknown Error: Failed to create server")
68 + result(FlutterError(code: "Unknown Error", message: "Failed to create server", details: nil))
69 + }
70 + } else {
71 + print("Server already running on port: \(CwMwebPlugin.port)")
72 + result(CwMwebPlugin.port)
73 + }
74 + }
75 +
76 + private func stopServer() {
77 + print("Stopping server")
78 + CwMwebPlugin.server?.stop()
79 + CwMwebPlugin.server = nil
80 + CwMwebPlugin.port = 0
81 + }
82 +
83 + deinit {
84 + stopServer()
85 + }
86 +}
cw_mweb/ios/cw_mweb.podspec new
+26
@@ -0,0 +1,26 @@
1 +#
2 +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
3 +# Run `pod lib lint cw_mweb.podspec` to validate before publishing.
4 +#
5 +Pod::Spec.new do |s|
6 + s.name = 'cw_mweb'
7 + s.version = '0.0.1'
8 + s.summary = 'A new Flutter plugin project.'
9 + s.description = <<-DESC
10 +A new Flutter plugin project.
11 + DESC
12 + s.homepage = 'http://example.com'
13 + s.license = { :file => '../LICENSE' }
14 + s.author = { 'Your Company' => 'email@example.com' }
15 + s.source = { :path => '.' }
16 + s.source_files = 'Classes/**/*'
17 + s.dependency 'Flutter'
18 + s.platform = :ios, '11.0'
19 +
20 + # Flutter.framework does not contain a i386 slice.
21 + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
22 + s.swift_version = '5.0'
23 + s.ios.vendored_frameworks = 'Mwebd.xcframework'
24 + s.preserve_paths = 'Mwebd.xcframework/**/*'
25 +
26 +end
cw_mweb/lib/cw_mweb.dart new
+133
@@ -0,0 +1,133 @@
1 +import 'dart:typed_data';
2 +
3 +import 'package:grpc/grpc.dart';
4 +import 'package:path_provider/path_provider.dart';
5 +import 'cw_mweb_platform_interface.dart';
6 +import 'mwebd.pbgrpc.dart';
7 +
8 +class CwMweb {
9 + static RpcClient? _rpcClient;
10 + static ClientChannel? _clientChannel;
11 + static int? _port;
12 + static const TIMEOUT_DURATION = Duration(seconds: 5);
13 +
14 + static Future<void> _initializeClient() async {
15 + await stop();
16 + // wait a few seconds to make sure the server is stopped
17 + await Future.delayed(const Duration(seconds: 5));
18 +
19 + final appDir = await getApplicationSupportDirectory();
20 + _port = await CwMwebPlatform.instance.start(appDir.path);
21 + if (_port == null || _port == 0) {
22 + throw Exception("Failed to start server");
23 + }
24 + print("Attempting to connect to server on port: $_port");
25 +
26 + // wait for the server to finish starting up before we try to connect to it:
27 + await Future.delayed(const Duration(seconds: 5));
28 +
29 + _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () {
30 + print("Channel is shutting down!");
31 + },
32 + options: const ChannelOptions(
33 + credentials: ChannelCredentials.insecure(),
34 + keepAlive: ClientKeepAliveOptions(permitWithoutCalls: true),
35 + ));
36 + _rpcClient = RpcClient(_clientChannel!);
37 + }
38 +
39 + static Future<RpcClient> stub({int maxRetries = 3}) async {
40 + for (int i = 0; i < maxRetries; i++) {
41 + try {
42 + if (_rpcClient == null) {
43 + await _initializeClient();
44 + }
45 + final status = await _rpcClient!
46 + .status(StatusRequest(), options: CallOptions(timeout: TIMEOUT_DURATION));
47 + if (status.blockTime == 0) {
48 + throw Exception("blockTime shouldn't be 0! (this connection is likely broken)");
49 + }
50 + return _rpcClient!;
51 + } catch (e) {
52 + print("Attempt $i failed: $e");
53 + _rpcClient = null;
54 + }
55 + }
56 + throw Exception("Failed to connect after $maxRetries attempts");
57 + }
58 +
59 + static Future<void> stop() async {
60 + try {
61 + await CwMwebPlatform.instance.stop();
62 + await cleanup();
63 + } catch (e) {
64 + print("Error stopping server: $e");
65 + }
66 + }
67 +
68 + static Future<String?> address(Uint8List scanSecret, Uint8List spendPub, int index) async {
69 + try {
70 + return CwMwebPlatform.instance.address(scanSecret, spendPub, index);
71 + } catch (e) {
72 + print("Error getting address: $e");
73 + return null;
74 + }
75 + }
76 +
77 + static Future<void> cleanup() async {
78 + await _clientChannel?.terminate();
79 + _rpcClient = null;
80 + _clientChannel = null;
81 + _port = null;
82 + }
83 +
84 + // wrappers that handle the connection issues:
85 + static Future<SpentResponse> spent(SpentRequest request) async {
86 + try {
87 + if (_rpcClient == null) {
88 + await _initializeClient();
89 + }
90 + return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION));
91 + } catch (e) {
92 + print("Error getting spent: $e");
93 + return SpentResponse();
94 + }
95 + }
96 +
97 + static Future<StatusResponse> status(StatusRequest request) async {
98 + try {
99 + if (_rpcClient == null) {
100 + await _initializeClient();
101 + }
102 + return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION));
103 + } catch (e) {
104 + print("Error getting status: $e");
105 + return StatusResponse();
106 + }
107 + }
108 +
109 + static Future<CreateResponse> create(CreateRequest request) async {
110 + try {
111 + if (_rpcClient == null) {
112 + await _initializeClient();
113 + }
114 + return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION));
115 + } catch (e) {
116 + print("Error getting create: $e");
117 + return CreateResponse();
118 + }
119 + }
120 +
121 + static Future<ResponseStream<Utxo>?> utxos(UtxosRequest request) async {
122 + try {
123 + if (_rpcClient == null) {
124 + await _initializeClient();
125 + }
126 + // this is a stream, so we should have an effectively infinite timeout:
127 + return _rpcClient!.utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365)));
128 + } catch (e) {
129 + print("Error getting utxos: $e");
130 + return null;
131 + }
132 + }
133 +}
cw_mweb/lib/cw_mweb_method_channel.dart new
+32
@@ -0,0 +1,32 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:flutter/services.dart';
3 +
4 +import 'cw_mweb_platform_interface.dart';
5 +
6 +/// An implementation of [CwMwebPlatform] that uses method channels.
7 +class MethodChannelCwMweb extends CwMwebPlatform {
8 + /// The method channel used to interact with the native platform.
9 + @visibleForTesting
10 + final methodChannel = const MethodChannel('cw_mweb');
11 +
12 + @override
13 + Future<int?> start(String dataDir) async {
14 + final result = await methodChannel.invokeMethod<int>('start', {'dataDir': dataDir});
15 + return result;
16 + }
17 +
18 + @override
19 + Future<void> stop() async {
20 + await methodChannel.invokeMethod<void>('stop');
21 + }
22 +
23 + @override
24 + Future<String?> address(Uint8List scanSecret, Uint8List spendPub, int index) async {
25 + final result = await methodChannel.invokeMethod<String>('address', {
26 + 'scanSecret': scanSecret,
27 + 'spendPub': spendPub,
28 + 'index': index,
29 + });
30 + return result;
31 + }
32 +}
cw_mweb/lib/cw_mweb_platform_interface.dart new
+39
@@ -0,0 +1,39 @@
1 +import 'dart:typed_data';
2 +
3 +import 'package:plugin_platform_interface/plugin_platform_interface.dart';
4 +
5 +import 'cw_mweb_method_channel.dart';
6 +
7 +abstract class CwMwebPlatform extends PlatformInterface {
8 + /// Constructs a CwMwebPlatform.
9 + CwMwebPlatform() : super(token: _token);
10 +
11 + static final Object _token = Object();
12 +
13 + static CwMwebPlatform _instance = MethodChannelCwMweb();
14 +
15 + /// The default instance of [CwMwebPlatform] to use.
16 + ///
17 + /// Defaults to [MethodChannelCwMweb].
18 + static CwMwebPlatform get instance => _instance;
19 +
20 + /// Platform-specific implementations should set this with their own
21 + /// platform-specific class that extends [CwMwebPlatform] when
22 + /// they register themselves.
23 + static set instance(CwMwebPlatform instance) {
24 + PlatformInterface.verifyToken(instance, _token);
25 + _instance = instance;
26 + }
27 +
28 + Future<int?> start(String dataDir) {
29 + throw UnimplementedError('start() has not been implemented.');
30 + }
31 +
32 + Future<void> stop() {
33 + throw UnimplementedError('stop() has not been implemented.');
34 + }
35 +
36 + Future<String?> address(Uint8List scanSecret, Uint8List spendPub, int index) {
37 + throw UnimplementedError('address(int) has not been implemented.');
38 + }
39 +}
cw_mweb/lib/mwebd.pb.dart new
+801
@@ -0,0 +1,801 @@
1 +//
2 +// Generated code. Do not modify.
3 +// source: mwebd.proto
4 +//
5 +// @dart = 2.12
6 +
7 +// ignore_for_file: annotate_overrides, camel_case_types, comment_references
8 +// ignore_for_file: constant_identifier_names, library_prefixes
9 +// ignore_for_file: non_constant_identifier_names, prefer_final_fields
10 +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
11 +
12 +import 'dart:core' as $core;
13 +
14 +import 'package:fixnum/fixnum.dart' as $fixnum;
15 +import 'package:protobuf/protobuf.dart' as $pb;
16 +
17 +class StatusRequest extends $pb.GeneratedMessage {
18 + factory StatusRequest() => create();
19 + StatusRequest._() : super();
20 + factory StatusRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
21 + factory StatusRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
22 +
23 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusRequest', createEmptyInstance: create)
24 + ..hasRequiredFields = false
25 + ;
26 +
27 + @$core.Deprecated(
28 + 'Using this can add significant overhead to your binary. '
29 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
30 + 'Will be removed in next major version')
31 + StatusRequest clone() => StatusRequest()..mergeFromMessage(this);
32 + @$core.Deprecated(
33 + 'Using this can add significant overhead to your binary. '
34 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
35 + 'Will be removed in next major version')
36 + StatusRequest copyWith(void Function(StatusRequest) updates) => super.copyWith((message) => updates(message as StatusRequest)) as StatusRequest;
37 +
38 + $pb.BuilderInfo get info_ => _i;
39 +
40 + @$core.pragma('dart2js:noInline')
41 + static StatusRequest create() => StatusRequest._();
42 + StatusRequest createEmptyInstance() => create();
43 + static $pb.PbList<StatusRequest> createRepeated() => $pb.PbList<StatusRequest>();
44 + @$core.pragma('dart2js:noInline')
45 + static StatusRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StatusRequest>(create);
46 + static StatusRequest? _defaultInstance;
47 +}
48 +
49 +class StatusResponse extends $pb.GeneratedMessage {
50 + factory StatusResponse({
51 + $core.int? blockHeaderHeight,
52 + $core.int? mwebHeaderHeight,
53 + $core.int? mwebUtxosHeight,
54 + $core.int? blockTime,
55 + }) {
56 + final $result = create();
57 + if (blockHeaderHeight != null) {
58 + $result.blockHeaderHeight = blockHeaderHeight;
59 + }
60 + if (mwebHeaderHeight != null) {
61 + $result.mwebHeaderHeight = mwebHeaderHeight;
62 + }
63 + if (mwebUtxosHeight != null) {
64 + $result.mwebUtxosHeight = mwebUtxosHeight;
65 + }
66 + if (blockTime != null) {
67 + $result.blockTime = blockTime;
68 + }
69 + return $result;
70 + }
71 + StatusResponse._() : super();
72 + factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
73 + factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
74 +
75 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', createEmptyInstance: create)
76 + ..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3)
77 + ..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3)
78 + ..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3)
79 + ..a<$core.int>(4, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3)
80 + ..hasRequiredFields = false
81 + ;
82 +
83 + @$core.Deprecated(
84 + 'Using this can add significant overhead to your binary. '
85 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
86 + 'Will be removed in next major version')
87 + StatusResponse clone() => StatusResponse()..mergeFromMessage(this);
88 + @$core.Deprecated(
89 + 'Using this can add significant overhead to your binary. '
90 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
91 + 'Will be removed in next major version')
92 + StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse;
93 +
94 + $pb.BuilderInfo get info_ => _i;
95 +
96 + @$core.pragma('dart2js:noInline')
97 + static StatusResponse create() => StatusResponse._();
98 + StatusResponse createEmptyInstance() => create();
99 + static $pb.PbList<StatusResponse> createRepeated() => $pb.PbList<StatusResponse>();
100 + @$core.pragma('dart2js:noInline')
101 + static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StatusResponse>(create);
102 + static StatusResponse? _defaultInstance;
103 +
104 + @$pb.TagNumber(1)
105 + $core.int get blockHeaderHeight => $_getIZ(0);
106 + @$pb.TagNumber(1)
107 + set blockHeaderHeight($core.int v) { $_setSignedInt32(0, v); }
108 + @$pb.TagNumber(1)
109 + $core.bool hasBlockHeaderHeight() => $_has(0);
110 + @$pb.TagNumber(1)
111 + void clearBlockHeaderHeight() => clearField(1);
112 +
113 + @$pb.TagNumber(2)
114 + $core.int get mwebHeaderHeight => $_getIZ(1);
115 + @$pb.TagNumber(2)
116 + set mwebHeaderHeight($core.int v) { $_setSignedInt32(1, v); }
117 + @$pb.TagNumber(2)
118 + $core.bool hasMwebHeaderHeight() => $_has(1);
119 + @$pb.TagNumber(2)
120 + void clearMwebHeaderHeight() => clearField(2);
121 +
122 + @$pb.TagNumber(3)
123 + $core.int get mwebUtxosHeight => $_getIZ(2);
124 + @$pb.TagNumber(3)
125 + set mwebUtxosHeight($core.int v) { $_setSignedInt32(2, v); }
126 + @$pb.TagNumber(3)
127 + $core.bool hasMwebUtxosHeight() => $_has(2);
128 + @$pb.TagNumber(3)
129 + void clearMwebUtxosHeight() => clearField(3);
130 +
131 + @$pb.TagNumber(4)
132 + $core.int get blockTime => $_getIZ(3);
133 + @$pb.TagNumber(4)
134 + set blockTime($core.int v) { $_setUnsignedInt32(3, v); }
135 + @$pb.TagNumber(4)
136 + $core.bool hasBlockTime() => $_has(3);
137 + @$pb.TagNumber(4)
138 + void clearBlockTime() => clearField(4);
139 +}
140 +
141 +class UtxosRequest extends $pb.GeneratedMessage {
142 + factory UtxosRequest({
143 + $core.int? fromHeight,
144 + $core.List<$core.int>? scanSecret,
145 + }) {
146 + final $result = create();
147 + if (fromHeight != null) {
148 + $result.fromHeight = fromHeight;
149 + }
150 + if (scanSecret != null) {
151 + $result.scanSecret = scanSecret;
152 + }
153 + return $result;
154 + }
155 + UtxosRequest._() : super();
156 + factory UtxosRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
157 + factory UtxosRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
158 +
159 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UtxosRequest', createEmptyInstance: create)
160 + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromHeight', $pb.PbFieldType.O3)
161 + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY)
162 + ..hasRequiredFields = false
163 + ;
164 +
165 + @$core.Deprecated(
166 + 'Using this can add significant overhead to your binary. '
167 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
168 + 'Will be removed in next major version')
169 + UtxosRequest clone() => UtxosRequest()..mergeFromMessage(this);
170 + @$core.Deprecated(
171 + 'Using this can add significant overhead to your binary. '
172 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
173 + 'Will be removed in next major version')
174 + UtxosRequest copyWith(void Function(UtxosRequest) updates) => super.copyWith((message) => updates(message as UtxosRequest)) as UtxosRequest;
175 +
176 + $pb.BuilderInfo get info_ => _i;
177 +
178 + @$core.pragma('dart2js:noInline')
179 + static UtxosRequest create() => UtxosRequest._();
180 + UtxosRequest createEmptyInstance() => create();
181 + static $pb.PbList<UtxosRequest> createRepeated() => $pb.PbList<UtxosRequest>();
182 + @$core.pragma('dart2js:noInline')
183 + static UtxosRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UtxosRequest>(create);
184 + static UtxosRequest? _defaultInstance;
185 +
186 + @$pb.TagNumber(1)
187 + $core.int get fromHeight => $_getIZ(0);
188 + @$pb.TagNumber(1)
189 + set fromHeight($core.int v) { $_setSignedInt32(0, v); }
190 + @$pb.TagNumber(1)
191 + $core.bool hasFromHeight() => $_has(0);
192 + @$pb.TagNumber(1)
193 + void clearFromHeight() => clearField(1);
194 +
195 + @$pb.TagNumber(2)
196 + $core.List<$core.int> get scanSecret => $_getN(1);
197 + @$pb.TagNumber(2)
198 + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); }
199 + @$pb.TagNumber(2)
200 + $core.bool hasScanSecret() => $_has(1);
201 + @$pb.TagNumber(2)
202 + void clearScanSecret() => clearField(2);
203 +}
204 +
205 +class Utxo extends $pb.GeneratedMessage {
206 + factory Utxo({
207 + $core.int? height,
208 + $fixnum.Int64? value,
209 + $core.String? address,
210 + $core.String? outputId,
211 + $core.int? blockTime,
212 + }) {
213 + final $result = create();
214 + if (height != null) {
215 + $result.height = height;
216 + }
217 + if (value != null) {
218 + $result.value = value;
219 + }
220 + if (address != null) {
221 + $result.address = address;
222 + }
223 + if (outputId != null) {
224 + $result.outputId = outputId;
225 + }
226 + if (blockTime != null) {
227 + $result.blockTime = blockTime;
228 + }
229 + return $result;
230 + }
231 + Utxo._() : super();
232 + factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
233 + factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
234 +
235 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Utxo', createEmptyInstance: create)
236 + ..a<$core.int>(1, _omitFieldNames ? '' : 'height', $pb.PbFieldType.O3)
237 + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
238 + ..aOS(3, _omitFieldNames ? '' : 'address')
239 + ..aOS(4, _omitFieldNames ? '' : 'outputId')
240 + ..a<$core.int>(5, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3)
241 + ..hasRequiredFields = false
242 + ;
243 +
244 + @$core.Deprecated(
245 + 'Using this can add significant overhead to your binary. '
246 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
247 + 'Will be removed in next major version')
248 + Utxo clone() => Utxo()..mergeFromMessage(this);
249 + @$core.Deprecated(
250 + 'Using this can add significant overhead to your binary. '
251 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
252 + 'Will be removed in next major version')
253 + Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo;
254 +
255 + $pb.BuilderInfo get info_ => _i;
256 +
257 + @$core.pragma('dart2js:noInline')
258 + static Utxo create() => Utxo._();
259 + Utxo createEmptyInstance() => create();
260 + static $pb.PbList<Utxo> createRepeated() => $pb.PbList<Utxo>();
261 + @$core.pragma('dart2js:noInline')
262 + static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Utxo>(create);
263 + static Utxo? _defaultInstance;
264 +
265 + @$pb.TagNumber(1)
266 + $core.int get height => $_getIZ(0);
267 + @$pb.TagNumber(1)
268 + set height($core.int v) { $_setSignedInt32(0, v); }
269 + @$pb.TagNumber(1)
270 + $core.bool hasHeight() => $_has(0);
271 + @$pb.TagNumber(1)
272 + void clearHeight() => clearField(1);
273 +
274 + @$pb.TagNumber(2)
275 + $fixnum.Int64 get value => $_getI64(1);
276 + @$pb.TagNumber(2)
277 + set value($fixnum.Int64 v) { $_setInt64(1, v); }
278 + @$pb.TagNumber(2)
279 + $core.bool hasValue() => $_has(1);
280 + @$pb.TagNumber(2)
281 + void clearValue() => clearField(2);
282 +
283 + @$pb.TagNumber(3)
284 + $core.String get address => $_getSZ(2);
285 + @$pb.TagNumber(3)
286 + set address($core.String v) { $_setString(2, v); }
287 + @$pb.TagNumber(3)
288 + $core.bool hasAddress() => $_has(2);
289 + @$pb.TagNumber(3)
290 + void clearAddress() => clearField(3);
291 +
292 + @$pb.TagNumber(4)
293 + $core.String get outputId => $_getSZ(3);
294 + @$pb.TagNumber(4)
295 + set outputId($core.String v) { $_setString(3, v); }
296 + @$pb.TagNumber(4)
297 + $core.bool hasOutputId() => $_has(3);
298 + @$pb.TagNumber(4)
299 + void clearOutputId() => clearField(4);
300 +
301 + @$pb.TagNumber(5)
302 + $core.int get blockTime => $_getIZ(4);
303 + @$pb.TagNumber(5)
304 + set blockTime($core.int v) { $_setUnsignedInt32(4, v); }
305 + @$pb.TagNumber(5)
306 + $core.bool hasBlockTime() => $_has(4);
307 + @$pb.TagNumber(5)
308 + void clearBlockTime() => clearField(5);
309 +}
310 +
311 +class AddressRequest extends $pb.GeneratedMessage {
312 + factory AddressRequest({
313 + $core.int? fromIndex,
314 + $core.int? toIndex,
315 + $core.List<$core.int>? scanSecret,
316 + $core.List<$core.int>? spendPubkey,
317 + }) {
318 + final $result = create();
319 + if (fromIndex != null) {
320 + $result.fromIndex = fromIndex;
321 + }
322 + if (toIndex != null) {
323 + $result.toIndex = toIndex;
324 + }
325 + if (scanSecret != null) {
326 + $result.scanSecret = scanSecret;
327 + }
328 + if (spendPubkey != null) {
329 + $result.spendPubkey = spendPubkey;
330 + }
331 + return $result;
332 + }
333 + AddressRequest._() : super();
334 + factory AddressRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
335 + factory AddressRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
336 +
337 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressRequest', createEmptyInstance: create)
338 + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromIndex', $pb.PbFieldType.OU3)
339 + ..a<$core.int>(2, _omitFieldNames ? '' : 'toIndex', $pb.PbFieldType.OU3)
340 + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY)
341 + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'spendPubkey', $pb.PbFieldType.OY)
342 + ..hasRequiredFields = false
343 + ;
344 +
345 + @$core.Deprecated(
346 + 'Using this can add significant overhead to your binary. '
347 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
348 + 'Will be removed in next major version')
349 + AddressRequest clone() => AddressRequest()..mergeFromMessage(this);
350 + @$core.Deprecated(
351 + 'Using this can add significant overhead to your binary. '
352 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
353 + 'Will be removed in next major version')
354 + AddressRequest copyWith(void Function(AddressRequest) updates) => super.copyWith((message) => updates(message as AddressRequest)) as AddressRequest;
355 +
356 + $pb.BuilderInfo get info_ => _i;
357 +
358 + @$core.pragma('dart2js:noInline')
359 + static AddressRequest create() => AddressRequest._();
360 + AddressRequest createEmptyInstance() => create();
361 + static $pb.PbList<AddressRequest> createRepeated() => $pb.PbList<AddressRequest>();
362 + @$core.pragma('dart2js:noInline')
363 + static AddressRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AddressRequest>(create);
364 + static AddressRequest? _defaultInstance;
365 +
366 + @$pb.TagNumber(1)
367 + $core.int get fromIndex => $_getIZ(0);
368 + @$pb.TagNumber(1)
369 + set fromIndex($core.int v) { $_setUnsignedInt32(0, v); }
370 + @$pb.TagNumber(1)
371 + $core.bool hasFromIndex() => $_has(0);
372 + @$pb.TagNumber(1)
373 + void clearFromIndex() => clearField(1);
374 +
375 + @$pb.TagNumber(2)
376 + $core.int get toIndex => $_getIZ(1);
377 + @$pb.TagNumber(2)
378 + set toIndex($core.int v) { $_setUnsignedInt32(1, v); }
379 + @$pb.TagNumber(2)
380 + $core.bool hasToIndex() => $_has(1);
381 + @$pb.TagNumber(2)
382 + void clearToIndex() => clearField(2);
383 +
384 + @$pb.TagNumber(3)
385 + $core.List<$core.int> get scanSecret => $_getN(2);
386 + @$pb.TagNumber(3)
387 + set scanSecret($core.List<$core.int> v) { $_setBytes(2, v); }
388 + @$pb.TagNumber(3)
389 + $core.bool hasScanSecret() => $_has(2);
390 + @$pb.TagNumber(3)
391 + void clearScanSecret() => clearField(3);
392 +
393 + @$pb.TagNumber(4)
394 + $core.List<$core.int> get spendPubkey => $_getN(3);
395 + @$pb.TagNumber(4)
396 + set spendPubkey($core.List<$core.int> v) { $_setBytes(3, v); }
397 + @$pb.TagNumber(4)
398 + $core.bool hasSpendPubkey() => $_has(3);
399 + @$pb.TagNumber(4)
400 + void clearSpendPubkey() => clearField(4);
401 +}
402 +
403 +class AddressResponse extends $pb.GeneratedMessage {
404 + factory AddressResponse({
405 + $core.Iterable<$core.String>? address,
406 + }) {
407 + final $result = create();
408 + if (address != null) {
409 + $result.address.addAll(address);
410 + }
411 + return $result;
412 + }
413 + AddressResponse._() : super();
414 + factory AddressResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
415 + factory AddressResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
416 +
417 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressResponse', createEmptyInstance: create)
418 + ..pPS(1, _omitFieldNames ? '' : 'address')
419 + ..hasRequiredFields = false
420 + ;
421 +
422 + @$core.Deprecated(
423 + 'Using this can add significant overhead to your binary. '
424 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
425 + 'Will be removed in next major version')
426 + AddressResponse clone() => AddressResponse()..mergeFromMessage(this);
427 + @$core.Deprecated(
428 + 'Using this can add significant overhead to your binary. '
429 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
430 + 'Will be removed in next major version')
431 + AddressResponse copyWith(void Function(AddressResponse) updates) => super.copyWith((message) => updates(message as AddressResponse)) as AddressResponse;
432 +
433 + $pb.BuilderInfo get info_ => _i;
434 +
435 + @$core.pragma('dart2js:noInline')
436 + static AddressResponse create() => AddressResponse._();
437 + AddressResponse createEmptyInstance() => create();
438 + static $pb.PbList<AddressResponse> createRepeated() => $pb.PbList<AddressResponse>();
439 + @$core.pragma('dart2js:noInline')
440 + static AddressResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AddressResponse>(create);
441 + static AddressResponse? _defaultInstance;
442 +
443 + @$pb.TagNumber(1)
444 + $core.List<$core.String> get address => $_getList(0);
445 +}
446 +
447 +class SpentRequest extends $pb.GeneratedMessage {
448 + factory SpentRequest({
449 + $core.Iterable<$core.String>? outputId,
450 + }) {
451 + final $result = create();
452 + if (outputId != null) {
453 + $result.outputId.addAll(outputId);
454 + }
455 + return $result;
456 + }
457 + SpentRequest._() : super();
458 + factory SpentRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
459 + factory SpentRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
460 +
461 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentRequest', createEmptyInstance: create)
462 + ..pPS(1, _omitFieldNames ? '' : 'outputId')
463 + ..hasRequiredFields = false
464 + ;
465 +
466 + @$core.Deprecated(
467 + 'Using this can add significant overhead to your binary. '
468 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
469 + 'Will be removed in next major version')
470 + SpentRequest clone() => SpentRequest()..mergeFromMessage(this);
471 + @$core.Deprecated(
472 + 'Using this can add significant overhead to your binary. '
473 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
474 + 'Will be removed in next major version')
475 + SpentRequest copyWith(void Function(SpentRequest) updates) => super.copyWith((message) => updates(message as SpentRequest)) as SpentRequest;
476 +
477 + $pb.BuilderInfo get info_ => _i;
478 +
479 + @$core.pragma('dart2js:noInline')
480 + static SpentRequest create() => SpentRequest._();
481 + SpentRequest createEmptyInstance() => create();
482 + static $pb.PbList<SpentRequest> createRepeated() => $pb.PbList<SpentRequest>();
483 + @$core.pragma('dart2js:noInline')
484 + static SpentRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpentRequest>(create);
485 + static SpentRequest? _defaultInstance;
486 +
487 + @$pb.TagNumber(1)
488 + $core.List<$core.String> get outputId => $_getList(0);
489 +}
490 +
491 +class SpentResponse extends $pb.GeneratedMessage {
492 + factory SpentResponse({
493 + $core.Iterable<$core.String>? outputId,
494 + }) {
495 + final $result = create();
496 + if (outputId != null) {
497 + $result.outputId.addAll(outputId);
498 + }
499 + return $result;
500 + }
501 + SpentResponse._() : super();
502 + factory SpentResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
503 + factory SpentResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
504 +
505 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentResponse', createEmptyInstance: create)
506 + ..pPS(1, _omitFieldNames ? '' : 'outputId')
507 + ..hasRequiredFields = false
508 + ;
509 +
510 + @$core.Deprecated(
511 + 'Using this can add significant overhead to your binary. '
512 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
513 + 'Will be removed in next major version')
514 + SpentResponse clone() => SpentResponse()..mergeFromMessage(this);
515 + @$core.Deprecated(
516 + 'Using this can add significant overhead to your binary. '
517 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
518 + 'Will be removed in next major version')
519 + SpentResponse copyWith(void Function(SpentResponse) updates) => super.copyWith((message) => updates(message as SpentResponse)) as SpentResponse;
520 +
521 + $pb.BuilderInfo get info_ => _i;
522 +
523 + @$core.pragma('dart2js:noInline')
524 + static SpentResponse create() => SpentResponse._();
525 + SpentResponse createEmptyInstance() => create();
526 + static $pb.PbList<SpentResponse> createRepeated() => $pb.PbList<SpentResponse>();
527 + @$core.pragma('dart2js:noInline')
528 + static SpentResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpentResponse>(create);
529 + static SpentResponse? _defaultInstance;
530 +
531 + @$pb.TagNumber(1)
532 + $core.List<$core.String> get outputId => $_getList(0);
533 +}
534 +
535 +class CreateRequest extends $pb.GeneratedMessage {
536 + factory CreateRequest({
537 + $core.List<$core.int>? rawTx,
538 + $core.List<$core.int>? scanSecret,
539 + $core.List<$core.int>? spendSecret,
540 + $fixnum.Int64? feeRatePerKb,
541 + $core.bool? dryRun,
542 + }) {
543 + final $result = create();
544 + if (rawTx != null) {
545 + $result.rawTx = rawTx;
546 + }
547 + if (scanSecret != null) {
548 + $result.scanSecret = scanSecret;
549 + }
550 + if (spendSecret != null) {
551 + $result.spendSecret = spendSecret;
552 + }
553 + if (feeRatePerKb != null) {
554 + $result.feeRatePerKb = feeRatePerKb;
555 + }
556 + if (dryRun != null) {
557 + $result.dryRun = dryRun;
558 + }
559 + return $result;
560 + }
561 + CreateRequest._() : super();
562 + factory CreateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
563 + factory CreateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
564 +
565 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateRequest', createEmptyInstance: create)
566 + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY)
567 + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY)
568 + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'spendSecret', $pb.PbFieldType.OY)
569 + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'feeRatePerKb', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
570 + ..aOB(5, _omitFieldNames ? '' : 'dryRun')
571 + ..hasRequiredFields = false
572 + ;
573 +
574 + @$core.Deprecated(
575 + 'Using this can add significant overhead to your binary. '
576 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
577 + 'Will be removed in next major version')
578 + CreateRequest clone() => CreateRequest()..mergeFromMessage(this);
579 + @$core.Deprecated(
580 + 'Using this can add significant overhead to your binary. '
581 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
582 + 'Will be removed in next major version')
583 + CreateRequest copyWith(void Function(CreateRequest) updates) => super.copyWith((message) => updates(message as CreateRequest)) as CreateRequest;
584 +
585 + $pb.BuilderInfo get info_ => _i;
586 +
587 + @$core.pragma('dart2js:noInline')
588 + static CreateRequest create() => CreateRequest._();
589 + CreateRequest createEmptyInstance() => create();
590 + static $pb.PbList<CreateRequest> createRepeated() => $pb.PbList<CreateRequest>();
591 + @$core.pragma('dart2js:noInline')
592 + static CreateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateRequest>(create);
593 + static CreateRequest? _defaultInstance;
594 +
595 + @$pb.TagNumber(1)
596 + $core.List<$core.int> get rawTx => $_getN(0);
597 + @$pb.TagNumber(1)
598 + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); }
599 + @$pb.TagNumber(1)
600 + $core.bool hasRawTx() => $_has(0);
601 + @$pb.TagNumber(1)
602 + void clearRawTx() => clearField(1);
603 +
604 + @$pb.TagNumber(2)
605 + $core.List<$core.int> get scanSecret => $_getN(1);
606 + @$pb.TagNumber(2)
607 + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); }
608 + @$pb.TagNumber(2)
609 + $core.bool hasScanSecret() => $_has(1);
610 + @$pb.TagNumber(2)
611 + void clearScanSecret() => clearField(2);
612 +
613 + @$pb.TagNumber(3)
614 + $core.List<$core.int> get spendSecret => $_getN(2);
615 + @$pb.TagNumber(3)
616 + set spendSecret($core.List<$core.int> v) { $_setBytes(2, v); }
617 + @$pb.TagNumber(3)
618 + $core.bool hasSpendSecret() => $_has(2);
619 + @$pb.TagNumber(3)
620 + void clearSpendSecret() => clearField(3);
621 +
622 + @$pb.TagNumber(4)
623 + $fixnum.Int64 get feeRatePerKb => $_getI64(3);
624 + @$pb.TagNumber(4)
625 + set feeRatePerKb($fixnum.Int64 v) { $_setInt64(3, v); }
626 + @$pb.TagNumber(4)
627 + $core.bool hasFeeRatePerKb() => $_has(3);
628 + @$pb.TagNumber(4)
629 + void clearFeeRatePerKb() => clearField(4);
630 +
631 + @$pb.TagNumber(5)
632 + $core.bool get dryRun => $_getBF(4);
633 + @$pb.TagNumber(5)
634 + set dryRun($core.bool v) { $_setBool(4, v); }
635 + @$pb.TagNumber(5)
636 + $core.bool hasDryRun() => $_has(4);
637 + @$pb.TagNumber(5)
638 + void clearDryRun() => clearField(5);
639 +}
640 +
641 +class CreateResponse extends $pb.GeneratedMessage {
642 + factory CreateResponse({
643 + $core.List<$core.int>? rawTx,
644 + $core.Iterable<$core.String>? outputId,
645 + }) {
646 + final $result = create();
647 + if (rawTx != null) {
648 + $result.rawTx = rawTx;
649 + }
650 + if (outputId != null) {
651 + $result.outputId.addAll(outputId);
652 + }
653 + return $result;
654 + }
655 + CreateResponse._() : super();
656 + factory CreateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
657 + factory CreateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
658 +
659 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateResponse', createEmptyInstance: create)
660 + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY)
661 + ..pPS(2, _omitFieldNames ? '' : 'outputId')
662 + ..hasRequiredFields = false
663 + ;
664 +
665 + @$core.Deprecated(
666 + 'Using this can add significant overhead to your binary. '
667 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
668 + 'Will be removed in next major version')
669 + CreateResponse clone() => CreateResponse()..mergeFromMessage(this);
670 + @$core.Deprecated(
671 + 'Using this can add significant overhead to your binary. '
672 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
673 + 'Will be removed in next major version')
674 + CreateResponse copyWith(void Function(CreateResponse) updates) => super.copyWith((message) => updates(message as CreateResponse)) as CreateResponse;
675 +
676 + $pb.BuilderInfo get info_ => _i;
677 +
678 + @$core.pragma('dart2js:noInline')
679 + static CreateResponse create() => CreateResponse._();
680 + CreateResponse createEmptyInstance() => create();
681 + static $pb.PbList<CreateResponse> createRepeated() => $pb.PbList<CreateResponse>();
682 + @$core.pragma('dart2js:noInline')
683 + static CreateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateResponse>(create);
684 + static CreateResponse? _defaultInstance;
685 +
686 + @$pb.TagNumber(1)
687 + $core.List<$core.int> get rawTx => $_getN(0);
688 + @$pb.TagNumber(1)
689 + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); }
690 + @$pb.TagNumber(1)
691 + $core.bool hasRawTx() => $_has(0);
692 + @$pb.TagNumber(1)
693 + void clearRawTx() => clearField(1);
694 +
695 + @$pb.TagNumber(2)
696 + $core.List<$core.String> get outputId => $_getList(1);
697 +}
698 +
699 +class BroadcastRequest extends $pb.GeneratedMessage {
700 + factory BroadcastRequest({
701 + $core.List<$core.int>? rawTx,
702 + }) {
703 + final $result = create();
704 + if (rawTx != null) {
705 + $result.rawTx = rawTx;
706 + }
707 + return $result;
708 + }
709 + BroadcastRequest._() : super();
710 + factory BroadcastRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
711 + factory BroadcastRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
712 +
713 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastRequest', createEmptyInstance: create)
714 + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY)
715 + ..hasRequiredFields = false
716 + ;
717 +
718 + @$core.Deprecated(
719 + 'Using this can add significant overhead to your binary. '
720 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
721 + 'Will be removed in next major version')
722 + BroadcastRequest clone() => BroadcastRequest()..mergeFromMessage(this);
723 + @$core.Deprecated(
724 + 'Using this can add significant overhead to your binary. '
725 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
726 + 'Will be removed in next major version')
727 + BroadcastRequest copyWith(void Function(BroadcastRequest) updates) => super.copyWith((message) => updates(message as BroadcastRequest)) as BroadcastRequest;
728 +
729 + $pb.BuilderInfo get info_ => _i;
730 +
731 + @$core.pragma('dart2js:noInline')
732 + static BroadcastRequest create() => BroadcastRequest._();
733 + BroadcastRequest createEmptyInstance() => create();
734 + static $pb.PbList<BroadcastRequest> createRepeated() => $pb.PbList<BroadcastRequest>();
735 + @$core.pragma('dart2js:noInline')
736 + static BroadcastRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BroadcastRequest>(create);
737 + static BroadcastRequest? _defaultInstance;
738 +
739 + @$pb.TagNumber(1)
740 + $core.List<$core.int> get rawTx => $_getN(0);
741 + @$pb.TagNumber(1)
742 + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); }
743 + @$pb.TagNumber(1)
744 + $core.bool hasRawTx() => $_has(0);
745 + @$pb.TagNumber(1)
746 + void clearRawTx() => clearField(1);
747 +}
748 +
749 +class BroadcastResponse extends $pb.GeneratedMessage {
750 + factory BroadcastResponse({
751 + $core.String? txid,
752 + }) {
753 + final $result = create();
754 + if (txid != null) {
755 + $result.txid = txid;
756 + }
757 + return $result;
758 + }
759 + BroadcastResponse._() : super();
760 + factory BroadcastResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
761 + factory BroadcastResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
762 +
763 + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastResponse', createEmptyInstance: create)
764 + ..aOS(1, _omitFieldNames ? '' : 'txid')
765 + ..hasRequiredFields = false
766 + ;
767 +
768 + @$core.Deprecated(
769 + 'Using this can add significant overhead to your binary. '
770 + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
771 + 'Will be removed in next major version')
772 + BroadcastResponse clone() => BroadcastResponse()..mergeFromMessage(this);
773 + @$core.Deprecated(
774 + 'Using this can add significant overhead to your binary. '
775 + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
776 + 'Will be removed in next major version')
777 + BroadcastResponse copyWith(void Function(BroadcastResponse) updates) => super.copyWith((message) => updates(message as BroadcastResponse)) as BroadcastResponse;
778 +
779 + $pb.BuilderInfo get info_ => _i;
780 +
781 + @$core.pragma('dart2js:noInline')
782 + static BroadcastResponse create() => BroadcastResponse._();
783 + BroadcastResponse createEmptyInstance() => create();
784 + static $pb.PbList<BroadcastResponse> createRepeated() => $pb.PbList<BroadcastResponse>();
785 + @$core.pragma('dart2js:noInline')
786 + static BroadcastResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BroadcastResponse>(create);
787 + static BroadcastResponse? _defaultInstance;
788 +
789 + @$pb.TagNumber(1)
790 + $core.String get txid => $_getSZ(0);
791 + @$pb.TagNumber(1)
792 + set txid($core.String v) { $_setString(0, v); }
793 + @$pb.TagNumber(1)
794 + $core.bool hasTxid() => $_has(0);
795 + @$pb.TagNumber(1)
796 + void clearTxid() => clearField(1);
797 +}
798 +
799 +
800 +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
801 +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
cw_mweb/lib/mwebd.pbgrpc.dart new
+159
@@ -0,0 +1,159 @@
1 +//
2 +// Generated code. Do not modify.
3 +// source: mwebd.proto
4 +//
5 +// @dart = 2.12
6 +
7 +// ignore_for_file: annotate_overrides, camel_case_types, comment_references
8 +// ignore_for_file: constant_identifier_names, library_prefixes
9 +// ignore_for_file: non_constant_identifier_names, prefer_final_fields
10 +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
11 +
12 +import 'dart:async' as $async;
13 +import 'dart:core' as $core;
14 +
15 +import 'package:grpc/service_api.dart' as $grpc;
16 +import 'package:protobuf/protobuf.dart' as $pb;
17 +
18 +import 'mwebd.pb.dart' as $0;
19 +
20 +export 'mwebd.pb.dart';
21 +
22 +@$pb.GrpcServiceName('Rpc')
23 +class RpcClient extends $grpc.Client {
24 + static final _$status = $grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>(
25 + '/Rpc/Status',
26 + ($0.StatusRequest value) => value.writeToBuffer(),
27 + ($core.List<$core.int> value) => $0.StatusResponse.fromBuffer(value));
28 + static final _$utxos = $grpc.ClientMethod<$0.UtxosRequest, $0.Utxo>(
29 + '/Rpc/Utxos',
30 + ($0.UtxosRequest value) => value.writeToBuffer(),
31 + ($core.List<$core.int> value) => $0.Utxo.fromBuffer(value));
32 + static final _$addresses = $grpc.ClientMethod<$0.AddressRequest, $0.AddressResponse>(
33 + '/Rpc/Addresses',
34 + ($0.AddressRequest value) => value.writeToBuffer(),
35 + ($core.List<$core.int> value) => $0.AddressResponse.fromBuffer(value));
36 + static final _$spent = $grpc.ClientMethod<$0.SpentRequest, $0.SpentResponse>(
37 + '/Rpc/Spent',
38 + ($0.SpentRequest value) => value.writeToBuffer(),
39 + ($core.List<$core.int> value) => $0.SpentResponse.fromBuffer(value));
40 + static final _$create = $grpc.ClientMethod<$0.CreateRequest, $0.CreateResponse>(
41 + '/Rpc/Create',
42 + ($0.CreateRequest value) => value.writeToBuffer(),
43 + ($core.List<$core.int> value) => $0.CreateResponse.fromBuffer(value));
44 + static final _$broadcast = $grpc.ClientMethod<$0.BroadcastRequest, $0.BroadcastResponse>(
45 + '/Rpc/Broadcast',
46 + ($0.BroadcastRequest value) => value.writeToBuffer(),
47 + ($core.List<$core.int> value) => $0.BroadcastResponse.fromBuffer(value));
48 +
49 + RpcClient($grpc.ClientChannel channel,
50 + {$grpc.CallOptions? options,
51 + $core.Iterable<$grpc.ClientInterceptor>? interceptors})
52 + : super(channel, options: options,
53 + interceptors: interceptors);
54 +
55 + $grpc.ResponseFuture<$0.StatusResponse> status($0.StatusRequest request, {$grpc.CallOptions? options}) {
56 + return $createUnaryCall(_$status, request, options: options);
57 + }
58 +
59 + $grpc.ResponseStream<$0.Utxo> utxos($0.UtxosRequest request, {$grpc.CallOptions? options}) {
60 + return $createStreamingCall(_$utxos, $async.Stream.fromIterable([request]), options: options);
61 + }
62 +
63 + $grpc.ResponseFuture<$0.AddressResponse> addresses($0.AddressRequest request, {$grpc.CallOptions? options}) {
64 + return $createUnaryCall(_$addresses, request, options: options);
65 + }
66 +
67 + $grpc.ResponseFuture<$0.SpentResponse> spent($0.SpentRequest request, {$grpc.CallOptions? options}) {
68 + return $createUnaryCall(_$spent, request, options: options);
69 + }
70 +
71 + $grpc.ResponseFuture<$0.CreateResponse> create($0.CreateRequest request, {$grpc.CallOptions? options}) {
72 + return $createUnaryCall(_$create, request, options: options);
73 + }
74 +
75 + $grpc.ResponseFuture<$0.BroadcastResponse> broadcast($0.BroadcastRequest request, {$grpc.CallOptions? options}) {
76 + return $createUnaryCall(_$broadcast, request, options: options);
77 + }
78 +}
79 +
80 +@$pb.GrpcServiceName('Rpc')
81 +abstract class RpcServiceBase extends $grpc.Service {
82 + $core.String get $name => 'Rpc';
83 +
84 + RpcServiceBase() {
85 + $addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>(
86 + 'Status',
87 + status_Pre,
88 + false,
89 + false,
90 + ($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value),
91 + ($0.StatusResponse value) => value.writeToBuffer()));
92 + $addMethod($grpc.ServiceMethod<$0.UtxosRequest, $0.Utxo>(
93 + 'Utxos',
94 + utxos_Pre,
95 + false,
96 + true,
97 + ($core.List<$core.int> value) => $0.UtxosRequest.fromBuffer(value),
98 + ($0.Utxo value) => value.writeToBuffer()));
99 + $addMethod($grpc.ServiceMethod<$0.AddressRequest, $0.AddressResponse>(
100 + 'Addresses',
101 + addresses_Pre,
102 + false,
103 + false,
104 + ($core.List<$core.int> value) => $0.AddressRequest.fromBuffer(value),
105 + ($0.AddressResponse value) => value.writeToBuffer()));
106 + $addMethod($grpc.ServiceMethod<$0.SpentRequest, $0.SpentResponse>(
107 + 'Spent',
108 + spent_Pre,
109 + false,
110 + false,
111 + ($core.List<$core.int> value) => $0.SpentRequest.fromBuffer(value),
112 + ($0.SpentResponse value) => value.writeToBuffer()));
113 + $addMethod($grpc.ServiceMethod<$0.CreateRequest, $0.CreateResponse>(
114 + 'Create',
115 + create_Pre,
116 + false,
117 + false,
118 + ($core.List<$core.int> value) => $0.CreateRequest.fromBuffer(value),
119 + ($0.CreateResponse value) => value.writeToBuffer()));
120 + $addMethod($grpc.ServiceMethod<$0.BroadcastRequest, $0.BroadcastResponse>(
121 + 'Broadcast',
122 + broadcast_Pre,
123 + false,
124 + false,
125 + ($core.List<$core.int> value) => $0.BroadcastRequest.fromBuffer(value),
126 + ($0.BroadcastResponse value) => value.writeToBuffer()));
127 + }
128 +
129 + $async.Future<$0.StatusResponse> status_Pre($grpc.ServiceCall call, $async.Future<$0.StatusRequest> request) async {
130 + return status(call, await request);
131 + }
132 +
133 + $async.Stream<$0.Utxo> utxos_Pre($grpc.ServiceCall call, $async.Future<$0.UtxosRequest> request) async* {
134 + yield* utxos(call, await request);
135 + }
136 +
137 + $async.Future<$0.AddressResponse> addresses_Pre($grpc.ServiceCall call, $async.Future<$0.AddressRequest> request) async {
138 + return addresses(call, await request);
139 + }
140 +
141 + $async.Future<$0.SpentResponse> spent_Pre($grpc.ServiceCall call, $async.Future<$0.SpentRequest> request) async {
142 + return spent(call, await request);
143 + }
144 +
145 + $async.Future<$0.CreateResponse> create_Pre($grpc.ServiceCall call, $async.Future<$0.CreateRequest> request) async {
146 + return create(call, await request);
147 + }
148 +
149 + $async.Future<$0.BroadcastResponse> broadcast_Pre($grpc.ServiceCall call, $async.Future<$0.BroadcastRequest> request) async {
150 + return broadcast(call, await request);
151 + }
152 +
153 + $async.Future<$0.StatusResponse> status($grpc.ServiceCall call, $0.StatusRequest request);
154 + $async.Stream<$0.Utxo> utxos($grpc.ServiceCall call, $0.UtxosRequest request);
155 + $async.Future<$0.AddressResponse> addresses($grpc.ServiceCall call, $0.AddressRequest request);
156 + $async.Future<$0.SpentResponse> spent($grpc.ServiceCall call, $0.SpentRequest request);
157 + $async.Future<$0.CreateResponse> create($grpc.ServiceCall call, $0.CreateRequest request);
158 + $async.Future<$0.BroadcastResponse> broadcast($grpc.ServiceCall call, $0.BroadcastRequest request);
159 +}
cw_mweb/macos/Classes/CwMwebPlugin.swift new
+19
@@ -0,0 +1,19 @@
1 +import Cocoa
2 +import FlutterMacOS
3 +
4 +public class CwMwebPlugin: NSObject, FlutterPlugin {
5 + public static func register(with registrar: FlutterPluginRegistrar) {
6 + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger)
7 + let instance = CwMwebPlugin()
8 + registrar.addMethodCallDelegate(instance, channel: channel)
9 + }
10 +
11 + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
12 + switch call.method {
13 + case "getPlatformVersion":
14 + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString)
15 + default:
16 + result(FlutterMethodNotImplemented)
17 + }
18 + }
19 +}
cw_mweb/macos/cw_mweb.podspec new
+23
@@ -0,0 +1,23 @@
1 +#
2 +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
3 +# Run `pod lib lint cw_mweb.podspec` to validate before publishing.
4 +#
5 +Pod::Spec.new do |s|
6 + s.name = 'cw_mweb'
7 + s.version = '0.0.1'
8 + s.summary = 'A new Flutter plugin project.'
9 + s.description = <<-DESC
10 +A new Flutter plugin project.
11 + DESC
12 + s.homepage = 'http://example.com'
13 + s.license = { :file => '../LICENSE' }
14 + s.author = { 'Your Company' => 'email@example.com' }
15 +
16 + s.source = { :path => '.' }
17 + s.source_files = 'Classes/**/*'
18 + s.dependency 'FlutterMacOS'
19 +
20 + s.platform = :osx, '10.11'
21 + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
22 + s.swift_version = '5.0'
23 +end
cw_mweb/pubspec.yaml new
+76
@@ -0,0 +1,76 @@
1 +name: cw_mweb
2 +description: A new Flutter plugin project.
3 +version: 0.0.1
4 +homepage:
5 +
6 +environment:
7 + sdk: '>=3.0.6 <4.0.0'
8 + flutter: ">=3.3.0"
9 +
10 +dependencies:
11 + flutter:
12 + sdk: flutter
13 + grpc: ^3.2.4
14 + path_provider: ^2.1.2
15 + plugin_platform_interface: ^2.0.2
16 +
17 +dev_dependencies:
18 + flutter_test:
19 + sdk: flutter
20 + flutter_lints: ^2.0.0
21 +
22 +# For information on the generic Dart part of this file, see the
23 +# following page: https://dart.dev/tools/pub/pubspec
24 +
25 +# The following section is specific to Flutter packages.
26 +flutter:
27 + # This section identifies this Flutter project as a plugin project.
28 + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
29 + # which should be registered in the plugin registry. This is required for
30 + # using method channels.
31 + # The Android 'package' specifies package in which the registered class is.
32 + # This is required for using method channels on Android.
33 + # The 'ffiPlugin' specifies that native code should be built and bundled.
34 + # This is required for using `dart:ffi`.
35 + # All these are used by the tooling to maintain consistency when
36 + # adding or updating assets for this project.
37 + plugin:
38 + platforms:
39 + android:
40 + package: com.cakewallet.mweb
41 + pluginClass: CwMwebPlugin
42 + ios:
43 + pluginClass: CwMwebPlugin
44 + macos:
45 + pluginClass: CwMwebPlugin
46 +
47 + # To add assets to your plugin package, add an assets section, like this:
48 + # assets:
49 + # - images/a_dot_burr.jpeg
50 + # - images/a_dot_ham.jpeg
51 + #
52 + # For details regarding assets in packages, see
53 + # https://flutter.dev/assets-and-images/#from-packages
54 + #
55 + # An image asset can refer to one or more resolution-specific "variants", see
56 + # https://flutter.dev/assets-and-images/#resolution-aware
57 +
58 + # To add custom fonts to your plugin package, add a fonts section here,
59 + # in this "flutter" section. Each entry in this list should have a
60 + # "family" key with the font family name, and a "fonts" key with a
61 + # list giving the asset and other descriptors for the font. For
62 + # example:
63 + # fonts:
64 + # - family: Schyler
65 + # fonts:
66 + # - asset: fonts/Schyler-Regular.ttf
67 + # - asset: fonts/Schyler-Italic.ttf
68 + # style: italic
69 + # - family: Trajan Pro
70 + # fonts:
71 + # - asset: fonts/TrajanPro.ttf
72 + # - asset: fonts/TrajanPro_Bold.ttf
73 + # weight: 700
74 + #
75 + # For details regarding fonts in packages, see
76 + # https://flutter.dev/custom-fonts/#from-packages
ios/.gitignore
+2
@@ -30,3 +30,5 @@ Runner/GeneratedPluginRegistrant.*
30 !default.mode2v3
31 !default.pbxuser
32 !default.perspectivev3
33 +
34 +Mwebd.xcframework
\ No newline at end of file
ios/Podfile.lock
+5
@@ -7,6 +7,7 @@ PODS:
7 - Flutter
8 - ReachabilitySwift
9 - CryptoSwift (1.8.2)
10 + - cw_mweb (0.0.1):
11 - device_display_brightness (0.0.1):
12 - Flutter
13 - device_info_plus (0.0.1):
@@ -111,6 +112,7 @@ DEPENDENCIES:
112 - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
113 - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
114 - CryptoSwift
115 + - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`)
116 - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`)
117 - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
118 - devicelocale (from `.symlinks/plugins/devicelocale/ios`)
@@ -156,6 +158,8 @@ EXTERNAL SOURCES:
158 :path: ".symlinks/plugins/barcode_scan2/ios"
159 connectivity_plus:
160 :path: ".symlinks/plugins/connectivity_plus/ios"
161 + cw_mweb:
162 + :path: ".symlinks/plugins/cw_mweb/ios"
163 device_display_brightness:
164 :path: ".symlinks/plugins/device_display_brightness/ios"
165 device_info_plus:
@@ -211,6 +215,7 @@ SPEC CHECKSUMS:
215 barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0
216 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
217 CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea
218 + cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae
219 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
220 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
221 devicelocale: b22617f40038496deffba44747101255cee005b0
ios/Runner.xcodeproj/project.pbxproj
+6
@@ -28,6 +28,7 @@
28 A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
29 B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
30 CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
31 + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C58D93382C00FAC6004BCF69 /* libresolv.tbd */; };
32 CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */ = {isa = PBXBuildFile; fileRef = 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
33 D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */ = {isa = PBXBuildFile; fileRef = 91C524F800843E0A3F17E004 /* ReceivePayment.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
34 D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */ = {isa = PBXBuildFile; fileRef = ABD6FCBB0F4244B090459128 /* BreezSDK.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
@@ -84,7 +85,9 @@
85 ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = "<group>"; };
86 AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
87 B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = "<group>"; };
88 + C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
89 CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = "<group>"; };
90 + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = "<group>"; };
91 DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = "<group>"; };
92 F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = "<group>"; };
93 /* End PBXFileReference section */
@@ -95,6 +98,7 @@
98 buildActionMask = 2147483647;
99 files = (
100 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */,
101 + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */,
102 );
103 runOnlyForDeploymentPostprocessing = 0;
104 };
@@ -104,6 +108,8 @@
108 06957875428D0F5AAE053765 /* Frameworks */ = {
109 isa = PBXGroup;
110 children = (
111 + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */,
112 + C58D93382C00FAC6004BCF69 /* libresolv.tbd */,
113 0C9986A3251A932F00D566FD /* CryptoSwift.framework */,
114 3C663361C56EBB242598F609 /* Pods_Runner.framework */,
115 );
lib/bitcoin/cw_bitcoin.dart
+49 -4
@@ -220,9 +220,9 @@ class CWBitcoin extends Bitcoin {
220 return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
221 }
222
223 - WalletService createLitecoinWalletService(
224 - Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
225 - return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect);
223 + WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
224 + Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
225 + return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
226 }
227
228 @override
@@ -261,6 +261,9 @@ class CWBitcoin extends Bitcoin {
261 @override
262 List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
263
264 + @override
265 + List<ReceivePageOption> getLitecoinReceivePageOptions() => BitcoinReceivePageOption.allLitecoin;
266 +
267 @override
268 BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) {
269 switch (option) {
@@ -272,6 +275,8 @@ class CWBitcoin extends Bitcoin {
275 return SegwitAddresType.p2tr;
276 case BitcoinReceivePageOption.p2wsh:
277 return SegwitAddresType.p2wsh;
278 + case BitcoinReceivePageOption.mweb:
279 + return SegwitAddresType.mweb;
280 case BitcoinReceivePageOption.p2wpkh:
281 default:
282 return SegwitAddresType.p2wpkh;
@@ -555,6 +560,9 @@ class CWBitcoin extends Bitcoin {
560 return await getBitcoinHeightByDate(date: date);
561 }
562
563 + @override
564 + int getLitecoinHeightByDate({required DateTime date}) => getLtcHeightByDate(date: date);
565 +
566 @override
567 Future<void> rescan(Object wallet, {required int height, bool? doSingleScan}) async {
568 final bitcoinWallet = wallet as ElectrumWallet;
@@ -580,6 +588,17 @@ class CWBitcoin extends Bitcoin {
588 }
589
590 @override
591 + Future<void> setMwebEnabled(Object wallet, bool enabled) async {
592 + final litecoinWallet = wallet as LitecoinWallet;
593 + litecoinWallet.setMwebEnabled(enabled);
594 + }
595 +
596 + @override
597 + bool getMwebEnabled(Object wallet) {
598 + final litecoinWallet = wallet as LitecoinWallet;
599 + return litecoinWallet.mwebEnabled;
600 + }
601 +
602 List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
603 final pendingTx = pendingTransaction as PendingBitcoinTransaction;
604
@@ -588,7 +607,6 @@ class CWBitcoin extends Bitcoin {
607 }
608
609 final updatedOutputs = outputs.map((output) {
591 -
610 try {
611 final pendingOut = pendingTx!.outputs[outputs.indexOf(output)];
612 final updatedOutput = output;
@@ -609,4 +627,31 @@ class CWBitcoin extends Bitcoin {
627 final tx = txInfo as ElectrumTransactionInfo;
628 return tx.isReceivedSilentPayment;
629 }
630 +
631 + @override
632 + bool txIsMweb(TransactionInfo txInfo) {
633 + final tx = txInfo as ElectrumTransactionInfo;
634 +
635 + List<String> inputAddresses = tx.inputAddresses ?? [];
636 + List<String> outputAddresses = tx.outputAddresses ?? [];
637 + bool inputAddressesContainMweb = false;
638 + bool outputAddressesContainMweb = false;
639 +
640 + for (var address in inputAddresses) {
641 + if (address.toLowerCase().contains('mweb')) {
642 + inputAddressesContainMweb = true;
643 + break;
644 + }
645 + }
646 +
647 + for (var address in outputAddresses) {
648 + if (address.toLowerCase().contains('mweb')) {
649 + outputAddressesContainMweb = true;
650 + break;
651 + }
652 + }
653 +
654 + // TODO: this could be improved:
655 + return inputAddressesContainMweb || outputAddressesContainMweb;
656 + }
657 }
lib/core/address_validator.dart
+12 -9
@@ -12,10 +12,12 @@ class AddressValidator extends TextValidator {
12 AddressValidator({required CryptoCurrency type})
13 : super(
14 errorMessage: S.current.error_text_address,
15 - useAdditionalValidation: type == CryptoCurrency.btc
15 + useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc
16 ? (String txt) => BitcoinAddressUtils.validateAddress(
17 address: txt,
18 - network: BitcoinNetwork.mainnet,
18 + network: type == CryptoCurrency.btc
19 + ? BitcoinNetwork.mainnet
20 + : LitecoinNetwork.mainnet,
21 )
22 : null,
23 pattern: getPattern(type),
@@ -35,6 +37,8 @@ class AddressValidator extends TextValidator {
37 case CryptoCurrency.btc:
38 pattern =
39 '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
40 + case CryptoCurrency.ltc:
41 + pattern = '^${P2wpkhAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$';
42 case CryptoCurrency.nano:
43 pattern = '[0-9a-zA-Z_]+';
44 case CryptoCurrency.banano:
@@ -106,8 +110,6 @@ class AddressValidator extends TextValidator {
110 '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}';
111 case CryptoCurrency.bnb:
112 pattern = '[0-9a-zA-Z]+';
109 - case CryptoCurrency.ltc:
110 - pattern = '((?!(ltc|LTC)1)[0-9a-zA-Z]*)|(LTC1[A-Z0-9]*)|(ltc1[a-z0-9]*)';
113 case CryptoCurrency.hbar:
114 pattern = '[0-9a-zA-Z.]+';
115 case CryptoCurrency.zaddr:
@@ -157,6 +159,8 @@ class AddressValidator extends TextValidator {
159 return null;
160 case CryptoCurrency.btc:
161 return null;
162 + case CryptoCurrency.ltc:
163 + return null;
164 case CryptoCurrency.dash:
165 return [34];
166 case CryptoCurrency.eos:
@@ -203,8 +207,6 @@ class AddressValidator extends TextValidator {
207 return [42, 43, 44, 54, 55];
208 case CryptoCurrency.bnb:
209 return [42];
206 - case CryptoCurrency.ltc:
207 - return [34, 43, 63];
210 case CryptoCurrency.nano:
211 return [64, 65];
212 case CryptoCurrency.banano:
@@ -289,9 +291,10 @@ class AddressValidator extends TextValidator {
291 pattern =
292 '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
293 case CryptoCurrency.ltc:
292 - pattern = '(L[a-zA-Z0-9]{26,33})'
293 - '|([LM][a-km-zA-HJ-NP-Z1-9]{26,33})'
294 - '|(ltc[a-zA-Z0-9]{26,45})';
294 + pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
295 + '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
296 + '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'
297 + '|([^0-9a-zA-Z]|^)((ltc|t)mweb1q[ac-hj-np-z02-9]{90,120})([^0-9a-zA-Z]|\$)';
298 case CryptoCurrency.eth:
299 case CryptoCurrency.maticpoly:
300 pattern = '0x[0-9a-zA-Z]+';
lib/core/wallet_loading_service.dart
+2 -1
@@ -85,7 +85,8 @@ class WalletLoadingService {
85 authenticatedErrorStreamController.add(corruptedWalletsSeeds);
86
87 return wallet;
88 - } catch (_) {
88 + } catch (e) {
89 + print(e);
90 // save seeds and show corrupted wallets' seeds to the user
91 try {
92 final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type);
lib/di.dart
+24 -9
@@ -106,6 +106,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin
106 import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
107 import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
108 import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
109 +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart';
110 import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
111 import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
112 import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
@@ -156,6 +157,7 @@ import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
157 import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
158 import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart';
159 import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart';
160 +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
161 import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
162 import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
163 import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
@@ -671,7 +673,8 @@ Future<void> setup({
673 getIt.registerFactory<Modify2FAPage>(
674 () => Modify2FAPage(setup2FAViewModel: getIt.get<Setup2FAViewModel>()));
675
674 - getIt.registerFactory<DesktopSettingsPage>(() => DesktopSettingsPage(getIt.get<DashboardViewModel>()));
676 + getIt.registerFactory<DesktopSettingsPage>(
677 + () => DesktopSettingsPage(getIt.get<DashboardViewModel>()));
678
679 getIt.registerFactoryParam<ReceiveOptionViewModel, ReceivePageOption?, void>(
680 (pageOption, _) => ReceiveOptionViewModel(getIt.get<AppStore>().wallet!, pageOption));
@@ -803,7 +806,9 @@ Future<void> setup({
806
807 getIt.registerFactory<MoneroAccountListViewModel>(() {
808 final wallet = getIt.get<AppStore>().wallet!;
806 - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) {
809 + if (wallet.type == WalletType.monero ||
810 + wallet.type == WalletType.wownero ||
811 + wallet.type == WalletType.haven) {
812 return MoneroAccountListViewModel(wallet);
813 }
814 throw Exception(
@@ -863,6 +868,9 @@ Future<void> setup({
868 getIt.registerFactory(() =>
869 SilentPaymentsSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
870
871 + getIt.registerFactory(
872 + () => MwebSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
873 +
874 getIt.registerFactory(() {
875 return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
876 });
@@ -929,6 +937,8 @@ Future<void> setup({
937 getIt.registerFactory(
938 () => SilentPaymentsSettingsPage(getIt.get<SilentPaymentsSettingsViewModel>()));
939
940 + getIt.registerFactory(() => MwebSettingsPage(getIt.get<MwebSettingsViewModel>()));
941 +
942 getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>()));
943
944 getIt.registerFactory(() => NanoChangeRepPage(
@@ -1023,8 +1033,12 @@ Future<void> setup({
1033 SettingsStoreBase.walletPasswordDirectInput,
1034 );
1035 case WalletType.litecoin:
1026 - return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource,
1027 - SettingsStoreBase.walletPasswordDirectInput);
1036 + return bitcoin!.createLitecoinWalletService(
1037 + _walletInfoSource,
1038 + _unspentCoinsInfoSource,
1039 + getIt.get<SettingsStore>().mwebAlwaysScan,
1040 + SettingsStoreBase.walletPasswordDirectInput,
1041 + );
1042 case WalletType.ethereum:
1043 return ethereum!.createEthereumWalletService(
1044 _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
@@ -1236,7 +1250,8 @@ Future<void> setup({
1250 getIt.registerFactory<CakePayService>(
1251 () => CakePayService(getIt.get<SecureStorage>(), getIt.get<CakePayApi>()));
1252
1239 - getIt.registerFactory(() => CakePayCardsListViewModel(cakePayService: getIt.get<CakePayService>()));
1253 + getIt.registerFactory(
1254 + () => CakePayCardsListViewModel(cakePayService: getIt.get<CakePayService>()));
1255
1256 getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get<CakePayService>()));
1257
@@ -1268,12 +1283,12 @@ Future<void> setup({
1283 getIt.registerFactoryParam<CakePayBuyCardPage, List<dynamic>, void>((List<dynamic> args, _) {
1284 final vendor = args.first as CakePayVendor;
1285
1271 - return CakePayBuyCardPage(getIt.get<CakePayBuyCardViewModel>(param1: vendor),
1272 - getIt.get<CakePayService>());
1286 + return CakePayBuyCardPage(
1287 + getIt.get<CakePayBuyCardViewModel>(param1: vendor), getIt.get<CakePayService>());
1288 });
1289
1275 - getIt.registerFactoryParam<CakePayBuyCardDetailPage, List<dynamic>, void>(
1276 - (List<dynamic> args, _) {
1290 + getIt
1291 + .registerFactoryParam<CakePayBuyCardDetailPage, List<dynamic>, void>((List<dynamic> args, _) {
1292 final paymentCredential = args.first as PaymentCredential;
1293 final card = args[1] as CakePayCard;
1294 return CakePayBuyCardDetailPage(
lib/entities/preferences_key.dart
+4
@@ -48,6 +48,10 @@ class PreferencesKey {
48 static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
49 static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
50 static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan';
51 + static const mwebCardDisplay = 'mwebCardDisplay';
52 + static const mwebEnabled = 'mwebEnabled';
53 + static const hasEnabledMwebBefore = 'hasEnabledMwebBefore';
54 + static const mwebAlwaysScan = 'mwebAlwaysScan';
55 static const shouldShowReceiveWarning = 'should_show_receive_warning';
56 static const shouldShowYatPopup = 'should_show_yat_popup';
57 static const shouldShowRepWarning = 'should_show_rep_warning';
lib/main.dart
+5 -1
@@ -30,6 +30,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
30 import 'package:cw_core/address_info.dart';
31 import 'package:cw_core/cake_hive.dart';
32 import 'package:cw_core/hive_type_ids.dart';
33 +import 'package:cw_core/mweb_utxo.dart';
34 import 'package:cw_core/node.dart';
35 import 'package:cw_core/unspent_coins_info.dart';
36 import 'package:cw_core/wallet_info.dart';
@@ -152,6 +153,10 @@ Future<void> initializeAppConfigs() async {
153 CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter());
154 }
155
156 + if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) {
157 + CakeHive.registerAdapter(MwebUtxoAdapter());
158 + }
159 +
160 final secureStorage = secureStorageShared;
161 final transactionDescriptionsBoxKey =
162 await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@@ -233,7 +238,6 @@ Future<void> initialSetup(
238 secureStorage: secureStorage,
239 );
240 await bootstrap(navigatorKey);
236 - monero?.onStartup();
241 }
242
243 class App extends StatefulWidget {
lib/router.dart
+5
@@ -72,6 +72,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin
72 import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
73 import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
74 import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
75 +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart';
76 import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
77 import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
78 import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
@@ -450,6 +451,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
451 return CupertinoPageRoute<void>(
452 fullscreenDialog: true, builder: (_) => getIt.get<SilentPaymentsSettingsPage>());
453
454 + case Routes.mwebSettings:
455 + return CupertinoPageRoute<void>(
456 + fullscreenDialog: true, builder: (_) => getIt.get<MwebSettingsPage>());
457 +
458 case Routes.connectionSync:
459 return CupertinoPageRoute<void>(
460 fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>());
lib/routes.dart
+1
@@ -73,6 +73,7 @@ class Routes {
73 static const cakePayAccountPage = '/cake_pay_account_page';
74 static const webViewPage = '/web_view_page';
75 static const silentPaymentsSettings = '/silent_payments_settings';
76 + static const mwebSettings = '/mweb_settings';
77 static const connectionSync = '/connection_sync_page';
78 static const securityBackupPage = '/security_and_backup_page';
79 static const privacyPage = '/privacy_page';
lib/src/screens/dashboard/pages/address_page.dart
+2 -1
@@ -225,7 +225,8 @@ class AddressPage extends BasePage {
225 }
226 break;
227 default:
228 - if (addressListViewModel.type == WalletType.bitcoin) {
228 + if (addressListViewModel.type == WalletType.bitcoin ||
229 + addressListViewModel.type == WalletType.litecoin) {
230 addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
231 }
232 }
lib/src/screens/dashboard/pages/balance_page.dart
+217 -19
@@ -7,6 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
7 import 'package:cake_wallet/routes.dart';
8 import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
9 import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
10 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
11 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
12 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
13 import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
@@ -25,6 +26,7 @@ import 'package:cw_core/crypto_currency.dart';
26 import 'package:flutter/material.dart';
27 import 'package:flutter/widgets.dart';
28 import 'package:flutter_mobx/flutter_mobx.dart';
29 +import 'package:mobx/mobx.dart';
30 import 'package:url_launcher/url_launcher.dart';
31
32 class BalancePage extends StatelessWidget {
@@ -129,7 +131,7 @@ class CryptoBalanceWidget extends StatelessWidget {
131 builder: (_) {
132 if (dashboardViewModel.getMoneroError != null) {
133 return Padding(
132 - padding: const EdgeInsets.fromLTRB(16,0,16,16),
134 + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
135 child: DashBoardRoundedCardWidget(
136 title: "Invalid monero bindings",
137 subTitle: dashboardViewModel.getMoneroError.toString(),
@@ -144,13 +146,12 @@ class CryptoBalanceWidget extends StatelessWidget {
146 builder: (_) {
147 if (dashboardViewModel.getWowneroError != null) {
148 return Padding(
147 - padding: const EdgeInsets.fromLTRB(16,0,16,16),
148 - child: DashBoardRoundedCardWidget(
149 - title: "Invalid wownero bindings",
150 - subTitle: dashboardViewModel.getWowneroError.toString(),
151 - onTap: () {},
152 - )
153 - );
149 + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
150 + child: DashBoardRoundedCardWidget(
151 + title: "Invalid wownero bindings",
152 + subTitle: dashboardViewModel.getWowneroError.toString(),
153 + onTap: () {},
154 + ));
155 }
156 return Container();
157 },
@@ -271,6 +272,18 @@ class CryptoBalanceWidget extends StatelessWidget {
272 currency: balance.asset,
273 hasAdditionalBalance:
274 dashboardViewModel.balanceViewModel.hasAdditionalBalance,
275 + hasSecondAdditionalBalance:
276 + dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance,
277 + hasSecondAvailableBalance:
278 + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance,
279 + secondAdditionalBalance: balance.secondAdditionalBalance,
280 + secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance,
281 + secondAvailableBalance: balance.secondAvailableBalance,
282 + secondAvailableFiatBalance: balance.fiatSecondAvailableBalance,
283 + secondAdditionalBalanceLabel:
284 + '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}',
285 + secondAvailableBalanceLabel:
286 + '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}',
287 isTestnet: dashboardViewModel.isTestnet,
288 );
289 });
@@ -284,16 +297,15 @@ class CryptoBalanceWidget extends StatelessWidget {
297 if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[
298 SizedBox(height: 10),
299 Padding(
287 - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
288 - child: DashBoardRoundedCardWidget(
289 - customBorder: 30,
290 - title: "This wallet has encountered an issue",
291 - subTitle: "Here are the things that you should note:\n - "
292 - +dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ")
293 - +"\n\nPlease restart your wallet and if it doesn't help contact our support.",
294 - onTap: () {},
295 - )
296 - )
300 + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
301 + child: DashBoardRoundedCardWidget(
302 + customBorder: 30,
303 + title: "This wallet has encountered an issue",
304 + subTitle: "Here are the things that you should note:\n - " +
305 + dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") +
306 + "\n\nPlease restart your wallet and if it doesn't help contact our support.",
307 + onTap: () {},
308 + ))
309 ],
310 if (dashboardViewModel.showSilentPaymentsCard) ...[
311 SizedBox(height: 10),
@@ -360,7 +372,73 @@ class CryptoBalanceWidget extends StatelessWidget {
372 ),
373 ),
374 ),
363 - ]
375 + ],
376 + if (dashboardViewModel.showMwebCard) ...[
377 + SizedBox(height: 10),
378 + Padding(
379 + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
380 + child: DashBoardRoundedCardWidget(
381 + customBorder: 30,
382 + title: S.current.litecoin_mweb,
383 + subTitle: S.current.litecoin_enable_mweb_sync,
384 + hint: Column(
385 + children: [
386 + Row(
387 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
388 + children: [
389 + GestureDetector(
390 + behavior: HitTestBehavior.opaque,
391 + onTap: () => launchUrl(
392 + Uri.parse(
393 + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"),
394 + mode: LaunchMode.externalApplication,
395 + ),
396 + child: Row(
397 + children: [
398 + Text(
399 + S.current.litecoin_what_is_mweb,
400 + style: TextStyle(
401 + fontSize: 12,
402 + fontFamily: 'Lato',
403 + fontWeight: FontWeight.w400,
404 + color: Theme.of(context)
405 + .extension<BalancePageTheme>()!
406 + .labelTextColor,
407 + height: 1,
408 + ),
409 + softWrap: true,
410 + ),
411 + Padding(
412 + padding: const EdgeInsets.symmetric(horizontal: 4),
413 + child: Icon(Icons.help_outline,
414 + size: 16,
415 + color: Theme.of(context)
416 + .extension<BalancePageTheme>()!
417 + .labelTextColor),
418 + )
419 + ],
420 + ),
421 + ),
422 + Observer(
423 + builder: (_) => StandardSwitch(
424 + value: dashboardViewModel.mwebScanningActive,
425 + onTaped: () => _toggleMweb(context),
426 + ),
427 + )
428 + ],
429 + ),
430 + ],
431 + ),
432 + onTap: () => _toggleMweb(context),
433 + icon: ImageIcon(
434 + AssetImage('assets/images/mweb_logo.png'),
435 + color:
436 + Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
437 + size: 50,
438 + ),
439 + ),
440 + ),
441 + ],
442 ],
443 );
444 }),
@@ -400,6 +478,22 @@ class CryptoBalanceWidget extends StatelessWidget {
478
479 return dashboardViewModel.setSilentPaymentsScanning(newValue);
480 }
481 +
482 + Future<void> _toggleMweb(BuildContext context) async {
483 + if (!dashboardViewModel.hasEnabledMwebBefore) {
484 + await showPopUp<void>(
485 + context: context,
486 + builder: (BuildContext context) => AlertWithOneAction(
487 + alertTitle: S.of(context).warning,
488 + alertContent: S.current.litecoin_mweb_warning,
489 + buttonText: S.of(context).ok,
490 + buttonAction: () {
491 + Navigator.of(context).pop();
492 + },
493 + ));
494 + }
495 + dashboardViewModel.setMwebScanningActive(!dashboardViewModel.mwebScanningActive);
496 + }
497 }
498
499 class BalanceRowWidget extends StatelessWidget {
@@ -410,10 +504,18 @@ class BalanceRowWidget extends StatelessWidget {
504 required this.additionalBalanceLabel,
505 required this.additionalBalance,
506 required this.additionalFiatBalance,
507 + required this.secondAvailableBalanceLabel,
508 + required this.secondAvailableBalance,
509 + required this.secondAvailableFiatBalance,
510 + required this.secondAdditionalBalanceLabel,
511 + required this.secondAdditionalBalance,
512 + required this.secondAdditionalFiatBalance,
513 required this.frozenBalance,
514 required this.frozenFiatBalance,
515 required this.currency,
516 required this.hasAdditionalBalance,
517 + required this.hasSecondAvailableBalance,
518 + required this.hasSecondAdditionalBalance,
519 required this.isTestnet,
520 super.key,
521 });
@@ -424,10 +526,18 @@ class BalanceRowWidget extends StatelessWidget {
526 final String additionalBalanceLabel;
527 final String additionalBalance;
528 final String additionalFiatBalance;
529 + final String secondAvailableBalanceLabel;
530 + final String secondAvailableBalance;
531 + final String secondAvailableFiatBalance;
532 + final String secondAdditionalBalanceLabel;
533 + final String secondAdditionalBalance;
534 + final String secondAdditionalFiatBalance;
535 final String frozenBalance;
536 final String frozenFiatBalance;
537 final CryptoCurrency currency;
538 final bool hasAdditionalBalance;
539 + final bool hasSecondAvailableBalance;
540 + final bool hasSecondAdditionalBalance;
541 final bool isTestnet;
542
543 // void _showBalanceDescription(BuildContext context) {
@@ -675,6 +785,94 @@ class BalanceRowWidget extends StatelessWidget {
785 ),
786 ],
787 ),
788 + if (hasSecondAvailableBalance)
789 + Column(
790 + crossAxisAlignment: CrossAxisAlignment.start,
791 + children: [
792 + SizedBox(height: 24),
793 + Text(
794 + '${secondAvailableBalanceLabel}',
795 + textAlign: TextAlign.center,
796 + style: TextStyle(
797 + fontSize: 12,
798 + fontFamily: 'Lato',
799 + fontWeight: FontWeight.w400,
800 + color: Theme.of(context).extension<BalancePageTheme>()!.labelTextColor,
801 + height: 1,
802 + ),
803 + ),
804 + SizedBox(height: 8),
805 + AutoSizeText(
806 + secondAvailableBalance,
807 + style: TextStyle(
808 + fontSize: 20,
809 + fontFamily: 'Lato',
810 + fontWeight: FontWeight.w400,
811 + color: Theme.of(context).extension<BalancePageTheme>()!.assetTitleColor,
812 + height: 1,
813 + ),
814 + maxLines: 1,
815 + textAlign: TextAlign.center,
816 + ),
817 + SizedBox(height: 4),
818 + if (!isTestnet)
819 + Text(
820 + '${secondAvailableFiatBalance}',
821 + textAlign: TextAlign.center,
822 + style: TextStyle(
823 + fontSize: 12,
824 + fontFamily: 'Lato',
825 + fontWeight: FontWeight.w400,
826 + color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
827 + height: 1,
828 + ),
829 + ),
830 + ],
831 + ),
832 + if (hasSecondAdditionalBalance)
833 + Column(
834 + crossAxisAlignment: CrossAxisAlignment.start,
835 + children: [
836 + SizedBox(height: 24),
837 + Text(
838 + '${secondAdditionalBalanceLabel}',
839 + textAlign: TextAlign.center,
840 + style: TextStyle(
841 + fontSize: 12,
842 + fontFamily: 'Lato',
843 + fontWeight: FontWeight.w400,
844 + color: Theme.of(context).extension<BalancePageTheme>()!.labelTextColor,
845 + height: 1,
846 + ),
847 + ),
848 + SizedBox(height: 8),
849 + AutoSizeText(
850 + secondAdditionalBalance,
851 + style: TextStyle(
852 + fontSize: 20,
853 + fontFamily: 'Lato',
854 + fontWeight: FontWeight.w400,
855 + color: Theme.of(context).extension<BalancePageTheme>()!.assetTitleColor,
856 + height: 1,
857 + ),
858 + maxLines: 1,
859 + textAlign: TextAlign.center,
860 + ),
861 + SizedBox(height: 4),
862 + if (!isTestnet)
863 + Text(
864 + '${secondAdditionalFiatBalance}',
865 + textAlign: TextAlign.center,
866 + style: TextStyle(
867 + fontSize: 12,
868 + fontFamily: 'Lato',
869 + fontWeight: FontWeight.w400,
870 + color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
871 + height: 1,
872 + ),
873 + ),
874 + ],
875 + ),
876 ],
877 ),
878 ),
lib/src/screens/dashboard/pages/transactions_page.dart
+13 -3
@@ -86,6 +86,18 @@ class TransactionsPage extends StatelessWidget {
86 final transaction = item.transaction;
87 final transactionType = dashboardViewModel.getTransactionType(transaction);
88
89 + List<String> tags = [];
90 + if (dashboardViewModel.type == WalletType.bitcoin) {
91 + if (bitcoin!.txIsReceivedSilentPayment(transaction)) {
92 + tags.add(S.of(context).silent_payment);
93 + }
94 + }
95 + if (dashboardViewModel.type == WalletType.litecoin) {
96 + if (bitcoin!.txIsMweb(transaction)) {
97 + tags.add("MWEB");
98 + }
99 + }
100 +
101 return Observer(
102 builder: (_) => TransactionRow(
103 onTap: () => Navigator.of(context)
@@ -100,9 +112,7 @@ class TransactionsPage extends StatelessWidget {
112 isPending: transaction.isPending,
113 title:
114 item.formattedTitle + item.formattedStatus + transactionType,
103 - isReceivedSilentPayment:
104 - dashboardViewModel.type == WalletType.bitcoin &&
105 - bitcoin!.txIsReceivedSilentPayment(transaction),
115 + tags: tags,
116 ),
117 );
118 }
lib/src/screens/dashboard/widgets/menu_widget.dart
+5
@@ -191,6 +191,11 @@ class MenuWidgetState extends State<MenuWidget> {
191
192 final item = items[index];
193
194 + if (!widget.dashboardViewModel.hasMweb &&
195 + item.name(context) == S.current.litecoin_mweb_settings) {
196 + return const SizedBox();
197 + }
198 +
199 final isLastTile = index == itemCount - 1;
200
201 return SettingActionButton(
lib/src/screens/dashboard/widgets/transaction_raw.dart
+3 -4
@@ -1,4 +1,3 @@
1 -import 'package:cake_wallet/generated/i18n.dart';
1 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
2 import 'package:flutter/material.dart';
3 import 'package:cw_core/transaction_direction.dart';
@@ -12,7 +11,7 @@ class TransactionRow extends StatelessWidget {
11 required this.formattedAmount,
12 required this.formattedFiatAmount,
13 required this.isPending,
15 - required this.isReceivedSilentPayment,
14 + required this.tags,
15 required this.title,
16 required this.onTap,
17 });
@@ -23,8 +22,8 @@ class TransactionRow extends StatelessWidget {
22 final String formattedAmount;
23 final String formattedFiatAmount;
24 final bool isPending;
26 - final bool isReceivedSilentPayment;
25 final String title;
26 + final List<String> tags;
27
28 @override
29 Widget build(BuildContext context) {
@@ -61,7 +60,7 @@ class TransactionRow extends StatelessWidget {
60 fontWeight: FontWeight.w500,
61 color: Theme.of(context).extension<DashboardPageTheme>()!.textColor,
62 )),
64 - if (isReceivedSilentPayment) TxTag(tag: S.of(context).silent_payment),
63 + ...tags.map((tag) => Row(children: [SizedBox(width: 8), TxTag(tag: tag)])),
64 ],
65 ),
66 Text(formattedAmount,
lib/src/screens/receive/widgets/address_list.dart
+3
@@ -1,3 +1,6 @@
1 +
2 +import 'dart:math';
3 +
4 import 'package:cake_wallet/di.dart';
5 import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/routes.dart';
lib/src/screens/rescan/rescan_page.dart
+2
@@ -33,7 +33,9 @@ class RescanPage extends BasePage {
33 key: _blockchainHeightWidgetKey,
34 onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value,
35 isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan,
36 + isMwebScan: _rescanViewModel.isMwebScan,
37 doSingleScan: _rescanViewModel.doSingleScan,
38 + hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now
39 toggleSingleScan: () =>
40 _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan,
41 walletType: _rescanViewModel.wallet.type,
lib/src/screens/root/root.dart
+8
@@ -5,6 +5,7 @@ import 'package:cake_wallet/core/totp_request_details.dart';
5 import 'package:cake_wallet/utils/device_info.dart';
6 import 'package:cake_wallet/view_model/link_view_model.dart';
7 import 'package:cw_core/wallet_base.dart';
8 +import 'package:cw_core/wallet_type.dart';
9 import 'package:flutter/material.dart';
10 import 'package:cake_wallet/routes.dart';
11 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@@ -134,6 +135,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
135 setState(() => _setInactive(true));
136 }
137
138 + if (widget.appStore.wallet?.type == WalletType.litecoin) {
139 + widget.appStore.wallet?.stopSync();
140 + }
141 +
142 break;
143 case AppLifecycleState.resumed:
144 widget.authService.requireAuth().then((value) {
@@ -143,6 +148,9 @@ class RootState extends State<Root> with WidgetsBindingObserver {
148 });
149 }
150 });
151 + if (widget.appStore.wallet?.type == WalletType.litecoin) {
152 + widget.appStore.wallet?.startSync();
153 + }
154 break;
155 default:
156 break;
lib/src/screens/settings/desktop_settings/desktop_settings_page.dart
+5
@@ -60,6 +60,11 @@ class _DesktopSettingsPageState extends State<DesktopSettingsPage> {
60 return Container();
61 }
62
63 + if (!widget.dashboardViewModel.hasMweb &&
64 + item.name(context) == S.of(context).litecoin_mweb_settings) {
65 + return Container();
66 + }
67 +
68 final isLastTile = index == itemCount - 1;
69 return SettingActionButton(
70 isLastTile: isLastTile,
lib/src/screens/settings/mweb_settings.dart new
+51
@@ -0,0 +1,51 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/routes.dart';
3 +import 'package:cake_wallet/src/screens/base_page.dart';
4 +import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
5 +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
6 +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
7 +import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart';
8 +import 'package:flutter/material.dart';
9 +import 'package:flutter_mobx/flutter_mobx.dart';
10 +
11 +class MwebSettingsPage extends BasePage {
12 + MwebSettingsPage(this._mwebSettingsViewModel);
13 +
14 + @override
15 + String get title => S.current.litecoin_mweb_settings;
16 +
17 + final MwebSettingsViewModel _mwebSettingsViewModel;
18 +
19 + @override
20 + Widget body(BuildContext context) {
21 + return SingleChildScrollView(
22 + child: Observer(builder: (_) {
23 + return Container(
24 + padding: EdgeInsets.only(top: 10),
25 + child: Column(
26 + children: [
27 + SettingsSwitcherCell(
28 + title: S.current.litecoin_mweb_display_card,
29 + value: _mwebSettingsViewModel.mwebCardDisplay,
30 + onValueChange: (_, bool value) {
31 + _mwebSettingsViewModel.setMwebCardDisplay(value);
32 + },
33 + ),
34 + SettingsSwitcherCell(
35 + title: S.current.litecoin_mweb_always_scan,
36 + value: _mwebSettingsViewModel.mwebAlwaysScan,
37 + onValueChange: (_, bool value) {
38 + _mwebSettingsViewModel.setMwebAlwaysScan(value);
39 + },
40 + ),
41 + SettingsCellWithArrow(
42 + title: S.current.litecoin_mweb_scanning,
43 + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan),
44 + ),
45 + ],
46 + ),
47 + );
48 + }),
49 + );
50 + }
51 +}
lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart
+55 -32
@@ -103,40 +103,63 @@ class UnspentCoinsListItem extends StatelessWidget {
103 ),
104 maxLines: 1,
105 ),
106 - if (isChange)
107 - Container(
108 - height: 17,
109 - padding: EdgeInsets.only(left: 6, right: 6),
110 - decoration: BoxDecoration(
111 - borderRadius: BorderRadius.all(Radius.circular(8.5)),
112 - color: Colors.white),
113 - alignment: Alignment.center,
114 - child: Text(
115 - S.of(context).unspent_change,
116 - style: TextStyle(
117 - color: itemColor,
118 - fontSize: 7,
119 - fontWeight: FontWeight.w600,
106 + Row(
107 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
108 + children: [
109 + if (isChange)
110 + Container(
111 + height: 17,
112 + padding: EdgeInsets.only(left: 6, right: 6),
113 + decoration: BoxDecoration(
114 + borderRadius: BorderRadius.all(Radius.circular(8.5)),
115 + color: Colors.white),
116 + alignment: Alignment.center,
117 + child: Text(
118 + S.of(context).unspent_change,
119 + style: TextStyle(
120 + color: itemColor,
121 + fontSize: 7,
122 + fontWeight: FontWeight.w600,
123 + ),
124 + ),
125 ),
121 - ),
122 - ),
123 - if (isSilentPayment)
124 - Container(
125 - height: 17,
126 - padding: EdgeInsets.only(left: 6, right: 6),
127 - decoration: BoxDecoration(
128 - borderRadius: BorderRadius.all(Radius.circular(8.5)),
129 - color: Colors.white),
130 - alignment: Alignment.center,
131 - child: Text(
132 - S.of(context).silent_payments,
133 - style: TextStyle(
134 - color: itemColor,
135 - fontSize: 7,
136 - fontWeight: FontWeight.w600,
126 + if (address.toLowerCase().contains("mweb"))
127 + Container(
128 + height: 17,
129 + padding: EdgeInsets.only(left: 6, right: 6),
130 + margin: EdgeInsets.only(left: 6),
131 + decoration: BoxDecoration(
132 + borderRadius: BorderRadius.all(Radius.circular(8.5)),
133 + color: Colors.white),
134 + alignment: Alignment.center,
135 + child: Text(
136 + "MWEB",
137 + style: TextStyle(
138 + color: itemColor,
139 + fontSize: 7,
140 + fontWeight: FontWeight.w600,
141 + ),
142 + ),
143 ),
138 - ),
139 - ),
144 + if (isSilentPayment)
145 + Container(
146 + height: 17,
147 + padding: EdgeInsets.only(left: 6, right: 6),
148 + decoration: BoxDecoration(
149 + borderRadius: BorderRadius.all(Radius.circular(8.5)),
150 + color: Colors.white),
151 + alignment: Alignment.center,
152 + child: Text(
153 + S.of(context).silent_payments,
154 + style: TextStyle(
155 + color: itemColor,
156 + fontSize: 7,
157 + fontWeight: FontWeight.w600,
158 + ),
159 + ),
160 + ),
161 + ],
162 + ),
163 ],
164 ),
165 ),
lib/src/widgets/blockchain_height_widget.dart
+5 -1
@@ -18,6 +18,7 @@ class BlockchainHeightWidget extends StatefulWidget {
18 this.onHeightOrDateEntered,
19 this.hasDatePicker = true,
20 this.isSilentPaymentsScan = false,
21 + this.isMwebScan = false,
22 this.toggleSingleScan,
23 this.doSingleScan = false,
24 this.bitcoinMempoolAPIEnabled,
@@ -29,6 +30,7 @@ class BlockchainHeightWidget extends StatefulWidget {
30 final FocusNode? focusNode;
31 final bool hasDatePicker;
32 final bool isSilentPaymentsScan;
33 + final bool isMwebScan;
34 final bool doSingleScan;
35 final Future<bool>? bitcoinMempoolAPIEnabled;
36 final Function()? toggleSingleScan;
@@ -170,7 +172,9 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
172
173 if (date != null) {
174 int height;
173 - if (widget.isSilentPaymentsScan) {
175 + if (widget.isMwebScan) {
176 + height = bitcoin!.getLitecoinHeightByDate(date: date);
177 + } else if (widget.isSilentPaymentsScan) {
178 height = await bitcoin!.getHeightByDate(
179 date: date,
180 bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled,
lib/src/widgets/dashboard_card_widget.dart
+1 -1
@@ -23,8 +23,8 @@ class DashBoardRoundedCardWidget extends StatelessWidget {
23 final String subTitle;
24 final Widget? hint;
25 final SvgPicture? svgPicture;
26 + final Widget? icon;
27 final Image? image;
27 - final Icon? icon;
28 final double? customBorder;
29
30 @override
lib/src/widgets/setting_actions.dart
+11
@@ -18,6 +18,7 @@ class SettingActions {
18 walletSettingAction,
19 addressBookSettingAction,
20 silentPaymentsSettingAction,
21 + litecoinMwebSettingAction,
22 securityBackupSettingAction,
23 privacySettingAction,
24 displaySettingAction,
@@ -30,6 +31,7 @@ class SettingActions {
31 walletSettingAction,
32 addressBookSettingAction,
33 silentPaymentsSettingAction,
34 + litecoinMwebSettingAction,
35 securityBackupSettingAction,
36 privacySettingAction,
37 displaySettingAction,
@@ -46,6 +48,15 @@ class SettingActions {
48 },
49 );
50
51 + static SettingActions litecoinMwebSettingAction = SettingActions._(
52 + name: (context) => S.current.litecoin_mweb_settings,
53 + image: 'assets/images/bitcoin_menu.png',
54 + onTap: (BuildContext context) {
55 + Navigator.pop(context);
56 + Navigator.of(context).pushNamed(Routes.mwebSettings);
57 + },
58 + );
59 +
60 static SettingActions connectionSettingAction = SettingActions._(
61 name: (context) => S.of(context).connection_sync,
62 image: 'assets/images/nodes_menu.png',
lib/store/settings_store.dart
+47
@@ -114,6 +114,10 @@ abstract class SettingsStoreBase with Store {
114 required this.customBitcoinFeeRate,
115 required this.silentPaymentsCardDisplay,
116 required this.silentPaymentsAlwaysScan,
117 + required this.mwebAlwaysScan,
118 + required this.mwebCardDisplay,
119 + required this.mwebEnabled,
120 + required this.hasEnabledMwebBefore,
121 TransactionPriority? initialBitcoinTransactionPriority,
122 TransactionPriority? initialMoneroTransactionPriority,
123 TransactionPriority? initialWowneroTransactionPriority,
@@ -555,6 +559,24 @@ abstract class SettingsStoreBase with Store {
559 (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool(
560 PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan));
561
562 + reaction(
563 + (_) => mwebAlwaysScan,
564 + (bool mwebAlwaysScan) =>
565 + _sharedPreferences.setBool(PreferencesKey.mwebAlwaysScan, mwebAlwaysScan));
566 +
567 + reaction(
568 + (_) => mwebCardDisplay,
569 + (bool mwebCardDisplay) =>
570 + _sharedPreferences.setBool(PreferencesKey.mwebCardDisplay, mwebCardDisplay));
571 +
572 + reaction((_) => mwebEnabled,
573 + (bool mwebEnabled) => _sharedPreferences.setBool(PreferencesKey.mwebEnabled, mwebEnabled));
574 +
575 + reaction(
576 + (_) => hasEnabledMwebBefore,
577 + (bool hasEnabledMwebBefore) =>
578 + _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore));
579 +
580 this.nodes.observe((change) {
581 if (change.newValue != null && change.key != null) {
582 _saveCurrentNode(change.newValue!, change.key!);
@@ -768,6 +790,18 @@ abstract class SettingsStoreBase with Store {
790 @observable
791 bool silentPaymentsAlwaysScan;
792
793 + @observable
794 + bool mwebAlwaysScan;
795 +
796 + @observable
797 + bool mwebCardDisplay;
798 +
799 + @observable
800 + bool mwebEnabled;
801 +
802 + @observable
803 + bool hasEnabledMwebBefore;
804 +
805 final SecureStorage _secureStorage;
806 final SharedPreferences _sharedPreferences;
807 final BackgroundTasks _backgroundTasks;
@@ -925,6 +959,11 @@ abstract class SettingsStoreBase with Store {
959 sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
960 final silentPaymentsAlwaysScan =
961 sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false;
962 + final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false;
963 + final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true;
964 + final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
965 + final hasEnabledMwebBefore =
966 + sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false;
967
968 // If no value
969 if (pinLength == null || pinLength == 0) {
@@ -1191,6 +1230,10 @@ abstract class SettingsStoreBase with Store {
1230 customBitcoinFeeRate: customBitcoinFeeRate,
1231 silentPaymentsCardDisplay: silentPaymentsCardDisplay,
1232 silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,
1233 + mwebAlwaysScan: mwebAlwaysScan,
1234 + mwebCardDisplay: mwebCardDisplay,
1235 + mwebEnabled: mwebEnabled,
1236 + hasEnabledMwebBefore: hasEnabledMwebBefore,
1237 initialMoneroTransactionPriority: moneroTransactionPriority,
1238 initialWowneroTransactionPriority: wowneroTransactionPriority,
1239 initialBitcoinTransactionPriority: bitcoinTransactionPriority,
@@ -1353,6 +1396,10 @@ abstract class SettingsStoreBase with Store {
1396 sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
1397 silentPaymentsAlwaysScan =
1398 sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false;
1399 + mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false;
1400 + mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true;
1401 + mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
1402 + hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false;
1403 final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
1404 final bitcoinElectrumServerId =
1405 sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
lib/utils/exception_handler.dart
+1 -1
@@ -83,7 +83,7 @@ class ExceptionHandler {
83 }
84
85 static void onError(FlutterErrorDetails errorDetails) async {
86 - if (kDebugMode) {
86 + if (kDebugMode || kProfileMode) {
87 FlutterError.presentError(errorDetails);
88 debugPrint(errorDetails.toString());
89 return;
lib/view_model/dashboard/balance_view_model.dart
+70 -2
@@ -21,10 +21,14 @@ class BalanceRecord {
21 const BalanceRecord(
22 {required this.availableBalance,
23 required this.additionalBalance,
24 + required this.secondAvailableBalance,
25 + required this.secondAdditionalBalance,
26 required this.frozenBalance,
27 required this.fiatAvailableBalance,
28 required this.fiatAdditionalBalance,
29 required this.fiatFrozenBalance,
30 + required this.fiatSecondAvailableBalance,
31 + required this.fiatSecondAdditionalBalance,
32 required this.asset,
33 required this.formattedAssetTitle});
34 final String fiatAdditionalBalance;
@@ -33,6 +37,10 @@ class BalanceRecord {
37 final String additionalBalance;
38 final String availableBalance;
39 final String frozenBalance;
40 + final String secondAvailableBalance;
41 + final String secondAdditionalBalance;
42 + final String fiatSecondAdditionalBalance;
43 + final String fiatSecondAvailableBalance;
44 final CryptoCurrency asset;
45 final String formattedAssetTitle;
46 }
@@ -158,6 +166,26 @@ abstract class BalanceViewModelBase with Store {
166 }
167 }
168
169 + @computed
170 + String get secondAvailableBalanceLabel {
171 + switch (wallet.type) {
172 + case WalletType.litecoin:
173 + return S.current.mweb_confirmed;
174 + default:
175 + return S.current.confirmed;
176 + }
177 + }
178 +
179 + @computed
180 + String get secondAdditionalBalanceLabel {
181 + switch (wallet.type) {
182 + case WalletType.litecoin:
183 + return S.current.mweb_unconfirmed;
184 + default:
185 + return S.current.unconfirmed;
186 + }
187 + }
188 +
189 @computed
190 bool get hasMultiBalance => appStore.wallet!.type == WalletType.haven;
191
@@ -243,9 +271,13 @@ abstract class BalanceViewModelBase with Store {
271 availableBalance: '---',
272 additionalBalance: '---',
273 frozenBalance: '---',
274 + secondAvailableBalance: '---',
275 + secondAdditionalBalance: '---',
276 fiatAdditionalBalance: isFiatDisabled ? '' : '---',
277 fiatAvailableBalance: isFiatDisabled ? '' : '---',
278 fiatFrozenBalance: isFiatDisabled ? '' : '---',
279 + fiatSecondAvailableBalance: isFiatDisabled ? '' : '---',
280 + fiatSecondAdditionalBalance: isFiatDisabled ? '' : '---',
281 asset: key,
282 formattedAssetTitle: _formatterAsset(key)));
283 }
@@ -274,24 +306,46 @@ abstract class BalanceViewModelBase with Store {
306 ' ' +
307 _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value)));
308
309 + final secondAdditionalFiatBalance = isFiatDisabled
310 + ? ''
311 + : (fiatCurrency.toString() +
312 + ' ' +
313 + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAdditionalBalance));
314 +
315 + final secondAvailableFiatBalance = isFiatDisabled
316 + ? ''
317 + : (fiatCurrency.toString() +
318 + ' ' +
319 + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAvailableBalance));
320 +
321 return MapEntry(
322 key,
323 BalanceRecord(
324 availableBalance: value.formattedAvailableBalance,
325 additionalBalance: value.formattedAdditionalBalance,
326 frozenBalance: getFormattedFrozenBalance(value),
327 + secondAvailableBalance: value.formattedSecondAvailableBalance,
328 + secondAdditionalBalance: value.formattedSecondAdditionalBalance,
329 fiatAdditionalBalance: additionalFiatBalance,
330 fiatAvailableBalance: availableFiatBalance,
331 fiatFrozenBalance: frozenFiatBalance,
332 + fiatSecondAvailableBalance: secondAvailableFiatBalance,
333 + fiatSecondAdditionalBalance: secondAdditionalFiatBalance,
334 asset: key,
335 formattedAssetTitle: _formatterAsset(key)));
336 });
337 }
338
339 @computed
292 - bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type);
340 + bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type);
341 +
342 + @computed
343 + bool get hasSecondAdditionalBalance => _hasSecondAdditionalBalanceForWalletType(wallet.type);
344
294 - bool _hasAdditionBalanceForWalletType(WalletType type) {
345 + @computed
346 + bool get hasSecondAvailableBalance => _hasSecondAvailableBalanceForWalletType(wallet.type);
347 +
348 + bool _hasAdditionalBalanceForWalletType(WalletType type) {
349 switch (type) {
350 case WalletType.ethereum:
351 case WalletType.polygon:
@@ -303,6 +357,20 @@ abstract class BalanceViewModelBase with Store {
357 }
358 }
359
360 + bool _hasSecondAdditionalBalanceForWalletType(WalletType type) {
361 + if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) {
362 + return true;
363 + }
364 + return false;
365 + }
366 +
367 + bool _hasSecondAvailableBalanceForWalletType(WalletType type) {
368 + if (wallet.type == WalletType.litecoin && settingsStore.mwebEnabled) {
369 + return true;
370 + }
371 + return false;
372 + }
373 +
374 @computed
375 List<BalanceRecord> get formattedBalances {
376 final balance = balances.values.toList();
lib/view_model/dashboard/dashboard_view_model.dart
+40 -2
@@ -137,8 +137,8 @@ abstract class DashboardViewModelBase with Store {
137 FilterItem(
138 value: () => tradeFilterStore.displayLetsExchange,
139 caption: ExchangeProviderDescription.letsExchange.title,
140 - onChanged: () =>
141 - tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
140 + onChanged: () => tradeFilterStore
141 + .toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
142 FilterItem(
143 value: () => tradeFilterStore.displayStealthEx,
144 caption: ExchangeProviderDescription.stealthEx.title,
@@ -255,6 +255,16 @@ abstract class DashboardViewModelBase with Store {
255 silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
256 });
257 }
258 +
259 + if (hasMweb) {
260 + mwebScanningActive = bitcoin!.getMwebEnabled(wallet);
261 + settingsStore.mwebEnabled = mwebScanningActive;
262 + reaction((_) => settingsStore.mwebAlwaysScan, (bool alwaysScan) {
263 + if (alwaysScan) {
264 + mwebScanningActive = true;
265 + }
266 + });
267 + }
268 }
269
270 @observable
@@ -348,6 +358,7 @@ abstract class DashboardViewModelBase with Store {
358 bool get hasRescan =>
359 wallet.type == WalletType.bitcoin ||
360 wallet.type == WalletType.monero ||
361 + wallet.type == WalletType.litecoin ||
362 wallet.type == WalletType.wownero ||
363 wallet.type == WalletType.haven;
364
@@ -416,6 +427,33 @@ abstract class DashboardViewModelBase with Store {
427 }
428 }
429
430 + @computed
431 + bool get hasMweb => wallet.type == WalletType.litecoin;
432 +
433 + @computed
434 + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay;
435 +
436 + @observable
437 + bool mwebScanningActive = false;
438 +
439 + @computed
440 + bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore;
441 +
442 + @action
443 + void setMwebScanningActive(bool active) {
444 + if (!hasMweb) {
445 + return;
446 + }
447 +
448 + if (active) {
449 + settingsStore.hasEnabledMwebBefore = true;
450 + }
451 +
452 + settingsStore.mwebEnabled = active;
453 + mwebScanningActive = active;
454 + bitcoin!.setMwebEnabled(wallet, active);
455 + }
456 +
457 BalanceViewModel balanceViewModel;
458
459 AppStore appStore;
lib/view_model/dashboard/receive_option_view_model.dart
+21 -9
@@ -11,19 +11,31 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi
11 abstract class ReceiveOptionViewModelBase with Store {
12 ReceiveOptionViewModelBase(this._wallet, this.initialPageOption)
13 : selectedReceiveOption = initialPageOption ??
14 - (_wallet.type == WalletType.bitcoin
14 + (_wallet.type == WalletType.bitcoin ||
15 + _wallet.type == WalletType.litecoin
16 ? bitcoin!.getSelectedAddressType(_wallet)
17 : ReceivePageOption.mainnet),
18 _options = [] {
19 final walletType = _wallet.type;
19 - _options = walletType == WalletType.haven
20 - ? [ReceivePageOption.mainnet]
21 - : walletType == WalletType.bitcoin
22 - ? [
23 - ...bitcoin!.getBitcoinReceivePageOptions(),
24 - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
25 - ]
26 - : ReceivePageOptions;
20 + switch (walletType) {
21 + case WalletType.bitcoin:
22 + _options = [
23 + ...bitcoin!.getBitcoinReceivePageOptions(),
24 + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
25 + ];
26 + break;
27 + case WalletType.litecoin:
28 + _options = [
29 + ...bitcoin!.getLitecoinReceivePageOptions(),
30 + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
31 + ];
32 + break;
33 + case WalletType.haven:
34 + _options = [ReceivePageOption.mainnet];
35 + break;
36 + default:
37 + _options = ReceivePageOptions;
38 + }
39 }
40
41 final WalletBase _wallet;
lib/view_model/dashboard/transaction_list_item.dart
+9 -1
@@ -56,7 +56,8 @@ class TransactionListItem extends ActionListItem with Keyable {
56 }
57
58 String get formattedPendingStatus {
59 - if (balanceViewModel.wallet.type == WalletType.monero || balanceViewModel.wallet.type == WalletType.haven) {
59 + if (balanceViewModel.wallet.type == WalletType.monero ||
60 + balanceViewModel.wallet.type == WalletType.haven) {
61 if (transaction.confirmations >= 0 && transaction.confirmations < 10) {
62 return ' (${transaction.confirmations}/10)';
63 }
@@ -77,6 +78,13 @@ class TransactionListItem extends ActionListItem with Keyable {
78 return transaction.isPending ? S.current.pending : '';
79 }
80
81 + String get formattedType {
82 + if (transaction.evmSignatureName == 'approval') {
83 + return ' (${transaction.evmSignatureName})';
84 + }
85 + return '';
86 + }
87 +
88 CryptoCurrency? get assetOfTransaction {
89 try {
90 if (balanceViewModel.wallet.type == WalletType.ethereum) {
lib/view_model/rescan_view_model.dart
+2
@@ -30,6 +30,8 @@ abstract class RescanViewModelBase with Store {
30 bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin;
31
32 @computed
33 + bool get isMwebScan => wallet.type == WalletType.litecoin;
34 +
35 Future<bool> get isBitcoinMempoolAPIEnabled async =>
36 wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet);
37
lib/view_model/send/send_view_model.dart
+4 -1
@@ -217,7 +217,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
217 PendingTransaction? pendingTransaction;
218
219 @computed
220 - String get balance => wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance;
220 + String get balance => wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance;
221
222 @computed
223 bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
@@ -675,6 +675,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
675 return S.current.tx_no_dust_exception;
676 }
677 if (error is TransactionCommitFailed) {
678 + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) {
679 + return S.current.tx_commit_failed_no_peers;
680 + }
681 return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}";
682 }
683 if (error is TransactionCommitFailedDustChange) {
lib/view_model/settings/mweb_settings_view_model.dart new
+32
@@ -0,0 +1,32 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 +import 'package:cake_wallet/store/settings_store.dart';
3 +import 'package:cw_core/wallet_base.dart';
4 +import 'package:mobx/mobx.dart';
5 +
6 +part 'mweb_settings_view_model.g.dart';
7 +
8 +class MwebSettingsViewModel = MwebSettingsViewModelBase with _$MwebSettingsViewModel;
9 +
10 +abstract class MwebSettingsViewModelBase with Store {
11 + MwebSettingsViewModelBase(this._settingsStore, this._wallet);
12 +
13 + final SettingsStore _settingsStore;
14 + final WalletBase _wallet;
15 +
16 + @computed
17 + bool get mwebCardDisplay => _settingsStore.mwebCardDisplay;
18 +
19 + @computed
20 + bool get mwebAlwaysScan => _settingsStore.mwebAlwaysScan;
21 +
22 + @action
23 + void setMwebCardDisplay(bool value) {
24 + _settingsStore.mwebCardDisplay = value;
25 + }
26 +
27 + @action
28 + void setMwebAlwaysScan(bool value) {
29 + _settingsStore.mwebAlwaysScan = value;
30 + bitcoin!.setMwebEnabled(_wallet, value);
31 + }
32 +}
lib/view_model/unspent_coins/unspent_coins_list_view_model.dart
+29 -9
@@ -38,6 +38,10 @@ abstract class UnspentCoinsListViewModelBase with Store {
38 final info =
39 getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage);
40
41 + if (info == null) {
42 + return;
43 + }
44 +
45 info.isFrozen = item.isFrozen;
46 info.isSending = item.isSending;
47 info.note = item.note;
@@ -50,15 +54,21 @@ abstract class UnspentCoinsListViewModelBase with Store {
54 }
55 }
56
53 - UnspentCoinsInfo getUnspentCoinInfo(
54 - String hash, String address, int value, int vout, String? keyImage) =>
55 - _unspentCoinsInfo.values.firstWhere((element) =>
57 + UnspentCoinsInfo? getUnspentCoinInfo(
58 + String hash, String address, int value, int vout, String? keyImage) {
59 + try {
60 + return _unspentCoinsInfo.values.firstWhere((element) =>
61 element.walletId == wallet.id &&
62 element.hash == hash &&
63 element.address == address &&
64 element.value == value &&
65 element.vout == vout &&
66 element.keyImage == keyImage);
67 + } catch (e) {
68 + print("UnspentCoinsInfo not found for coin: $e");
69 + return null;
70 + }
71 + }
72
73 String formatAmountToString(int fullBalance) {
74 if (wallet.type == WalletType.monero)
@@ -85,11 +95,18 @@ abstract class UnspentCoinsListViewModelBase with Store {
95 }
96
97 List<Unspent> _getUnspents() {
88 - if (wallet.type == WalletType.monero) return monero!.getUnspents(wallet);
89 - if (wallet.type == WalletType.wownero) return wownero!.getUnspents(wallet);
90 - if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type))
91 - return bitcoin!.getUnspents(wallet);
92 - return List.empty();
98 + switch (wallet.type) {
99 + case WalletType.monero:
100 + return monero!.getUnspents(wallet);
101 + case WalletType.wownero:
102 + return wownero!.getUnspents(wallet);
103 + case WalletType.bitcoin:
104 + case WalletType.litecoin:
105 + case WalletType.bitcoinCash:
106 + return bitcoin!.getUnspents(wallet);
107 + default:
108 + return List.empty();
109 + }
110 }
111
112 @action
@@ -97,10 +114,13 @@ abstract class UnspentCoinsListViewModelBase with Store {
114 _items.clear();
115
116 List<UnspentCoinsItem> unspents = [];
100 - _getUnspents().forEach((elem) {
117 + _getUnspents().forEach((Unspent elem) {
118 try {
119 final info =
120 getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
121 + if (info == null) {
122 + return;
123 + }
124
125 unspents.add(UnspentCoinsItem(
126 address: elem.address,
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+24 -7
@@ -1,3 +1,5 @@
1 +import 'dart:math';
2 +
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
5 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
@@ -217,8 +219,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
219 }) : _baseItems = <ListItem>[],
220 selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
221 _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
220 - hasAccounts =
221 - appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.wownero || appStore.wallet!.type == WalletType.haven,
222 + hasAccounts = appStore.wallet!.type == WalletType.monero ||
223 + appStore.wallet!.type == WalletType.wownero ||
224 + appStore.wallet!.type == WalletType.haven,
225 amount = '',
226 _settingsStore = appStore.settingsStore,
227 super(appStore: appStore) {
@@ -230,7 +233,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
233 _init();
234
235 selectedCurrency = walletTypeToCryptoCurrency(wallet.type);
233 - hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven;
236 + hasAccounts = wallet.type == WalletType.monero ||
237 + wallet.type == WalletType.wownero ||
238 + wallet.type == WalletType.haven;
239 }
240
241 static const String _cryptoNumberPattern = '0.00000000';
@@ -404,7 +409,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
409 });
410 addressList.addAll(receivedAddressItems);
411 } else {
407 - final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
412 + var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
413 final isPrimary = subaddress.id == 0;
414
415 return WalletAddressListItem(
@@ -417,6 +422,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
422 walletTypeToCryptoCurrency(type), subaddress.balance),
423 isChange: subaddress.isChange);
424 });
425 +
426 + // don't show all 1000+ mweb addresses:
427 + if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) {
428 + // find the index of the last item with a txCount > 0
429 + final addressItemsList = addressItems.toList();
430 + final lastItemWithTxCount = addressItemsList.lastWhere((item) => (item.txCount ?? 0) > 0);
431 + final index = addressItemsList.indexOf(lastItemWithTxCount);
432 + // show only up to that index + 20:
433 + addressItems = addressItemsList.sublist(0, index + 20);
434 + }
435 addressList.addAll(addressItems);
436 }
437 }
@@ -446,7 +461,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
461 address: wallet.walletAddresses.address,
462 ));
463 }
449 -
464 +
465 if (wallet.type == WalletType.tron) {
466 final primaryAddress = tron!.getAddress(wallet);
467
@@ -519,7 +534,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
534
535 @action
536 Future<void> setAddressType(dynamic option) async {
522 - if (wallet.type == WalletType.bitcoin) {
537 + if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) {
538 await bitcoin!.setAddressType(wallet, option);
539 }
540 }
@@ -527,7 +542,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
542 void _init() {
543 _baseItems = [];
544
530 - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) {
545 + if (wallet.type == WalletType.monero ||
546 + wallet.type == WalletType.wownero ||
547 + wallet.type == WalletType.haven) {
548 _baseItems.add(WalletAccountListHeader());
549 }
550
macos/Flutter/GeneratedPluginRegistrant.swift
+2
@@ -6,6 +6,7 @@ import FlutterMacOS
6 import Foundation
7
8 import connectivity_plus
9 +import cw_mweb
10 import device_info_plus
11 import devicelocale
12 import flutter_inappwebview_macos
@@ -21,6 +22,7 @@ import wakelock_plus
22
23 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
24 ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
25 + CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin"))
26 DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
27 DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin"))
28 InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
model_generator.sh
+1
@@ -11,4 +11,5 @@ cd cw_tron; flutter pub get; flutter packages pub run build_runner build --delet
11 cd cw_wownero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
12 cd cw_polygon; flutter pub get; cd ..
13 cd cw_ethereum; flutter pub get; cd ..
14 +cd cw_mweb && flutter pub get && cd ..
15 flutter packages pub run build_runner build --delete-conflicting-outputs
pubspec_base.yaml
+3 -5
@@ -97,10 +97,6 @@ dependencies:
97 polyseed: ^0.0.6
98 nostr_tools: ^1.0.9
99 solana: ^0.30.1
100 - bitcoin_base:
101 - git:
102 - url: https://github.com/cake-tech/bitcoin_base
103 - ref: cake-update-v7
100 ledger_flutter: ^1.0.1
101 hashlib: ^1.19.2
102
@@ -138,10 +134,12 @@ dependency_overrides:
134 url: https://github.com/cake-tech/web3dart.git
135 ref: cake
136 flutter_secure_storage_platform_interface: 1.0.2
137 + protobuf: ^3.1.0
138 bitcoin_base:
139 git:
140 url: https://github.com/cake-tech/bitcoin_base
144 - ref: cake-update-v7
141 + ref: cake-update-v8
142 + ffi: 2.1.0
143
144 flutter_icons:
145 image_path: "assets/images/app_logo.png"
res/values/strings_ar.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "يرجى التأكد",
364 "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك",
365 "light_theme": "فاتح",
366 + "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي",
367 + "litecoin_mweb": "mweb",
368 + "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي",
369 + "litecoin_mweb_display_card": "عرض بطاقة mweb",
370 + "litecoin_mweb_scanning": "MWEB المسح الضوئي",
371 + "litecoin_mweb_settings": "إعدادات MWEB",
372 + "litecoin_mweb_warning": "سيقوم استخدام MWEB في البداية بتنزيل ~ 600 ميجابايت من البيانات ، وقد يستغرق ما يصل إلى 30 دقيقة حسب سرعة الشبكة. سيتم تنزيل هذه البيانات الأولية مرة واحدة فقط وستكون متاحة لجميع محافظ Litecoin",
373 + "litecoin_what_is_mweb": "ما هو MWEB؟",
374 "live_fee_rates": "أسعار الرسوم المباشرة عبر API",
375 "load_more": "تحميل المزيد",
376 "loading_your_wallet": "يتم تحميل محفظتك",
@@ -393,6 +401,8 @@
401 "monero_light_theme": " ضوء مونيرو",
402 "moonpay_alert_text": "يجب أن تكون قيمة المبلغ أكبر من أو تساوي ${minAmount} ${fiatCurrency}",
403 "more_options": "المزيد من الخيارات",
404 + "mweb_confirmed": "أكد MWEB",
405 + "mweb_unconfirmed": "غير مؤكد MWEB",
406 "name": "ﻢﺳﺍ",
407 "nano_current_rep": "الممثل الحالي",
408 "nano_gpt_thanks_message": "شكرا لاستخدام nanogpt! تذكر أن تعود إلى المتصفح بعد اكتمال معاملتك!",
@@ -814,6 +824,7 @@
824 "trusted": "موثوق به",
825 "tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.",
826 "tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.",
827 + "tx_commit_failed_no_peers": "فشل المعاملة في البث ، يرجى المحاولة مرة أخرى في ثانية أو نحو ذلك",
828 "tx_invalid_input": "أنت تستخدم نوع الإدخال الخاطئ لهذا النوع من الدفع",
829 "tx_no_dust_exception": "يتم رفض المعاملة عن طريق إرسال مبلغ صغير جدًا. يرجى محاولة زيادة المبلغ.",
830 "tx_not_enough_inputs_exception": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة",
res/values/strings_bg.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга",
364 "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга",
365 "light_theme": "Светло",
366 + "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране",
369 + "litecoin_mweb_display_card": "Показване на MWEB карта",
370 + "litecoin_mweb_scanning": "Сканиране на MWEB",
371 + "litecoin_mweb_settings": "Настройки на MWEB",
372 + "litecoin_mweb_warning": "Използването на MWEB първоначално ще изтегли ~ 600MB данни и може да отнеме до 30 минути в зависимост от скоростта на мрежата. Тези първоначални данни ще изтеглят само веднъж и ще бъдат достъпни за всички портфейли Litecoin",
373 + "litecoin_what_is_mweb": "Какво е MWEB?",
374 "live_fee_rates": "Цени на таксите на живо чрез API",
375 "load_more": "Зареди още",
376 "loading_your_wallet": "Зареждане на портфейл",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Лека тема Monero",
402 "moonpay_alert_text": "Сумата трябва да бъде най-малко ${minAmount} ${fiatCurrency}",
403 "more_options": "Още настройки",
404 + "mweb_confirmed": "Потвърден MWeb",
405 + "mweb_unconfirmed": "Непотвърден mweb",
406 "name": "Име",
407 "nano_current_rep": "Настоящ представител",
408 "nano_gpt_thanks_message": "Благодаря, че използвахте Nanogpt! Не забравяйте да се върнете обратно към браузъра, след като транзакцията ви приключи!",
@@ -814,6 +824,7 @@
824 "trusted": "Надежден",
825 "tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.",
826 "tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.",
827 + "tx_commit_failed_no_peers": "Сделката не успя да излъчи, моля, опитайте отново след секунда или така",
828 "tx_invalid_input": "Използвате грешен тип вход за този тип плащане",
829 "tx_no_dust_exception": "Сделката се отхвърля чрез изпращане на сума твърде малка. Моля, опитайте да увеличите сумата.",
830 "tx_not_enough_inputs_exception": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети",
res/values/strings_cs.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Ujistěte se, že se na své knize otevřete správnou aplikaci",
364 "ledger_please_enable_bluetooth": "Umožněte prosím Bluetooth detekovat vaši knihu",
365 "light_theme": "Světlý",
366 + "litecoin_enable_mweb_sync": "Povolit skenování MWeb",
367 + "litecoin_mweb": "MWeb",
368 + "litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování",
369 + "litecoin_mweb_display_card": "Zobrazit kartu MWeb",
370 + "litecoin_mweb_scanning": "Skenování mWeb",
371 + "litecoin_mweb_settings": "Nastavení mWeb",
372 + "litecoin_mweb_warning": "Pomocí MWeb zpočátku stahuje ~ 600 MB dat a může trvat až 30 minut v závislosti na rychlosti sítě. Tato počáteční data si stáhnou pouze jednou a budou k dispozici pro všechny litecoinové peněženky",
373 + "litecoin_what_is_mweb": "Co je Mweb?",
374 "live_fee_rates": "Živé sazby poplatků prostřednictvím API",
375 "load_more": "Načíst další",
376 "loading_your_wallet": "Načítám peněženku",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Světlé téma Monero",
402 "moonpay_alert_text": "Částka musí být větší nebo rovna ${minAmount} ${fiatCurrency}",
403 "more_options": "Více možností",
404 + "mweb_confirmed": "Potvrzený mweb",
405 + "mweb_unconfirmed": "Nepotvrzené mWeb",
406 "name": "název",
407 "nano_current_rep": "Současný zástupce",
408 "nano_gpt_thanks_message": "Děkujeme za používání Nanogpt! Nezapomeňte se po dokončení transakce vydat zpět do prohlížeče!",
@@ -814,6 +824,7 @@
824 "trusted": "Důvěřovat",
825 "tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.",
826 "tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.",
827 + "tx_commit_failed_no_peers": "Transakce se nepodařilo vysílat, zkuste to prosím znovu za vteřinu",
828 "tx_invalid_input": "Pro tento typ platby používáte nesprávný typ vstupu",
829 "tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.",
830 "tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí",
res/values/strings_de.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben",
364 "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.",
365 "light_theme": "Hell",
366 + "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB -Scannen",
367 + "litecoin_mweb": "MWeb",
368 + "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen",
369 + "litecoin_mweb_display_card": "MWEB -Karte anzeigen",
370 + "litecoin_mweb_scanning": "MWEB Scanning",
371 + "litecoin_mweb_settings": "MWEB -Einstellungen",
372 + "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin -Brieftaschen verfügbar",
373 + "litecoin_what_is_mweb": "Was ist MWeb?",
374 "live_fee_rates": "Live -Gebührenpreise über API",
375 "load_more": "Mehr laden",
376 "loading_your_wallet": "Wallet wird geladen",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero Light-Thema",
402 "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein",
403 "more_options": "Weitere Optionen",
404 + "mweb_confirmed": "Bestätigt MWeb",
405 + "mweb_unconfirmed": "Unbestätigter MWeb",
406 "name": "Name",
407 "nano_current_rep": "Aktueller Vertreter",
408 "nano_gpt_thanks_message": "Danke, dass du Nanogpt benutzt hast! Denken Sie daran, nach Abschluss Ihrer Transaktion zurück zum Browser zu gehen!",
@@ -815,6 +825,7 @@
825 "trusted": "Vertrauenswürdige",
826 "tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.",
827 "tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.",
828 + "tx_commit_failed_no_peers": "Transaktion konnte nicht übertragen werden. Bitte versuchen Sie es in einer Sekunde oder so erneut",
829 "tx_invalid_input": "Sie verwenden den falschen Eingangstyp für diese Art von Zahlung",
830 "tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.",
831 "tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus",
res/values/strings_en.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Please make sure you opend the right app on your ledger",
364 "ledger_please_enable_bluetooth": "Please enable Bluetooth to detect your Ledger",
365 "light_theme": "Light",
366 + "litecoin_enable_mweb_sync": "Enable MWEB scanning",
367 + "litecoin_mweb": "MWEB",
368 + "litecoin_mweb_always_scan": "Set MWEB always scanning",
369 + "litecoin_mweb_display_card": "Show MWEB card",
370 + "litecoin_mweb_scanning": "MWEB Scanning",
371 + "litecoin_mweb_settings": "MWEB settings",
372 + "litecoin_mweb_warning": "Using MWEB will initially download ~600MB of data, and may take up to 30 minutes depending on network speed. This initial data will only download once and be available for all Litecoin wallets",
373 + "litecoin_what_is_mweb": "What is MWEB?",
374 "live_fee_rates": "Live fee rates via API",
375 "load_more": "Load more",
376 "loading_your_wallet": "Loading your wallet",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero Light Theme",
402 "moonpay_alert_text": "Value of the amount must be more or equal to ${minAmount} ${fiatCurrency}",
403 "more_options": "More Options",
404 + "mweb_confirmed": "Confirmed MWEB",
405 + "mweb_unconfirmed": "Unconfirmed MWEB",
406 "name": "Name",
407 "nano_current_rep": "Current Representative",
408 "nano_gpt_thanks_message": "Thanks for using NanoGPT! Remember to head back to the browser after your transaction completes!",
@@ -814,6 +824,7 @@
824 "trusted": "Trusted",
825 "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.",
826 "tx_commit_failed": "Transaction commit failed. Please contact support.",
827 + "tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second or so",
828 "tx_invalid_input": "You are using the wrong input type for this type of payment",
829 "tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.",
830 "tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control",
res/values/strings_es.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.",
364 "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor",
365 "light_theme": "Ligera",
366 + "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo",
369 + "litecoin_mweb_display_card": "Mostrar tarjeta MWEB",
370 + "litecoin_mweb_scanning": "Escaneo mweb",
371 + "litecoin_mweb_settings": "Configuración de MWEB",
372 + "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin",
373 + "litecoin_what_is_mweb": "¿Qué es mweb?",
374 "live_fee_rates": "Tasas de tarifas en vivo a través de API",
375 "load_more": "Carga más",
376 "loading_your_wallet": "Cargando tu billetera",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Tema ligero de Monero",
402 "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}",
403 "more_options": "Más Opciones",
404 + "mweb_confirmed": "Confirmado mweb",
405 + "mweb_unconfirmed": "Mweb no confirmado",
406 "name": "Nombre",
407 "nano_current_rep": "Representante actual",
408 "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!",
@@ -815,6 +825,7 @@
825 "trusted": "de confianza",
826 "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.",
827 "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.",
828 + "tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos",
829 "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago",
830 "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.",
831 "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas",
res/values/strings_fr.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre grand livre",
364 "ledger_please_enable_bluetooth": "Veuillez activer Bluetooth pour détecter votre grand livre",
365 "light_theme": "Clair",
366 + "litecoin_enable_mweb_sync": "Activer la numérisation MWEB",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Définir MWEB Score Scanning",
369 + "litecoin_mweb_display_card": "Afficher la carte MWeb",
370 + "litecoin_mweb_scanning": "Scann mweb",
371 + "litecoin_mweb_settings": "Paramètres MWEB",
372 + "litecoin_mweb_warning": "L'utilisation de MWEB téléchargera initialement ~ 600 Mo de données et peut prendre jusqu'à 30 minutes en fonction de la vitesse du réseau. Ces données initiales ne téléchargeront qu'une seule fois et seront disponibles pour tous les portefeuilles litecoin",
373 + "litecoin_what_is_mweb": "Qu'est-ce que MWEB?",
374 "live_fee_rates": "Taux de frais en direct via l'API",
375 "load_more": "Charger plus",
376 "loading_your_wallet": "Chargement de votre portefeuille (wallet)",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Thème de lumière Monero",
402 "moonpay_alert_text": "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}",
403 "more_options": "Plus d'options",
404 + "mweb_confirmed": "Confirmé MWEB",
405 + "mweb_unconfirmed": "Mweb non confirmé",
406 "name": "Nom",
407 "nano_current_rep": "Représentant actuel",
408 "nano_gpt_thanks_message": "Merci d'avoir utilisé Nanogpt! N'oubliez pas de retourner au navigateur une fois votre transaction terminée!",
@@ -814,6 +824,7 @@
824 "trusted": "de confiance",
825 "tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.",
826 "tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.",
827 + "tx_commit_failed_no_peers": "La transaction n'a pas été diffusée, veuillez réessayer dans une seconde environ",
828 "tx_invalid_input": "Vous utilisez le mauvais type d'entrée pour ce type de paiement",
829 "tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.",
830 "tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control",
res/values/strings_ha.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Da fatan za a tabbata kun yi amfani da app ɗin dama akan dillalarku",
364 "ledger_please_enable_bluetooth": "Da fatan za a kunna Bluetooth don gano Ledger ɗinku",
365 "light_theme": "Haske",
366 + "litecoin_enable_mweb_sync": "Kunna binciken Mweb",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Saita Mweb koyaushe",
369 + "litecoin_mweb_display_card": "Nuna katin Mweb",
370 + "litecoin_mweb_scanning": "Mweb scanning",
371 + "litecoin_mweb_settings": "Saitunan Mweb",
372 + "litecoin_mweb_warning": "Amfani da Mweb zai fara saukewa ~ 600MB na bayanai, kuma yana iya ɗaukar minti 30 dangane da saurin cibiyar sadarwa. Wannan bayanan farko zai saika saukarwa sau ɗaya kawai kuma a samu don duk wuraren shakatawa",
373 + "litecoin_what_is_mweb": "Menene Mweb?",
374 "live_fee_rates": "Kudin Kiɗa ta API",
375 "load_more": "Like more",
376 "loading_your_wallet": "Ana loda walat ɗin ku",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Jigon Hasken Monero",
402 "moonpay_alert_text": "Darajar adadin dole ne ya zama fiye ko daidai da ${minAmount} ${fiatCurrency}",
403 "more_options": "Ƙarin Zaɓuɓɓuka",
404 + "mweb_confirmed": "Tabbatar da Mweb",
405 + "mweb_unconfirmed": "Myconfired",
406 "name": "Suna",
407 "nano_current_rep": "Wakilin Yanzu",
408 "nano_gpt_thanks_message": "Na gode da amfani da Nanogpt! Ka tuna da komawa zuwa mai bincike bayan ma'amalar ka ta cika!",
@@ -816,6 +826,7 @@
826 "trusted": "Amintacce",
827 "tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.",
828 "tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.",
829 + "tx_commit_failed_no_peers": "Kasuwanci ya kasa watsa, don Allah sake gwadawa a cikin na biyu ko",
830 "tx_invalid_input": "Kuna amfani da nau'in shigar da ba daidai ba don wannan nau'in biyan kuɗi",
831 "tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.",
832 "tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin",
res/values/strings_hi.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं",
364 "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें",
365 "light_theme": "रोशनी",
366 + "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें",
367 + "litecoin_mweb": "मावली",
368 + "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें",
369 + "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं",
370 + "litecoin_mweb_scanning": "MWEB स्कैनिंग",
371 + "litecoin_mweb_settings": "MWEB सेटिंग्स",
372 + "litecoin_mweb_warning": "MWEB का उपयोग शुरू में ~ 600MB डेटा डाउनलोड करेगा, और नेटवर्क की गति के आधार पर 30 मिनट तक का समय लग सकता है। यह प्रारंभिक डेटा केवल एक बार डाउनलोड करेगा और सभी लिटकोइन वॉलेट के लिए उपलब्ध होगा",
373 + "litecoin_what_is_mweb": "MWEB क्या है?",
374 "live_fee_rates": "एपीआई के माध्यम से लाइव शुल्क दरें",
375 "load_more": "और लोड करें",
376 "loading_your_wallet": "अपना बटुआ लोड कर रहा है",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "मोनेरो लाइट थीम",
402 "moonpay_alert_text": "राशि का मूल्य अधिक है या करने के लिए बराबर होना चाहिए ${minAmount} ${fiatCurrency}",
403 "more_options": "और विकल्प",
404 + "mweb_confirmed": "MWEB की पुष्टि की",
405 + "mweb_unconfirmed": "अपुष्ट MWEB",
406 "name": "नाम",
407 "nano_current_rep": "वर्तमान प्रतिनिधि",
408 "nano_gpt_thanks_message": "Nanogpt का उपयोग करने के लिए धन्यवाद! अपने लेन -देन के पूरा होने के बाद ब्राउज़र पर वापस जाना याद रखें!",
@@ -816,6 +826,7 @@
826 "trusted": "भरोसा",
827 "tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।",
828 "tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।",
829 + "tx_commit_failed_no_peers": "लेन -देन प्रसारित करने में विफल रहा, कृपया एक या दो सेकंड में पुनः प्रयास करें",
830 "tx_invalid_input": "आप इस प्रकार के भुगतान के लिए गलत इनपुट प्रकार का उपयोग कर रहे हैं",
831 "tx_no_dust_exception": "लेनदेन को बहुत छोटी राशि भेजकर अस्वीकार कर दिया जाता है। कृपया राशि बढ़ाने का प्रयास करें।",
832 "tx_not_enough_inputs_exception": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें",
res/values/strings_hr.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi",
364 "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu",
365 "light_theme": "Svijetla",
366 + "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje",
367 + "litecoin_mweb": "MWeb",
368 + "litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje",
369 + "litecoin_mweb_display_card": "Prikaži MWeb karticu",
370 + "litecoin_mweb_scanning": "MWEB skeniranje",
371 + "litecoin_mweb_settings": "Postavke MWEB -a",
372 + "litecoin_mweb_warning": "Korištenje MWEB -a u početku će preuzeti ~ 600MB podataka, a može potrajati do 30 minuta, ovisno o brzini mreže. Ovi početni podaci preuzet će samo jednom i biti dostupni za sve Litecoin novčanike",
373 + "litecoin_what_is_mweb": "Što je MWEB?",
374 "live_fee_rates": "Stope naknada uživo putem API -ja",
375 "load_more": "Učitaj više",
376 "loading_your_wallet": "Novčanik se učitava",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero lagana tema",
402 "moonpay_alert_text": "Vrijednost iznosa mora biti veća ili jednaka ${minAmount} ${fiatCurrency}",
403 "more_options": "Više opcija",
404 + "mweb_confirmed": "Potvrđen MWeb",
405 + "mweb_unconfirmed": "Nepotvrđeni mWeb",
406 "name": "Ime",
407 "nano_current_rep": "Trenutni predstavnik",
408 "nano_gpt_thanks_message": "Hvala što ste koristili nanogpt! Ne zaboravite da se vratite u preglednik nakon što vam se transakcija završi!",
@@ -814,6 +824,7 @@
824 "trusted": "vjerovao",
825 "tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.",
826 "tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.",
827 + "tx_commit_failed_no_peers": "Transakcija nije uspjela emitirati, pokušajte ponovo u sekundi ili tako",
828 "tx_invalid_input": "Koristite pogrešnu vrstu ulaza za ovu vrstu plaćanja",
829 "tx_no_dust_exception": "Transakcija se odbija slanjem iznosa premalo. Pokušajte povećati iznos.",
830 "tx_not_enough_inputs_exception": "Nema dovoljno unosa. Molimo odaberite više pod kontrolom novčića",
res/values/strings_hy.arb
+3
@@ -393,6 +393,8 @@
393 "monero_light_theme": "Monero պայծառ տեսք",
394 "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}",
395 "more_options": "Այլ տարբերակներ",
396 + "mweb_confirmed": "Հաստատված MWEB",
397 + "mweb_unconfirmed": "Չկարգավորված Mweb",
398 "name": "Անուն",
399 "nano_current_rep": "Ընթացիկ ներկայացուցիչ",
400 "nano_gpt_thanks_message": "Շնորհակալություն NanoGPT-ն օգտագործելու համար: Հիշեք վերադառնալ դիտարկիչ ձեր փոխանցումն ավարտելուց հետո",
@@ -814,6 +816,7 @@
816 "trusted": "Վստահելի",
817 "tx_commit_exception_no_dust_on_change": "Փոխանցումը մերժվել է այս գումարով: Այս արժույթներով կարող եք ուղարկել ${min} առանց փոփոխության կամ ${max} որը վերադարձնում է փոփոխությունը",
818 "tx_commit_failed": "Փոխանցումը ձախողվել է: Խնդրում ենք դիմել աջակցությանը",
819 + "tx_commit_failed_no_peers": "Գործարքը չի հաջողվել հեռարձակել, խնդրում ենք կրկին փորձել մեկ վայրկյանում",
820 "tx_invalid_input": "Դուք օգտագործում եք սխալ մուտքային տիպ այս տեսակի վճարման համար",
821 "tx_no_dust_exception": "Փոխանցումը մերժվել է շատ փոքր գումարով: Խնդրում ենք փորձել ավելացնել գումարը",
822 "tx_not_enough_inputs_exception": "Չկան բավարար մուտքեր: Խնդրում ենք ընտրել ավելին Coin Control֊ում",
res/values/strings_id.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Pastikan Anda membuka aplikasi yang tepat di buku besar Anda",
364 "ledger_please_enable_bluetooth": "Harap aktifkan Bluetooth untuk mendeteksi buku besar Anda",
365 "light_theme": "Terang",
366 + "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Atur mWeb selalu memindai",
369 + "litecoin_mweb_display_card": "Tunjukkan kartu mWeb",
370 + "litecoin_mweb_scanning": "Pemindaian MWEB",
371 + "litecoin_mweb_settings": "Pengaturan MWEB",
372 + "litecoin_mweb_warning": "Menggunakan MWEB pada awalnya akan mengunduh ~ 600MB data, dan dapat memakan waktu hingga 30 menit tergantung pada kecepatan jaringan. Data awal ini hanya akan mengunduh sekali dan tersedia untuk semua dompet litecoin",
373 + "litecoin_what_is_mweb": "Apa itu MWEB?",
374 "live_fee_rates": "Tarif biaya langsung melalui API",
375 "load_more": "Muat lebih banyak",
376 "loading_your_wallet": "Memuat dompet Anda",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Tema Cahaya Monero",
402 "moonpay_alert_text": "Nilai jumlah harus lebih atau sama dengan ${minAmount} ${fiatCurrency}",
403 "more_options": "Opsi Lainnya",
404 + "mweb_confirmed": "Mengkonfirmasi mWeb",
405 + "mweb_unconfirmed": "MWEB yang belum dikonfirmasi",
406 "name": "Nama",
407 "nano_current_rep": "Perwakilan saat ini",
408 "nano_gpt_thanks_message": "Terima kasih telah menggunakan Nanogpt! Ingatlah untuk kembali ke browser setelah transaksi Anda selesai!",
@@ -817,6 +827,7 @@
827 "trusted": "Dipercayai",
828 "tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.",
829 "tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.",
830 + "tx_commit_failed_no_peers": "Transaksi gagal untuk disiarkan, silakan coba lagi sebentar lagi",
831 "tx_invalid_input": "Anda menggunakan jenis input yang salah untuk jenis pembayaran ini",
832 "tx_no_dust_exception": "Transaksi ditolak dengan mengirimkan jumlah yang terlalu kecil. Silakan coba tingkatkan jumlahnya.",
833 "tx_not_enough_inputs_exception": "Tidak cukup input yang tersedia. Pilih lebih banyak lagi di bawah Kontrol Koin",
res/values/strings_it.arb
+11
@@ -364,6 +364,14 @@
364 "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro",
365 "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro",
366 "light_theme": "Bianco",
367 + "litecoin_enable_mweb_sync": "Abilita la scansione MWeb",
368 + "litecoin_mweb": "MWeb",
369 + "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre",
370 + "litecoin_mweb_display_card": "Mostra la scheda MWeb",
371 + "litecoin_mweb_scanning": "Scansione MWeb",
372 + "litecoin_mweb_settings": "Impostazioni MWeb",
373 + "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~ 600 MB di dati e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali scaricheranno solo una volta e saranno disponibili per tutti i portafogli Litecoin",
374 + "litecoin_what_is_mweb": "Cos'è MWeb?",
375 "live_fee_rates": "Tariffe delle commissioni dal vivo tramite API",
376 "load_more": "Carica di più",
377 "loading_your_wallet": "Caricamento portafoglio",
@@ -394,6 +402,8 @@
402 "monero_light_theme": "Tema leggero Monero",
403 "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}",
404 "more_options": "Altre opzioni",
405 + "mweb_confirmed": "MWeb confermato",
406 + "mweb_unconfirmed": "MWeb non confermato",
407 "name": "Nome",
408 "nano_current_rep": "Rappresentante attuale",
409 "nano_gpt_thanks_message": "Grazie per aver usato il nanogpt! Ricorda di tornare al browser dopo il completamento della transazione!",
@@ -816,6 +826,7 @@
826 "trusted": "di fiducia",
827 "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.",
828 "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.",
829 + "tx_commit_failed_no_peers": "La transazione non è riuscita a trasmettere, riprovare in un secondo o giù di lì",
830 "tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento",
831 "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.",
832 "tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete",
res/values/strings_ja.arb
+11
@@ -364,6 +364,14 @@
364 "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください",
365 "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください",
366 "light_theme": "光",
367 + "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします",
368 + "litecoin_mweb": "mweb",
369 + "litecoin_mweb_always_scan": "MWEBを常にスキャンします",
370 + "litecoin_mweb_display_card": "MWEBカードを表示します",
371 + "litecoin_mweb_scanning": "MWEBスキャン",
372 + "litecoin_mweb_settings": "MWEB設定",
373 + "litecoin_mweb_warning": "MWEBを使用すると、最初は〜600MBのデータをダウンロードし、ネットワーク速度に応じて最大30分かかる場合があります。この最初のデータは一度だけダウンロードされ、すべてのLitecoinウォレットで利用可能になります",
374 + "litecoin_what_is_mweb": "MWEBとは何ですか?",
375 "live_fee_rates": "API経由のライブ料金",
376 "load_more": "もっと読み込む",
377 "loading_your_wallet": "ウォレットをロードしています",
@@ -394,6 +402,8 @@
402 "monero_light_theme": "モネロ ライト テーマ",
403 "moonpay_alert_text": "金額の値は以上でなければなりません ${minAmount} ${fiatCurrency}",
404 "more_options": "その他のオプション",
405 + "mweb_confirmed": "確認されたMWEB",
406 + "mweb_unconfirmed": "未確認のMWEB",
407 "name": "名前",
408 "nano_current_rep": "現在の代表",
409 "nano_gpt_thanks_message": "NanoGptを使用してくれてありがとう!トランザクションが完了したら、ブラウザに戻ることを忘れないでください!",
@@ -815,6 +825,7 @@
825 "trusted": "信頼できる",
826 "tx_commit_exception_no_dust_on_change": "この金額ではトランザクションは拒否されます。 これらのコインを使用すると、おつりなしの ${min} またはおつりを返す ${max} を送信できます。",
827 "tx_commit_failed": "トランザクションコミットは失敗しました。サポートに連絡してください。",
828 + "tx_commit_failed_no_peers": "トランザクションはブロードキャストに失敗しました。一瞬かそこらで再試行してください",
829 "tx_invalid_input": "このタイプの支払いに間違った入力タイプを使用しています",
830 "tx_no_dust_exception": "トランザクションは、小さすぎる金額を送信することにより拒否されます。量を増やしてみてください。",
831 "tx_not_enough_inputs_exception": "利用可能な入力が十分ではありません。コイン制御下でもっと選択してください",
res/values/strings_ko.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.",
364 "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오",
365 "light_theme": "빛",
366 + "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다",
367 + "litecoin_mweb": "mweb",
368 + "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다",
369 + "litecoin_mweb_display_card": "mweb 카드를 보여주십시오",
370 + "litecoin_mweb_scanning": "mweb 스캔",
371 + "litecoin_mweb_settings": "mweb 설정",
372 + "litecoin_mweb_warning": "MWEB를 사용하면 처음에는 ~ 600MB의 데이터를 다운로드하며 네트워크 속도에 따라 최대 30 분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드하여 모든 조명 지갑에 사용할 수 있습니다.",
373 + "litecoin_what_is_mweb": "MWEB 란 무엇입니까?",
374 "live_fee_rates": "API를 통한 라이브 요금 요금",
375 "load_more": "더로드하십시오",
376 "loading_your_wallet": "지갑 넣기",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "모네로 라이트 테마",
402 "moonpay_alert_text": "금액은 다음보다 크거나 같아야합니다 ${minAmount} ${fiatCurrency}",
403 "more_options": "추가 옵션",
404 + "mweb_confirmed": "확인 mweb",
405 + "mweb_unconfirmed": "확인되지 않은 mweb",
406 "name": "이름",
407 "nano_current_rep": "현재 대표",
408 "nano_gpt_thanks_message": "Nanogpt를 사용해 주셔서 감사합니다! 거래가 완료된 후 브라우저로 돌아가는 것을 잊지 마십시오!",
@@ -815,6 +825,7 @@
825 "trusted": "신뢰할 수 있는",
826 "tx_commit_exception_no_dust_on_change": "이 금액으로 거래가 거부되었습니다. 이 코인을 사용하면 거스름돈 없이 ${min}를 보내거나 거스름돈을 반환하는 ${max}를 보낼 수 있습니다.",
827 "tx_commit_failed": "거래 커밋이 실패했습니다. 지원에 연락하십시오.",
828 + "tx_commit_failed_no_peers": "트랜잭션이 방송에 실패했는데 1 초 정도 후에 다시 시도하십시오.",
829 "tx_invalid_input": "이 유형의 지불에 잘못 입력 유형을 사용하고 있습니다.",
830 "tx_no_dust_exception": "너무 작은 금액을 보내면 거래가 거부됩니다. 금액을 늘리십시오.",
831 "tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오",
res/values/strings_my.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ",
364 "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ",
365 "light_theme": "အလင်း",
366 + "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ",
367 + "litecoin_mweb": "မင်္ဂလာပါ",
368 + "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ",
369 + "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ",
370 + "litecoin_mweb_scanning": "mweb scanning",
371 + "litecoin_mweb_settings": "Mweb ဆက်တင်များ",
372 + "litecoin_mweb_warning": "MweB ကိုအသုံးပြုခြင်းသည်အစပိုင်းတွင် ~ 600MB ဒေတာများကို download လုပ်ပြီးကွန်ယက်အမြန်နှုန်းပေါ် မူတည်. မိနစ် 30 အထိကြာနိုင်သည်။ ဤကန ဦး ဒေတာကိုတစ်ကြိမ်သာ download လုပ်ပြီး litecoin Walkets အားလုံးအတွက်ရနိုင်သည်",
373 + "litecoin_what_is_mweb": "MweB ဆိုတာဘာလဲ။",
374 "live_fee_rates": "API မှတစ်ဆင့် Live အခကြေးငွေနှုန်းထားများ",
375 "load_more": "ပိုပြီး load",
376 "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero Light အပြင်အဆင်",
402 "moonpay_alert_text": "ပမာဏ၏တန်ဖိုးသည် ${minAmount} ${fiatCurrency} နှင့် ပိုနေရမည်",
403 "more_options": "နောက်ထပ် ရွေးချယ်စရာများ",
404 + "mweb_confirmed": "အတည်ပြုလိုက် mweb",
405 + "mweb_unconfirmed": "အတည်မပြုနိုင်သော mweb",
406 "name": "နာမည်",
407 "nano_current_rep": "လက်ရှိကိုယ်စားလှယ်",
408 "nano_gpt_thanks_message": "nanogpt ကိုသုံးပြီးကျေးဇူးတင်ပါတယ် သင်၏ငွေပေးငွေယူပြီးနောက် browser သို့ပြန်သွားရန်သတိရပါ။",
@@ -814,6 +824,7 @@
824 "trusted": "ယုံတယ်။",
825 "tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။",
826 "tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။",
827 + "tx_commit_failed_no_peers": "ငွေပေးငွေယူထုတ်လွှင့်ရန်ပျက်ကွက်ပါက ကျေးဇူးပြု. ဒုတိယသို့မဟုတ်ထိုအတိုင်းထပ်မံကြိုးစားပါ",
828 "tx_invalid_input": "သင်သည်ဤငွေပေးချေမှုအမျိုးအစားအတွက်မှားယွင်းသော input type ကိုအသုံးပြုနေသည်",
829 "tx_no_dust_exception": "ငွေပမာဏကိုသေးငယ်လွန်းသောငွေပမာဏကိုပေးပို့ခြင်းဖြင့်ပယ်ဖျက်ခြင်းကိုငြင်းပယ်သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ကြိုးစားပါ။",
830 "tx_not_enough_inputs_exception": "အလုံအလောက်သွင်းအားစုများမလုံလောက်။ ကျေးဇူးပြုပြီးဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ပိုမိုရွေးချယ်ပါ",
res/values/strings_nl.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Zorg ervoor dat u de juiste app op uw grootboek opent",
364 "ledger_please_enable_bluetooth": "Schakel Bluetooth in staat om uw grootboek te detecteren",
365 "light_theme": "Licht",
366 + "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Stel mweb altijd op scannen",
369 + "litecoin_mweb_display_card": "Toon MWEB -kaart",
370 + "litecoin_mweb_scanning": "MWEB -scanning",
371 + "litecoin_mweb_settings": "MWEB -instellingen",
372 + "litecoin_mweb_warning": "Het gebruik van MWeb downloadt in eerste instantie ~ 600 MB aan gegevens en kan tot 30 minuten duren, afhankelijk van de netwerksnelheid. Deze eerste gegevens worden slechts eenmaal gedownload en zijn beschikbaar voor alle Litecoin -portefeuilles",
373 + "litecoin_what_is_mweb": "Wat is Mweb?",
374 "live_fee_rates": "Live -tarieven via API",
375 "load_more": "Meer laden",
376 "loading_your_wallet": "Uw portemonnee laden",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero Light-thema",
402 "moonpay_alert_text": "Waarde van het bedrag moet meer of gelijk zijn aan ${minAmount} ${fiatCurrency}",
403 "more_options": "Meer opties",
404 + "mweb_confirmed": "Bevestigde MWEB",
405 + "mweb_unconfirmed": "Onbevestigde MWEB",
406 "name": "Naam",
407 "nano_current_rep": "Huidige vertegenwoordiger",
408 "nano_gpt_thanks_message": "Bedankt voor het gebruik van Nanogpt! Vergeet niet om terug te gaan naar de browser nadat uw transactie is voltooid!",
@@ -814,6 +824,7 @@
824 "trusted": "vertrouwd",
825 "tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.",
826 "tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.",
827 + "tx_commit_failed_no_peers": "De transactie is niet uitgezonden, probeer het opnieuw binnen een seconde of zo",
828 "tx_invalid_input": "U gebruikt het verkeerde invoertype voor dit type betaling",
829 "tx_no_dust_exception": "De transactie wordt afgewezen door een te klein bedrag te verzenden. Probeer het bedrag te verhogen.",
830 "tx_not_enough_inputs_exception": "Niet genoeg ingangen beschikbaar. Selecteer meer onder muntenbesturing",
res/values/strings_pl.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze",
364 "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi",
365 "light_theme": "Jasny",
366 + "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB",
367 + "litecoin_mweb": "MWEB",
368 + "litecoin_mweb_always_scan": "Ustaw MWEB zawsze skanowanie",
369 + "litecoin_mweb_display_card": "Pokaż kartę MWEB",
370 + "litecoin_mweb_scanning": "Skanowanie MWEB",
371 + "litecoin_mweb_settings": "Ustawienia MWEB",
372 + "litecoin_mweb_warning": "Korzystanie z MWEB początkowo pobiera ~ 600 MB danych i może potrwać do 30 minut w zależności od prędkości sieci. Te początkowe dane pobierają tylko raz i będą dostępne dla wszystkich portfeli Litecoin",
373 + "litecoin_what_is_mweb": "Co to jest MWEB?",
374 "live_fee_rates": "Stawki opłaty na żywo za pośrednictwem API",
375 "load_more": "Załaduj więcej",
376 "loading_your_wallet": "Ładowanie portfela",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Lekki motyw Monero",
402 "moonpay_alert_text": "Wartość kwoty musi być większa lub równa ${minAmount} ${fiatCurrency}",
403 "more_options": "Więcej opcji",
404 + "mweb_confirmed": "Potwierdził MWEB",
405 + "mweb_unconfirmed": "Niepotwierdzone MWEB",
406 "name": "Nazwa",
407 "nano_current_rep": "Obecny przedstawiciel",
408 "nano_gpt_thanks_message": "Dzięki za użycie Nanogpt! Pamiętaj, aby wrócić do przeglądarki po zakończeniu transakcji!",
@@ -814,6 +824,7 @@
824 "trusted": "Zaufany",
825 "tx_commit_exception_no_dust_on_change": "Transakcja jest odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez zmiany lub ${max}, które zwraca zmianę.",
826 "tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.",
827 + "tx_commit_failed_no_peers": "Transakcja nie była transmitowana, spróbuj ponownie za około sekundę",
828 "tx_invalid_input": "Używasz niewłaściwego typu wejściowego dla tego rodzaju płatności",
829 "tx_no_dust_exception": "Transakcja jest odrzucana przez wysyłanie zbyt małej ilości. Spróbuj zwiększyć kwotę.",
830 "tx_not_enough_inputs_exception": "Za mało dostępnych danych wejściowych. Wybierz więcej pod kontrolą monet",
res/values/strings_pt.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Por favor, certifique -se de optar pelo aplicativo certo no seu livro",
364 "ledger_please_enable_bluetooth": "Ative o Bluetooth para detectar seu livro",
365 "light_theme": "Luz",
366 + "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando",
369 + "litecoin_mweb_display_card": "Mostre o cartão MWEB",
370 + "litecoin_mweb_scanning": "MWEB Scanning",
371 + "litecoin_mweb_settings": "Configurações do MWEB",
372 + "litecoin_mweb_warning": "O uso do MWEB baixará inicialmente ~ 600 MB de dados e pode levar até 30 minutos, dependendo da velocidade da rede. Esses dados iniciais serão baixados apenas uma vez e estarão disponíveis para todas as carteiras Litecoin",
373 + "litecoin_what_is_mweb": "O que é MWeb?",
374 "live_fee_rates": "Taxas de taxas ao vivo via API",
375 "load_more": "Carregue mais",
376 "loading_your_wallet": "Abrindo sua carteira",
@@ -394,6 +402,8 @@
402 "monero_light_theme": "Monero Light Theme",
403 "moonpay_alert_text": "O valor do montante deve ser maior ou igual a ${minAmount} ${fiatCurrency}",
404 "more_options": "Mais opções",
405 + "mweb_confirmed": "MWEB confirmado",
406 + "mweb_unconfirmed": "MWEB não confirmado",
407 "name": "Nome",
408 "nano_current_rep": "Representante atual",
409 "nano_gpt_thanks_message": "Obrigado por usar o Nanogpt! Lembre -se de voltar para o navegador após a conclusão da transação!",
@@ -816,6 +826,7 @@
826 "trusted": "confiável",
827 "tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.",
828 "tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.",
829 + "tx_commit_failed_no_peers": "A transação não foi transmitida, tente novamente em um segundo",
830 "tx_invalid_input": "Você está usando o tipo de entrada errado para este tipo de pagamento",
831 "tx_no_dust_exception": "A transação é rejeitada enviando uma quantia pequena demais. Por favor, tente aumentar o valor.",
832 "tx_not_enough_inputs_exception": "Não há entradas disponíveis. Selecione mais sob controle de moedas",
res/values/strings_ru.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге",
364 "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу",
365 "light_theme": "Светлая",
366 + "litecoin_enable_mweb_sync": "Включить MWEB сканирование",
367 + "litecoin_mweb": "Мвеб",
368 + "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование",
369 + "litecoin_mweb_display_card": "Показать карту MWEB",
370 + "litecoin_mweb_scanning": "MWEB сканирование",
371 + "litecoin_mweb_settings": "Настройки MWEB",
372 + "litecoin_mweb_warning": "Использование MWEB изначально загрузит ~ 600 МБ данных и может занять до 30 минут в зависимости от скорости сети. Эти начальные данные будут загружаться только один раз и будут доступны для всех кошельков Litecoin",
373 + "litecoin_what_is_mweb": "Что такое MWEB?",
374 "live_fee_rates": "Ставки по сбору вживую через API",
375 "load_more": "Загрузи больше",
376 "loading_your_wallet": "Загрузка кошелька",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Светлая тема Monero",
402 "moonpay_alert_text": "Сумма должна быть больше или равна ${minAmount} ${fiatCurrency}",
403 "more_options": "Дополнительные параметры",
404 + "mweb_confirmed": "Подтверждено MWEB",
405 + "mweb_unconfirmed": "Неподтвержденная MWEB",
406 "name": "Имя",
407 "nano_current_rep": "Нынешний представитель",
408 "nano_gpt_thanks_message": "Спасибо за использование Nanogpt! Не забудьте вернуться в браузер после завершения транзакции!",
@@ -815,6 +825,7 @@
825 "trusted": "доверенный",
826 "tx_commit_exception_no_dust_on_change": "Транзакция отклоняется с этой суммой. С этими монетами вы можете отправлять ${min} без изменения или ${max}, которые возвращают изменение.",
827 "tx_commit_failed": "Комплект транзакции не удался. Пожалуйста, свяжитесь с поддержкой.",
828 + "tx_commit_failed_no_peers": "Транзакция не смогла передать, попробуйте еще раз через секунду или около того",
829 "tx_invalid_input": "Вы используете неправильный тип ввода для этого типа оплаты",
830 "tx_no_dust_exception": "Транзакция отклоняется путем отправки слишком маленькой суммы. Пожалуйста, попробуйте увеличить сумму.",
831 "tx_not_enough_inputs_exception": "Недостаточно входов доступны. Пожалуйста, выберите больше под контролем монет",
res/values/strings_th.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ",
364 "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ",
365 "light_theme": "สว่าง",
366 + "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB",
367 + "litecoin_mweb": "mweb",
368 + "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ",
369 + "litecoin_mweb_display_card": "แสดงการ์ด mweb",
370 + "litecoin_mweb_scanning": "การสแกน MWEB",
371 + "litecoin_mweb_settings": "การตั้งค่า MWEB",
372 + "litecoin_mweb_warning": "การใช้ MWEB จะดาวน์โหลดข้อมูล ~ 600MB ในขั้นต้นและอาจใช้เวลาสูงสุด 30 นาทีขึ้นอยู่กับความเร็วเครือข่าย ข้อมูลเริ่มต้นนี้จะดาวน์โหลดได้เพียงครั้งเดียวและพร้อมใช้งานสำหรับกระเป๋าเงินทั้งหมดของ Litecoin",
373 + "litecoin_what_is_mweb": "MWEB คืออะไร?",
374 "live_fee_rates": "อัตราค่าธรรมเนียมสดผ่าน API",
375 "load_more": "โหลดมากขึ้น",
376 "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "ธีมแสง Monero",
402 "moonpay_alert_text": "มูลค่าของจำนวนต้องมากกว่าหรือเท่ากับ ${minAmount} ${fiatCurrency}",
403 "more_options": "ตัวเลือกเพิ่มเติม",
404 + "mweb_confirmed": "MWEB ยืนยันแล้ว",
405 + "mweb_unconfirmed": "mweb ที่ไม่ได้รับการยืนยัน",
406 "name": "ชื่อ",
407 "nano_current_rep": "ตัวแทนปัจจุบัน",
408 "nano_gpt_thanks_message": "ขอบคุณที่ใช้ Nanogpt! อย่าลืมกลับไปที่เบราว์เซอร์หลังจากการทำธุรกรรมของคุณเสร็จสิ้น!",
@@ -814,6 +824,7 @@
824 "trusted": "มั่นคง",
825 "tx_commit_exception_no_dust_on_change": "ธุรกรรมถูกปฏิเสธด้วยจำนวนเงินนี้ ด้วยเหรียญเหล่านี้คุณสามารถส่ง ${min} โดยไม่ต้องเปลี่ยนแปลงหรือ ${max} ที่ส่งคืนการเปลี่ยนแปลง",
826 "tx_commit_failed": "การทำธุรกรรมล้มเหลว กรุณาติดต่อฝ่ายสนับสนุน",
827 + "tx_commit_failed_no_peers": "การทำธุรกรรมล้มเหลวในการออกอากาศโปรดลองอีกครั้งในวินาทีหรือมากกว่านั้น",
828 "tx_invalid_input": "คุณกำลังใช้ประเภทอินพุตที่ไม่ถูกต้องสำหรับการชำระเงินประเภทนี้",
829 "tx_no_dust_exception": "การทำธุรกรรมถูกปฏิเสธโดยการส่งจำนวนน้อยเกินไป โปรดลองเพิ่มจำนวนเงิน",
830 "tx_not_enough_inputs_exception": "มีอินพุตไม่เพียงพอ โปรดเลือกเพิ่มเติมภายใต้การควบคุมเหรียญ",
res/values/strings_tl.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong Ledger",
364 "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong Ledger",
365 "light_theme": "Light",
366 + "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "Itakda ang MWeb na laging nag -scan",
369 + "litecoin_mweb_display_card": "Ipakita ang MWEB Card",
370 + "litecoin_mweb_scanning": "Pag -scan ng Mweb",
371 + "litecoin_mweb_settings": "Mga Setting ng Mweb",
372 + "litecoin_mweb_warning": "Ang paggamit ng MWEB ay unang i -download ang ~ 600MB ng data, at maaaring tumagal ng hanggang sa 30 minuto depende sa bilis ng network. Ang paunang data na ito ay mag -download lamang ng isang beses at magagamit para sa lahat ng mga wallets ng Litecoin",
373 + "litecoin_what_is_mweb": "Ano ang MWEB?",
374 "live_fee_rates": "Mga rate ng live na bayad sa pamamagitan ng API",
375 "load_more": "Mag-load pa",
376 "loading_your_wallet": "Naglo-load ng iyong wallet",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero Light Theme",
402 "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}",
403 "more_options": "Higit pang mga Pagpipilian",
404 + "mweb_confirmed": "Nakumpirma na MWeb",
405 + "mweb_unconfirmed": "Hindi nakumpirma si Mweb",
406 "name": "Pangalan",
407 "nano_current_rep": "Kasalukuyang Representative",
408 "nano_gpt_thanks_message": "Salamat sa paggamit ng NanoGPT! Tandaan na bumalik sa browser matapos makumpleto ang iyong transaksyon!",
@@ -814,6 +824,7 @@
824 "trusted": "Pinagkakatiwalaan",
825 "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang sukli o ${max} na nagbabalik ng sukli.",
826 "tx_commit_failed": "Nabigo ang transaksyon. Mangyaring makipag-ugnay sa suporta.",
827 + "tx_commit_failed_no_peers": "Nabigo ang transaksyon na mag -broadcast, mangyaring subukang muli sa isang segundo o higit pa",
828 "tx_invalid_input": "Gumagamit ka ng maling uri ng pag-input para sa ganitong uri ng pagbabayad",
829 "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na halaga. Mangyaring subukang dagdagan ang halaga.",
830 "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng Coin Control",
res/values/strings_tr.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Lütfen defterinizde doğru uygulamayı açtığınızdan emin olun",
364 "ledger_please_enable_bluetooth": "Defterinizi algılamak için lütfen Bluetooth'u etkinleştirin",
365 "light_theme": "Aydınlık",
366 + "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir",
367 + "litecoin_mweb": "Mweb",
368 + "litecoin_mweb_always_scan": "MWEB'i her zaman taramayı ayarlayın",
369 + "litecoin_mweb_display_card": "MWEB kartını göster",
370 + "litecoin_mweb_scanning": "MWEB taraması",
371 + "litecoin_mweb_settings": "MWEB ayarları",
372 + "litecoin_mweb_warning": "MWEB kullanmak başlangıçta ~ 600MB veri indirir ve ağ hızına bağlı olarak 30 dakikaya kadar sürebilir. Bu ilk veriler yalnızca bir kez indirilecek ve tüm Litecoin cüzdanları için kullanılabilir olacak",
373 + "litecoin_what_is_mweb": "MWEB nedir?",
374 "live_fee_rates": "API üzerinden canlı ücret oranları",
375 "load_more": "Daha fazla yükle",
376 "loading_your_wallet": "Cüzdanın yükleniyor",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Monero Hafif Tema",
402 "moonpay_alert_text": "Tutar ${minAmount} ${fiatCurrency} miktarına eşit veya daha fazla olmalıdır",
403 "more_options": "Daha Fazla Seçenek",
404 + "mweb_confirmed": "Onaylanmış mweb",
405 + "mweb_unconfirmed": "Doğrulanmamış mweb",
406 "name": "İsim",
407 "nano_current_rep": "Mevcut temsilci",
408 "nano_gpt_thanks_message": "Nanogpt kullandığınız için teşekkürler! İşleminiz tamamlandıktan sonra tarayıcıya geri dönmeyi unutmayın!",
@@ -814,6 +824,7 @@
824 "trusted": "Güvenilir",
825 "tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.",
826 "tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.",
827 + "tx_commit_failed_no_peers": "İşlem yayın yapamadı, lütfen bir saniye içinde tekrar deneyin",
828 "tx_invalid_input": "Bu tür ödeme için yanlış giriş türünü kullanıyorsunuz",
829 "tx_no_dust_exception": "İşlem, çok küçük bir miktar gönderilerek reddedilir. Lütfen miktarı artırmayı deneyin.",
830 "tx_not_enough_inputs_exception": "Yeterli giriş yok. Lütfen madeni para kontrolü altında daha fazlasını seçin",
res/values/strings_uk.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі",
364 "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу",
365 "light_theme": "Світла",
366 + "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB",
367 + "litecoin_mweb": "Мвеб",
368 + "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати",
369 + "litecoin_mweb_display_card": "Показати карту MWeb",
370 + "litecoin_mweb_scanning": "Сканування Mweb",
371 + "litecoin_mweb_settings": "Налаштування MWEB",
372 + "litecoin_mweb_warning": "Використання MWEB спочатку завантажить ~ 600 Мб даних і може зайняти до 30 хвилин залежно від швидкості мережі. Ці початкові дані завантажуються лише один раз і будуть доступні для всіх гаманців Litecoin",
373 + "litecoin_what_is_mweb": "Що таке mweb?",
374 "live_fee_rates": "Ставки плати за живий через API",
375 "load_more": "Завантажити ще",
376 "loading_your_wallet": "Завантаження гаманця",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "Легка тема Monero",
402 "moonpay_alert_text": "Значення суми має бути більшим або дорівнювати ${minAmount} ${fiatCurrency}",
403 "more_options": "Більше параметрів",
404 + "mweb_confirmed": "Підтвердив Mweb",
405 + "mweb_unconfirmed": "Неперевірений MWEB",
406 "name": "Ім'я",
407 "nano_current_rep": "Поточний представник",
408 "nano_gpt_thanks_message": "Дякуємо за використання наногпта! Не забудьте повернутися до браузера після завершення транзакції!",
@@ -815,6 +825,7 @@
825 "trusted": "довіряють",
826 "tx_commit_exception_no_dust_on_change": "Транзакція відхилена цією сумою. За допомогою цих монет ви можете надіслати ${min} без змін або ${max}, що повертає зміни.",
827 "tx_commit_failed": "Транзакційна комісія не вдалося. Будь ласка, зв'яжіться з підтримкою.",
828 + "tx_commit_failed_no_peers": "Транзакція не вдалося транслювати, спробуйте ще раз за секунду або близько того",
829 "tx_invalid_input": "Ви використовуєте неправильний тип введення для цього типу оплати",
830 "tx_no_dust_exception": "Угода відхиляється, відправивши суму занадто мала. Будь ласка, спробуйте збільшити суму.",
831 "tx_not_enough_inputs_exception": "Недостатньо доступних входів. Виберіть більше під контролем монети",
res/values/strings_ur.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں",
364 "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں",
365 "light_theme": "روشنی",
366 + "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں",
367 + "litecoin_mweb": "MWEB",
368 + "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں",
369 + "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں",
370 + "litecoin_mweb_scanning": "MWEB اسکیننگ",
371 + "litecoin_mweb_settings": "MWEB کی ترتیبات",
372 + "litecoin_mweb_warning": "MWEB کا استعمال ابتدائی طور پر m 600mb ڈیٹا ڈاؤن لوڈ کرے گا ، اور نیٹ ورک کی رفتار کے لحاظ سے 30 منٹ تک کا وقت لگ سکتا ہے۔ یہ ابتدائی اعداد و شمار صرف ایک بار ڈاؤن لوڈ کریں گے اور تمام لیٹیکوئن بٹوے کے لئے دستیاب ہوں گے",
373 + "litecoin_what_is_mweb": "MWEB کیا ہے؟",
374 "live_fee_rates": "API کے ذریعے براہ راست فیس کی شرح",
375 "load_more": "مزید لوڈ کریں",
376 "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "مونیرو لائٹ تھیم",
402 "moonpay_alert_text": "رقم کی قدر ${minAmount} ${fiatCurrency} کے برابر یا زیادہ ہونی چاہیے۔",
403 "more_options": "مزید زرائے",
404 + "mweb_confirmed": "تصدیق شدہ MWEB",
405 + "mweb_unconfirmed": "غیر مصدقہ MWEB",
406 "name": "ﻡﺎﻧ",
407 "nano_current_rep": "موجودہ نمائندہ",
408 "nano_gpt_thanks_message": "نانوگپٹ استعمال کرنے کا شکریہ! اپنے لین دین کی تکمیل کے بعد براؤزر کی طرف واپس جانا یاد رکھیں!",
@@ -816,6 +826,7 @@
826 "trusted": "قابل اعتماد",
827 "tx_commit_exception_no_dust_on_change": "اس رقم سے لین دین کو مسترد کردیا گیا ہے۔ ان سککوں کے ذریعہ آپ بغیر کسی تبدیلی کے ${min} یا ${max} بھیج سکتے ہیں جو لوٹتے ہیں۔",
828 "tx_commit_failed": "ٹرانزیکشن کمٹ ناکام ہوگیا۔ براہ کرم سپورٹ سے رابطہ کریں۔",
829 + "tx_commit_failed_no_peers": "ٹرانزیکشن نشر کرنے میں ناکام ، براہ کرم ایک سیکنڈ یا اس میں دوبارہ کوشش کریں",
830 "tx_invalid_input": "آپ اس قسم کی ادائیگی کے لئے غلط ان پٹ کی قسم استعمال کررہے ہیں",
831 "tx_no_dust_exception": "لین دین کو بہت چھوٹی رقم بھیج کر مسترد کردیا جاتا ہے۔ براہ کرم رقم میں اضافہ کرنے کی کوشش کریں۔",
832 "tx_not_enough_inputs_exception": "کافی ان پٹ دستیاب نہیں ہے۔ براہ کرم سکے کے کنٹرول میں مزید منتخب کریں",
res/values/strings_vi.arb
+3
@@ -394,6 +394,8 @@
394 "monero_light_theme": "Chủ đề sáng Monero",
395 "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}",
396 "more_options": "Thêm tùy chọn",
397 + "mweb_confirmed": "Xác nhận MWEB",
398 + "mweb_unconfirmed": "MWEB chưa được xác nhận",
399 "name": "Tên",
400 "nano_current_rep": "Đại diện hiện tại",
401 "nano_gpt_thanks_message": "Cảm ơn bạn đã sử dụng NanoGPT! Hãy nhớ quay lại trình duyệt sau khi giao dịch của bạn hoàn tất!",
@@ -815,6 +817,7 @@
817 "trusted": "Đã tin cậy",
818 "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.",
819 "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.",
820 + "tx_commit_failed_no_peers": "Giao dịch không phát sóng, vui lòng thử lại trong một giây hoặc lâu hơn",
821 "tx_invalid_input": "Bạn đang sử dụng loại đầu vào sai cho loại thanh toán này",
822 "tx_no_dust_exception": "Giao dịch bị từ chối vì gửi một số tiền quá nhỏ. Vui lòng thử tăng số tiền.",
823 "tx_not_enough_inputs_exception": "Không đủ đầu vào có sẵn. Vui lòng chọn thêm dưới Coin Control",
res/values/strings_yo.arb
+11
@@ -364,6 +364,14 @@
364 "ledger_error_wrong_app": "Jọwọ rii daju pe iwọ yoo sọ app ti o tọ loju omi rẹ",
365 "ledger_please_enable_bluetooth": "Jọwọ jẹ ki Bluetooth lati rii iṣupọ rẹ",
366 "light_theme": "Funfun bí eérú",
367 + "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ",
368 + "litecoin_mweb": "Mweb",
369 + "litecoin_mweb_always_scan": "Ṣeto mweb nigbagbogbo n ṣayẹwo",
370 + "litecoin_mweb_display_card": "Fihan kaadi Mweb",
371 + "litecoin_mweb_scanning": "Mweb scanning",
372 + "litecoin_mweb_settings": "Awọn eto Mweb",
373 + "litecoin_mweb_warning": "Lilo Mweb yoo wa lakoko igbasilẹ ~ 600MB ti data, o le gba to iṣẹju 30 da lori iyara nẹtiwọọki. Awọn data akọkọ yii yoo ṣe igbasilẹ lẹẹkan si ki o wa fun gbogbo awọn Wolinkun LiveCooin",
374 + "litecoin_what_is_mweb": "Kini mweb?",
375 "live_fee_rates": "Awọn oṣuwọn Owo laaye laaye nipasẹ API",
376 "load_more": "Ẹru diẹ sii",
377 "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín",
@@ -394,6 +402,8 @@
402 "monero_light_theme": "Monero Light Akori",
403 "moonpay_alert_text": "Iye owó kò gbọ́dọ̀ kéré ju ${minAmount} ${fiatCurrency}",
404 "more_options": "Ìyàn àfikún",
405 + "mweb_confirmed": "Jẹrisi Mweb",
406 + "mweb_unconfirmed": "Ajopo Mweb",
407 "name": "Oruko",
408 "nano_current_rep": "Aṣoju lọwọlọwọ",
409 "nano_gpt_thanks_message": "O ṣeun fun lilo Nonnogt! Ranti lati tẹle pada si ẹrọ lilọ kiri ayelujara lẹhin iṣowo rẹ pari!",
@@ -815,6 +825,7 @@
825 "trusted": "A ti fọkàn ẹ̀ tán",
826 "tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.",
827 "tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.",
828 + "tx_commit_failed_no_peers": "Idunadura kuna lati wa igbohungbe, jọwọ gbiyanju lẹẹkansi ni iṣẹju keji tabi bẹẹ",
829 "tx_invalid_input": "O nlo iru titẹ nkan ti ko tọ fun iru isanwo yii",
830 "tx_no_dust_exception": "Iṣowo naa ni kọ nipa fifiranṣẹ iye ti o kere ju. Jọwọ gbiyanju pọ si iye naa.",
831 "tx_not_enough_inputs_exception": "Ko to awọn titẹsi to. Jọwọ yan diẹ sii labẹ iṣakoso owo",
res/values/strings_zh.arb
+11
@@ -363,6 +363,14 @@
363 "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序",
364 "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐",
365 "light_theme": "艳丽",
366 + "litecoin_enable_mweb_sync": "启用MWEB扫描",
367 + "litecoin_mweb": "MWEB",
368 + "litecoin_mweb_always_scan": "设置MWEB总是扫描",
369 + "litecoin_mweb_display_card": "显示MWEB卡",
370 + "litecoin_mweb_scanning": "MWEB扫描",
371 + "litecoin_mweb_settings": "MWEB设置",
372 + "litecoin_mweb_warning": "使用MWEB最初将下载约600MB的数据,并且最多可能需要30分钟的时间,具体取决于网络速度。此初始数据只能下载一次,并适用于所有莱特币钱包",
373 + "litecoin_what_is_mweb": "什么是MWEB?",
374 "live_fee_rates": "通过API的实时费率",
375 "load_more": "装载更多",
376 "loading_your_wallet": "加载您的钱包",
@@ -393,6 +401,8 @@
401 "monero_light_theme": "门罗币浅色主题",
402 "moonpay_alert_text": "金额的价值必须大于或等于 ${minAmount} ${fiatCurrency}",
403 "more_options": "更多选项",
404 + "mweb_confirmed": "确认的MWEB",
405 + "mweb_unconfirmed": "未经证实的MWEB",
406 "name": "姓名",
407 "nano_current_rep": "当前代表",
408 "nano_gpt_thanks_message": "感谢您使用Nanogpt!事务完成后,请记住回到浏览器!",
@@ -814,6 +824,7 @@
824 "trusted": "值得信赖",
825 "tx_commit_exception_no_dust_on_change": "交易被此金额拒绝。使用这些硬币,您可以发送${min}无需更改或返回${max}的变化。",
826 "tx_commit_failed": "交易承诺失败。请联系支持。",
827 + "tx_commit_failed_no_peers": "交易无法广播,请在一秒钟左右的时间内重试",
828 "tx_invalid_input": "您正在使用错误的输入类型进行此类付款",
829 "tx_no_dust_exception": "通过发送太小的金额来拒绝交易。请尝试增加金额。",
830 "tx_not_enough_inputs_exception": "没有足够的输入。请在硬币控制下选择更多",
scripts/android/build_all.sh
+2 -1
@@ -10,6 +10,7 @@ DIR=$(dirname "$0")
10 case $APP_ANDROID_TYPE in
11 "monero.com") $DIR/build_monero_all.sh ;;
12 "cakewallet") $DIR/build_monero_all.sh
13 - $DIR/build_haven_all.sh ;;
13 + $DIR/build_haven_all.sh
14 + $DIR/build_mwebd.sh ;;
15 "haven") $DIR/build_haven_all.sh ;;
16 esac
scripts/android/build_mwebd.sh new
+22
@@ -0,0 +1,22 @@
1 +if [[ "$1" == "--dont-install" ]]; then
2 + echo "Skipping Go installation as per --dont-install flag"
3 +else
4 + # install go > 1.23:
5 + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
6 + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
7 + export PATH=$PATH:/usr/local/go/bin
8 + export PATH=$PATH:~/go/bin
9 + go install golang.org/x/mobile/cmd/gomobile@latest
10 + gomobile init
11 +fi
12 +
13 +# build mwebd:
14 +git clone https://github.com/ltcmweb/mwebd
15 +cd mwebd
16 +git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935
17 +gomobile bind -target=android -androidapi 21 .
18 +mkdir -p ../../../cw_mweb/android/libs/
19 +mv ./mwebd.aar $_
20 +# cleanup:
21 +cd ..
22 +rm -rf mwebd
\ No newline at end of file
scripts/ios/build_all.sh
+1 -1
@@ -9,6 +9,6 @@ DIR=$(dirname "$0")
9
10 case $APP_IOS_TYPE in
11 "monero.com") $DIR/build_monero_all.sh ;;
12 - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh ;;
12 + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;;
13 "haven") $DIR/build_haven_all.sh ;;
14 esac
scripts/ios/build_mwebd.sh new
+20
@@ -0,0 +1,20 @@
1 +#!/bin/bash
2 +if [[ "$1" == "--dont-install" ]]; then
3 + echo "Skipping Go installation as per --dont-install flag"
4 +else
5 + # install go > 1.23:
6 + brew install go
7 + export PATH=$PATH:~/go/bin
8 + go install golang.org/x/mobile/cmd/gomobile@latest
9 + gomobile init
10 +fi
11 +
12 +# build mwebd:
13 +git clone https://github.com/ltcmweb/mwebd
14 +cd mwebd
15 +git reset --hard f6ea8a9e3d348b01bb44f03a1cc4ad65b0abe935
16 +gomobile bind -target=ios .
17 +mv -fn ./Mwebd.xcframework ../../../ios/
18 +# cleanup:
19 +cd ..
20 +rm -rf mwebd
\ No newline at end of file
tool/configure.dart
+8 -3
@@ -115,6 +115,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
115 import 'package:cw_bitcoin/bitcoin_address_record.dart';
116 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
117 import 'package:cw_bitcoin/litecoin_wallet_service.dart';
118 +import 'package:cw_bitcoin/litecoin_wallet.dart';
119 import 'package:cw_core/get_height_by_date.dart';
120 import 'package:cw_core/transaction_info.dart';
121 import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart';
@@ -180,8 +181,8 @@ abstract class Bitcoin {
181 List<Unspent> getUnspents(Object wallet);
182 Future<void> updateUnspents(Object wallet);
183 WalletService createBitcoinWalletService(
183 - Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
184 - WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect);
184 + Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
185 + WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
186 TransactionPriority getBitcoinTransactionPriorityMedium();
187 TransactionPriority getBitcoinTransactionPriorityCustom();
188 TransactionPriority getLitecoinTransactionPriorityMedium();
@@ -195,6 +196,7 @@ abstract class Bitcoin {
196 Future<void> setAddressType(Object wallet, dynamic option);
197 ReceivePageOption getSelectedAddressType(Object wallet);
198 List<ReceivePageOption> getBitcoinReceivePageOptions();
199 + List<ReceivePageOption> getLitecoinReceivePageOptions();
200 BitcoinAddressType getBitcoinAddressType(ReceivePageOption option);
201 bool hasSelectedSilentPayments(Object wallet);
202 bool isBitcoinReceivePageOption(ReceivePageOption option);
@@ -214,16 +216,19 @@ abstract class Bitcoin {
216 int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size});
217 Future<bool> checkIfMempoolAPIIsEnabled(Object wallet);
218 Future<int> getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled});
219 + int getLitecoinHeightByDate({required DateTime date});
220 Future<void> rescan(Object wallet, {required int height, bool? doSingleScan});
221 Future<bool> getNodeIsElectrsSPEnabled(Object wallet);
222 void deleteSilentPaymentAddress(Object wallet, String address);
223 Future<void> updateFeeRates(Object wallet);
224 int getMaxCustomFeeRate(Object wallet);
222 -
225 void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device);
226 Future<List<HardwareAccountData>> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5});
227 List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs);
228 bool txIsReceivedSilentPayment(TransactionInfo txInfo);
229 + bool txIsMweb(TransactionInfo txInfo);
230 + Future<void> setMwebEnabled(Object wallet, bool enabled);
231 + bool getMwebEnabled(Object wallet);
232 }
233 """;
234