Merge branch 'main' into CW-607-Update-list-of-Trocador-providers-to-be-fetched-from-the-API

This commit is contained in:
Serhii 2024-10-04 17:48:19 +03:00
commit 12f2a280e6
177 changed files with 5539 additions and 766 deletions

View file

@ -96,6 +96,25 @@ jobs:
cd /opt/android/cake_wallet
flutter pub get
- name: Install go and gomobile
run: |
# install go > 1.23:
wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
- name: Build mwebd
run: |
# paths are reset after each step, so we need to set them again:
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:~/go/bin
cd /opt/android/cake_wallet/scripts/android/
./build_mwebd.sh --dont-install
- name: Generate KeyStore
run: |
cd /opt/android/cake_wallet/android/app

View file

@ -89,6 +89,25 @@ jobs:
cd /opt/android/cake_wallet
flutter pub get
- name: Install go and gomobile
run: |
# install go > 1.23:
wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
- name: Build mwebd
run: |
# paths are reset after each step, so we need to set them again:
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:~/go/bin
# build mwebd:
cd /opt/android/cake_wallet/scripts/android/
./build_mwebd.sh --dont-install
- name: Generate localization
run: |
cd /opt/android/cake_wallet

BIN
assets/images/mweb_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

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

View file

@ -8,6 +8,7 @@ import 'package:cw_core/sec_random_native.dart';
import 'package:cw_core/utils/text_normalizer.dart';
const segwit = '100';
const mweb = 'eb';
final wordlist = englishWordlist;
double logBase(num x, num base) => log(x) / log(base);
@ -125,7 +126,7 @@ Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
return Uint8List.fromList(bytes);
}
bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el);
bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit, mweb]).any((el) => el);
bool validateMnemonic(String mnemonic, {String prefix = segwit}) {
try {

View file

@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption {
static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)');
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
static const mweb = BitcoinReceivePageOption._('MWEB');
static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
@ -27,6 +28,11 @@ class BitcoinReceivePageOption implements ReceivePageOption {
BitcoinReceivePageOption.p2pkh
];
static const allLitecoin = [
BitcoinReceivePageOption.p2wpkh,
BitcoinReceivePageOption.mweb
];
BitcoinAddressType toType() {
switch (this) {
case BitcoinReceivePageOption.p2tr:
@ -39,6 +45,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
return P2shAddressType.p2wpkhInP2sh;
case BitcoinReceivePageOption.silent_payments:
return SilentPaymentsAddresType.p2sp;
case BitcoinReceivePageOption.mweb:
return SegwitAddresType.mweb;
case BitcoinReceivePageOption.p2wpkh:
default:
return SegwitAddresType.p2wpkh;
@ -51,6 +59,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
return BitcoinReceivePageOption.p2tr;
case SegwitAddresType.p2wsh:
return BitcoinReceivePageOption.p2wsh;
case SegwitAddresType.mweb:
return BitcoinReceivePageOption.mweb;
case P2pkhAddressType.p2pkh:
return BitcoinReceivePageOption.p2pkh;
case P2shAddressType.p2wpkhInP2sh:

View file

@ -87,7 +87,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority {
}
@override
String get units => 'Latoshi';
String get units => 'Litoshi';
@override
String toString() {

View file

@ -506,6 +506,12 @@ class ElectrumClient {
void _methodHandler({required String method, required Map<String, dynamic> request}) {
switch (method) {
case 'blockchain.headers.subscribe':
final params = request['params'] as List<dynamic>;
final id = 'blockchain.headers.subscribe';
_tasks[id]?.subject?.add(params.last);
break;
case 'blockchain.scripthash.subscribe':
final params = request['params'] as List<dynamic>;
final scripthash = params.first as String?;

View file

@ -7,7 +7,14 @@ class ElectrumBalance extends Balance {
required this.confirmed,
required this.unconfirmed,
required this.frozen,
}) : super(confirmed, unconfirmed);
this.secondConfirmed = 0,
this.secondUnconfirmed = 0,
}) : super(
confirmed,
unconfirmed,
secondAvailable: secondConfirmed,
secondAdditional: secondUnconfirmed,
);
static ElectrumBalance? fromJSON(String? jsonSource) {
if (jsonSource == null) {
@ -25,9 +32,12 @@ class ElectrumBalance extends Balance {
int confirmed;
int unconfirmed;
final int frozen;
int secondConfirmed = 0;
int secondUnconfirmed = 0;
@override
String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
String get formattedAvailableBalance =>
bitcoinAmountToString(amount: confirmed - frozen);
@override
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
@ -38,6 +48,21 @@ class ElectrumBalance extends Balance {
return frozenFormatted == '0.0' ? '' : frozenFormatted;
}
String toJSON() =>
json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
@override
String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed);
@override
String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed);
@override
String get formattedFullAvailableBalance =>
bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen);
String toJSON() => json.encode({
'confirmed': confirmed,
'unconfirmed': unconfirmed,
'frozen': frozen,
'secondConfirmed': secondConfirmed,
'secondUnconfirmed': secondUnconfirmed
});
}

View file

@ -35,7 +35,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
List<String>? outputAddresses,
required TransactionDirection direction,
required bool isPending,
required bool isReplaced,
bool isReplaced = false,
required DateTime date,
required int confirmations,
String? to,

View file

@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
@ -23,6 +23,7 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/crypto_currency.dart';
@ -112,11 +113,18 @@ abstract class ElectrumWalletBase
}
if (seedBytes != null) {
return currency == CryptoCurrency.bch
? bitcoinCashHDWallet(seedBytes)
: Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
switch (currency) {
case CryptoCurrency.btc:
case CryptoCurrency.ltc:
case CryptoCurrency.tbtc:
return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
as Bip32Slip10Secp256k1;
case CryptoCurrency.bch:
return bitcoinCashHDWallet(seedBytes);
default:
throw Exception("Unsupported currency");
}
}
return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
@ -163,11 +171,13 @@ abstract class ElectrumWalletBase
Set<String> get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet();
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
.map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network))
.toList();
List<String> get publicScriptHashes => walletAddresses.allAddresses
.where((addr) => !addr.isHidden)
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
.map((addr) => addr.getScriptHash(network))
.toList();
@ -274,6 +284,7 @@ abstract class ElectrumWalletBase
void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer;
StreamSubscription<dynamic>? _receiveStream;
Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1;
@ -324,7 +335,8 @@ abstract class ElectrumWalletBase
isSingleScan: doSingleScan ?? false,
));
await for (var message in receivePort) {
_receiveStream?.cancel();
_receiveStream = receivePort.listen((var message) async {
if (message is Map<String, ElectrumTransactionInfo>) {
for (final map in message.entries) {
final txid = map.key;
@ -387,10 +399,16 @@ abstract class ElectrumWalletBase
nodeSupportsSilentPayments = false;
}
syncStatus = message.syncStatus;
if (message.syncStatus is SyncingSyncStatus) {
var status = message.syncStatus as SyncingSyncStatus;
syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc);
} else {
syncStatus = message.syncStatus;
}
await walletInfo.updateRestoreHeight(message.height);
}
}
});
}
void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) {
@ -430,9 +448,9 @@ abstract class ElectrumWalletBase
await _setInitialHeight();
}
await _subscribeForUpdates();
await subscribeForUpdates();
await updateTransactions();
await updateAllUnspents();
await updateBalance();
await updateFeeRates();
@ -537,6 +555,7 @@ abstract class ElectrumWalletBase
try {
syncStatus = ConnectingSyncStatus();
await _receiveStream?.cancel();
await electrumClient.close();
electrumClient.onConnectionStatusChange = _onConnectionStatusChange;
@ -682,26 +701,15 @@ abstract class ElectrumWalletBase
paysToSilentPayment: hasSilentPayment,
);
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxoDetails.utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else {
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxoDetails.utxos,
outputs: outputs,
network: network,
memo: memo,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
vinOutpoints: utxoDetails.vinOutpoints,
);
}
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
int fee = await calcFee(
utxos: utxoDetails.utxos,
outputs: outputs,
network: network,
memo: memo,
feeRate: feeRate,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
vinOutpoints: utxoDetails.vinOutpoints,
);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
@ -788,7 +796,10 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress();
final changeAddress = await walletAddresses.getChangeAddress(
outputs: outputs,
utxoDetails: utxoDetails,
);
final address = RegexUtils.addressTypeFromStr(changeAddress, network);
outputs.add(BitcoinOutput(
address: address,
@ -796,26 +807,13 @@ abstract class ElectrumWalletBase
isChange: true,
));
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxoDetails.utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else {
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxoDetails.utxos,
outputs: outputs,
network: network,
memo: memo,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
vinOutpoints: utxoDetails.vinOutpoints,
);
}
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
int fee = await calcFee(
utxos: utxoDetails.utxos,
outputs: outputs,
network: network,
memo: memo,
feeRate: feeRate,
);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
@ -825,6 +823,8 @@ abstract class ElectrumWalletBase
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
print(amountLeftForChangeAndFee);
if (!_isBelowDust(amountLeftForChange)) {
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
outputs[outputs.length - 1] = BitcoinOutput(
@ -874,7 +874,7 @@ abstract class ElectrumWalletBase
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed) {
if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) {
throw BitcoinTransactionWrongBalanceException();
}
@ -909,6 +909,37 @@ abstract class ElectrumWalletBase
);
}
Future<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
required BasedUtxoNetwork network,
String? memo,
required int feeRate,
List<ECPrivateInfo>? inputPrivKeyInfos,
List<Outpoint>? vinOutpoints,
}) async {
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
} else {
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
inputPrivKeyInfos: inputPrivKeyInfos,
vinOutpoints: vinOutpoints,
);
}
return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
@ -1134,6 +1165,7 @@ abstract class ElectrumWalletBase
'derivationPath': walletInfo.derivationInfo?.derivationPath,
'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(),
'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(),
'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(),
});
int feeRate(TransactionPriority priority) {
@ -1253,6 +1285,7 @@ abstract class ElectrumWalletBase
@override
Future<void> close() async {
try {
await _receiveStream?.cancel();
await electrumClient.close();
} catch (_) {}
_autoSaveTimer?.cancel();
@ -1272,77 +1305,66 @@ abstract class ElectrumWalletBase
});
}
// Set the balance of all non-silent payment addresses to 0 before updating
walletAddresses.allAddresses.forEach((addr) {
if(addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
});
await Future.wait(walletAddresses.allAddresses.map((address) async {
updatedUnspentCoins.addAll(await fetchUnspent(address));
}));
unspentCoins = updatedUnspentCoins;
if (unspentCoinsInfo.isEmpty) {
unspentCoins.forEach((coin) => _addCoinInfo(coin));
if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin));
return;
}
if (unspentCoins.isNotEmpty) {
unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where((element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout);
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
_addCoinInfo(coin);
}
});
}
await updateCoins(unspentCoins);
await _refreshUnspentCoinsInfo();
}
@action
Future<void> updateUnspents(BitcoinAddressRecord address) async {
final newUnspentCoins = await fetchUnspent(address);
if (newUnspentCoins.isNotEmpty) {
unspentCoins.addAll(newUnspentCoins);
newUnspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
_addCoinInfo(coin);
}
});
Future<void> updateCoins(List<BitcoinUnspent> newUnspentCoins) async {
if (newUnspentCoins.isEmpty) {
return;
}
newUnspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
addCoinInfo(coin);
}
});
}
@action
Future<void> updateUnspentsForAddress(BitcoinAddressRecord address) async {
final newUnspentCoins = await fetchUnspent(address);
await updateCoins(newUnspentCoins);
}
@action
Future<List<BitcoinUnspent>> fetchUnspent(BitcoinAddressRecord address) async {
final unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
List<Map<String, dynamic>> unspents = [];
List<BitcoinUnspent> updatedUnspentCoins = [];
unspents = await electrumClient.getListUnspent(address.getScriptHash(network));
await Future.wait(unspents.map((unspent) async {
try {
final coin = BitcoinUnspent.fromJSON(address, unspent);
@ -1358,7 +1380,7 @@ abstract class ElectrumWalletBase
}
@action
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
Future<void> addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo(
walletId: id,
hash: coin.hash,
@ -1709,12 +1731,14 @@ abstract class ElectrumWalletBase
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
if (type == WalletType.bitcoin) {
await Future.wait(ADDRESS_TYPES
await Future.wait(BITCOIN_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
} else if (type == WalletType.bitcoinCash) {
await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh);
await Future.wait(BITCOIN_CASH_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
} else if (type == WalletType.litecoin) {
await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh);
await Future.wait(LITECOIN_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
}
transactionHistory.transactions.values.forEach((tx) async {
@ -1748,7 +1772,8 @@ abstract class ElectrumWalletBase
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true);
final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false);
walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
await walletAddresses.saveAddressesInBox();
await Future.wait(addressesByType.map((addressRecord) async {
final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
@ -1771,7 +1796,7 @@ abstract class ElectrumWalletBase
matchedAddresses.toList(),
addressRecord.isHidden,
(address) async {
await _subscribeForUpdates();
await subscribeForUpdates();
return _fetchAddressHistory(address, await getCurrentChainTip())
.then((history) => history.isNotEmpty ? address.address : null);
},
@ -1860,7 +1885,7 @@ abstract class ElectrumWalletBase
}
}
Future<void> _subscribeForUpdates() async {
Future<void> subscribeForUpdates() async {
final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
(address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)),
);
@ -1871,7 +1896,7 @@ abstract class ElectrumWalletBase
_scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh);
_scripthashesUpdateSubject[sh]?.listen((event) async {
try {
await updateUnspents(address);
await updateUnspentsForAddress(address);
await updateBalance();
@ -1888,8 +1913,10 @@ abstract class ElectrumWalletBase
}));
}
Future<ElectrumBalance> _fetchBalances() async {
final addresses = walletAddresses.allAddresses.toList();
Future<ElectrumBalance> fetchBalances() async {
final addresses = walletAddresses.allAddresses
.where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
.toList();
final balanceFutures = <Future<Map<String, dynamic>>>[];
for (var i = 0; i < addresses.length; i++) {
final addressRecord = addresses[i];
@ -1902,6 +1929,18 @@ abstract class ElectrumWalletBase
var totalConfirmed = 0;
var totalUnconfirmed = 0;
unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) {
if (element.hash == info.hash &&
element.vout == info.vout &&
info.isFrozen &&
element.bitcoinAddressRecord.address == info.address &&
element.value == info.value) {
totalFrozen += element.value;
}
});
});
if (hasSilentPaymentsScanning) {
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
@ -1927,6 +1966,7 @@ abstract class ElectrumWalletBase
totalConfirmed += confirmed;
totalUnconfirmed += unconfirmed;
addressRecord.balance = confirmed + unconfirmed;
if (confirmed > 0 || unconfirmed > 0) {
addressRecord.setAsUsed();
}
@ -1940,22 +1980,10 @@ abstract class ElectrumWalletBase
}
Future<void> updateBalance() async {
balance[currency] = await _fetchBalances();
balance[currency] = await fetchBalances();
await save();
}
String getChangeAddress() {
const minCountOfHiddenAddresses = 5;
final random = Random();
var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList();
if (addresses.length < minCountOfHiddenAddresses) {
addresses = walletAddresses.allAddresses.toList();
}
return addresses[random.nextInt(addresses.length)].address;
}
@override
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
@ -2458,6 +2486,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
return SegwitAddresType.p2wsh;
} else if (type is P2trAddress) {
return SegwitAddresType.p2tr;
} else if (type is MwebAddress) {
return SegwitAddresType.mweb;
} else if (type is SilentPaymentsAddresType) {
return SilentPaymentsAddresType.p2sp;
} else {

View file

@ -1,6 +1,7 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
@ -10,7 +11,7 @@ part 'electrum_wallet_addresses.g.dart';
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
const List<BitcoinAddressType> ADDRESS_TYPES = [
const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
SegwitAddresType.p2wpkh,
P2pkhAddressType.p2pkh,
SegwitAddresType.p2tr,
@ -18,6 +19,15 @@ const List<BitcoinAddressType> ADDRESS_TYPES = [
P2shAddressType.p2wpkhInP2sh,
];
const List<BitcoinAddressType> LITECOIN_ADDRESS_TYPES = [
SegwitAddresType.p2wpkh,
SegwitAddresType.mweb,
];
const List<BitcoinAddressType> BITCOIN_CASH_ADDRESS_TYPES = [
P2pkhAddressType.p2pkh,
];
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(
WalletInfo walletInfo, {
@ -29,6 +39,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Map<String, int>? initialChangeAddressIndex,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
List<BitcoinAddressRecord>? initialMwebAddresses,
Bip32Slip10Secp256k1? masterHd,
BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
@ -49,6 +60,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
(initialSilentAddresses ?? []).toSet()),
currentSilentAddressIndex = initialSilentAddressIndex,
mwebAddresses =
ObservableList<BitcoinAddressRecord>.of((initialMwebAddresses ?? []).toSet()),
super(walletInfo) {
if (masterHd != null) {
silentAddress = SilentPaymentOwner.fromPrivateKeys(
@ -91,6 +104,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses;
final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
final ObservableList<BitcoinAddressRecord> mwebAddresses;
final BasedUtxoNetwork network;
final Bip32Slip10Secp256k1 mainHd;
final Bip32Slip10Secp256k1 sideHd;
@ -149,6 +163,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
set address(String addr) {
if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) {
return;
}
if (addressPageType == SilentPaymentsAddresType.p2sp) {
final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
@ -160,12 +177,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
return;
}
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
try {
final addressRecord = _addresses.firstWhere(
(addressRecord) => addressRecord.address == addr,
);
previousAddressRecord = addressRecord;
receiveAddresses.remove(addressRecord);
receiveAddresses.insert(0, addressRecord);
} catch (e) {
print("ElectrumWalletAddressBase: set address ($addr): $e");
}
}
@override
@ -213,7 +235,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.litecoin) {
await _generateInitialAddresses();
await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
await _generateInitialAddresses(type: SegwitAddresType.mweb);
} else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses();
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
@ -221,6 +244,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
}
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
@ -237,7 +261,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
Future<String> getChangeAddress() async {
Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
updateChangeAddresses();
if (changeAddresses.isEmpty) {
@ -317,12 +341,110 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address;
}
String getAddress(
{required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType}) =>
String getAddress({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) =>
'';
Future<String> getAddressAsync({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) async =>
getAddress(index: index, hd: hd, addressType: addressType);
void addBitcoinAddressTypes() {
final lastP2wpkh = _addresses
.where((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
.toList()
.last;
if (lastP2wpkh.address != address) {
addressesMap[lastP2wpkh.address] = 'P2WPKH';
} else {
addressesMap[address] = 'Active - P2WPKH';
}
final lastP2pkh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
if (lastP2pkh.address != address) {
addressesMap[lastP2pkh.address] = 'P2PKH';
} else {
addressesMap[address] = 'Active - P2PKH';
}
final lastP2sh = _addresses.firstWhere((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
if (lastP2sh.address != address) {
addressesMap[lastP2sh.address] = 'P2SH';
} else {
addressesMap[address] = 'Active - P2SH';
}
final lastP2tr = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
if (lastP2tr.address != address) {
addressesMap[lastP2tr.address] = 'P2TR';
} else {
addressesMap[address] = 'Active - P2TR';
}
final lastP2wsh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
if (lastP2wsh.address != address) {
addressesMap[lastP2wsh.address] = 'P2WSH';
} else {
addressesMap[address] = 'Active - P2WSH';
}
silentAddresses.forEach((addressRecord) {
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
return;
}
if (addressRecord.address != address) {
addressesMap[addressRecord.address] = addressRecord.name.isEmpty
? "Silent Payments"
: "Silent Payments - " + addressRecord.name;
} else {
addressesMap[address] = 'Active - Silent Payments';
}
});
}
void addLitecoinAddressTypes() {
final lastP2wpkh = _addresses
.where((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
.toList()
.last;
if (lastP2wpkh.address != address) {
addressesMap[lastP2wpkh.address] = 'P2WPKH';
} else {
addressesMap[address] = 'Active - P2WPKH';
}
final lastMweb = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb));
if (lastMweb.address != address) {
addressesMap[lastMweb.address] = 'MWEB';
} else {
addressesMap[address] = 'Active - MWEB';
}
}
void addBitcoinCashAddressTypes() {
final lastP2pkh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
if (lastP2pkh.address != address) {
addressesMap[lastP2pkh.address] = 'P2PKH';
} else {
addressesMap[address] = 'Active - P2PKH';
}
}
@override
Future<void> updateAddressesInBox() async {
try {
@ -334,63 +456,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
allAddressesMap[addressRecord.address] = addressRecord.name;
});
final lastP2wpkh = _addresses
.where((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
.toList()
.last;
if (lastP2wpkh.address != address) {
addressesMap[lastP2wpkh.address] = 'P2WPKH';
} else {
addressesMap[address] = 'Active - P2WPKH';
switch (walletInfo.type) {
case WalletType.bitcoin:
addBitcoinAddressTypes();
break;
case WalletType.litecoin:
addLitecoinAddressTypes();
break;
case WalletType.bitcoinCash:
addBitcoinCashAddressTypes();
break;
default:
break;
}
final lastP2pkh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
if (lastP2pkh.address != address) {
addressesMap[lastP2pkh.address] = 'P2PKH';
} else {
addressesMap[address] = 'Active - P2PKH';
}
final lastP2sh = _addresses.firstWhere((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
if (lastP2sh.address != address) {
addressesMap[lastP2sh.address] = 'P2SH';
} else {
addressesMap[address] = 'Active - P2SH';
}
final lastP2tr = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
if (lastP2tr.address != address) {
addressesMap[lastP2tr.address] = 'P2TR';
} else {
addressesMap[address] = 'Active - P2TR';
}
final lastP2wsh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
if (lastP2wsh.address != address) {
addressesMap[lastP2wsh.address] = 'P2WSH';
} else {
addressesMap[address] = 'Active - P2WSH';
}
silentAddresses.forEach((addressRecord) {
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
return;
}
if (addressRecord.address != address) {
addressesMap[addressRecord.address] = addressRecord.name.isEmpty
? "Silent Payments"
: "Silent Payments - " + addressRecord.name;
} else {
addressesMap[address] = 'Active - Silent Payments';
}
});
await saveAddressesInBox();
} catch (e) {
print(e.toString());
@ -410,6 +489,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
foundAddress = addressRecord;
}
});
mwebAddresses.forEach((addressRecord) {
if (addressRecord.address == address) {
foundAddress = addressRecord;
}
});
if (foundAddress != null) {
foundAddress!.setNewName(label);
@ -510,7 +594,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord(
getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
index: i,
isHidden: isHidden,
type: type ?? addressPageType,
@ -540,15 +624,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch();
}
@action
void addMwebAddresses(Iterable<BitcoinAddressRecord> addresses) {
final addressesSet = this.mwebAddresses.toSet();
addressesSet.addAll(addresses);
this.mwebAddresses.clear();
this.mwebAddresses.addAll(addressesSet);
updateAddressesByMatch();
}
void _validateAddresses() {
_addresses.forEach((element) {
_addresses.forEach((element) async {
if (element.type == SegwitAddresType.mweb) {
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
return;
}
if (!element.isHidden &&
element.address !=
getAddress(index: element.index, hd: mainHd, addressType: element.type)) {
await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
element.isHidden = true;
} else if (element.isHidden &&
element.address !=
getAddress(index: element.index, hd: sideHd, addressType: element.type)) {
await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
element.isHidden = false;
}
});

View file

@ -23,6 +23,7 @@ class ElectrumWalletSnapshot {
required this.addressPageType,
required this.silentAddresses,
required this.silentAddressIndex,
required this.mwebAddresses,
this.passphrase,
this.derivationType,
this.derivationPath,
@ -44,6 +45,8 @@ class ElectrumWalletSnapshot {
List<BitcoinAddressRecord> addresses;
List<BitcoinSilentPaymentAddressRecord> silentAddresses;
List<BitcoinAddressRecord> mwebAddresses;
ElectrumBalance balance;
Map<String, int> regularAddressIndex;
Map<String, int> changeAddressIndex;
@ -56,10 +59,11 @@ class ElectrumWalletSnapshot {
final path = await pathForWallet(name: name, type: type);
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List? ?? <Object>[];
final mnemonic = data['mnemonic'] as String?;
final xpub = data['xpub'] as String?;
final passphrase = data['passphrase'] as String? ?? '';
final addressesTmp = data['addresses'] as List? ?? <Object>[];
final addresses = addressesTmp
.whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
@ -71,6 +75,12 @@ class ElectrumWalletSnapshot {
.map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network))
.toList();
final mwebAddressTmp = data['mweb_addresses'] as List? ?? <Object>[];
final mwebAddresses = mwebAddressTmp
.whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
.toList();
final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ??
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
@ -113,6 +123,7 @@ class ElectrumWalletSnapshot {
derivationPath: derivationPath,
silentAddresses: silentAddresses,
silentAddressIndex: silentAddressIndex,
mwebAddresses: mwebAddresses,
);
}
}

View file

@ -1,15 +1,31 @@
import 'dart:async';
import 'dart:convert';
import 'package:convert/convert.dart' as convert;
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart';
import 'package:fixnum/fixnum.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
@ -19,8 +35,11 @@ import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
import 'package:grpc/grpc.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_mweb/cw_mweb.dart';
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
import 'package:pointycastle/ecc/api.dart';
import 'package:pointycastle/ecc/curves/secp256k1.dart';
@ -40,34 +59,54 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
List<BitcoinAddressRecord>? initialMwebAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
int? initialMwebHeight,
bool? alwaysScan,
}) : super(
mnemonic: mnemonic,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
network: LitecoinNetwork.mainnet,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase,
currency: CryptoCurrency.ltc) {
mnemonic: mnemonic,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
network: LitecoinNetwork.mainnet,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
currency: CryptoCurrency.ltc,
) {
mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
mwebEnabled = alwaysScan ?? false;
walletAddresses = LitecoinWalletAddresses(
walletInfo,
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
initialMwebAddresses: initialMwebAddresses,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network,
mwebHd: mwebHd,
mwebEnabled: mwebEnabled,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
}
late final Bip32Slip10Secp256k1 mwebHd;
late final Box<MwebUtxo> mwebUtxosBox;
Timer? _syncTimer;
Timer? _feeRatesTimer;
Timer? _processingTimer;
StreamSubscription<Utxo>? _utxoStream;
late RpcClient _stub;
late bool mwebEnabled;
bool processingUtxos = false;
List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;
static Future<LitecoinWallet> create(
{required String mnemonic,
@ -78,6 +117,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
List<BitcoinAddressRecord>? initialMwebAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex}) async {
@ -101,6 +141,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialMwebAddresses: initialMwebAddresses,
initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase,
@ -111,12 +152,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
);
}
static Future<LitecoinWallet> open(
{required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required EncryptionFileUtils encryptionFileUtils}) async {
static Future<LitecoinWallet> open({
required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required bool alwaysScan,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
ElectrumWalletSnapshot? snp = null;
@ -178,6 +221,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp?.addresses,
initialMwebAddresses: snp?.mwebAddresses,
initialBalance: snp?.balance,
seedBytes: seedBytes!,
passphrase: passphrase,
@ -185,6 +229,551 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp?.addressPageType,
alwaysScan: alwaysScan,
);
}
Future<void> waitForMwebAddresses() async {
// ensure that we have the full 1000 mweb addresses generated before continuing:
// should no longer be needed, but leaving here just in case
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
while (mwebAddrs.length < 1000) {
print("waiting for mweb addresses to finish generating...");
await Future.delayed(const Duration(milliseconds: 1000));
}
}
@action
@override
Future<void> startSync() async {
if (syncStatus is SyncronizingSyncStatus) {
return;
}
print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
try {
syncStatus = SyncronizingSyncStatus();
await subscribeForUpdates();
updateFeeRates();
_feeRatesTimer?.cancel();
_feeRatesTimer =
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
if (!mwebEnabled) {
try {
await updateAllUnspents();
await updateTransactions();
await updateBalance();
syncStatus = SyncedSyncStatus();
} catch (e, s) {
print(e);
print(s);
syncStatus = FailedSyncStatus();
}
return;
}
await waitForMwebAddresses();
await getStub();
await processMwebUtxos();
await updateTransactions();
await updateUnspent();
await updateBalance();
} catch (e) {
print("failed to start mweb sync: $e");
syncStatus = FailedSyncStatus();
return;
}
_syncTimer?.cancel();
_syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async {
if (syncStatus is FailedSyncStatus) return;
final nodeHeight =
await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
final resp = await CwMweb.status(StatusRequest());
print("resp.mwebUtxosHeight: ${resp.mwebUtxosHeight}");
print("resp.mwebHeaderHeight: ${resp.mwebHeaderHeight}");
print("resp.blockHeaderHeight: ${resp.blockHeaderHeight}");
if (resp.blockHeaderHeight < nodeHeight) {
int h = resp.blockHeaderHeight;
syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
} else if (resp.mwebHeaderHeight < nodeHeight) {
int h = resp.mwebHeaderHeight;
syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight);
} else if (resp.mwebUtxosHeight < nodeHeight) {
syncStatus = SyncingSyncStatus(1, 0.999);
} else {
if (resp.mwebUtxosHeight > walletInfo.restoreHeight) {
await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight);
await checkMwebUtxosSpent();
// update the confirmations for each transaction:
for (final transaction in transactionHistory.transactions.values) {
if (transaction.isPending) continue;
int txHeight = transaction.height ?? resp.mwebUtxosHeight;
final confirmations = (resp.mwebUtxosHeight - txHeight) + 1;
if (transaction.confirmations == confirmations) continue;
transaction.confirmations = confirmations;
transactionHistory.addOne(transaction);
}
await transactionHistory.save();
}
// prevent unnecessary reaction triggers:
if (syncStatus is! SyncedSyncStatus) {
// mwebd is synced, but we could still be processing incoming utxos:
if (!processingUtxos) {
syncStatus = SyncedSyncStatus();
}
}
return;
}
});
}
@action
@override
Future<void> stopSync() async {
_syncTimer?.cancel();
_utxoStream?.cancel();
_feeRatesTimer?.cancel();
await CwMweb.stop();
}
Future<void> initMwebUtxosBox() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(boxName);
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
// rename the hive box:
final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
final oldBox = await CakeHive.openBox<MwebUtxo>(oldBoxName);
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
for (final key in oldBox.keys) {
await mwebUtxosBox.put(key, oldBox.get(key)!);
}
oldBox.deleteFromDisk();
await super.renameWalletFiles(newWalletName);
}
@action
@override
Future<void> rescan({
required int height,
int? chainTip,
ScanData? scanData,
bool? doSingleScan,
bool? usingElectrs,
}) async {
_syncTimer?.cancel();
int oldHeight = walletInfo.restoreHeight;
await walletInfo.updateRestoreHeight(height);
// go through mwebUtxos and clear any that are above the new restore height:
if (height == 0) {
await mwebUtxosBox.clear();
transactionHistory.clear();
} else {
for (final utxo in mwebUtxosBox.values) {
if (utxo.height > height) {
await mwebUtxosBox.delete(utxo.outputId);
}
}
// TODO: remove transactions that are above the new restore height!
}
// reset coin balances and txCount to 0:
unspentCoins.forEach((coin) {
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance = 0;
coin.bitcoinAddressRecord.txCount = 0;
});
for (var addressRecord in walletAddresses.allAddresses) {
addressRecord.balance = 0;
addressRecord.txCount = 0;
}
await startSync();
}
@override
Future<void> init() async {
await super.init();
await initMwebUtxosBox();
}
Future<void> handleIncoming(MwebUtxo utxo, RpcClient stub) async {
final status = await stub.status(StatusRequest());
var date = DateTime.now();
var confirmations = 0;
if (utxo.height > 0) {
date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000);
confirmations = status.blockHeaderHeight - utxo.height + 1;
}
var tx = transactionHistory.transactions.values
.firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false);
if (tx == null) {
tx = ElectrumTransactionInfo(
WalletType.litecoin,
id: utxo.outputId,
height: utxo.height,
amount: utxo.value.toInt(),
fee: 0,
direction: TransactionDirection.incoming,
isPending: utxo.height == 0,
date: date,
confirmations: confirmations,
inputAddresses: [],
outputAddresses: [utxo.outputId],
isReplaced: false,
);
}
// don't update the confirmations if the tx is updated by electrum:
if (tx.confirmations == 0 || utxo.height != 0) {
tx.height = utxo.height;
tx.isPending = utxo.height == 0;
tx.confirmations = confirmations;
}
bool isNew = transactionHistory.transactions[tx.id] == null;
if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) {
tx.outputAddresses?.add(utxo.address);
isNew = true;
}
if (isNew) {
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
if (addressRecord == null) {
print("we don't have this address in the wallet! ${utxo.address}");
return;
}
// update the txCount:
addressRecord.txCount++;
addressRecord.balance += utxo.value.toInt();
addressRecord.setAsUsed();
}
transactionHistory.addOne(tx);
if (isNew) {
// update the unconfirmed balance when a new tx is added:
// we do this after adding the tx to the history so that sub address balances are updated correctly
// (since that calculation is based on the tx history)
await updateBalance();
}
}
Future<void> processMwebUtxos() async {
if (!mwebEnabled) {
return;
}
int restoreHeight = walletInfo.restoreHeight;
print("SCANNING FROM HEIGHT: $restoreHeight");
final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight);
// process new utxos as they come in:
_utxoStream?.cancel();
ResponseStream<Utxo>? responseStream = await CwMweb.utxos(req);
if (responseStream == null) {
throw Exception("failed to get utxos stream!");
}
_utxoStream = responseStream.listen((Utxo sUtxo) async {
// we're processing utxos, so our balance could still be innacurate:
if (syncStatus is! SyncronizingSyncStatus && syncStatus is! SyncingSyncStatus) {
syncStatus = SyncronizingSyncStatus();
processingUtxos = true;
_processingTimer?.cancel();
_processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
processingUtxos = false;
timer.cancel();
});
}
final utxo = MwebUtxo(
address: sUtxo.address,
blockTime: sUtxo.blockTime,
height: sUtxo.height,
outputId: sUtxo.outputId,
value: sUtxo.value.toInt(),
);
// if (mwebUtxosBox.containsKey(utxo.outputId)) {
// // we've already stored this utxo, skip it:
// return;
// }
await updateUnspent();
await updateBalance();
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
// don't process utxos with addresses that are not in the mwebAddrs list:
if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) {
return;
}
await mwebUtxosBox.put(utxo.outputId, utxo);
await handleIncoming(utxo, _stub);
});
}
Future<void> checkMwebUtxosSpent() async {
if (!mwebEnabled) {
return;
}
final pendingOutgoingTransactions = transactionHistory.transactions.values
.where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending);
// check if any of the pending outgoing transactions are now confirmed:
bool updatedAny = false;
for (final tx in pendingOutgoingTransactions) {
updatedAny = await isConfirmed(tx) || updatedAny;
}
// get output ids of all the mweb utxos that have > 0 height:
final outputIds =
mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList();
final resp = await CwMweb.spent(SpentRequest(outputId: outputIds));
final spent = resp.outputId;
if (spent.isEmpty) {
return;
}
final status = await CwMweb.status(StatusRequest());
final height = await electrumClient.getCurrentBlockChainTip();
if (height == null || status.blockHeaderHeight != height) return;
if (status.mwebUtxosHeight != height) return; // we aren't synced
int amount = 0;
Set<String> inputAddresses = {};
var output = convert.AccumulatorSink<Digest>();
var input = sha256.startChunkedConversion(output);
for (final outputId in spent) {
final utxo = mwebUtxosBox.get(outputId);
await mwebUtxosBox.delete(outputId);
if (utxo == null) continue;
final addressRecord = walletAddresses.allAddresses
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
if (!inputAddresses.contains(utxo.address)) {
addressRecord.txCount++;
}
addressRecord.balance -= utxo.value.toInt();
amount += utxo.value.toInt();
inputAddresses.add(utxo.address);
input.add(hex.decode(outputId));
}
if (inputAddresses.isEmpty) return;
input.close();
var digest = output.events.single;
final tx = ElectrumTransactionInfo(
WalletType.litecoin,
id: digest.toString(),
height: height,
amount: amount,
fee: 0,
direction: TransactionDirection.outgoing,
isPending: false,
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
confirmations: 1,
inputAddresses: inputAddresses.toList(),
outputAddresses: [],
isReplaced: false,
);
transactionHistory.addOne(tx);
await transactionHistory.save();
if (updatedAny) {
await updateBalance();
}
}
// checks if a pending transaction is now confirmed, and updates the tx info accordingly:
Future<bool> isConfirmed(ElectrumTransactionInfo tx) async {
if (!mwebEnabled) return false;
if (!tx.isPending) return false;
final outputId = <String>[], target = <String>{};
final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch;
final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? [];
final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? [];
outputId.addAll(spendingOutputIds);
outputId.addAll(payingToOutputIds);
target.addAll(spendingOutputIds);
for (final outputId in payingToOutputIds) {
final spendingTx = transactionHistory.transactions.values
.firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false);
if (spendingTx != null && !spendingTx.isPending) {
target.add(outputId);
}
}
if (outputId.isEmpty) {
return false;
}
final resp = await CwMweb.spent(SpentRequest(outputId: outputId));
if (!setEquals(resp.outputId.toSet(), target)) {
return false;
}
final status = await CwMweb.status(StatusRequest());
tx.height = status.mwebUtxosHeight;
tx.confirmations = 1;
tx.isPending = false;
await transactionHistory.save();
return true;
}
Future<void> updateUnspent() async {
await checkMwebUtxosSpent();
await updateAllUnspents();
}
@override
@action
Future<void> updateAllUnspents() async {
// get ltc unspents:
await super.updateAllUnspents();
if (!mwebEnabled) {
return;
}
await getStub();
// add the mweb unspents to the list:
List<BitcoinUnspent> mwebUnspentCoins = [];
// update mweb unspents:
final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs;
mwebUtxosBox.keys.forEach((dynamic oId) {
final String outputId = oId as String;
final utxo = mwebUtxosBox.get(outputId);
if (utxo == null) {
return;
}
if (utxo.address.isEmpty) {
// not sure if a bug or a special case but we definitely ignore these
return;
}
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address);
if (addressRecord == null) {
print("utxo contains an address that is not in the wallet: ${utxo.address}");
return;
}
final unspent = BitcoinUnspent(
addressRecord,
outputId,
utxo.value.toInt(),
mwebAddrs.indexOf(utxo.address),
);
if (unspent.vout == 0) {
unspent.isChange = true;
}
mwebUnspentCoins.add(unspent);
});
unspentCoins.addAll(mwebUnspentCoins);
}
@override
Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances();
if (!mwebEnabled) {
return balance;
}
await getStub();
// update unspent balances:
await updateUnspent();
int confirmed = balance.confirmed;
int unconfirmed = balance.unconfirmed;
int confirmedMweb = 0;
int unconfirmedMweb = 0;
try {
mwebUtxosBox.values.forEach((utxo) {
if (utxo.height > 0) {
confirmedMweb += utxo.value.toInt();
} else {
unconfirmedMweb += utxo.value.toInt();
}
});
if (unconfirmedMweb > 0) {
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
}
} catch (_) {}
for (var addressRecord in walletAddresses.allAddresses) {
addressRecord.balance = 0;
addressRecord.txCount = 0;
}
unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
super.addCoinInfo(coin);
}
});
// update the txCount for each address using the tx history, since we can't rely on mwebd
// to have an accurate count, we should just keep it in sync with what we know from the tx history:
for (final tx in transactionHistory.transactions.values) {
// if (tx.isPending) continue;
if (tx.inputAddresses == null || tx.outputAddresses == null) {
continue;
}
final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
for (final address in txAddresses) {
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == address);
if (addressRecord == null) {
continue;
}
addressRecord.txCount++;
}
}
return ElectrumBalance(
confirmed: confirmed,
unconfirmed: unconfirmed,
frozen: balance.frozen,
secondConfirmed: confirmedMweb,
secondUnconfirmed: unconfirmedMweb,
);
}
@ -204,6 +793,227 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return 0;
}
@override
Future<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
required BasedUtxoNetwork network,
String? memo,
required int feeRate,
List<ECPrivateInfo>? inputPrivKeyInfos,
List<Outpoint>? vinOutpoints,
}) async {
final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
final paysToMweb = outputs
.any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
if (!spendsMweb && !paysToMweb) {
return await super.calcFee(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
feeRate: feeRate,
inputPrivKeyInfos: inputPrivKeyInfos,
vinOutpoints: vinOutpoints,
);
}
if (!mwebEnabled) {
throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
}
if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) {
outputs = [
BitcoinScriptOutput(
script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue())
];
}
// https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation
final preOutputSum =
outputs.fold<BigInt>(BigInt.zero, (acc, output) => acc + output.toOutput.amount);
final fee = utxos.sumOfUtxosValue() - preOutputSum;
final txb =
BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network);
final resp = await CwMweb.create(CreateRequest(
rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(),
scanSecret: scanSecret,
spendSecret: spendSecret,
feeRatePerKb: Int64(feeRate * 1000),
dryRun: true));
final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
final posUtxos = utxos
.where((utxo) => tx.inputs
.any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout))
.toList();
final posOutputSum = tx.outputs.fold<int>(0, (acc, output) => acc + output.amount.toInt());
final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue();
final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt());
var feeIncrease = posOutputSum - expectedPegin;
if (expectedPegin > 0 && fee == BigInt.zero) {
feeIncrease += await super.calcFee(
utxos: posUtxos,
outputs: tx.outputs
.map((output) =>
BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount))
.toList(),
network: network,
memo: memo,
feeRate: feeRate) +
feeRate * 41;
}
return fee.toInt() + feeIncrease;
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction;
tx.isMweb = mwebEnabled;
if (!mwebEnabled) {
return tx;
}
await waitForMwebAddresses();
await getStub();
final resp = await CwMweb.create(CreateRequest(
rawTx: hex.decode(tx.hex),
scanSecret: scanSecret,
spendSecret: spendSecret,
feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000,
));
final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
// check if the transaction doesn't contain any mweb inputs or outputs:
final transactionCredentials = credentials as BitcoinTransactionCredentials;
bool hasMwebInput = false;
bool hasMwebOutput = false;
for (final output in transactionCredentials.outputs) {
if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) {
hasMwebOutput = true;
break;
}
}
if (tx2.mwebBytes != null && tx2.mwebBytes!.isNotEmpty) {
hasMwebInput = true;
}
if (!hasMwebInput && !hasMwebOutput) {
return tx;
}
// check if any of the inputs of this transaction are hog-ex:
// this list is only non-mweb inputs:
tx2.inputs.forEach((txInput) {
bool isHogEx = true;
final utxo = unspentCoins
.firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex);
// TODO: detect actual hog-ex inputs
if (!isHogEx) {
return;
}
int confirmations = utxo.confirmations ?? 0;
if (confirmations < 6) {
throw Exception(
"A transaction input has less than 6 confirmations, please try again later.");
}
});
tx.hexOverride = tx2
.copyWith(
witnesses: tx2.inputs.asMap().entries.map((e) {
final utxo = unspentCoins
.firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
final key = generateECPrivate(
hd: utxo.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: utxo.bitcoinAddressRecord.index,
network: network);
final digest = tx2.getTransactionSegwitDigit(
txInIndex: e.key,
script: key.getPublic().toP2pkhAddress().toScriptPubKey(),
amount: BigInt.from(utxo.value),
);
return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]);
}).toList())
.toHex();
tx.outputAddresses = resp.outputId;
return tx
..addListener((transaction) async {
final addresses = <String>{};
transaction.inputAddresses?.forEach((id) async {
final utxo = mwebUtxosBox.get(id);
// await mwebUtxosBox.delete(id);// gets deleted in checkMwebUtxosSpent
if (utxo == null) return;
final addressRecord = walletAddresses.allAddresses
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
if (!addresses.contains(utxo.address)) {
addresses.add(utxo.address);
}
addressRecord.balance -= utxo.value.toInt();
});
transaction.inputAddresses?.addAll(addresses);
transactionHistory.addOne(transaction);
await updateUnspent();
await updateBalance();
});
} catch (e, s) {
print(e);
print(s);
if (e.toString().contains("commit failed")) {
throw Exception("Transaction commit failed (no peers responded), please try again.");
}
rethrow;
}
}
@override
Future<void> save() async {
await super.save();
}
@override
Future<void> close() async {
_utxoStream?.cancel();
_feeRatesTimer?.cancel();
_syncTimer?.cancel();
_processingTimer?.cancel();
await stopSync();
await super.close();
}
Future<void> setMwebEnabled(bool enabled) async {
if (mwebEnabled == enabled) {
return;
}
mwebEnabled = enabled;
(walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled;
await stopSync();
await startSync();
}
Future<RpcClient> getStub() async {
_stub = await CwMweb.stub();
return _stub;
}
Future<StatusResponse> getStatusRequest() async {
final resp = await CwMweb.status(StatusRequest());
return resp;
}
@override
Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null

View file

@ -1,8 +1,15 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_mweb/cw_mweb.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
part 'litecoin_wallet_addresses.g.dart';
@ -15,15 +22,145 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
required super.mainHd,
required super.sideHd,
required super.network,
required this.mwebHd,
required this.mwebEnabled,
super.initialAddresses,
super.initialMwebAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
}) : super(walletInfo);
}) : super(walletInfo) {
for (int i = 0; i < mwebAddresses.length; i++) {
mwebAddrs.add(mwebAddresses[i].address);
}
print("initialized with ${mwebAddrs.length} mweb addresses");
}
final Bip32Slip10Secp256k1 mwebHd;
bool mwebEnabled;
int mwebTopUpIndex = 1000;
List<String> mwebAddrs = [];
List<int> get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendPubkey =>
mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed;
@override
String getAddress(
{required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType}) =>
generateP2WPKHAddress(hd: hd, index: index, network: network);
Future<void> init() async {
await initMwebAddresses();
await super.init();
}
@computed
@override
List<BitcoinAddressRecord> get allAddresses {
return List.from(super.allAddresses)..addAll(mwebAddresses);
}
Future<void> ensureMwebAddressUpToIndexExists(int index) async {
Uint8List scan = Uint8List.fromList(scanSecret);
Uint8List spend = Uint8List.fromList(spendPubkey);
int count = 0;
while (mwebAddrs.length <= (index + 1)) {
final address = await CwMweb.address(scan, spend, mwebAddrs.length);
mwebAddrs.add(address!);
count++;
// sleep for a bit to avoid making the main thread unresponsive:
if (count > 50) {
count = 0;
await Future.delayed(Duration(milliseconds: 100));
}
}
}
Future<void> initMwebAddresses() async {
if (mwebAddrs.length < 1000) {
print("Generating MWEB addresses...");
await ensureMwebAddressUpToIndexExists(1020);
print("done generating MWEB addresses");
List<BitcoinAddressRecord> addressRecords = mwebAddrs
.asMap()
.entries
.map((e) => BitcoinAddressRecord(
e.value,
index: e.key,
type: SegwitAddresType.mweb,
network: network,
))
.toList();
addMwebAddresses(addressRecords);
print("added ${addressRecords.length} mweb addresses");
return;
}
}
@override
String getAddress({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) {
if (addressType == SegwitAddresType.mweb) {
return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
}
return generateP2WPKHAddress(hd: hd, index: index, network: network);
}
@override
Future<String> getAddressAsync({
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) async {
if (addressType == SegwitAddresType.mweb) {
await ensureMwebAddressUpToIndexExists(index);
}
return getAddress(index: index, hd: hd, addressType: addressType);
}
@action
@override
Future<String> getChangeAddress({List<BitcoinOutput>? outputs, UtxoDetails? utxoDetails}) async {
// use regular change address on peg in, otherwise use mweb for change address:
if (!mwebEnabled) {
return super.getChangeAddress();
}
if (outputs != null && utxoDetails != null) {
// check if this is a PEGIN:
bool outputsToMweb = false;
bool comesFromMweb = false;
for (var i = 0; i < outputs.length; i++) {
// TODO: probably not the best way to tell if this is an mweb address
// (but it doesn't contain the "mweb" text at this stage)
if (outputs[i].address.toAddress(network).length > 110) {
outputsToMweb = true;
}
}
// TODO: this doesn't respect coin control because it doesn't know which available inputs are selected
utxoDetails.availableInputs.forEach((element) {
if (element.address.contains("mweb")) {
comesFromMweb = true;
}
});
bool isPegIn = !comesFromMweb && outputsToMweb;
if (isPegIn && mwebEnabled) {
return super.getChangeAddress();
}
// use regular change address if it's not an mweb tx:
if (!comesFromMweb && !outputsToMweb) {
return super.getChangeAddress();
}
}
if (mwebEnabled) {
await ensureMwebAddressUpToIndexExists(1);
return mwebAddrs[0];
}
return super.getChangeAddress();
}
}

View file

@ -1,10 +1,10 @@
import 'dart:io';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:hive/hive.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
import 'package:cw_core/wallet_service.dart';
@ -14,16 +14,19 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:path_provider/path_provider.dart';
class LitecoinWalletService extends WalletService<
BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinNewWalletCredentials> {
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
LitecoinWalletService(
this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan;
final bool isDirect;
@override
@ -64,6 +67,7 @@ class LitecoinWalletService extends WalletService<
@override
Future<LitecoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
@ -73,6 +77,7 @@ class LitecoinWalletService extends WalletService<
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -85,6 +90,7 @@ class LitecoinWalletService extends WalletService<
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -98,6 +104,23 @@ class LitecoinWalletService extends WalletService<
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
// if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd:
if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) {
final appDirPath = (await getApplicationSupportDirectory()).path;
File neturinoDb = File('$appDirPath/neutrino.db');
File blockHeaders = File('$appDirPath/block_headers.bin');
File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin');
if (neturinoDb.existsSync()) {
neturinoDb.deleteSync();
}
if (blockHeaders.existsSync()) {
blockHeaders.deleteSync();
}
if (regFilterHeaders.existsSync()) {
regFilterHeaders.deleteSync();
}
}
}
@override
@ -109,6 +132,7 @@ class LitecoinWalletService extends WalletService<
name: currentName,
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);

View file

@ -1,11 +1,15 @@
import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_mweb/cw_mweb.dart';
import 'package:cw_mweb/mwebd.pb.dart';
class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(
@ -19,6 +23,7 @@ class PendingBitcoinTransaction with PendingTransaction {
required this.hasChange,
this.isSendAll = false,
this.hasTaprootInputs = false,
this.isMweb = false,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@ -28,15 +33,19 @@ class PendingBitcoinTransaction with PendingTransaction {
final int fee;
final String feeRate;
final BasedUtxoNetwork? network;
final bool hasChange;
final bool isSendAll;
final bool hasChange;
final bool hasTaprootInputs;
bool isMweb;
String? idOverride;
String? hexOverride;
List<String>? outputAddresses;
@override
String get id => _tx.txId();
String get id => idOverride ?? _tx.txId();
@override
String get hex => _tx.serialize();
String get hex => hexOverride ?? _tx.serialize();
@override
String get amountFormatted => bitcoinAmountToString(amount: amount);
@ -62,8 +71,7 @@ class PendingBitcoinTransaction with PendingTransaction {
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
Future<void> _commit() async {
int? callId;
final result = await electrumClient.broadcastTransaction(
@ -100,6 +108,25 @@ class PendingBitcoinTransaction with PendingTransaction {
throw BitcoinTransactionCommitFailed();
}
}
Future<void> _ltcCommit() async {
try {
final stub = await CwMweb.stub();
final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
idOverride = resp.txid;
} on GrpcError catch (e) {
throw BitcoinTransactionCommitFailed(errorMessage: e.message);
}
}
@override
Future<void> commit() async {
if (isMweb) {
await _ltcCommit();
} else {
await _commit();
}
_listeners.forEach((listener) => listener(transactionInfo()));
}
@ -116,5 +143,7 @@ class PendingBitcoinTransaction with PendingTransaction {
isPending: true,
isReplaced: false,
confirmations: 0,
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
outputAddresses: outputAddresses,
fee: fee);
}

View file

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.7.0"
archive:
dependency: transitive
description:
name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
url: "https://pub.dev"
source: hosted
version: "3.4.10"
args:
dependency: transitive
description:
@ -29,10 +37,10 @@ packages:
dependency: transitive
description:
name: asn1lib
sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda"
sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70"
url: "https://pub.dev"
source: hosted
version: "1.5.3"
version: "1.5.5"
async:
dependency: transitive
description:
@ -71,7 +79,7 @@ packages:
description:
path: "."
ref: cake-update-v7
resolved-ref: bc49e3b1cba601828f8ddc3d016188d8c2499088
resolved-ref: f577e83fe78766b2655ea0602baa9299b953a31b
url: "https://github.com/cake-tech/bitcoin_base"
source: git
version: "4.7.0"
@ -260,6 +268,13 @@ packages:
relative: true
source: path
version: "0.0.1"
cw_mweb:
dependency: "direct main"
description:
path: "../cw_mweb"
relative: true
source: path
version: "0.0.1"
dart_style:
dependency: transitive
description:
@ -296,10 +311,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
ffigen:
dependency: transitive
description:
@ -379,6 +394,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
googleapis_auth:
dependency: transitive
description:
name: googleapis_auth
sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da
url: "https://pub.dev"
source: hosted
version: "1.4.1"
graphs:
dependency: transitive
description:
@ -387,6 +410,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
grpc:
dependency: "direct main"
description:
name: grpc
sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40
url: "https://pub.dev"
source: hosted
version: "3.2.4"
hex:
dependency: transitive
description:
@ -553,10 +584,10 @@ packages:
dependency: transitive
description:
name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
version: "1.0.6"
mobx:
dependency: "direct main"
description:
@ -713,10 +744,10 @@ packages:
dependency: transitive
description:
name: quiver
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
reactive_ble_mobile:
dependency: transitive
description:
@ -855,7 +886,7 @@ packages:
description:
path: "."
ref: "sp_v4.0.0"
resolved-ref: "9b04f4b0af80dd7dae9274b496a53c23dcc80ea5"
resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2
url: "https://github.com/cake-tech/sp_scanner"
source: git
version: "0.0.1"

View file

@ -34,11 +34,16 @@ dependencies:
ledger_bitcoin:
git:
url: https://github.com/cake-tech/ledger-bitcoin
cw_mweb:
path: ../cw_mweb
grpc: ^3.2.4
sp_scanner:
git:
url: https://github.com/cake-tech/sp_scanner
ref: sp_v4.0.0
bech32:
git:
url: https://github.com/cake-tech/bech32.git
dev_dependencies:
flutter_test:
@ -54,10 +59,13 @@ dependency_overrides:
url: https://github.com/cake-tech/ledger-flutter.git
ref: cake-v3
watcher: ^1.1.0
protobuf: ^3.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v7
ref: cake-update-v8
pointycastle: 3.7.4
ffi: 2.1.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View file

@ -42,7 +42,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v7
ref: cake-update-v8
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View file

@ -1,13 +1,17 @@
abstract class Balance {
const Balance(this.available, this.additional);
const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional});
final int available;
final int additional;
final int? secondAvailable;
final int? secondAdditional;
String get formattedAvailableBalance;
String get formattedAdditionalBalance;
String get formattedUnAvailableBalance => '';
String get formattedSecondAvailableBalance => '';
String get formattedSecondAdditionalBalance => '';
String get formattedFullAvailableBalance => formattedAvailableBalance;
}

View file

@ -174,11 +174,11 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8);
static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8);
static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8);
static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8);
static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18);
static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18);
static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18);
static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18);
static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18);
static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24);
@ -215,8 +215,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18);
static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18);
static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29);
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8);
static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);

View file

@ -310,6 +310,11 @@ DateTime getDateByBitcoinHeight(int height) {
return estimatedDate;
}
int getLtcHeightByDate({required DateTime date}) {
// TODO: use the proxy layer to get the height with a binary search of blocked header heights
return 0;
}
// TODO: enhance all of this global const lists
const wowDates = {
"2023-12": 583048,

View file

@ -18,3 +18,4 @@ const SPL_TOKEN_TYPE_ID = 16;
const DERIVATION_INFO_TYPE_ID = 17;
const TRON_TOKEN_TYPE_ID = 18;
const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
const MWEB_UTXO_TYPE_ID = 20;

View file

@ -0,0 +1,33 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'mweb_utxo.g.dart';
@HiveType(typeId: MWEB_UTXO_TYPE_ID)
class MwebUtxo extends HiveObject {
MwebUtxo({
required this.height,
required this.value,
required this.address,
required this.outputId,
required this.blockTime,
});
static const typeId = MWEB_UTXO_TYPE_ID;
static const boxName = 'MwebUtxo';
@HiveField(0)
int height;
@HiveField(1)
int value;
@HiveField(2)
String address;
@HiveField(3)
String outputId;
@HiveField(4)
int blockTime;
}

View file

@ -1,12 +1,22 @@
class Subaddress {
Subaddress({required this.id, required this.address, required this.label});
Subaddress({
required this.id,
required this.address,
required this.label,
this.balance = null,
this.txCount = null,
});
Subaddress.fromMap(Map<String, Object?> map)
: this.id = map['id'] == null ? 0 : int.parse(map['id'] as String),
this.address = (map['address'] ?? '') as String,
this.label = (map['label'] ?? '') as String;
this.label = (map['label'] ?? '') as String,
this.balance = (map['balance'] ?? '') as String?,
this.txCount = (map['txCount'] ?? '') as int?;
final int id;
final String address;
final String label;
final String? balance;
final int? txCount;
}

View file

@ -89,4 +89,4 @@ class TimedOutSyncStatus extends NotConnectedSyncStatus {
class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Reconnecting';
}
}

View file

@ -1,26 +1,58 @@
import 'package:cw_core/address_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
abstract class WalletAddresses {
WalletAddresses(this.walletInfo)
: addressesMap = {},
allAddressesMap = {},
addressInfos = {};
addressInfos = {},
usedAddresses = {},
hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {},
manualAddresses = walletInfo.manualAddresses?.toSet() ?? {};
final WalletInfo walletInfo;
String get address;
String get latestAddress {
if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) {
if (addressesMap.keys.length == 0) return address;
return addressesMap[addressesMap.keys.last] ?? address;
}
return _localAddress ?? address;
}
String? get primaryAddress => null;
set address(String address);
String? _localAddress;
set address(String address) => _localAddress = address;
String get addressForExchange => address;
Map<String, String> addressesMap;
Map<String, String> allAddressesMap;
Map<String, String> get usableAddressesMap {
final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map
tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key));
return tmp;
}
Map<String, String> get usableAllAddressesMap {
final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map
tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key));
return tmp;
}
Map<int, List<AddressInfo>> addressInfos;
Set<String> usedAddresses = {};
Set<String> usedAddresses;
Set<String> hiddenAddresses;
Set<String> manualAddresses;
Future<void> init();
@ -32,6 +64,8 @@ abstract class WalletAddresses {
walletInfo.addresses = addressesMap;
walletInfo.addressInfos = addressInfos;
walletInfo.usedAddresses = usedAddresses.toList();
walletInfo.hiddenAddresses = hiddenAddresses.toList();
walletInfo.manualAddresses = manualAddresses.toList();
if (walletInfo.isInBox) {
await walletInfo.save();

View file

@ -67,6 +67,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
Future<void> startSync();
Future<void> stopSync() async {}
Future<PendingTransaction> createTransaction(Object credentials);
int calculateEstimatedFee(TransactionPriority priority, int? amount);

View file

@ -189,6 +189,15 @@ class WalletInfo extends HiveObject {
@HiveField(22)
String? parentAddress;
@HiveField(23)
List<String>? hiddenAddresses;
@HiveField(24)
List<String>? manualAddresses;
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';

View file

@ -35,7 +35,6 @@ android {
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}

View file

@ -1,6 +1,7 @@
import 'package:cw_core/wallet_addresses_with_account.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/account.dart';
import 'package:cw_haven/api/wallet.dart';
import 'package:cw_haven/haven_account_list.dart';
import 'package:cw_haven/haven_subaddress_list.dart';
import 'package:cw_core/subaddress.dart';
@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
@override
Future<void> init() async {
accountList.update();
account = accountList.accounts.first;
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
updateSubaddressList(accountIndex: account?.id ?? 0);
await updateAddressesInBox();
}
@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
void updateSubaddressList({required int accountIndex}) {
subaddressList.update(accountIndex: accountIndex);
subaddress = subaddressList.subaddresses.first;
address = subaddress!.address;
address = subaddressList.subaddresses.isNotEmpty
? subaddressList.subaddresses.first.address
: getAddress();
}
@override

View file

@ -1,5 +1,6 @@
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/transaction_history.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:monero/monero.dart' as monero;
@ -14,6 +15,10 @@ class SubaddressInfoMetadata {
SubaddressInfoMetadata? subaddress = null;
String getRawLabel({required int accountIndex, required int addressIndex}) {
return monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
}
void refreshSubaddresses({required int accountIndex}) {
try {
isUpdating = true;
@ -29,31 +34,94 @@ class Subaddress {
Subaddress({
required this.addressIndex,
required this.accountIndex,
required this.received,
required this.txCount,
});
String get address => monero.Wallet_address(
wptr!,
accountIndex: accountIndex,
addressIndex: addressIndex,
);
late String address = getAddress(
accountIndex: accountIndex,
addressIndex: addressIndex,
);
final int addressIndex;
final int accountIndex;
String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
final int received;
final int txCount;
String get label {
final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen
return "#$addressIndex ${localLabel}".trim();
}
}
class TinyTransactionDetails {
TinyTransactionDetails({
required this.address,
required this.amount,
});
final List<String> address;
final int amount;
}
int lastWptr = 0;
int lastTxCount = 0;
List<TinyTransactionDetails> ttDetails = [];
List<Subaddress> getAllSubaddresses() {
txhistory = monero.Wallet_history(wptr!);
final txCount = monero.TransactionHistory_count(txhistory!);
if (lastTxCount != txCount && lastWptr != wptr!.address) {
final List<TinyTransactionDetails> newttDetails = [];
lastTxCount = txCount;
lastWptr = wptr!.address;
for (var i = 0; i < txCount; i++) {
final tx = monero.TransactionHistory_transaction(txhistory!, index: i);
if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue;
final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(",");
final account = monero.TransactionInfo_subaddrAccount(tx);
newttDetails.add(TinyTransactionDetails(
address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
amount: monero.TransactionInfo_amount(tx),
));
}
ttDetails.clear();
ttDetails.addAll(newttDetails);
}
final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
final list = List.generate(size, (index) {
final ttDetailsLocal = ttDetails.where((element) {
final address = getAddress(
accountIndex: subaddress!.accountIndex,
addressIndex: index,
);
if (element.address.contains(address)) return true;
return false;
}).toList();
int received = 0;
for (var i = 0; i < ttDetailsLocal.length; i++) {
received += ttDetailsLocal[i].amount;
}
return Subaddress(
accountIndex: subaddress!.accountIndex,
addressIndex: index,
received: received,
txCount: ttDetailsLocal.length,
);
}).reversed.toList();
if (list.length == 0) {
list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0));
list.add(
Subaddress(
addressIndex: subaddress!.accountIndex,
accountIndex: 0,
received: 0,
txCount: 0,
));
}
return list;
}
int numSubaddresses(int subaccountIndex) {
return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex);
}
void addSubaddressSync({required int accountIndex, required String label}) {
monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label);
refreshSubaddresses(accountIndex: accountIndex);

View file

@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_monero/api/monero_output.dart';
import 'package:cw_monero/api/structs/pending_transaction.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:ffi/ffi.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
import 'package:mutex/mutex.dart';
String getTxKey(String txId) {
return monero.Wallet_getTxKey(wptr!, txid: txId);
}
final txHistoryMutex = Mutex();
monero.TransactionHistory? txhistory;
void refreshTransactions() {
bool isRefreshingTx = false;
Future<void> refreshTransactions() async {
if (isRefreshingTx == true) return;
isRefreshingTx = true;
txhistory ??= monero.Wallet_history(wptr!);
monero.TransactionHistory_refresh(txhistory!);
final ptr = txhistory!.address;
await txHistoryMutex.acquire();
await Isolate.run(() {
monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
});
txHistoryMutex.release();
isRefreshingTx = false;
}
int countOfTransactions() => monero.TransactionHistory_count(txhistory!);
List<Transaction> getAllTransactions() {
Future<List<Transaction>> getAllTransactions() async {
List<Transaction> dummyTxs = [];
await txHistoryMutex.acquire();
txhistory ??= monero.Wallet_history(wptr!);
monero.TransactionHistory_refresh(txhistory!);
int size = countOfTransactions();
final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index)));
txHistoryMutex.release();
final accts = monero.Wallet_numSubaddressAccounts(wptr!);
for (var i = 0; i < accts; i++) {
final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i);
@ -45,6 +55,8 @@ List<Transaction> getAllTransactions() {
confirmations: 0,
blockheight: 0,
accountIndex: i,
addressIndex: 0,
addressIndexList: [0],
paymentId: "",
amount: fullBalance - availBalance,
isSpend: false,
@ -251,19 +263,28 @@ Future<PendingTransactionDescription> createTransactionMultDest(
class Transaction {
final String displayLabel;
String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0);
late final String address = monero.Wallet_address(
late final String subaddressLabel = monero.Wallet_getSubaddressLabel(
wptr!,
accountIndex: 0,
addressIndex: 0,
accountIndex: accountIndex,
addressIndex: addressIndex,
);
late final String address = getAddress(
accountIndex: accountIndex,
addressIndex: addressIndex,
);
late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
getAddress(
accountIndex: accountIndex,
addressIndex: addressIndexList[index],
));
final String description;
final int fee;
final int confirmations;
late final bool isPending = confirmations < 10;
final int blockheight;
final int addressIndex = 0;
final int addressIndex;
final int accountIndex;
final List<int> addressIndexList;
final String paymentId;
final int amount;
final bool isSpend;
@ -309,6 +330,8 @@ class Transaction {
amount = monero.TransactionInfo_amount(txInfo),
paymentId = monero.TransactionInfo_paymentId(txInfo),
accountIndex = monero.TransactionInfo_subaddrAccount(txInfo),
addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
blockheight = monero.TransactionInfo_blockHeight(txInfo),
confirmations = monero.TransactionInfo_confirmations(txInfo),
fee = monero.TransactionInfo_fee(txInfo),
@ -331,6 +354,8 @@ class Transaction {
required this.confirmations,
required this.blockheight,
required this.accountIndex,
required this.addressIndexList,
required this.addressIndex,
required this.paymentId,
required this.amount,
required this.isSpend,

View file

@ -66,9 +66,20 @@ String getSeedLegacy(String? language) {
return legacy;
}
String getAddress({int accountIndex = 0, int addressIndex = 0}) =>
monero.Wallet_address(wptr!,
Map<int, Map<int, Map<int, String>>> addressCache = {};
String getAddress({int accountIndex = 0, int addressIndex = 0}) {
// print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}");
while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
print("adding subaddress");
monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
}
addressCache[wptr!.address] ??= {};
addressCache[wptr!.address]![accountIndex] ??= {};
addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!,
accountIndex: accountIndex, addressIndex: addressIndex);
return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
}
int getFullBalance({int accountIndex = 0}) =>
monero.Wallet_balance(wptr!, accountIndex: accountIndex);

View file

@ -1,6 +1,7 @@
import 'package:cw_core/subaddress.dart';
import 'package:cw_monero/api/coins_info.dart';
import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list;
import 'package:cw_monero/api/wallet.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store {
final address = s.address;
final label = s.label;
final id = s.addressIndex;
final hasDefaultAddressName =
label.toLowerCase() == 'Primary account'.toLowerCase() ||
label.toLowerCase() == 'Untitled account'.toLowerCase();
final isPrimaryAddress = id == 0 && hasDefaultAddressName;
return Subaddress(
id: id,
address: address,
label: isPrimaryAddress
? 'Primary address'
: hasDefaultAddressName
? ''
: label);
balance: (s.received/1e12).toStringAsFixed(6),
txCount: s.txCount,
label: label);
}).toList();
}
@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store {
required List<String> usedAddresses,
}) async {
_usedAddresses.addAll(usedAddresses);
final _all = _usedAddresses.toSet().toList();
_usedAddresses.clear();
_usedAddresses.addAll(_all);
if (_isUpdating) {
return;
}
@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store {
Future<List<Subaddress>> _getAllUnusedAddresses(
{required int accountIndex, required String label}) async {
final allAddresses = subaddress_list.getAllSubaddresses();
if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) {
// first because addresses come in reversed order.
if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) {
final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label);
if (!isAddressUnused) {
return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label);
@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store {
return Subaddress(
id: id,
address: address,
balance: (s.received/1e12).toStringAsFixed(6),
txCount: s.txCount,
label: id == 0 &&
label.toLowerCase() == 'Primary account'.toLowerCase()
? 'Primary address'
: label);
})
.toList();
}).toList().reversed.toList();
}
Future<bool> _newSubaddress({required int accountIndex, required String label}) async {

View file

@ -59,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}),
_isTransactionUpdating = false,
_hasSyncAfterStartup = false,
isEnabledAutoGenerateSubaddress = false,
isEnabledAutoGenerateSubaddress = true,
_password = password,
syncStatus = NotConnectedSyncStatus(),
unspentCoins = [],
@ -86,6 +86,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
_updateSubAddress(enabled, account: walletAddresses.account);
});
_onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) {
_updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account);
});
}
static const int _autoSaveInterval = 30;
@ -128,6 +131,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
monero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
ReactionDisposer? _onTxHistoryChangeReaction;
bool _isTransactionUpdating;
bool _hasSyncAfterStartup;
Timer? _autoSaveTimer;
@ -158,6 +162,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
_autoSaveTimer = Timer.periodic(
Duration(seconds: _autoSaveInterval), (_) async => await save());
// update transaction details after restore
walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0);
}
@override
@ -167,6 +173,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
void close() async {
_listener?.stop();
_onAccountChangeReaction?.reaction.dispose();
_onTxHistoryChangeReaction?.reaction.dispose();
_autoSaveTimer?.cancel();
}
@ -578,7 +585,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@override
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
transaction_history.refreshTransactions();
return _getAllTransactionsOfAccount(walletAddresses.account?.id)
return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
.fold<Map<String, MoneroTransactionInfo>>(
<String, MoneroTransactionInfo>{},
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
@ -594,8 +601,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
_isTransactionUpdating = true;
transactionHistory.clear();
final transactions = await fetchTransactions();
transactionHistory.clear();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
@ -608,9 +615,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
String getSubaddressLabel(int accountIndex, int addressIndex) =>
monero_wallet.getSubaddressLabel(accountIndex, addressIndex);
List<MoneroTransactionInfo> _getAllTransactionsOfAccount(int? accountIndex) =>
transaction_history
.getAllTransactions()
Future<List<MoneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
(await transaction_history
.getAllTransactions())
.map(
(row) => MoneroTransactionInfo(
row.hash,

View file

@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart';
import 'package:cw_core/subaddress.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list;
import 'package:cw_monero/api/transaction_history.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/monero_account_list.dart';
import 'package:cw_monero/monero_subaddress_list.dart';
@ -27,6 +29,30 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
@observable
String address;
@override
String get latestAddress {
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
while (hiddenAddresses.contains(address)) {
addressIndex++;
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
subaddressList.update(accountIndex: account?.id??0);
}
return address;
}
@override
String get addressForExchange {
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) {
addressIndex++;
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
subaddressList.update(accountIndex: account?.id??0);
}
return address;
}
@observable
Account? account;
@ -37,10 +63,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
MoneroAccountList accountList;
Set<String> usedAddresses = Set();
@override
Future<void> init() async {
accountList.update();
account = accountList.accounts.first;
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
updateSubaddressList(accountIndex: account?.id ?? 0);
await updateAddressesInBox();
}
@ -89,8 +117,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
void updateSubaddressList({required int accountIndex}) {
subaddressList.update(accountIndex: accountIndex);
subaddress = subaddressList.subaddresses.first;
address = subaddress!.address;
address = subaddressList.subaddresses.isNotEmpty
? subaddressList.subaddresses.first.address
: getAddress();
}
Future<void> updateUsedSubaddress() async {
@ -109,7 +138,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
accountIndex: accountIndex,
defaultLabel: defaultLabel,
usedAddresses: usedAddresses.toList());
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last;
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
if (num.tryParse(subaddress!.balance??'0') != 0) {
getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1);
}
address = subaddress!.address;
}

View file

@ -463,8 +463,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7"
resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7"
ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b"
resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b"
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"

View file

@ -25,7 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash
ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0

30
cw_mweb/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

36
cw_mweb/.metadata Normal file
View file

@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: stable
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: android
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: ios
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: macos
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
cw_mweb/CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
cw_mweb/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

15
cw_mweb/README.md Normal file
View file

@ -0,0 +1,15 @@
# cw_mweb
A new Flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/developing-packages/),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

10
cw_mweb/android/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
/libs
.cxx

View file

@ -0,0 +1,76 @@
group 'com.cakewallet.mweb'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.allprojects {
repositories {
flatDir {
dirs project(':cw_mweb').file('libs')
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
}
defaultConfig {
minSdkVersion 16
}
dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.0.0'
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
dependencies {
implementation (name: 'mwebd', ext: 'aar')
}

View file

@ -0,0 +1 @@
rootProject.name = 'cw_mweb'

View file

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cakewallet.mweb">
</manifest>

View file

@ -0,0 +1,58 @@
package com.cakewallet.mweb
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import mwebd.Mwebd
import mwebd.Server
/** CwMwebPlugin */
class CwMwebPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
private var server: Server? = null
private var port: Long? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "start") {
server?.stop()
val dataDir = call.argument("dataDir") ?: ""
server = server ?: Mwebd.newServer("", dataDir, "")
port = server?.start(0)
result.success(port)
} else if (call.method == "stop") {
server?.stop()
server = null
port = null
result.success(null)
} else if (call.method == "address") {
val scanSecret: ByteArray = call.argument<ByteArray>("scanSecret") ?: ByteArray(0)
val spendPub: ByteArray = call.argument<ByteArray>("spendPub") ?: ByteArray(0)
val index: Int = call.argument<Int>("index") ?: 0
val res = Mwebd.address(scanSecret, spendPub, index)
result.success(res)
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
server?.stop()
server = null
port = null
}
}

38
cw_mweb/ios/.gitignore vendored Normal file
View file

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

View file

View file

@ -0,0 +1,86 @@
import Flutter
import UIKit
import Mwebd
public class CwMwebPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger())
let instance = CwMwebPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
private static var server: MwebdServer?
private static var port: Int = 0
private static var dataDir: String?
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
break
case "start":
stopServer()
let args = call.arguments as? [String: String]
let dataDir = args?["dataDir"]
CwMwebPlugin.dataDir = dataDir
startServer(result: result)
break
case "stop":
stopServer()
result(nil)
break
case "address":
let args = call.arguments as! [String: Any]
let scanSecret = args["scanSecret"] as! FlutterStandardTypedData
let spendPub = args["spendPub"] as! FlutterStandardTypedData
let index = args["index"] as! Int32
let scanSecretData = scanSecret.data
let spendPubData = spendPub.data
result(MwebdAddress(scanSecretData, spendPubData, index))
break
default:
result(FlutterMethodNotImplemented)
break
}
}
private func startServer(result: @escaping FlutterResult) {
if CwMwebPlugin.server == nil {
var error: NSError?
CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, "", &error)
if let server = CwMwebPlugin.server {
do {
print("Starting server...")
try server.start(0, ret0_: &CwMwebPlugin.port)
print("Server started successfully on port: \(CwMwebPlugin.port)")
result(CwMwebPlugin.port)
} catch let startError as NSError {
print("Server Start Error: \(startError.localizedDescription)")
result(FlutterError(code: "Server Start Error", message: startError.localizedDescription, details: nil))
}
} else if let error = error {
print("Server Creation Error: \(error.localizedDescription)")
result(FlutterError(code: "Server Creation Error", message: error.localizedDescription, details: nil))
} else {
print("Unknown Error: Failed to create server")
result(FlutterError(code: "Unknown Error", message: "Failed to create server", details: nil))
}
} else {
print("Server already running on port: \(CwMwebPlugin.port)")
result(CwMwebPlugin.port)
}
}
private func stopServer() {
print("Stopping server")
CwMwebPlugin.server?.stop()
CwMwebPlugin.server = nil
CwMwebPlugin.port = 0
}
deinit {
stopServer()
}
}

View file

@ -0,0 +1,26 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint cw_mweb.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'cw_mweb'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '11.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
s.ios.vendored_frameworks = 'Mwebd.xcframework'
s.preserve_paths = 'Mwebd.xcframework/**/*'
end

133
cw_mweb/lib/cw_mweb.dart Normal file
View file

@ -0,0 +1,133 @@
import 'dart:typed_data';
import 'package:grpc/grpc.dart';
import 'package:path_provider/path_provider.dart';
import 'cw_mweb_platform_interface.dart';
import 'mwebd.pbgrpc.dart';
class CwMweb {
static RpcClient? _rpcClient;
static ClientChannel? _clientChannel;
static int? _port;
static const TIMEOUT_DURATION = Duration(seconds: 5);
static Future<void> _initializeClient() async {
await stop();
// wait a few seconds to make sure the server is stopped
await Future.delayed(const Duration(seconds: 5));
final appDir = await getApplicationSupportDirectory();
_port = await CwMwebPlatform.instance.start(appDir.path);
if (_port == null || _port == 0) {
throw Exception("Failed to start server");
}
print("Attempting to connect to server on port: $_port");
// wait for the server to finish starting up before we try to connect to it:
await Future.delayed(const Duration(seconds: 5));
_clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () {
print("Channel is shutting down!");
},
options: const ChannelOptions(
credentials: ChannelCredentials.insecure(),
keepAlive: ClientKeepAliveOptions(permitWithoutCalls: true),
));
_rpcClient = RpcClient(_clientChannel!);
}
static Future<RpcClient> stub({int maxRetries = 3}) async {
for (int i = 0; i < maxRetries; i++) {
try {
if (_rpcClient == null) {
await _initializeClient();
}
final status = await _rpcClient!
.status(StatusRequest(), options: CallOptions(timeout: TIMEOUT_DURATION));
if (status.blockTime == 0) {
throw Exception("blockTime shouldn't be 0! (this connection is likely broken)");
}
return _rpcClient!;
} catch (e) {
print("Attempt $i failed: $e");
_rpcClient = null;
}
}
throw Exception("Failed to connect after $maxRetries attempts");
}
static Future<void> stop() async {
try {
await CwMwebPlatform.instance.stop();
await cleanup();
} catch (e) {
print("Error stopping server: $e");
}
}
static Future<String?> address(Uint8List scanSecret, Uint8List spendPub, int index) async {
try {
return CwMwebPlatform.instance.address(scanSecret, spendPub, index);
} catch (e) {
print("Error getting address: $e");
return null;
}
}
static Future<void> cleanup() async {
await _clientChannel?.terminate();
_rpcClient = null;
_clientChannel = null;
_port = null;
}
// wrappers that handle the connection issues:
static Future<SpentResponse> spent(SpentRequest request) async {
try {
if (_rpcClient == null) {
await _initializeClient();
}
return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION));
} catch (e) {
print("Error getting spent: $e");
return SpentResponse();
}
}
static Future<StatusResponse> status(StatusRequest request) async {
try {
if (_rpcClient == null) {
await _initializeClient();
}
return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION));
} catch (e) {
print("Error getting status: $e");
return StatusResponse();
}
}
static Future<CreateResponse> create(CreateRequest request) async {
try {
if (_rpcClient == null) {
await _initializeClient();
}
return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION));
} catch (e) {
print("Error getting create: $e");
return CreateResponse();
}
}
static Future<ResponseStream<Utxo>?> utxos(UtxosRequest request) async {
try {
if (_rpcClient == null) {
await _initializeClient();
}
// this is a stream, so we should have an effectively infinite timeout:
return _rpcClient!.utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365)));
} catch (e) {
print("Error getting utxos: $e");
return null;
}
}
}

View file

@ -0,0 +1,32 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'cw_mweb_platform_interface.dart';
/// An implementation of [CwMwebPlatform] that uses method channels.
class MethodChannelCwMweb extends CwMwebPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('cw_mweb');
@override
Future<int?> start(String dataDir) async {
final result = await methodChannel.invokeMethod<int>('start', {'dataDir': dataDir});
return result;
}
@override
Future<void> stop() async {
await methodChannel.invokeMethod<void>('stop');
}
@override
Future<String?> address(Uint8List scanSecret, Uint8List spendPub, int index) async {
final result = await methodChannel.invokeMethod<String>('address', {
'scanSecret': scanSecret,
'spendPub': spendPub,
'index': index,
});
return result;
}
}

View file

@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'cw_mweb_method_channel.dart';
abstract class CwMwebPlatform extends PlatformInterface {
/// Constructs a CwMwebPlatform.
CwMwebPlatform() : super(token: _token);
static final Object _token = Object();
static CwMwebPlatform _instance = MethodChannelCwMweb();
/// The default instance of [CwMwebPlatform] to use.
///
/// Defaults to [MethodChannelCwMweb].
static CwMwebPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [CwMwebPlatform] when
/// they register themselves.
static set instance(CwMwebPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<int?> start(String dataDir) {
throw UnimplementedError('start() has not been implemented.');
}
Future<void> stop() {
throw UnimplementedError('stop() has not been implemented.');
}
Future<String?> address(Uint8List scanSecret, Uint8List spendPub, int index) {
throw UnimplementedError('address(int) has not been implemented.');
}
}

801
cw_mweb/lib/mwebd.pb.dart Normal file
View file

@ -0,0 +1,801 @@
//
// Generated code. Do not modify.
// source: mwebd.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
class StatusRequest extends $pb.GeneratedMessage {
factory StatusRequest() => create();
StatusRequest._() : super();
factory StatusRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory StatusRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusRequest', createEmptyInstance: create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
StatusRequest clone() => StatusRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
StatusRequest copyWith(void Function(StatusRequest) updates) => super.copyWith((message) => updates(message as StatusRequest)) as StatusRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static StatusRequest create() => StatusRequest._();
StatusRequest createEmptyInstance() => create();
static $pb.PbList<StatusRequest> createRepeated() => $pb.PbList<StatusRequest>();
@$core.pragma('dart2js:noInline')
static StatusRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StatusRequest>(create);
static StatusRequest? _defaultInstance;
}
class StatusResponse extends $pb.GeneratedMessage {
factory StatusResponse({
$core.int? blockHeaderHeight,
$core.int? mwebHeaderHeight,
$core.int? mwebUtxosHeight,
$core.int? blockTime,
}) {
final $result = create();
if (blockHeaderHeight != null) {
$result.blockHeaderHeight = blockHeaderHeight;
}
if (mwebHeaderHeight != null) {
$result.mwebHeaderHeight = mwebHeaderHeight;
}
if (mwebUtxosHeight != null) {
$result.mwebUtxosHeight = mwebUtxosHeight;
}
if (blockTime != null) {
$result.blockTime = blockTime;
}
return $result;
}
StatusResponse._() : super();
factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3)
..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3)
..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3)
..a<$core.int>(4, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
StatusResponse clone() => StatusResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static StatusResponse create() => StatusResponse._();
StatusResponse createEmptyInstance() => create();
static $pb.PbList<StatusResponse> createRepeated() => $pb.PbList<StatusResponse>();
@$core.pragma('dart2js:noInline')
static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StatusResponse>(create);
static StatusResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.int get blockHeaderHeight => $_getIZ(0);
@$pb.TagNumber(1)
set blockHeaderHeight($core.int v) { $_setSignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasBlockHeaderHeight() => $_has(0);
@$pb.TagNumber(1)
void clearBlockHeaderHeight() => clearField(1);
@$pb.TagNumber(2)
$core.int get mwebHeaderHeight => $_getIZ(1);
@$pb.TagNumber(2)
set mwebHeaderHeight($core.int v) { $_setSignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasMwebHeaderHeight() => $_has(1);
@$pb.TagNumber(2)
void clearMwebHeaderHeight() => clearField(2);
@$pb.TagNumber(3)
$core.int get mwebUtxosHeight => $_getIZ(2);
@$pb.TagNumber(3)
set mwebUtxosHeight($core.int v) { $_setSignedInt32(2, v); }
@$pb.TagNumber(3)
$core.bool hasMwebUtxosHeight() => $_has(2);
@$pb.TagNumber(3)
void clearMwebUtxosHeight() => clearField(3);
@$pb.TagNumber(4)
$core.int get blockTime => $_getIZ(3);
@$pb.TagNumber(4)
set blockTime($core.int v) { $_setUnsignedInt32(3, v); }
@$pb.TagNumber(4)
$core.bool hasBlockTime() => $_has(3);
@$pb.TagNumber(4)
void clearBlockTime() => clearField(4);
}
class UtxosRequest extends $pb.GeneratedMessage {
factory UtxosRequest({
$core.int? fromHeight,
$core.List<$core.int>? scanSecret,
}) {
final $result = create();
if (fromHeight != null) {
$result.fromHeight = fromHeight;
}
if (scanSecret != null) {
$result.scanSecret = scanSecret;
}
return $result;
}
UtxosRequest._() : super();
factory UtxosRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory UtxosRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UtxosRequest', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'fromHeight', $pb.PbFieldType.O3)
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
UtxosRequest clone() => UtxosRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
UtxosRequest copyWith(void Function(UtxosRequest) updates) => super.copyWith((message) => updates(message as UtxosRequest)) as UtxosRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static UtxosRequest create() => UtxosRequest._();
UtxosRequest createEmptyInstance() => create();
static $pb.PbList<UtxosRequest> createRepeated() => $pb.PbList<UtxosRequest>();
@$core.pragma('dart2js:noInline')
static UtxosRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UtxosRequest>(create);
static UtxosRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.int get fromHeight => $_getIZ(0);
@$pb.TagNumber(1)
set fromHeight($core.int v) { $_setSignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasFromHeight() => $_has(0);
@$pb.TagNumber(1)
void clearFromHeight() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get scanSecret => $_getN(1);
@$pb.TagNumber(2)
set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasScanSecret() => $_has(1);
@$pb.TagNumber(2)
void clearScanSecret() => clearField(2);
}
class Utxo extends $pb.GeneratedMessage {
factory Utxo({
$core.int? height,
$fixnum.Int64? value,
$core.String? address,
$core.String? outputId,
$core.int? blockTime,
}) {
final $result = create();
if (height != null) {
$result.height = height;
}
if (value != null) {
$result.value = value;
}
if (address != null) {
$result.address = address;
}
if (outputId != null) {
$result.outputId = outputId;
}
if (blockTime != null) {
$result.blockTime = blockTime;
}
return $result;
}
Utxo._() : super();
factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Utxo', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'height', $pb.PbFieldType.O3)
..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..aOS(3, _omitFieldNames ? '' : 'address')
..aOS(4, _omitFieldNames ? '' : 'outputId')
..a<$core.int>(5, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Utxo clone() => Utxo()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Utxo create() => Utxo._();
Utxo createEmptyInstance() => create();
static $pb.PbList<Utxo> createRepeated() => $pb.PbList<Utxo>();
@$core.pragma('dart2js:noInline')
static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Utxo>(create);
static Utxo? _defaultInstance;
@$pb.TagNumber(1)
$core.int get height => $_getIZ(0);
@$pb.TagNumber(1)
set height($core.int v) { $_setSignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasHeight() => $_has(0);
@$pb.TagNumber(1)
void clearHeight() => clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get value => $_getI64(1);
@$pb.TagNumber(2)
set value($fixnum.Int64 v) { $_setInt64(1, v); }
@$pb.TagNumber(2)
$core.bool hasValue() => $_has(1);
@$pb.TagNumber(2)
void clearValue() => clearField(2);
@$pb.TagNumber(3)
$core.String get address => $_getSZ(2);
@$pb.TagNumber(3)
set address($core.String v) { $_setString(2, v); }
@$pb.TagNumber(3)
$core.bool hasAddress() => $_has(2);
@$pb.TagNumber(3)
void clearAddress() => clearField(3);
@$pb.TagNumber(4)
$core.String get outputId => $_getSZ(3);
@$pb.TagNumber(4)
set outputId($core.String v) { $_setString(3, v); }
@$pb.TagNumber(4)
$core.bool hasOutputId() => $_has(3);
@$pb.TagNumber(4)
void clearOutputId() => clearField(4);
@$pb.TagNumber(5)
$core.int get blockTime => $_getIZ(4);
@$pb.TagNumber(5)
set blockTime($core.int v) { $_setUnsignedInt32(4, v); }
@$pb.TagNumber(5)
$core.bool hasBlockTime() => $_has(4);
@$pb.TagNumber(5)
void clearBlockTime() => clearField(5);
}
class AddressRequest extends $pb.GeneratedMessage {
factory AddressRequest({
$core.int? fromIndex,
$core.int? toIndex,
$core.List<$core.int>? scanSecret,
$core.List<$core.int>? spendPubkey,
}) {
final $result = create();
if (fromIndex != null) {
$result.fromIndex = fromIndex;
}
if (toIndex != null) {
$result.toIndex = toIndex;
}
if (scanSecret != null) {
$result.scanSecret = scanSecret;
}
if (spendPubkey != null) {
$result.spendPubkey = spendPubkey;
}
return $result;
}
AddressRequest._() : super();
factory AddressRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory AddressRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressRequest', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'fromIndex', $pb.PbFieldType.OU3)
..a<$core.int>(2, _omitFieldNames ? '' : 'toIndex', $pb.PbFieldType.OU3)
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'spendPubkey', $pb.PbFieldType.OY)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
AddressRequest clone() => AddressRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
AddressRequest copyWith(void Function(AddressRequest) updates) => super.copyWith((message) => updates(message as AddressRequest)) as AddressRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AddressRequest create() => AddressRequest._();
AddressRequest createEmptyInstance() => create();
static $pb.PbList<AddressRequest> createRepeated() => $pb.PbList<AddressRequest>();
@$core.pragma('dart2js:noInline')
static AddressRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AddressRequest>(create);
static AddressRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.int get fromIndex => $_getIZ(0);
@$pb.TagNumber(1)
set fromIndex($core.int v) { $_setUnsignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasFromIndex() => $_has(0);
@$pb.TagNumber(1)
void clearFromIndex() => clearField(1);
@$pb.TagNumber(2)
$core.int get toIndex => $_getIZ(1);
@$pb.TagNumber(2)
set toIndex($core.int v) { $_setUnsignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasToIndex() => $_has(1);
@$pb.TagNumber(2)
void clearToIndex() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get scanSecret => $_getN(2);
@$pb.TagNumber(3)
set scanSecret($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasScanSecret() => $_has(2);
@$pb.TagNumber(3)
void clearScanSecret() => clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get spendPubkey => $_getN(3);
@$pb.TagNumber(4)
set spendPubkey($core.List<$core.int> v) { $_setBytes(3, v); }
@$pb.TagNumber(4)
$core.bool hasSpendPubkey() => $_has(3);
@$pb.TagNumber(4)
void clearSpendPubkey() => clearField(4);
}
class AddressResponse extends $pb.GeneratedMessage {
factory AddressResponse({
$core.Iterable<$core.String>? address,
}) {
final $result = create();
if (address != null) {
$result.address.addAll(address);
}
return $result;
}
AddressResponse._() : super();
factory AddressResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory AddressResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressResponse', createEmptyInstance: create)
..pPS(1, _omitFieldNames ? '' : 'address')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
AddressResponse clone() => AddressResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
AddressResponse copyWith(void Function(AddressResponse) updates) => super.copyWith((message) => updates(message as AddressResponse)) as AddressResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AddressResponse create() => AddressResponse._();
AddressResponse createEmptyInstance() => create();
static $pb.PbList<AddressResponse> createRepeated() => $pb.PbList<AddressResponse>();
@$core.pragma('dart2js:noInline')
static AddressResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AddressResponse>(create);
static AddressResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.String> get address => $_getList(0);
}
class SpentRequest extends $pb.GeneratedMessage {
factory SpentRequest({
$core.Iterable<$core.String>? outputId,
}) {
final $result = create();
if (outputId != null) {
$result.outputId.addAll(outputId);
}
return $result;
}
SpentRequest._() : super();
factory SpentRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory SpentRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentRequest', createEmptyInstance: create)
..pPS(1, _omitFieldNames ? '' : 'outputId')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
SpentRequest clone() => SpentRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
SpentRequest copyWith(void Function(SpentRequest) updates) => super.copyWith((message) => updates(message as SpentRequest)) as SpentRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SpentRequest create() => SpentRequest._();
SpentRequest createEmptyInstance() => create();
static $pb.PbList<SpentRequest> createRepeated() => $pb.PbList<SpentRequest>();
@$core.pragma('dart2js:noInline')
static SpentRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpentRequest>(create);
static SpentRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.String> get outputId => $_getList(0);
}
class SpentResponse extends $pb.GeneratedMessage {
factory SpentResponse({
$core.Iterable<$core.String>? outputId,
}) {
final $result = create();
if (outputId != null) {
$result.outputId.addAll(outputId);
}
return $result;
}
SpentResponse._() : super();
factory SpentResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory SpentResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentResponse', createEmptyInstance: create)
..pPS(1, _omitFieldNames ? '' : 'outputId')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
SpentResponse clone() => SpentResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
SpentResponse copyWith(void Function(SpentResponse) updates) => super.copyWith((message) => updates(message as SpentResponse)) as SpentResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SpentResponse create() => SpentResponse._();
SpentResponse createEmptyInstance() => create();
static $pb.PbList<SpentResponse> createRepeated() => $pb.PbList<SpentResponse>();
@$core.pragma('dart2js:noInline')
static SpentResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpentResponse>(create);
static SpentResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.String> get outputId => $_getList(0);
}
class CreateRequest extends $pb.GeneratedMessage {
factory CreateRequest({
$core.List<$core.int>? rawTx,
$core.List<$core.int>? scanSecret,
$core.List<$core.int>? spendSecret,
$fixnum.Int64? feeRatePerKb,
$core.bool? dryRun,
}) {
final $result = create();
if (rawTx != null) {
$result.rawTx = rawTx;
}
if (scanSecret != null) {
$result.scanSecret = scanSecret;
}
if (spendSecret != null) {
$result.spendSecret = spendSecret;
}
if (feeRatePerKb != null) {
$result.feeRatePerKb = feeRatePerKb;
}
if (dryRun != null) {
$result.dryRun = dryRun;
}
return $result;
}
CreateRequest._() : super();
factory CreateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory CreateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateRequest', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'spendSecret', $pb.PbFieldType.OY)
..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'feeRatePerKb', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..aOB(5, _omitFieldNames ? '' : 'dryRun')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
CreateRequest clone() => CreateRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
CreateRequest copyWith(void Function(CreateRequest) updates) => super.copyWith((message) => updates(message as CreateRequest)) as CreateRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static CreateRequest create() => CreateRequest._();
CreateRequest createEmptyInstance() => create();
static $pb.PbList<CreateRequest> createRepeated() => $pb.PbList<CreateRequest>();
@$core.pragma('dart2js:noInline')
static CreateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateRequest>(create);
static CreateRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get rawTx => $_getN(0);
@$pb.TagNumber(1)
set rawTx($core.List<$core.int> v) { $_setBytes(0, v); }
@$pb.TagNumber(1)
$core.bool hasRawTx() => $_has(0);
@$pb.TagNumber(1)
void clearRawTx() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get scanSecret => $_getN(1);
@$pb.TagNumber(2)
set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasScanSecret() => $_has(1);
@$pb.TagNumber(2)
void clearScanSecret() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get spendSecret => $_getN(2);
@$pb.TagNumber(3)
set spendSecret($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasSpendSecret() => $_has(2);
@$pb.TagNumber(3)
void clearSpendSecret() => clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get feeRatePerKb => $_getI64(3);
@$pb.TagNumber(4)
set feeRatePerKb($fixnum.Int64 v) { $_setInt64(3, v); }
@$pb.TagNumber(4)
$core.bool hasFeeRatePerKb() => $_has(3);
@$pb.TagNumber(4)
void clearFeeRatePerKb() => clearField(4);
@$pb.TagNumber(5)
$core.bool get dryRun => $_getBF(4);
@$pb.TagNumber(5)
set dryRun($core.bool v) { $_setBool(4, v); }
@$pb.TagNumber(5)
$core.bool hasDryRun() => $_has(4);
@$pb.TagNumber(5)
void clearDryRun() => clearField(5);
}
class CreateResponse extends $pb.GeneratedMessage {
factory CreateResponse({
$core.List<$core.int>? rawTx,
$core.Iterable<$core.String>? outputId,
}) {
final $result = create();
if (rawTx != null) {
$result.rawTx = rawTx;
}
if (outputId != null) {
$result.outputId.addAll(outputId);
}
return $result;
}
CreateResponse._() : super();
factory CreateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory CreateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateResponse', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY)
..pPS(2, _omitFieldNames ? '' : 'outputId')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
CreateResponse clone() => CreateResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
CreateResponse copyWith(void Function(CreateResponse) updates) => super.copyWith((message) => updates(message as CreateResponse)) as CreateResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static CreateResponse create() => CreateResponse._();
CreateResponse createEmptyInstance() => create();
static $pb.PbList<CreateResponse> createRepeated() => $pb.PbList<CreateResponse>();
@$core.pragma('dart2js:noInline')
static CreateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateResponse>(create);
static CreateResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get rawTx => $_getN(0);
@$pb.TagNumber(1)
set rawTx($core.List<$core.int> v) { $_setBytes(0, v); }
@$pb.TagNumber(1)
$core.bool hasRawTx() => $_has(0);
@$pb.TagNumber(1)
void clearRawTx() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.String> get outputId => $_getList(1);
}
class BroadcastRequest extends $pb.GeneratedMessage {
factory BroadcastRequest({
$core.List<$core.int>? rawTx,
}) {
final $result = create();
if (rawTx != null) {
$result.rawTx = rawTx;
}
return $result;
}
BroadcastRequest._() : super();
factory BroadcastRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory BroadcastRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastRequest', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
BroadcastRequest clone() => BroadcastRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
BroadcastRequest copyWith(void Function(BroadcastRequest) updates) => super.copyWith((message) => updates(message as BroadcastRequest)) as BroadcastRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static BroadcastRequest create() => BroadcastRequest._();
BroadcastRequest createEmptyInstance() => create();
static $pb.PbList<BroadcastRequest> createRepeated() => $pb.PbList<BroadcastRequest>();
@$core.pragma('dart2js:noInline')
static BroadcastRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BroadcastRequest>(create);
static BroadcastRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get rawTx => $_getN(0);
@$pb.TagNumber(1)
set rawTx($core.List<$core.int> v) { $_setBytes(0, v); }
@$pb.TagNumber(1)
$core.bool hasRawTx() => $_has(0);
@$pb.TagNumber(1)
void clearRawTx() => clearField(1);
}
class BroadcastResponse extends $pb.GeneratedMessage {
factory BroadcastResponse({
$core.String? txid,
}) {
final $result = create();
if (txid != null) {
$result.txid = txid;
}
return $result;
}
BroadcastResponse._() : super();
factory BroadcastResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory BroadcastResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastResponse', createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'txid')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
BroadcastResponse clone() => BroadcastResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
BroadcastResponse copyWith(void Function(BroadcastResponse) updates) => super.copyWith((message) => updates(message as BroadcastResponse)) as BroadcastResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static BroadcastResponse create() => BroadcastResponse._();
BroadcastResponse createEmptyInstance() => create();
static $pb.PbList<BroadcastResponse> createRepeated() => $pb.PbList<BroadcastResponse>();
@$core.pragma('dart2js:noInline')
static BroadcastResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BroadcastResponse>(create);
static BroadcastResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.String get txid => $_getSZ(0);
@$pb.TagNumber(1)
set txid($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasTxid() => $_has(0);
@$pb.TagNumber(1)
void clearTxid() => clearField(1);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,159 @@
//
// Generated code. Do not modify.
// source: mwebd.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'mwebd.pb.dart' as $0;
export 'mwebd.pb.dart';
@$pb.GrpcServiceName('Rpc')
class RpcClient extends $grpc.Client {
static final _$status = $grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>(
'/Rpc/Status',
($0.StatusRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.StatusResponse.fromBuffer(value));
static final _$utxos = $grpc.ClientMethod<$0.UtxosRequest, $0.Utxo>(
'/Rpc/Utxos',
($0.UtxosRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.Utxo.fromBuffer(value));
static final _$addresses = $grpc.ClientMethod<$0.AddressRequest, $0.AddressResponse>(
'/Rpc/Addresses',
($0.AddressRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.AddressResponse.fromBuffer(value));
static final _$spent = $grpc.ClientMethod<$0.SpentRequest, $0.SpentResponse>(
'/Rpc/Spent',
($0.SpentRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.SpentResponse.fromBuffer(value));
static final _$create = $grpc.ClientMethod<$0.CreateRequest, $0.CreateResponse>(
'/Rpc/Create',
($0.CreateRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.CreateResponse.fromBuffer(value));
static final _$broadcast = $grpc.ClientMethod<$0.BroadcastRequest, $0.BroadcastResponse>(
'/Rpc/Broadcast',
($0.BroadcastRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.BroadcastResponse.fromBuffer(value));
RpcClient($grpc.ClientChannel channel,
{$grpc.CallOptions? options,
$core.Iterable<$grpc.ClientInterceptor>? interceptors})
: super(channel, options: options,
interceptors: interceptors);
$grpc.ResponseFuture<$0.StatusResponse> status($0.StatusRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$status, request, options: options);
}
$grpc.ResponseStream<$0.Utxo> utxos($0.UtxosRequest request, {$grpc.CallOptions? options}) {
return $createStreamingCall(_$utxos, $async.Stream.fromIterable([request]), options: options);
}
$grpc.ResponseFuture<$0.AddressResponse> addresses($0.AddressRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$addresses, request, options: options);
}
$grpc.ResponseFuture<$0.SpentResponse> spent($0.SpentRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$spent, request, options: options);
}
$grpc.ResponseFuture<$0.CreateResponse> create($0.CreateRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$create, request, options: options);
}
$grpc.ResponseFuture<$0.BroadcastResponse> broadcast($0.BroadcastRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$broadcast, request, options: options);
}
}
@$pb.GrpcServiceName('Rpc')
abstract class RpcServiceBase extends $grpc.Service {
$core.String get $name => 'Rpc';
RpcServiceBase() {
$addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>(
'Status',
status_Pre,
false,
false,
($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value),
($0.StatusResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.UtxosRequest, $0.Utxo>(
'Utxos',
utxos_Pre,
false,
true,
($core.List<$core.int> value) => $0.UtxosRequest.fromBuffer(value),
($0.Utxo value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.AddressRequest, $0.AddressResponse>(
'Addresses',
addresses_Pre,
false,
false,
($core.List<$core.int> value) => $0.AddressRequest.fromBuffer(value),
($0.AddressResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.SpentRequest, $0.SpentResponse>(
'Spent',
spent_Pre,
false,
false,
($core.List<$core.int> value) => $0.SpentRequest.fromBuffer(value),
($0.SpentResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.CreateRequest, $0.CreateResponse>(
'Create',
create_Pre,
false,
false,
($core.List<$core.int> value) => $0.CreateRequest.fromBuffer(value),
($0.CreateResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.BroadcastRequest, $0.BroadcastResponse>(
'Broadcast',
broadcast_Pre,
false,
false,
($core.List<$core.int> value) => $0.BroadcastRequest.fromBuffer(value),
($0.BroadcastResponse value) => value.writeToBuffer()));
}
$async.Future<$0.StatusResponse> status_Pre($grpc.ServiceCall call, $async.Future<$0.StatusRequest> request) async {
return status(call, await request);
}
$async.Stream<$0.Utxo> utxos_Pre($grpc.ServiceCall call, $async.Future<$0.UtxosRequest> request) async* {
yield* utxos(call, await request);
}
$async.Future<$0.AddressResponse> addresses_Pre($grpc.ServiceCall call, $async.Future<$0.AddressRequest> request) async {
return addresses(call, await request);
}
$async.Future<$0.SpentResponse> spent_Pre($grpc.ServiceCall call, $async.Future<$0.SpentRequest> request) async {
return spent(call, await request);
}
$async.Future<$0.CreateResponse> create_Pre($grpc.ServiceCall call, $async.Future<$0.CreateRequest> request) async {
return create(call, await request);
}
$async.Future<$0.BroadcastResponse> broadcast_Pre($grpc.ServiceCall call, $async.Future<$0.BroadcastRequest> request) async {
return broadcast(call, await request);
}
$async.Future<$0.StatusResponse> status($grpc.ServiceCall call, $0.StatusRequest request);
$async.Stream<$0.Utxo> utxos($grpc.ServiceCall call, $0.UtxosRequest request);
$async.Future<$0.AddressResponse> addresses($grpc.ServiceCall call, $0.AddressRequest request);
$async.Future<$0.SpentResponse> spent($grpc.ServiceCall call, $0.SpentRequest request);
$async.Future<$0.CreateResponse> create($grpc.ServiceCall call, $0.CreateRequest request);
$async.Future<$0.BroadcastResponse> broadcast($grpc.ServiceCall call, $0.BroadcastRequest request);
}

View file

@ -0,0 +1,19 @@
import Cocoa
import FlutterMacOS
public class CwMwebPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger)
let instance = CwMwebPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString)
default:
result(FlutterMethodNotImplemented)
}
}
}

View file

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint cw_mweb.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'cw_mweb'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
s.platform = :osx, '10.11'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
end

76
cw_mweb/pubspec.yaml Normal file
View file

@ -0,0 +1,76 @@
name: cw_mweb
description: A new Flutter plugin project.
version: 0.0.1
homepage:
environment:
sdk: '>=3.0.6 <4.0.0'
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
grpc: ^3.2.4
path_provider: ^2.1.2
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.cakewallet.mweb
pluginClass: CwMwebPlugin
ios:
pluginClass: CwMwebPlugin
macos:
pluginClass: CwMwebPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View file

@ -1,4 +1,5 @@
import 'package:cw_wownero/api/account_list.dart';
import 'package:cw_wownero/api/transaction_history.dart';
import 'package:cw_wownero/api/wallet.dart';
import 'package:monero/wownero.dart' as wownero;
@ -28,27 +29,75 @@ class Subaddress {
Subaddress({
required this.addressIndex,
required this.accountIndex,
required this.txCount,
required this.received,
});
String get address => wownero.Wallet_address(
wptr!,
accountIndex: accountIndex,
addressIndex: addressIndex,
);
late String address = getAddress(
accountIndex: accountIndex,
addressIndex: addressIndex,
);
final int addressIndex;
final int accountIndex;
String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
final int txCount;
final int received;
}
class TinyTransactionDetails {
TinyTransactionDetails({
required this.address,
required this.amount,
});
final List<String> address;
final int amount;
}
int lastWptr = 0;
int lastTxCount = 0;
List<TinyTransactionDetails> ttDetails = [];
List<Subaddress> getAllSubaddresses() {
txhistory = wownero.Wallet_history(wptr!);
final txCount = wownero.TransactionHistory_count(txhistory!);
if (lastTxCount != txCount && lastWptr != wptr!.address) {
final List<TinyTransactionDetails> newttDetails = [];
lastTxCount = txCount;
lastWptr = wptr!.address;
for (var i = 0; i < txCount; i++) {
final tx = wownero.TransactionHistory_transaction(txhistory!, index: i);
final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(",");
final account = wownero.TransactionInfo_subaddrAccount(tx);
newttDetails.add(TinyTransactionDetails(
address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
amount: wownero.TransactionInfo_amount(tx),
));
}
ttDetails.clear();
ttDetails.addAll(newttDetails);
}
final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
final list = List.generate(size, (index) {
final ttDetailsLocal = ttDetails.where((element) {
final address = getAddress(
accountIndex: subaddress!.accountIndex,
addressIndex: index,
);
if (address == element.address) return true;
return false;
}).toList();
int received = 0;
for (var i = 0; i < ttDetailsLocal.length; i++) {
received += ttDetailsLocal[i].amount;
}
return Subaddress(
accountIndex: subaddress!.accountIndex,
addressIndex: index,
received: received,
txCount: ttDetailsLocal.length,
);
}).reversed.toList();
if (list.isEmpty) {
list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex));
list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0));
}
return list;
}
@ -58,6 +107,10 @@ void addSubaddressSync({required int accountIndex, required String label}) {
refreshSubaddresses(accountIndex: accountIndex);
}
int numSubaddresses(int subaccountIndex) {
return wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex);
}
void setLabelForSubaddressSync(
{required int accountIndex, required int addressIndex, required String label}) {
wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label);

View file

@ -3,6 +3,7 @@ import 'dart:isolate';
import 'package:cw_wownero/api/account_list.dart';
import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_wownero/api/wallet.dart';
import 'package:cw_wownero/api/wownero_output.dart';
import 'package:cw_wownero/api/structs/pending_transaction.dart';
import 'package:ffi/ffi.dart';
@ -16,9 +17,16 @@ String getTxKey(String txId) {
wownero.TransactionHistory? txhistory;
void refreshTransactions() {
bool isRefreshingTx = false;
Future<void> refreshTransactions() async {
if (isRefreshingTx == true) return;
isRefreshingTx = true;
txhistory ??= wownero.Wallet_history(wptr!);
wownero.TransactionHistory_refresh(txhistory!);
final ptr = txhistory!.address;
await Isolate.run(() {
wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
});
isRefreshingTx = false;
}
int countOfTransactions() => wownero.TransactionHistory_count(txhistory!);
@ -45,6 +53,8 @@ List<Transaction> getAllTransactions() {
confirmations: 0,
blockheight: 0,
accountIndex: i,
addressIndex: 0,
addressIndexList: [0],
paymentId: "",
amount: fullBalance - availBalance,
isSpend: false,
@ -243,23 +253,28 @@ Future<PendingTransactionDescription> createTransactionMultDest(
class Transaction {
final String displayLabel;
String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0);
late final String address = wownero.Wallet_address(
wptr!,
accountIndex: 0,
addressIndex: 0,
late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
late final String address = getAddress(
accountIndex: accountIndex,
addressIndex: addressIndex,
);
late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
getAddress(
accountIndex: accountIndex,
addressIndex: addressIndexList[index],
));
final String description;
final int fee;
final int confirmations;
late final bool isPending = confirmations < 3;
final int blockheight;
final int addressIndex = 0;
final int addressIndex;
final int accountIndex;
final List<int> addressIndexList;
final String paymentId;
final int amount;
final bool isSpend;
late DateTime timeStamp;
late final DateTime timeStamp;
late final bool isConfirmed = !isPending;
final String hash;
final String key;
@ -301,6 +316,8 @@ class Transaction {
amount = wownero.TransactionInfo_amount(txInfo),
paymentId = wownero.TransactionInfo_paymentId(txInfo),
accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo),
addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
blockheight = wownero.TransactionInfo_blockHeight(txInfo),
confirmations = wownero.TransactionInfo_confirmations(txInfo),
fee = wownero.TransactionInfo_fee(txInfo),
@ -314,6 +331,8 @@ class Transaction {
required this.confirmations,
required this.blockheight,
required this.accountIndex,
required this.addressIndex,
required this.addressIndexList,
required this.paymentId,
required this.amount,
required this.isSpend,

View file

@ -67,10 +67,19 @@ String getSeedLegacy(String? language) {
}
return legacy;
}
Map<int, Map<int, Map<int, String>>> addressCache = {};
String getAddress({int accountIndex = 0, int addressIndex = 1}) =>
wownero.Wallet_address(wptr!,
String getAddress({int accountIndex = 0, int addressIndex = 1}) {
while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
print("adding subaddress");
wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
}
addressCache[wptr!.address] ??= {};
addressCache[wptr!.address]![accountIndex] ??= {};
addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!,
accountIndex: accountIndex, addressIndex: addressIndex);
return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
}
int getFullBalance({int accountIndex = 0}) =>
wownero.Wallet_balance(wptr!, accountIndex: accountIndex);

View file

@ -1,6 +1,7 @@
import 'package:cw_core/subaddress.dart';
import 'package:cw_wownero/api/coins_info.dart';
import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list;
import 'package:cw_wownero/api/wallet.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
@ -61,6 +62,8 @@ abstract class WowneroSubaddressListBase with Store {
return Subaddress(
id: id,
address: address,
balance: (s.received/1e12).toStringAsFixed(6),
txCount: s.txCount,
label: isPrimaryAddress
? 'Primary address'
: hasDefaultAddressName
@ -103,6 +106,9 @@ abstract class WowneroSubaddressListBase with Store {
required List<String> usedAddresses,
}) async {
_usedAddresses.addAll(usedAddresses);
final _all = _usedAddresses.toSet().toList();
_usedAddresses.clear();
_usedAddresses.addAll(_all);
if (_isUpdating) {
return;
}
@ -140,6 +146,8 @@ abstract class WowneroSubaddressListBase with Store {
return Subaddress(
id: id,
address: address,
balance: (s.received/1e12).toStringAsFixed(6),
txCount: s.txCount,
label: id == 0 &&
label.toLowerCase() == 'Primary account'.toLowerCase()
? 'Primary address'

View file

@ -59,7 +59,7 @@ abstract class WowneroWalletBase
_isTransactionUpdating = false,
_hasSyncAfterStartup = false,
_password = password,
isEnabledAutoGenerateSubaddress = false,
isEnabledAutoGenerateSubaddress = true,
syncStatus = NotConnectedSyncStatus(),
unspentCoins = [],
this.unspentCoinsInfo = unspentCoinsInfo,
@ -82,6 +82,10 @@ abstract class WowneroWalletBase
reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
_updateSubAddress(enabled, account: walletAddresses.account);
});
_onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) {
_updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account);
});
}
static const int _autoSaveInterval = 30;
@ -123,6 +127,7 @@ abstract class WowneroWalletBase
wownero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
ReactionDisposer? _onTxHistoryChangeReaction;
bool _isTransactionUpdating;
bool _hasSyncAfterStartup;
Timer? _autoSaveTimer;
@ -158,6 +163,7 @@ abstract class WowneroWalletBase
void close() async {
_listener?.stop();
_onAccountChangeReaction?.reaction.dispose();
_onTxHistoryChangeReaction?.reaction.dispose();
_autoSaveTimer?.cancel();
}
@ -564,8 +570,8 @@ abstract class WowneroWalletBase
}
_isTransactionUpdating = true;
transactionHistory.clear();
final transactions = await fetchTransactions();
transactionHistory.clear();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;

View file

@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart';
import 'package:cw_core/subaddress.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_wownero/api/transaction_history.dart';
import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list;
import 'package:cw_wownero/api/wallet.dart';
import 'package:cw_wownero/wownero_account_list.dart';
import 'package:cw_wownero/wownero_subaddress_list.dart';
@ -27,6 +29,27 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
@observable
String address;
@override
String get latestAddress {
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
while (hiddenAddresses.contains(address)) {
addressIndex++;
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
}
return address;
}
@override
String get addressForExchange {
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) {
addressIndex++;
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
}
return address;
}
@observable
Account? account;
@ -36,11 +59,14 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
WowneroSubaddressList subaddressList;
WowneroAccountList accountList;
@override
Set<String> usedAddresses = Set();
@override
Future<void> init() async {
accountList.update();
account = accountList.accounts.first;
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
updateSubaddressList(accountIndex: account?.id ?? 0);
await updateAddressesInBox();
}
@ -89,8 +115,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
void updateSubaddressList({required int accountIndex}) {
subaddressList.update(accountIndex: accountIndex);
subaddress = subaddressList.subaddresses.first;
address = subaddress!.address;
address = subaddressList.subaddresses.isNotEmpty
? subaddressList.subaddresses.first.address
: getAddress();
}
Future<void> updateUsedSubaddress() async {
@ -109,7 +136,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
accountIndex: accountIndex,
defaultLabel: defaultLabel,
usedAddresses: usedAddresses.toList());
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last;
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
address = subaddress!.address;
}

View file

@ -463,8 +463,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7"
resolved-ref: "3cb38bee9385faf46b03fd73aab85f3ac4115bf7"
ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b"
resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b"
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"

View file

@ -25,7 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: 3cb38bee9385faf46b03fd73aab85f3ac4115bf7 # monero_c hash
ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0

2
ios/.gitignore vendored
View file

@ -30,3 +30,5 @@ Runner/GeneratedPluginRegistrant.*
!default.mode2v3
!default.pbxuser
!default.perspectivev3
Mwebd.xcframework

View file

@ -7,6 +7,7 @@ PODS:
- Flutter
- ReachabilitySwift
- CryptoSwift (1.8.2)
- cw_mweb (0.0.1):
- device_display_brightness (0.0.1):
- Flutter
- device_info_plus (0.0.1):
@ -111,6 +112,7 @@ DEPENDENCIES:
- barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- CryptoSwift
- cw_mweb (from `.symlinks/plugins/cw_mweb/ios`)
- device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- devicelocale (from `.symlinks/plugins/devicelocale/ios`)
@ -156,6 +158,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/barcode_scan2/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cw_mweb:
:path: ".symlinks/plugins/cw_mweb/ios"
device_display_brightness:
:path: ".symlinks/plugins/device_display_brightness/ios"
device_info_plus:
@ -211,6 +215,7 @@ SPEC CHECKSUMS:
barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea
cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae
device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
devicelocale: b22617f40038496deffba44747101255cee005b0

View file

@ -28,6 +28,7 @@
A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C58D93382C00FAC6004BCF69 /* libresolv.tbd */; };
CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */ = {isa = PBXBuildFile; fileRef = 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */ = {isa = PBXBuildFile; fileRef = 91C524F800843E0A3F17E004 /* ReceivePayment.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */ = {isa = PBXBuildFile; fileRef = ABD6FCBB0F4244B090459128 /* BreezSDK.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
@ -84,7 +85,9 @@
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>"; };
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>"; };
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>"; };
C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = "<group>"; };
CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = "<group>"; };
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>"; };
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>"; };
/* End PBXFileReference section */
@ -95,6 +98,7 @@
buildActionMask = 2147483647;
files = (
4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */,
CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -104,6 +108,8 @@
06957875428D0F5AAE053765 /* Frameworks */ = {
isa = PBXGroup;
children = (
CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */,
C58D93382C00FAC6004BCF69 /* libresolv.tbd */,
0C9986A3251A932F00D566FD /* CryptoSwift.framework */,
3C663361C56EBB242598F609 /* Pods_Runner.framework */,
);

View file

@ -220,9 +220,9 @@ class CWBitcoin extends Bitcoin {
return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
}
WalletService createLitecoinWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect);
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
}
@override
@ -261,6 +261,9 @@ class CWBitcoin extends Bitcoin {
@override
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
@override
List<ReceivePageOption> getLitecoinReceivePageOptions() => BitcoinReceivePageOption.allLitecoin;
@override
BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) {
switch (option) {
@ -272,6 +275,8 @@ class CWBitcoin extends Bitcoin {
return SegwitAddresType.p2tr;
case BitcoinReceivePageOption.p2wsh:
return SegwitAddresType.p2wsh;
case BitcoinReceivePageOption.mweb:
return SegwitAddresType.mweb;
case BitcoinReceivePageOption.p2wpkh:
default:
return SegwitAddresType.p2wpkh;
@ -555,6 +560,9 @@ class CWBitcoin extends Bitcoin {
return await getBitcoinHeightByDate(date: date);
}
@override
int getLitecoinHeightByDate({required DateTime date}) => getLtcHeightByDate(date: date);
@override
Future<void> rescan(Object wallet, {required int height, bool? doSingleScan}) async {
final bitcoinWallet = wallet as ElectrumWallet;
@ -580,6 +588,17 @@ class CWBitcoin extends Bitcoin {
}
@override
Future<void> setMwebEnabled(Object wallet, bool enabled) async {
final litecoinWallet = wallet as LitecoinWallet;
litecoinWallet.setMwebEnabled(enabled);
}
@override
bool getMwebEnabled(Object wallet) {
final litecoinWallet = wallet as LitecoinWallet;
return litecoinWallet.mwebEnabled;
}
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
@ -588,7 +607,6 @@ class CWBitcoin extends Bitcoin {
}
final updatedOutputs = outputs.map((output) {
try {
final pendingOut = pendingTx!.outputs[outputs.indexOf(output)];
final updatedOutput = output;
@ -609,4 +627,31 @@ class CWBitcoin extends Bitcoin {
final tx = txInfo as ElectrumTransactionInfo;
return tx.isReceivedSilentPayment;
}
@override
bool txIsMweb(TransactionInfo txInfo) {
final tx = txInfo as ElectrumTransactionInfo;
List<String> inputAddresses = tx.inputAddresses ?? [];
List<String> outputAddresses = tx.outputAddresses ?? [];
bool inputAddressesContainMweb = false;
bool outputAddressesContainMweb = false;
for (var address in inputAddresses) {
if (address.toLowerCase().contains('mweb')) {
inputAddressesContainMweb = true;
break;
}
}
for (var address in outputAddresses) {
if (address.toLowerCase().contains('mweb')) {
outputAddressesContainMweb = true;
break;
}
}
// TODO: this could be improved:
return inputAddressesContainMweb || outputAddressesContainMweb;
}
}

View file

@ -5,35 +5,44 @@ import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
const BEFORE_REGEX = '(^|\\s)';
const AFTER_REGEX = '(\$|\\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type})
: super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: type == CryptoCurrency.btc
useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc
? (String txt) => BitcoinAddressUtils.validateAddress(
address: txt,
network: BitcoinNetwork.mainnet,
network: type == CryptoCurrency.btc
? BitcoinNetwork.mainnet
: LitecoinNetwork.mainnet,
)
: null,
pattern: getPattern(type),
length: getLength(type));
static String getPattern(CryptoCurrency type) {
var pattern = "";
if (type is Erc20Token) {
return '0x[0-9a-zA-Z]';
pattern = '0x[0-9a-zA-Z]+';
}
switch (type) {
case CryptoCurrency.xmr:
return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$';
pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}';
case CryptoCurrency.ada:
return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$'
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}'
'|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}';
case CryptoCurrency.btc:
return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$';
pattern =
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
case CryptoCurrency.ltc:
pattern = '^${P2wpkhAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$';
case CryptoCurrency.nano:
return '[0-9a-zA-Z_]';
pattern = '[0-9a-zA-Z_]+';
case CryptoCurrency.banano:
return '[0-9a-zA-Z_]';
pattern = '[0-9a-zA-Z_]+';
case CryptoCurrency.usdc:
case CryptoCurrency.usdcpoly:
case CryptoCurrency.usdtPoly:
@ -69,11 +78,11 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.dydx:
case CryptoCurrency.steth:
case CryptoCurrency.shib:
return '0x[0-9a-zA-Z]';
pattern = '0x[0-9a-zA-Z]+';
case CryptoCurrency.xrp:
return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$';
pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}';
case CryptoCurrency.xhv:
return '^hvx|hvi|hvs[0-9a-zA-Z]';
pattern = 'hvx|hvi|hvs[0-9a-zA-Z]+';
case CryptoCurrency.xag:
case CryptoCurrency.xau:
case CryptoCurrency.xaud:
@ -95,40 +104,41 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.dash:
case CryptoCurrency.eos:
case CryptoCurrency.wow:
return '[0-9a-zA-Z]';
pattern = '[0-9a-zA-Z]+';
case CryptoCurrency.bch:
return '^(?!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}\$';
pattern =
'(?!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}';
case CryptoCurrency.bnb:
return '[0-9a-zA-Z]';
case CryptoCurrency.ltc:
return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)';
pattern = '[0-9a-zA-Z]+';
case CryptoCurrency.hbar:
return '[0-9a-zA-Z.]';
pattern = '[0-9a-zA-Z.]+';
case CryptoCurrency.zaddr:
return '^zs[0-9a-zA-Z]{75}';
pattern = 'zs[0-9a-zA-Z]{75}';
case CryptoCurrency.zec:
return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$';
pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}';
case CryptoCurrency.dcr:
return 'D[ksecS]([0-9a-zA-Z])+';
pattern = 'D[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}';
pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}';
case CryptoCurrency.near:
return '[0-9a-f]{64}';
pattern = '[0-9a-f]{64}';
case CryptoCurrency.rune:
return 'thor1[0-9a-z]{38}';
pattern = 'thor1[0-9a-z]{38}';
case CryptoCurrency.scrt:
return 'secret1[0-9a-z]{38}';
pattern = 'secret1[0-9a-z]{38}';
case CryptoCurrency.stx:
return 'S[MP][0-9a-zA-Z]+';
pattern = 'S[MP][0-9a-zA-Z]+';
case CryptoCurrency.kmd:
return 'R[0-9a-zA-Z]{33}';
pattern = 'R[0-9a-zA-Z]{33}';
case CryptoCurrency.pivx:
return 'D([1-9a-km-zA-HJ-NP-Z]){33}';
pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}';
case CryptoCurrency.btcln:
return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)';
pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)';
default:
return '[0-9a-zA-Z]';
pattern = '[0-9a-zA-Z]+';
}
return '$BEFORE_REGEX($pattern)$AFTER_REGEX';
}
static List<int>? getLength(CryptoCurrency type) {
@ -149,6 +159,8 @@ class AddressValidator extends TextValidator {
return null;
case CryptoCurrency.btc:
return null;
case CryptoCurrency.ltc:
return null;
case CryptoCurrency.dash:
return [34];
case CryptoCurrency.eos:
@ -195,8 +207,6 @@ class AddressValidator extends TextValidator {
return [42, 43, 44, 54, 55];
case CryptoCurrency.bnb:
return [42];
case CryptoCurrency.ltc:
return [34, 43, 63];
case CryptoCurrency.nano:
return [64, 65];
case CryptoCurrency.banano:
@ -269,56 +279,54 @@ class AddressValidator extends TextValidator {
}
static String? getAddressFromStringPattern(CryptoCurrency type) {
String? pattern = null;
switch (type) {
case CryptoCurrency.xmr:
case CryptoCurrency.wow:
return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
pattern = '(4[0-9a-zA-Z]{94})'
'|(8[0-9a-zA-Z]{94})'
'|([0-9a-zA-Z]{106})';
case CryptoCurrency.btc:
return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type
'|${SilentPaymentAddress.regex.pattern}\$';
pattern =
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)';
'|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)((ltc|t)mweb1q[ac-hj-np-z02-9]{90,120})([^0-9a-zA-Z]|\$)';
case CryptoCurrency.eth:
return '0x[0-9a-zA-Z]{42}';
case CryptoCurrency.maticpoly:
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]+';
case CryptoCurrency.nano:
return 'nano_[0-9a-zA-Z]{60}';
pattern = 'nano_[0-9a-zA-Z]{60}';
case CryptoCurrency.banano:
return 'ban_[0-9a-zA-Z]{60}';
pattern = 'ban_[0-9a-zA-Z]{60}';
case CryptoCurrency.bch:
return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)'
'|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)';
pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}';
case CryptoCurrency.sol:
return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
pattern = '[1-9A-HJ-NP-Za-km-z]+';
case CryptoCurrency.trx:
return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
default:
if (type.tag == CryptoCurrency.eth.title) {
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]{42}';
}
if (type.tag == CryptoCurrency.maticpoly.tag) {
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]{42}';
}
if (type.tag == CryptoCurrency.sol.title) {
return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}';
}
if (type.tag == CryptoCurrency.trx.title) {
return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
}
return null;
}
if (pattern != null) {
return "$BEFORE_REGEX($pattern)$AFTER_REGEX";
}
return null;
}
}

View file

@ -85,7 +85,8 @@ class WalletLoadingService {
authenticatedErrorStreamController.add(corruptedWalletsSeeds);
return wallet;
} catch (_) {
} catch (e) {
print(e);
// save seeds and show corrupted wallets' seeds to the user
try {
final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type);

View file

@ -111,6 +111,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_settings.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
@ -161,6 +162,7 @@ import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart';
import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
@ -676,7 +678,8 @@ Future<void> setup({
getIt.registerFactory<Modify2FAPage>(
() => Modify2FAPage(setup2FAViewModel: getIt.get<Setup2FAViewModel>()));
getIt.registerFactory<DesktopSettingsPage>(() => DesktopSettingsPage(getIt.get<DashboardViewModel>()));
getIt.registerFactory<DesktopSettingsPage>(
() => DesktopSettingsPage(getIt.get<DashboardViewModel>()));
getIt.registerFactoryParam<ReceiveOptionViewModel, ReceivePageOption?, void>(
(pageOption, _) => ReceiveOptionViewModel(getIt.get<AppStore>().wallet!, pageOption));
@ -808,7 +811,9 @@ Future<void> setup({
getIt.registerFactory<MoneroAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) {
if (wallet.type == WalletType.monero ||
wallet.type == WalletType.wownero ||
wallet.type == WalletType.haven) {
return MoneroAccountListViewModel(wallet);
}
throw Exception(
@ -868,6 +873,9 @@ Future<void> setup({
getIt.registerFactory(() =>
SilentPaymentsSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(
() => MwebSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(() {
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
});
@ -937,6 +945,8 @@ Future<void> setup({
getIt.registerFactory(
() => SilentPaymentsSettingsPage(getIt.get<SilentPaymentsSettingsViewModel>()));
getIt.registerFactory(() => MwebSettingsPage(getIt.get<MwebSettingsViewModel>()));
getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>()));
getIt.registerFactory(() => NanoChangeRepPage(
@ -1031,8 +1041,12 @@ Future<void> setup({
SettingsStoreBase.walletPasswordDirectInput,
);
case WalletType.litecoin:
return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource,
SettingsStoreBase.walletPasswordDirectInput);
return bitcoin!.createLitecoinWalletService(
_walletInfoSource,
_unspentCoinsInfoSource,
getIt.get<SettingsStore>().mwebAlwaysScan,
SettingsStoreBase.walletPasswordDirectInput,
);
case WalletType.ethereum:
return ethereum!.createEthereumWalletService(
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
@ -1244,7 +1258,8 @@ Future<void> setup({
getIt.registerFactory<CakePayService>(
() => CakePayService(getIt.get<SecureStorage>(), getIt.get<CakePayApi>()));
getIt.registerFactory(() => CakePayCardsListViewModel(cakePayService: getIt.get<CakePayService>()));
getIt.registerFactory(
() => CakePayCardsListViewModel(cakePayService: getIt.get<CakePayService>()));
getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get<CakePayService>()));
@ -1276,12 +1291,12 @@ Future<void> setup({
getIt.registerFactoryParam<CakePayBuyCardPage, List<dynamic>, void>((List<dynamic> args, _) {
final vendor = args.first as CakePayVendor;
return CakePayBuyCardPage(getIt.get<CakePayBuyCardViewModel>(param1: vendor),
getIt.get<CakePayService>());
return CakePayBuyCardPage(
getIt.get<CakePayBuyCardViewModel>(param1: vendor), getIt.get<CakePayService>());
});
getIt.registerFactoryParam<CakePayBuyCardDetailPage, List<dynamic>, void>(
(List<dynamic> args, _) {
getIt
.registerFactoryParam<CakePayBuyCardDetailPage, List<dynamic>, void>((List<dynamic> args, _) {
final paymentCredential = args.first as PaymentCredential;
final card = args[1] as CakePayCard;
return CakePayBuyCardDetailPage(

View file

@ -48,6 +48,10 @@ class PreferencesKey {
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan';
static const mwebCardDisplay = 'mwebCardDisplay';
static const mwebEnabled = 'mwebEnabled';
static const hasEnabledMwebBefore = 'hasEnabledMwebBefore';
static const mwebAlwaysScan = 'mwebAlwaysScan';
static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup';
static const shouldShowRepWarning = 'should_show_rep_warning';

View file

@ -278,8 +278,6 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
return 'ERC20';
case 'BSC':
return 'BEP20';
case 'POLY':
return 'MATIC';
default:
return currency.tag!;
}

View file

@ -30,6 +30,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
@ -152,6 +153,10 @@ Future<void> initializeAppConfigs() async {
CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) {
CakeHive.registerAdapter(MwebUtxoAdapter());
}
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@ -233,7 +238,6 @@ Future<void> initialSetup(
secureStorage: secureStorage,
);
await bootstrap(navigatorKey);
monero?.onStartup();
}
class App extends StatefulWidget {

View file

@ -61,7 +61,13 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
ObservableList<Subaddress> get subaddresses {
final moneroWallet = _wallet as MoneroWallet;
final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses
.map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label))
.map((sub) => Subaddress(
id: sub.id,
address: sub.address,
label: sub.label,
received: sub.balance??"unknown",
txCount: sub.txCount??0,
))
.toList();
return ObservableList<Subaddress>.of(subAddresses);
}
@ -83,7 +89,12 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.walletAddresses.subaddressList
.getAll()
.map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address))
.map((sub) => Subaddress(
id: sub.id,
label: sub.label,
address: sub.address,
txCount: sub.txCount??0,
received: sub.balance??'unknown'))
.toList();
}
@ -91,7 +102,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
Future<void> addSubaddress(Object wallet,
{required int accountIndex, required String label}) async {
final moneroWallet = wallet as MoneroWallet;
await moneroWallet.walletAddresses.subaddressList
return await moneroWallet.walletAddresses.subaddressList
.addSubaddress(accountIndex: accountIndex, label: label);
}

View file

@ -72,6 +72,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_settings.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
@ -450,6 +451,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<SilentPaymentsSettingsPage>());
case Routes.mwebSettings:
return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<MwebSettingsPage>());
case Routes.connectionSync:
return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>());

View file

@ -73,6 +73,7 @@ class Routes {
static const cakePayAccountPage = '/cake_pay_account_page';
static const webViewPage = '/web_view_page';
static const silentPaymentsSettings = '/silent_payments_settings';
static const mwebSettings = '/mweb_settings';
static const connectionSync = '/connection_sync_page';
static const securityBackupPage = '/security_and_backup_page';
static const privacyPage = '/privacy_page';

View file

@ -155,13 +155,14 @@ class AddressPage extends BasePage {
amountTextFieldFocusNode: _cryptoAmountFocus,
amountController: _amountController,
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
ThemeType.light))),
ThemeType.light,
))),
SizedBox(height: 16),
Observer(builder: (_) {
if (addressListViewModel.hasAddressList) {
return SelectButton(
text: addressListViewModel.buttonTitle,
onTap: () async => Navigator.of(context).pushNamed(Routes.receive),
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
textColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
borderColor: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
@ -225,7 +226,8 @@ class AddressPage extends BasePage {
}
break;
default:
if (addressListViewModel.type == WalletType.bitcoin) {
if (addressListViewModel.type == WalletType.bitcoin ||
addressListViewModel.type == WalletType.litecoin) {
addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
}
}

View file

@ -7,6 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
@ -25,6 +26,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class BalancePage extends StatelessWidget {
@ -129,7 +131,7 @@ class CryptoBalanceWidget extends StatelessWidget {
builder: (_) {
if (dashboardViewModel.getMoneroError != null) {
return Padding(
padding: const EdgeInsets.fromLTRB(16,0,16,16),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: DashBoardRoundedCardWidget(
title: "Invalid monero bindings",
subTitle: dashboardViewModel.getMoneroError.toString(),
@ -144,13 +146,12 @@ class CryptoBalanceWidget extends StatelessWidget {
builder: (_) {
if (dashboardViewModel.getWowneroError != null) {
return Padding(
padding: const EdgeInsets.fromLTRB(16,0,16,16),
child: DashBoardRoundedCardWidget(
title: "Invalid wownero bindings",
subTitle: dashboardViewModel.getWowneroError.toString(),
onTap: () {},
)
);
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: DashBoardRoundedCardWidget(
title: "Invalid wownero bindings",
subTitle: dashboardViewModel.getWowneroError.toString(),
onTap: () {},
));
}
return Container();
},
@ -271,6 +272,18 @@ class CryptoBalanceWidget extends StatelessWidget {
currency: balance.asset,
hasAdditionalBalance:
dashboardViewModel.balanceViewModel.hasAdditionalBalance,
hasSecondAdditionalBalance:
dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance,
hasSecondAvailableBalance:
dashboardViewModel.balanceViewModel.hasSecondAvailableBalance,
secondAdditionalBalance: balance.secondAdditionalBalance,
secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance,
secondAvailableBalance: balance.secondAvailableBalance,
secondAvailableFiatBalance: balance.fiatSecondAvailableBalance,
secondAdditionalBalanceLabel:
'${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}',
secondAvailableBalanceLabel:
'${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}',
isTestnet: dashboardViewModel.isTestnet,
);
});
@ -284,16 +297,15 @@ class CryptoBalanceWidget extends StatelessWidget {
if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[
SizedBox(height: 10),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: DashBoardRoundedCardWidget(
customBorder: 30,
title: "This wallet has encountered an issue",
subTitle: "Here are the things that you should note:\n - "
+dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ")
+"\n\nPlease restart your wallet and if it doesn't help contact our support.",
onTap: () {},
)
)
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: DashBoardRoundedCardWidget(
customBorder: 30,
title: "This wallet has encountered an issue",
subTitle: "Here are the things that you should note:\n - " +
dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") +
"\n\nPlease restart your wallet and if it doesn't help contact our support.",
onTap: () {},
))
],
if (dashboardViewModel.showSilentPaymentsCard) ...[
SizedBox(height: 10),
@ -360,7 +372,73 @@ class CryptoBalanceWidget extends StatelessWidget {
),
),
),
]
],
if (dashboardViewModel.showMwebCard) ...[
SizedBox(height: 10),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: DashBoardRoundedCardWidget(
customBorder: 30,
title: S.current.litecoin_mweb,
subTitle: S.current.litecoin_enable_mweb_sync,
hint: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse(
"https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"),
mode: LaunchMode.externalApplication,
),
child: Row(
children: [
Text(
S.current.litecoin_what_is_mweb,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
height: 1,
),
softWrap: true,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.help_outline,
size: 16,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor),
)
],
),
),
Observer(
builder: (_) => StandardSwitch(
value: dashboardViewModel.mwebScanningActive,
onTaped: () => _toggleMweb(context),
),
)
],
),
],
),
onTap: () => _toggleMweb(context),
icon: ImageIcon(
AssetImage('assets/images/mweb_logo.png'),
color:
Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
size: 50,
),
),
),
],
],
);
}),
@ -400,6 +478,22 @@ class CryptoBalanceWidget extends StatelessWidget {
return dashboardViewModel.setSilentPaymentsScanning(newValue);
}
Future<void> _toggleMweb(BuildContext context) async {
if (!dashboardViewModel.hasEnabledMwebBefore) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).warning,
alertContent: S.current.litecoin_mweb_warning,
buttonText: S.of(context).ok,
buttonAction: () {
Navigator.of(context).pop();
},
));
}
dashboardViewModel.setMwebScanningActive(!dashboardViewModel.mwebScanningActive);
}
}
class BalanceRowWidget extends StatelessWidget {
@ -410,10 +504,18 @@ class BalanceRowWidget extends StatelessWidget {
required this.additionalBalanceLabel,
required this.additionalBalance,
required this.additionalFiatBalance,
required this.secondAvailableBalanceLabel,
required this.secondAvailableBalance,
required this.secondAvailableFiatBalance,
required this.secondAdditionalBalanceLabel,
required this.secondAdditionalBalance,
required this.secondAdditionalFiatBalance,
required this.frozenBalance,
required this.frozenFiatBalance,
required this.currency,
required this.hasAdditionalBalance,
required this.hasSecondAvailableBalance,
required this.hasSecondAdditionalBalance,
required this.isTestnet,
super.key,
});
@ -424,10 +526,18 @@ class BalanceRowWidget extends StatelessWidget {
final String additionalBalanceLabel;
final String additionalBalance;
final String additionalFiatBalance;
final String secondAvailableBalanceLabel;
final String secondAvailableBalance;
final String secondAvailableFiatBalance;
final String secondAdditionalBalanceLabel;
final String secondAdditionalBalance;
final String secondAdditionalFiatBalance;
final String frozenBalance;
final String frozenFiatBalance;
final CryptoCurrency currency;
final bool hasAdditionalBalance;
final bool hasSecondAvailableBalance;
final bool hasSecondAdditionalBalance;
final bool isTestnet;
// void _showBalanceDescription(BuildContext context) {
@ -675,6 +785,94 @@ class BalanceRowWidget extends StatelessWidget {
),
],
),
if (hasSecondAvailableBalance)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
Text(
'${secondAvailableBalanceLabel}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<BalancePageTheme>()!.labelTextColor,
height: 1,
),
),
SizedBox(height: 8),
AutoSizeText(
secondAvailableBalance,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<BalancePageTheme>()!.assetTitleColor,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.center,
),
SizedBox(height: 4),
if (!isTestnet)
Text(
'${secondAvailableFiatBalance}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1,
),
),
],
),
if (hasSecondAdditionalBalance)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
Text(
'${secondAdditionalBalanceLabel}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<BalancePageTheme>()!.labelTextColor,
height: 1,
),
),
SizedBox(height: 8),
AutoSizeText(
secondAdditionalBalance,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<BalancePageTheme>()!.assetTitleColor,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.center,
),
SizedBox(height: 4),
if (!isTestnet)
Text(
'${secondAdditionalFiatBalance}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1,
),
),
],
),
],
),
),

View file

@ -86,6 +86,18 @@ class TransactionsPage extends StatelessWidget {
final transaction = item.transaction;
final transactionType = dashboardViewModel.getTransactionType(transaction);
List<String> tags = [];
if (dashboardViewModel.type == WalletType.bitcoin) {
if (bitcoin!.txIsReceivedSilentPayment(transaction)) {
tags.add(S.of(context).silent_payment);
}
}
if (dashboardViewModel.type == WalletType.litecoin) {
if (bitcoin!.txIsMweb(transaction)) {
tags.add("MWEB");
}
}
return Observer(
builder: (_) => TransactionRow(
onTap: () => Navigator.of(context)
@ -100,9 +112,7 @@ class TransactionsPage extends StatelessWidget {
isPending: transaction.isPending,
title:
item.formattedTitle + item.formattedStatus + transactionType,
isReceivedSilentPayment:
dashboardViewModel.type == WalletType.bitcoin &&
bitcoin!.txIsReceivedSilentPayment(transaction),
tags: tags,
),
);
}

View file

@ -191,6 +191,11 @@ class MenuWidgetState extends State<MenuWidget> {
final item = items[index];
if (!widget.dashboardViewModel.hasMweb &&
item.name(context) == S.current.litecoin_mweb_settings) {
return const SizedBox();
}
final isLastTile = index == itemCount - 1;
return SettingActionButton(

View file

@ -20,7 +20,7 @@ class SignForm extends StatefulWidget {
SignFormState createState() => SignFormState();
}
class SignFormState extends State<SignForm> {
class SignFormState extends State<SignForm> with AutomaticKeepAliveClientMixin {
SignFormState()
: formKey = GlobalKey<FormState>(),
messageController = TextEditingController(),
@ -42,8 +42,12 @@ class SignFormState extends State<SignForm> {
super.dispose();
}
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
padding: EdgeInsets.only(left: 24, right: 24),
child: Column(

View file

@ -1,4 +1,3 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import 'package:cw_core/transaction_direction.dart';
@ -12,7 +11,7 @@ class TransactionRow extends StatelessWidget {
required this.formattedAmount,
required this.formattedFiatAmount,
required this.isPending,
required this.isReceivedSilentPayment,
required this.tags,
required this.title,
required this.onTap,
});
@ -23,8 +22,8 @@ class TransactionRow extends StatelessWidget {
final String formattedAmount;
final String formattedFiatAmount;
final bool isPending;
final bool isReceivedSilentPayment;
final String title;
final List<String> tags;
@override
Widget build(BuildContext context) {
@ -61,7 +60,7 @@ class TransactionRow extends StatelessWidget {
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<DashboardPageTheme>()!.textColor,
)),
if (isReceivedSilentPayment) TxTag(tag: S.of(context).silent_payment),
...tags.map((tag) => Row(children: [SizedBox(width: 8), TxTag(tag: tag)])),
],
),
Text(formattedAmount,

View file

@ -15,7 +15,7 @@ class VerifyForm extends StatefulWidget {
VerifyFormState createState() => VerifyFormState();
}
class VerifyFormState extends State<VerifyForm> {
class VerifyFormState extends State<VerifyForm> with AutomaticKeepAliveClientMixin {
VerifyFormState()
: formKey = GlobalKey<FormState>(),
messageController = TextEditingController(),
@ -36,9 +36,13 @@ class VerifyFormState extends State<VerifyForm> {
void dispose() {
super.dispose();
}
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
padding: EdgeInsets.only(left: 24, right: 24),
child: Form(

View file

@ -509,7 +509,7 @@ class ExchangePage extends BasePage {
}
});
reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) {
if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
depositKey.currentState!.changeAddress(address: address);
}
@ -565,7 +565,7 @@ class ExchangePage extends BasePage {
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '');
key.currentState!.changeAmount(amount: '');
}
@ -576,9 +576,9 @@ class ExchangePage extends BasePage {
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange;
} else if (key.currentState!.addressController.text ==
exchangeViewModel.wallet.walletAddresses.address) {
exchangeViewModel.wallet.walletAddresses.addressForExchange) {
key.currentState!.changeWalletName('');
key.currentState!.addressController.text = '';
}
@ -629,7 +629,7 @@ class ExchangePage extends BasePage {
initialCurrency: exchangeViewModel.depositCurrency,
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
@ -694,7 +694,7 @@ class ExchangePage extends BasePage {
initialCurrency: exchangeViewModel.receiveCurrency,
initialWalletName: receiveWalletName ?? '',
initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
isAmountEstimated: true,

View file

@ -129,7 +129,7 @@ class ExchangeTemplatePage extends BasePage {
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel.depositCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
@ -166,7 +166,7 @@ class ExchangeTemplatePage extends BasePage {
initialWalletName: receiveWalletName ?? '',
initialAddress: exchangeViewModel.receiveCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: false,
isAmountEstimated: true,

View file

@ -85,7 +85,9 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget {
class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBody> {
final TextEditingController passphraseController = TextEditingController();
final TextEditingController confirmPassphraseController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final _passphraseFormKey = GlobalKey<FormState>();
bool? testnetValue;
bool obscurePassphrase = true;
@ -93,9 +95,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
@override
void initState() {
passphraseController.text = widget.seedTypeViewModel.passphrase ?? '';
passphraseController
.addListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text));
confirmPassphraseController.text = widget.seedTypeViewModel.passphrase ?? '';
if (widget.isChildWallet) {
if (widget.privacySettingsViewModel.type == WalletType.bitcoin) {
@ -205,18 +205,47 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
if (widget.privacySettingsViewModel.hasPassphraseOption)
Padding(
padding: EdgeInsets.all(24),
child: BaseTextFormField(
hintText: S.current.passphrase,
controller: passphraseController,
obscureText: obscurePassphrase,
suffixIcon: GestureDetector(
onTap: () => setState(() {
obscurePassphrase = !obscurePassphrase;
}),
child: Icon(
Icons.remove_red_eye,
color: obscurePassphrase ? Colors.black54 : Colors.black26,
),
child: Form(
key: _passphraseFormKey,
child: Column(
children: [
BaseTextFormField(
hintText: S.of(context).passphrase,
controller: passphraseController,
obscureText: obscurePassphrase,
suffixIcon: GestureDetector(
onTap: () => setState(() {
obscurePassphrase = !obscurePassphrase;
}),
child: Icon(
Icons.remove_red_eye,
color: obscurePassphrase ? Colors.black54 : Colors.black26,
),
),
),
const SizedBox(height: 10),
BaseTextFormField(
hintText: S.of(context).confirm_passphrase,
controller: confirmPassphraseController,
obscureText: obscurePassphrase,
validator: (text) {
if (text == passphraseController.text) {
return null;
}
return S.of(context).passphrases_doesnt_match;
},
suffixIcon: GestureDetector(
onTap: () => setState(() {
obscurePassphrase = !obscurePassphrase;
}),
child: Icon(
Icons.remove_red_eye,
color: obscurePassphrase ? Colors.black54 : Colors.black26,
),
),
),
],
),
),
),
@ -272,7 +301,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
}
widget.nodeViewModel.save();
} else if (testnetValue == true) {
}
if (testnetValue == true) {
// TODO: add type (mainnet/testnet) to Node class so when switching wallets the node can be switched to a matching type
// Currently this is so you can create a working testnet wallet but you need to keep switching back the node if you use multiple wallets at once
widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress;
@ -280,6 +310,13 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
widget.nodeViewModel.save();
}
if (passphraseController.text.isNotEmpty) {
if (_passphraseFormKey.currentState != null && !_passphraseFormKey.currentState!.validate()) {
return;
}
widget.seedTypeViewModel.setPassphrase(passphraseController.text);
}
Navigator.pop(context);
},
@ -318,11 +355,4 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
);
});
}
@override
void dispose() {
passphraseController
.removeListener(() => widget.seedTypeViewModel.setPassphrase(passphraseController.text));
super.dispose();
}
}

View file

@ -3,10 +3,11 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:flutter_svg/svg.dart';
class WalletGroupDescriptionPage extends BasePage {
WalletGroupDescriptionPage({required this.selectedWalletType});
@ -16,16 +17,24 @@ class WalletGroupDescriptionPage extends BasePage {
@override
String get title => S.current.wallet_group;
@override
Widget body(BuildContext context) {
final lightImage = 'assets/images/wallet_group_light.png';
final darkImage = 'assets/images/wallet_group_dark.png';
final brightImage = 'assets/images/wallet_group_bright.png';
final image = currentTheme.type == ThemeType.light ? lightImage : darkImage;
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(24),
child: Column(
children: [
Image.asset(
'assets/images/wallet_group.png',
scale: 0.8,
_getThemedWalletGroupImage(currentTheme.type),
height: 200,
),
SizedBox(height: 32),
Expanded(
@ -87,4 +96,19 @@ class WalletGroupDescriptionPage extends BasePage {
),
);
}
String _getThemedWalletGroupImage(ThemeType theme) {
final lightImage = 'assets/images/wallet_group_light.png';
final darkImage = 'assets/images/wallet_group_dark.png';
final brightImage = 'assets/images/wallet_group_bright.png';
switch (theme) {
case ThemeType.bright:
return brightImage;
case ThemeType.light:
return lightImage;
default:
return darkImage;
}
}
}

View file

@ -58,6 +58,7 @@ class WalletGroupsDisplayBody extends StatelessWidget {
final groupName =
group.groupName ?? '${S.of(context).wallet_group} ${index + 1}';
return GroupedWalletExpansionTile(
shouldShowCurrentWalletPointer: false,
leadingWidget:
Icon(Icons.account_balance_wallet_outlined, size: 28),
borderRadius: BorderRadius.all(Radius.circular(16)),

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