Reworked electrum wallet. Added Litecoin wallet.

This commit is contained in:
M 2021-05-07 10:36:38 +03:00
parent c8a521a29f
commit 1330966794
71 changed files with 1503 additions and 1071 deletions

2
.gitignore vendored
View file

@ -96,3 +96,5 @@ vendor/
android/app/.cxx/**
ios/Flutter/.last_build_id
/lib/generated/**
#**#
/**/#**#

View file

@ -25,7 +25,6 @@ linter:
- empty_constructor_bodies
- empty_statements
- hash_and_equals
- implementation_imports
- invariant_booleans
- iterable_contains_unrelated_type
- library_names

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,2 @@
-
uri: 128.199.34.116:50002

View file

@ -179,8 +179,6 @@ extern "C"
Monero::SubaddressAccount *m_account;
uint64_t m_last_known_wallet_height;
uint64_t m_cached_syncing_blockchain_height = 0;
std::mutex store_mutex;
void change_current_wallet(Monero::Wallet *wallet)
{
@ -451,9 +449,7 @@ extern "C"
void store(char *path)
{
store_mutex.lock();
get_current_wallet()->store(std::string(path));
store_mutex.unlock();
}
bool transaction_create(char *address, char *payment_id, char *amount,

View file

@ -1,12 +1,12 @@
import 'dart:ffi';
import 'package:cw_monero/exceptions/wallet_opening_exception.dart';
import 'package:cw_monero/wallet.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:cw_monero/convert_utf8_to_string.dart';
import 'package:cw_monero/signatures.dart';
import 'package:cw_monero/types.dart';
import 'package:cw_monero/monero_api.dart';
import 'package:cw_monero/wallet.dart';
import 'package:cw_monero/exceptions/wallet_opening_exception.dart';
import 'package:cw_monero/exceptions/wallet_creation_exception.dart';
import 'package:cw_monero/exceptions/wallet_restore_from_keys_exception.dart';
import 'package:cw_monero/exceptions/wallet_restore_from_seed_exception.dart';

View file

@ -7,49 +7,49 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.2"
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.13"
version: "1.15.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
ffi:
dependency: "direct main"
description:
@ -73,21 +73,21 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.8"
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
path_provider:
dependency: "direct main"
description:
@ -113,56 +113,56 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.5"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.17"
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0"
sdks:
dart: ">=2.9.0-14.0.dev <3.0.0"
flutter: ">=0.1.4 <2.0.0"
dart: ">=2.12.0-0.0 <3.0.0"
flutter: ">=0.1.4"

View file

@ -1,25 +1,29 @@
import 'dart:typed_data';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:bitcoin_flutter/src/utils/constants/op.dart';
import 'package:bitcoin_flutter/src/utils/script.dart' as bscript;
import 'package:bitcoin_flutter/src/address.dart';
Uint8List p2shAddressToOutputScript(String address) {
final decodeBase58 = bs58check.decode(address);
final hash = decodeBase58.sublist(1);
return bscript.compile(<dynamic>[OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]);
}
Uint8List addressToOutputScript(String address) {
Uint8List addressToOutputScript(
String address, bitcoin.NetworkType networkType) {
try {
// FIXME: improve validation for p2sh addresses
if (address.startsWith('3')) {
// 3 for bitcoin
// m for litecoin
if (address.startsWith('3') || address.toLowerCase().startsWith('m')) {
return p2shAddressToOutputScript(address);
}
return Address.addressToOutputScript(address);
} catch (_) {
return Address.addressToOutputScript(address, networkType);
} catch (err) {
print(err);
return Uint8List(0);
}
}

View file

@ -1,14 +1,14 @@
import 'dart:convert';
import 'package:quiver/core.dart';
class BitcoinAddressRecord {
BitcoinAddressRecord(this.address, {this.index});
BitcoinAddressRecord(this.address, {this.index, bool isHidden})
: _isHidden = isHidden;
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(decoded['address'] as String,
index: decoded['index'] as int);
index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool);
}
@override
@ -16,10 +16,13 @@ class BitcoinAddressRecord {
o is BitcoinAddressRecord && address == o.address;
final String address;
bool get isHidden => _isHidden ?? false;
int index;
final bool _isHidden;
@override
int get hashCode => address.hashCode;
String toJSON() => json.encode({'address': address, 'index': index});
String toJSON() =>
json.encode({'address': address, 'index': index, 'isHidden': isHidden});
}

View file

@ -1,192 +0,0 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
part 'bitcoin_transaction_history.g.dart';
const _transactionsHistoryFileName = 'transactions.json';
class BitcoinTransactionHistory = BitcoinTransactionHistoryBase
with _$BitcoinTransactionHistory;
abstract class BitcoinTransactionHistoryBase
extends TransactionHistoryBase<BitcoinTransactionInfo> with Store {
BitcoinTransactionHistoryBase(
{this.eclient, String dirPath, @required String password})
: path = '$dirPath/$_transactionsHistoryFileName',
_password = password,
_height = 0,
_isUpdating = false {
transactions = ObservableMap<String, BitcoinTransactionInfo>();
}
BitcoinWalletBase wallet;
final ElectrumClient eclient;
final String path;
final String _password;
int _height;
bool _isUpdating;
Future<void> init() async {
await _load();
}
@override
Future update() async {
if (_isUpdating) {
return;
}
try {
_isUpdating = true;
final txs = await fetchTransactions();
await add(txs);
_isUpdating = false;
} catch (_) {
_isUpdating = false;
rethrow;
}
}
@override
Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async {
final histories =
wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash));
final _historiesWithDetails = await Future.wait(histories)
.then((histories) => histories.expand((i) => i).toList())
.then((histories) => histories.map((tx) => fetchTransactionInfo(
hash: tx['tx_hash'] as String, height: tx['height'] as int)));
final historiesWithDetails = await Future.wait(_historiesWithDetails);
return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>(
<String, BitcoinTransactionInfo>{}, (acc, tx) {
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
return acc;
});
}
Future<BitcoinTransactionInfo> fetchTransactionInfo(
{@required String hash, @required int height}) async {
final tx = await eclient.getTransactionExpanded(hash: hash);
return BitcoinTransactionInfo.fromElectrumVerbose(tx,
height: height, addresses: wallet.addresses);
}
Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
transactionsList.entries.forEach((entry) {
_updateOrInsert(entry.value);
if (entry.value.height > _height) {
_height = entry.value.height;
}
});
await save();
}
Future<void> addOne(BitcoinTransactionInfo tx) async {
_updateOrInsert(tx);
if (tx.height > _height) {
_height = tx.height;
}
await save();
}
BitcoinTransactionInfo get(String id) => transactions[id];
Future<void> save() async {
try {
final data = json.encode({'height': _height, 'transactions': transactions});
await writeData(path: path, password: _password, data: data);
} catch(e) {
print('Error while save bitcoin transaction history: ${e.toString()}');
}
}
@override
void updateAsync({void Function() onFinished}) {
fetchTransactionsAsync((transaction) => _updateOrInsert(transaction),
onFinished: onFinished);
}
@override
void fetchTransactionsAsync(
void Function(BitcoinTransactionInfo transaction) onTransactionLoaded,
{void Function() onFinished}) async {
final histories = await Future.wait(wallet.scriptHashes
.map((scriptHash) async => await eclient.getHistory(scriptHash)));
final transactionsCount =
histories.fold<int>(0, (acc, m) => acc + m.length);
var counter = 0;
final batches = histories.map((metaList) =>
_fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) {
onTransactionLoaded(transaction);
counter += 1;
if (counter == transactionsCount) {
onFinished?.call();
}
}));
await Future.wait(batches);
}
Future<void> _fetchBatchOfTransactions(
Iterable<Map<String, dynamic>> metaList,
{void Function(BitcoinTransactionInfo tranasaction)
onTransactionLoaded}) async =>
metaList.forEach((txMeta) => fetchTransactionInfo(
hash: txMeta['tx_hash'] as String,
height: txMeta['height'] as int)
.then((transaction) => onTransactionLoaded(transaction)));
Future<Map<String, Object>> _read() async {
final content = await read(path: path, password: _password);
return json.decode(content) as Map<String, Object>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, Object> ?? {};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, Object>) {
final tx = BitcoinTransactionInfo.fromJson(val);
_updateOrInsert(tx);
}
});
_height = content['height'] as int;
} catch (e) {
print(e);
}
}
void _updateOrInsert(BitcoinTransactionInfo transaction) {
if (transaction.id == null) {
return;
}
if (transactions[transaction.id] == null) {
transactions[transaction.id] = transaction;
} else {
final originalTx = transactions[transaction.id];
originalTx.confirmations = transaction.confirmations;
originalTx.amount = transaction.amount;
originalTx.height = transaction.height;
originalTx.date ??= transaction.date;
originalTx.isPending = transaction.isPending;
}
}
}

View file

@ -13,5 +13,6 @@ class BitcoinUnspent {
final int value;
final int vout;
bool get isP2wpkh => address.address.startsWith('bc1');
bool get isP2wpkh =>
address.address.startsWith('bc') || address.address.startsWith('ltc');
}

View file

@ -1,472 +1,51 @@
import 'dart:async';
import 'dart:convert';
import 'package:cake_wallet/bitcoin/address_to_output_script.dart';
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart';
import 'package:cake_wallet/bitcoin/script_hash.dart';
import 'package:cake_wallet/bitcoin/utils.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/entities/sync_status.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
import 'package:cake_wallet/entities/wallet_info.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart';
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
import 'package:cake_wallet/entities/node.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/bitcoin/electrum_balance.dart';
part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
BitcoinWalletBase._internal(
{@required this.eclient,
@required this.path,
@required String password,
@required WalletInfo walletInfo,
@required List<BitcoinAddressRecord> initialAddresses,
int accountIndex = 0,
this.transactionHistory,
this.mnemonic,
BitcoinBalance initialBalance})
: balance =
initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0),
hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic),
network: bitcoin.bitcoin)
.derivePath("m/0'/0"),
addresses = initialAddresses != null
? ObservableList<BitcoinAddressRecord>.of(initialAddresses.toSet())
: ObservableList<BitcoinAddressRecord>(),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_accountIndex = accountIndex,
_feeRates = <int>[],
super(walletInfo) {
_unspent = [];
_scripthashesUpdateSubject = {};
}
static BitcoinWallet fromJSON(
{@required String password,
@required String name,
@required String dirPath,
@required WalletInfo walletInfo,
String jsonSource}) {
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String;
final accountIndex =
(data['account_index'] == 'null' || data['account_index'] == null)
? 0
: int.parse(data['account_index'] as String);
final _addresses = data['addresses'] as List ?? <Object>[];
final addresses = <BitcoinAddressRecord>[];
final balance = BitcoinBalance.fromJSON(data['balance'] as String) ??
BitcoinBalance(confirmed: 0, unconfirmed: 0);
_addresses.forEach((Object el) {
if (el is String) {
addresses.add(BitcoinAddressRecord.fromJSON(el));
}
});
return BitcoinWalletBase.build(
dirPath: dirPath,
mnemonic: mnemonic,
password: password,
name: name,
accountIndex: accountIndex,
initialAddresses: addresses,
initialBalance: balance,
walletInfo: walletInfo);
}
static BitcoinWallet build(
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
BitcoinWalletBase(
{@required String mnemonic,
@required String password,
@required String name,
@required String dirPath,
@required WalletInfo walletInfo,
List<BitcoinAddressRecord> initialAddresses,
BitcoinBalance initialBalance,
int accountIndex = 0}) {
final walletPath = '$dirPath/$name';
final eclient = ElectrumClient();
final history = BitcoinTransactionHistory(
eclient: eclient, dirPath: dirPath, password: password);
return BitcoinWallet._internal(
eclient: eclient,
path: walletPath,
ElectrumBalance initialBalance,
int accountIndex = 0})
: super(
mnemonic: mnemonic,
password: password,
accountIndex: accountIndex,
walletInfo: walletInfo,
networkType: bitcoin.bitcoin,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
transactionHistory: history,
walletInfo: walletInfo);
}
accountIndex: accountIndex);
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 146 + outputsCounts * 33 + 8;
@override
final BitcoinTransactionHistory transactionHistory;
final String path;
final bitcoin.HDWallet hd;
final ElectrumClient eclient;
final String mnemonic;
List<BitcoinUnspent> _unspent;
@override
@observable
String address;
@override
@observable
BitcoinBalance balance;
@override
@observable
SyncStatus syncStatus;
ObservableList<BitcoinAddressRecord> addresses;
List<String> get scriptHashes =>
addresses.map((addr) => scriptHash(addr.address)).toList();
String get xpub => hd.base58;
@override
String get seed => mnemonic;
@override
BitcoinWalletKeys get keys => BitcoinWalletKeys(
wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
final String _password;
List<int> _feeRates;
int _accountIndex;
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
Future<void> init() async {
if (addresses.isEmpty || addresses.length < 33) {
final addressesCount = 33 - addresses.length;
await generateNewAddresses(addressesCount, startIndex: addresses.length);
}
address = addresses[_accountIndex].address;
transactionHistory.wallet = this;
await transactionHistory.init();
}
@action
Future<void> nextAddress() async {
_accountIndex += 1;
if (_accountIndex >= addresses.length) {
_accountIndex = 0;
}
address = addresses[_accountIndex].address;
await save();
}
Future<BitcoinAddressRecord> generateNewAddress() async {
_accountIndex += 1;
final address = BitcoinAddressRecord(_getAddress(index: _accountIndex),
index: _accountIndex);
addresses.add(address);
await save();
return address;
}
Future<List<BitcoinAddressRecord>> generateNewAddresses(int count,
{int startIndex = 0}) async {
final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord(_getAddress(index: i), index: i);
list.add(address);
}
addresses.addAll(list);
await save();
return list;
}
Future<void> updateAddress(String address) async {
for (final addr in addresses) {
if (addr.address == address) {
await save();
break;
}
}
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = StartingSyncStatus();
transactionHistory.updateAsync(onFinished: () {
print('transactionHistory update finished!');
transactionHistory.save();
});
_subscribeForUpdates();
await _updateBalance();
await _updateUnspent();
_feeRates = await eclient.feeRates();
Timer.periodic(const Duration(minutes: 1),
(timer) async => _feeRates = await eclient.feeRates());
syncStatus = SyncedSyncStatus();
} catch (e) {
print(e.toString());
syncStatus = FailedSyncStatus();
}
}
@action
@override
Future<void> connectToNode({@required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
await eclient.connectToUri(node.uri);
eclient.onConnectionStatusChange = (bool isConnected) {
if (!isConnected) {
syncStatus = LostConnectionSyncStatus();
}
};
syncStatus = ConnectedSyncStatus();
} catch (e) {
print(e.toString());
syncStatus = FailedSyncStatus();
}
static Future<BitcoinWallet> open({
@required String name,
@required WalletInfo walletInfo,
@required String password,
}) async {
final snp = ElectrumWallletSnapshot(name, walletInfo.type, password);
await snp.load();
return BitcoinWallet(
mnemonic: snp.mnemonic,
password: password,
walletInfo: walletInfo,
initialAddresses: snp.addresses,
initialBalance: snp.balance,
accountIndex: snp.accountIndex);
}
@override
Future<PendingBitcoinTransaction> createTransaction(
Object credentials) async {
const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
final allAmountFee =
calculateEstimatedFee(transactionCredentials.priority, null);
final allAmount = balance.confirmed - allAmountFee;
var fee = 0;
final credentialsAmount = transactionCredentials.amount != null
? stringDoubleToBitcoinAmount(transactionCredentials.amount)
: 0;
final amount = transactionCredentials.amount == null ||
allAmount - credentialsAmount < minAmount
? allAmount
: credentialsAmount;
final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin);
final changeAddress = address;
var leftAmount = amount;
var totalInputAmount = 0;
if (_unspent.isEmpty) {
await _updateUnspent();
}
for (final utx in _unspent) {
leftAmount = leftAmount - utx.value;
totalInputAmount += utx.value;
inputs.add(utx);
if (leftAmount <= 0) {
break;
}
}
if (inputs.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final totalAmount = amount + fee;
fee = transactionCredentials.amount != null
? feeAmountForPriority(transactionCredentials.priority, inputs.length,
amount == allAmount ? 1 : 2)
: allAmountFee;
if (totalAmount > balance.confirmed) {
throw BitcoinTransactionWrongBalanceException();
}
if (amount <= 0 || totalInputAmount < amount) {
throw BitcoinTransactionWrongBalanceException();
}
txb.setVersion(1);
inputs.forEach((input) {
if (input.isP2wpkh) {
final p2wpkh = bitcoin
.P2WPKH(
data: generatePaymentData(hd: hd, index: input.address.index),
network: bitcoin.bitcoin)
.data;
txb.addInput(input.hash, input.vout, null, p2wpkh.output);
} else {
txb.addInput(input.hash, input.vout);
}
});
txb.addOutput(
addressToOutputScript(transactionCredentials.address), amount);
final estimatedSize = estimatedTransactionSize(inputs.length, 2);
final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) {
txb.addOutput(changeAddress, changeValue);
}
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
final keyPair = generateKeyPair(hd: hd, index: input.address.index);
final witnessValue = input.isP2wpkh ? input.value : null;
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
}
return PendingBitcoinTransaction(txb.build(),
eclient: eclient, amount: amount, fee: fee)
..addListener((transaction) async {
transactionHistory.addOne(transaction);
await _updateBalance();
});
}
String toJSON() => json.encode({
'mnemonic': mnemonic,
'account_index': _accountIndex.toString(),
'addresses': addresses.map((addr) => addr.toJSON()).toList(),
'balance': balance?.toJSON()
});
int feeRate(TransactionPriority priority) {
if (priority is BitcoinTransactionPriority) {
return _feeRates[priority.raw];
}
return 0;
}
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount,
int outputsCount) =>
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
@override
int calculateEstimatedFee(TransactionPriority priority, int amount) {
if (priority is BitcoinTransactionPriority) {
int inputsCount = 0;
if (amount != null) {
int totalValue = 0;
for (final input in _unspent) {
if (totalValue >= amount) {
break;
}
totalValue += input.value;
inputsCount += 1;
}
} else {
inputsCount = _unspent.length;
}
// If send all, then we have no change value
return feeAmountForPriority(
priority, inputsCount, amount != null ? 2 : 1);
}
return 0;
}
@override
Future<void> save() async {
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
bitcoin.ECPair keyPairFor({@required int index}) =>
generateKeyPair(hd: hd, index: index);
@override
Future<void> rescan({int height}) async {
// FIXME: Unimplemented
}
@override
void close() async {
await eclient.close();
}
Future<void> _updateUnspent() async {
final unspent = await Future.wait(addresses.map((address) => eclient
.getListUnspentWithAddress(address.address)
.then((unspent) => unspent
.map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))));
_unspent = unspent.expand((e) => e).toList();
}
void _subscribeForUpdates() {
scriptHashes.forEach((sh) async {
await _scripthashesUpdateSubject[sh]?.close();
_scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh);
_scripthashesUpdateSubject[sh].listen((event) async {
try {
await _updateBalance();
await _updateUnspent();
transactionHistory.updateAsync();
} catch (e) {
print(e.toString());
}
});
});
}
Future<BitcoinBalance> _fetchBalances() async {
final balances = await Future.wait(
scriptHashes.map((sHash) => eclient.getBalance(sHash)));
final balance = balances.fold(
BitcoinBalance(confirmed: 0, unconfirmed: 0),
(BitcoinBalance acc, val) => BitcoinBalance(
confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0),
unconfirmed:
(val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0)));
return balance;
}
Future<void> _updateBalance() async {
balance = await _fetchBalances();
await save();
}
String _getAddress({@required int index}) =>
generateAddress(hd: hd, index: index);
String getAddress({@required int index, @required bitcoin.HDWallet hd}) =>
generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
}

View file

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart';
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/core/wallet_service.dart';
@ -19,44 +18,32 @@ class BitcoinWalletService extends WalletService<
final Box<WalletInfo> walletInfoSource;
@override
WalletType getType() => WalletType.bitcoin;
@override
Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials) async {
final dirPath = await pathForWalletDir(
type: WalletType.bitcoin, name: credentials.name);
final wallet = BitcoinWalletBase.build(
dirPath: dirPath,
final wallet = BitcoinWallet(
mnemonic: generateMnemonic(),
password: credentials.password,
name: credentials.name,
walletInfo: credentials.walletInfo);
await wallet.save();
await wallet.init();
return wallet;
}
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: WalletType.bitcoin))
.existsSync();
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<BitcoinWallet> openWallet(String name, String password) async {
final walletDirPath =
await pathForWalletDir(name: name, type: WalletType.bitcoin);
final walletPath = '$walletDirPath/$name';
final walletJSONRaw = await read(path: walletPath, password: password);
final walletInfo = walletInfoSource.values.firstWhere(
(info) => info.id == WalletBase.idFor(name, WalletType.bitcoin),
(info) => info.id == WalletBase.idFor(name, getType()),
orElse: () => null);
final wallet = BitcoinWalletBase.fromJSON(
password: password,
name: name,
dirPath: walletDirPath,
jsonSource: walletJSONRaw,
walletInfo: walletInfo);
final wallet = await BitcoinWalletBase.open(
password: password, name: name, walletInfo: walletInfo);
await wallet.init();
return wallet;
}
@ -67,10 +54,8 @@ class BitcoinWalletService extends WalletService<
@override
Future<BitcoinWallet> restoreFromKeys(
BitcoinRestoreWalletFromWIFCredentials credentials) async {
// TODO: implement restoreFromKeys
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
throw UnimplementedError();
}
@override
Future<BitcoinWallet> restoreFromSeed(
@ -79,17 +64,12 @@ class BitcoinWalletService extends WalletService<
throw BitcoinMnemonicIsIncorrectException();
}
final dirPath = await pathForWalletDir(
type: WalletType.bitcoin, name: credentials.name);
final wallet = BitcoinWalletBase.build(
dirPath: dirPath,
name: credentials.name,
final wallet = BitcoinWallet(
password: credentials.password,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo);
await wallet.save();
await wallet.init();
return wallet;
}
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart';
@ -173,10 +174,11 @@ class ElectrumClient {
});
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
String address) =>
String address, NetworkType networkType) =>
call(
method: 'blockchain.scripthash.listunspent',
params: [scriptHash(address)]).then((dynamic result) {
params: [scriptHash(address, networkType: networkType)])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, Object>) {
@ -259,7 +261,7 @@ class ElectrumClient {
if (result is String) {
return result;
}
print(result);
return '';
});
@ -303,14 +305,25 @@ class ElectrumClient {
});
Future<List<int>> feeRates() async {
try {
final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 20);
final bottomDoubleString = await estimatefee(p: 150);
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
final top =
(stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000)
.round();
final middle =
(stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000)
.round();
final bottom =
(stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000)
.round();
return [bottom, middle, top];
final res = [bottom, middle, top];
return res;
} catch (_) {
return [];
}
}
BehaviorSubject<Object> scripthashUpdate(String scripthash) {

View file

@ -1,21 +1,20 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/entities/balance.dart';
class BitcoinBalance extends Balance {
const BitcoinBalance({@required this.confirmed, @required this.unconfirmed})
class ElectrumBalance extends Balance {
const ElectrumBalance({@required this.confirmed, @required this.unconfirmed})
: super(confirmed, unconfirmed);
factory BitcoinBalance.fromJSON(String jsonSource) {
factory ElectrumBalance.fromJSON(String jsonSource) {
if (jsonSource == null) {
return null;
}
final decoded = json.decode(jsonSource) as Map;
return BitcoinBalance(
return ElectrumBalance(
confirmed: decoded['confirmed'] as int ?? 0,
unconfirmed: decoded['unconfirmed'] as int ?? 0);
}
@ -24,7 +23,8 @@ class BitcoinBalance extends Balance {
final int unconfirmed;
@override
String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed);
String get formattedAvailableBalance =>
bitcoinAmountToString(amount: confirmed);
@override
String get formattedAdditionalBalance =>

View file

@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:cake_wallet/entities/pathForWallet.dart';
import 'package:cake_wallet/entities/wallet_info.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
part 'electrum_transaction_history.g.dart';
const _transactionsHistoryFileName = 'transactions.json';
class ElectrumTransactionHistory = ElectrumTransactionHistoryBase
with _$ElectrumTransactionHistory;
abstract class ElectrumTransactionHistoryBase
extends TransactionHistoryBase<ElectrumTransactionInfo> with Store {
ElectrumTransactionHistoryBase(
{@required this.walletInfo, @required String password})
: _password = password,
_height = 0 {
transactions = ObservableMap<String, ElectrumTransactionInfo>();
}
final WalletInfo walletInfo;
final String _password;
int _height;
Future<void> init() async => await _load();
@override
void addOne(ElectrumTransactionInfo transaction) =>
transactions[transaction.id] = transaction;
@override
void addMany(Map<String, ElectrumTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
@override
Future<void> save() async {
try {
final dirPath =
await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$_transactionsHistoryFileName';
final data =
json.encode({'height': _height, 'transactions': transactions});
await writeData(path: path, password: _password, data: data);
} catch (e) {
print('Error while save bitcoin transaction history: ${e.toString()}');
}
}
Future<Map<String, Object>> _read() async {
final dirPath =
await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$_transactionsHistoryFileName';
final content = await read(path: path, password: _password);
return json.decode(content) as Map<String, Object>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, Object> ?? {};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, Object>) {
final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type);
_updateOrInsert(tx);
}
});
_height = content['height'] as int;
} catch (e) {
print(e);
}
}
void _updateOrInsert(ElectrumTransactionInfo transaction) {
if (transaction.id == null) {
return;
}
if (transactions[transaction.id] == null) {
transactions[transaction.id] = transaction;
} else {
final originalTx = transactions[transaction.id];
originalTx.confirmations = transaction.confirmations;
originalTx.amount = transaction.amount;
originalTx.height = transaction.height;
originalTx.date ??= transaction.date;
originalTx.isPending = transaction.isPending;
}
}
}

View file

@ -6,9 +6,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/entities/transaction_direction.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/entities/format_amount.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
class BitcoinTransactionInfo extends TransactionInfo {
BitcoinTransactionInfo(
class ElectrumTransactionInfo extends TransactionInfo {
ElectrumTransactionInfo(this.type,
{@required String id,
@required int height,
@required int amount,
@ -27,7 +28,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
this.confirmations = confirmations;
}
factory BitcoinTransactionInfo.fromElectrumVerbose(Map<String, Object> obj,
factory ElectrumTransactionInfo.fromElectrumVerbose(
Map<String, Object> obj, WalletType type,
{@required List<BitcoinAddressRecord> addresses, @required int height}) {
final addressesSet = addresses.map((addr) => addr.address).toSet();
final id = obj['txid'] as String;
@ -47,7 +49,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
final out = vin['tx']['vout'][vout] as Map;
final outAddresses =
(out['scriptPubKey']['addresses'] as List<Object>)?.toSet();
inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString());
inputsAmount +=
stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString());
if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) {
direction = TransactionDirection.outgoing;
@ -58,7 +61,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
final outAddresses =
out['scriptPubKey']['addresses'] as List<Object> ?? [];
final ntrs = outAddresses.toSet().intersection(addressesSet);
final value = stringDoubleToBitcoinAmount((out['value'] as double ?? 0.0).toString());
final value = stringDoubleToBitcoinAmount(
(out['value'] as double ?? 0.0).toString());
totalOutAmount += value;
if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
@ -69,7 +73,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
final fee = inputsAmount - totalOutAmount;
return BitcoinTransactionInfo(
return ElectrumTransactionInfo(type,
id: id,
height: height,
isPending: false,
@ -80,7 +84,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
confirmations: confirmations);
}
factory BitcoinTransactionInfo.fromHexAndHeader(String hex,
factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex,
{List<String> addresses, int height, int timestamp, int confirmations}) {
final tx = bitcoin.Transaction.fromHex(hex);
var exist = false;
@ -104,7 +108,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
: DateTime.now();
return BitcoinTransactionInfo(
return ElectrumTransactionInfo(type,
id: tx.getId(),
height: height,
isPending: false,
@ -115,8 +119,9 @@ class BitcoinTransactionInfo extends TransactionInfo {
confirmations: confirmations);
}
factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
return BitcoinTransactionInfo(
factory ElectrumTransactionInfo.fromJson(
Map<String, dynamic> data, WalletType type) {
return ElectrumTransactionInfo(type,
id: data['id'] as String,
height: data['height'] as int,
amount: data['amount'] as int,
@ -127,6 +132,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
confirmations: data['confirmations'] as int);
}
final WalletType type;
String _fiatAmount;
@override
@ -144,8 +151,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
BitcoinTransactionInfo updated(BitcoinTransactionInfo info) {
return BitcoinTransactionInfo(
ElectrumTransactionInfo updated(ElectrumTransactionInfo info) {
return ElectrumTransactionInfo(info.type,
id: id,
height: info.height,
amount: info.amount,

View file

@ -0,0 +1,467 @@
import 'dart:async';
import 'dart:convert';
import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart';
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
import 'package:cake_wallet/entities/pathForWallet.dart';
import 'package:cake_wallet/bitcoin/address_to_output_script.dart';
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/electrum_balance.dart';
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cake_wallet/bitcoin/electrum_transaction_history.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart';
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart';
import 'package:cake_wallet/bitcoin/script_hash.dart';
import 'package:cake_wallet/bitcoin/utils.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/entities/node.dart';
import 'package:cake_wallet/entities/sync_status.dart';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:cake_wallet/entities/wallet_info.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
part 'electrum_wallet.g.dart';
class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
ElectrumTransactionHistory, ElectrumTransactionInfo> with Store {
ElectrumWalletBase(
{@required String password,
@required WalletInfo walletInfo,
@required List<BitcoinAddressRecord> initialAddresses,
@required this.networkType,
@required this.mnemonic,
ElectrumClient electrumClient,
int accountIndex = 0,
ElectrumBalance initialBalance})
: balance = initialBalance ??
const ElectrumBalance(confirmed: 0, unconfirmed: 0),
hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic),
network: networkType)
.derivePath("m/0'/0"),
addresses = ObservableList<BitcoinAddressRecord>.of(
(initialAddresses ?? []).toSet()),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_accountIndex = accountIndex,
_feeRates = <int>[],
_isTransactionUpdating = false,
super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo;
transactionHistory =
ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
_unspent = [];
_scripthashesUpdateSubject = {};
}
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 146 + outputsCounts * 33 + 8;
final bitcoin.HDWallet hd;
final String mnemonic;
ElectrumClient electrumClient;
@override
@observable
String address;
@override
@observable
ElectrumBalance balance;
@override
@observable
SyncStatus syncStatus;
ObservableList<BitcoinAddressRecord> addresses;
List<String> get scriptHashes => addresses
.map((addr) => scriptHash(addr.address, networkType: networkType))
.toList();
String get xpub => hd.base58;
@override
String get seed => mnemonic;
bitcoin.NetworkType networkType;
@override
BitcoinWalletKeys get keys => BitcoinWalletKeys(
wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
final String _password;
List<BitcoinUnspent> _unspent;
List<int> _feeRates;
int _accountIndex;
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
bool _isTransactionUpdating;
Future<void> init() async {
await generateAddresses();
address = addresses[_accountIndex].address;
await transactionHistory.init();
}
@action
Future<void> nextAddress() async {
_accountIndex += 1;
if (_accountIndex >= addresses.length) {
_accountIndex = 0;
}
address = addresses[_accountIndex].address;
await save();
}
Future<void> generateAddresses() async {
if (addresses.length < 33) {
final addressesCount = 33 - addresses.length;
await generateNewAddresses(addressesCount,
startIndex: addresses.length, hd: hd);
}
}
Future<BitcoinAddressRecord> generateNewAddress(
{bool isHidden = false, bitcoin.HDWallet hd}) async {
_accountIndex += 1;
final _hd = hd ?? this.hd;
final address = BitcoinAddressRecord(
getAddress(index: _accountIndex, hd: _hd),
index: _accountIndex,
isHidden: isHidden);
addresses.add(address);
await save();
return address;
}
Future<List<BitcoinAddressRecord>> generateNewAddresses(int count,
{int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async {
final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord(getAddress(index: i, hd: hd),
index: i, isHidden: isHidden);
list.add(address);
}
addresses.addAll(list);
await save();
return list;
}
Future<void> updateAddress(String address) async {
for (final addr in addresses) {
if (addr.address == address) {
await save();
break;
}
}
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = StartingSyncStatus();
updateTransactions();
_subscribeForUpdates();
await _updateBalance();
await _updateUnspent();
_feeRates = await electrumClient.feeRates();
Timer.periodic(const Duration(minutes: 1),
(timer) async => _feeRates = await electrumClient.feeRates());
syncStatus = SyncedSyncStatus();
} catch (e) {
print(e.toString());
syncStatus = FailedSyncStatus();
}
}
@action
@override
Future<void> connectToNode({@required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
await electrumClient.connectToUri(node.uri);
electrumClient.onConnectionStatusChange = (bool isConnected) {
if (!isConnected) {
syncStatus = LostConnectionSyncStatus();
}
};
syncStatus = ConnectedSyncStatus();
} catch (e) {
print(e.toString());
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingBitcoinTransaction> createTransaction(
Object credentials) async {
const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
final allAmountFee =
calculateEstimatedFee(transactionCredentials.priority, null);
final allAmount = balance.confirmed - allAmountFee;
var fee = 0;
final credentialsAmount = transactionCredentials.amount != null
? stringDoubleToBitcoinAmount(transactionCredentials.amount)
: 0;
final amount = transactionCredentials.amount == null ||
allAmount - credentialsAmount < minAmount
? allAmount
: credentialsAmount;
final txb = bitcoin.TransactionBuilder(network: networkType);
final changeAddress = address;
var leftAmount = amount;
var totalInputAmount = 0;
if (_unspent.isEmpty) {
await _updateUnspent();
}
for (final utx in _unspent) {
leftAmount = leftAmount - utx.value;
totalInputAmount += utx.value;
inputs.add(utx);
if (leftAmount <= 0) {
break;
}
}
if (inputs.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final totalAmount = amount + fee;
fee = transactionCredentials.amount != null
? feeAmountForPriority(transactionCredentials.priority, inputs.length,
amount == allAmount ? 1 : 2)
: allAmountFee;
if (totalAmount > balance.confirmed) {
throw BitcoinTransactionWrongBalanceException();
}
if (amount <= 0 || totalInputAmount < amount) {
throw BitcoinTransactionWrongBalanceException();
}
txb.setVersion(1);
inputs.forEach((input) {
if (input.isP2wpkh) {
final p2wpkh = bitcoin
.P2WPKH(
data: generatePaymentData(hd: hd, index: input.address.index),
network: networkType)
.data;
txb.addInput(input.hash, input.vout, null, p2wpkh.output);
} else {
txb.addInput(input.hash, input.vout);
}
});
txb.addOutput(
addressToOutputScript(transactionCredentials.address, networkType),
amount);
final estimatedSize = estimatedTransactionSize(inputs.length, 2);
final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) {
txb.addOutput(changeAddress, changeValue);
}
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
final keyPair = generateKeyPair(
hd: hd, index: input.address.index, network: networkType);
final witnessValue = input.isP2wpkh ? input.value : null;
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
}
return PendingBitcoinTransaction(txb.build(), type,
electrumClient: electrumClient, amount: amount, fee: fee)
..addListener((transaction) async {
transactionHistory.addOne(transaction);
await _updateBalance();
});
}
String toJSON() => json.encode({
'mnemonic': mnemonic,
'account_index': _accountIndex.toString(),
'addresses': addresses.map((addr) => addr.toJSON()).toList(),
'balance': balance?.toJSON()
});
int feeRate(TransactionPriority priority) {
if (priority is BitcoinTransactionPriority) {
return _feeRates[priority.raw];
}
return 0;
}
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount,
int outputsCount) =>
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
@override
int calculateEstimatedFee(TransactionPriority priority, int amount) {
if (priority is BitcoinTransactionPriority) {
int inputsCount = 0;
if (amount != null) {
int totalValue = 0;
for (final input in _unspent) {
if (totalValue >= amount) {
break;
}
totalValue += input.value;
inputsCount += 1;
}
} else {
inputsCount = _unspent.length;
}
// If send all, then we have no change value
return feeAmountForPriority(
priority, inputsCount, amount != null ? 2 : 1);
}
return 0;
}
@override
Future<void> save() async {
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
bitcoin.ECPair keyPairFor({@required int index}) =>
generateKeyPair(hd: hd, index: index, network: networkType);
@override
Future<void> rescan({int height}) async => throw UnimplementedError();
@override
Future<void> close() async {
try {
await electrumClient?.close();
} catch (_) {}
}
String getAddress({@required int index, @required bitcoin.HDWallet hd}) => '';
Future<String> makePath() async =>
pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> _updateUnspent() async {
final unspent = await Future.wait(addresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, networkType)
.then((unspent) => unspent
.map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))));
_unspent = unspent.expand((e) => e).toList();
}
Future<ElectrumTransactionInfo> fetchTransactionInfo(
{@required String hash, @required int height}) async {
final tx = await electrumClient.getTransactionExpanded(hash: hash);
return ElectrumTransactionInfo.fromElectrumVerbose(tx, walletInfo.type,
height: height, addresses: addresses);
}
@override
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
final histories =
scriptHashes.map((scriptHash) => electrumClient.getHistory(scriptHash));
final _historiesWithDetails = await Future.wait(histories)
.then((histories) => histories.expand((i) => i).toList())
.then((histories) => histories.map((tx) => fetchTransactionInfo(
hash: tx['tx_hash'] as String, height: tx['height'] as int)));
final historiesWithDetails = await Future.wait(_historiesWithDetails);
return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
<String, ElectrumTransactionInfo>{}, (acc, tx) {
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
return acc;
});
}
Future<void> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (e) {
print(e);
_isTransactionUpdating = false;
}
}
void _subscribeForUpdates() {
scriptHashes.forEach((sh) async {
await _scripthashesUpdateSubject[sh]?.close();
_scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh);
_scripthashesUpdateSubject[sh].listen((event) async {
try {
await _updateBalance();
await _updateUnspent();
await updateTransactions();
} catch (e) {
print(e.toString());
}
});
});
}
Future<ElectrumBalance> _fetchBalances() async {
final balances = await Future.wait(
scriptHashes.map((sh) => electrumClient.getBalance(sh)));
final balance = balances.fold(
ElectrumBalance(confirmed: 0, unconfirmed: 0),
(ElectrumBalance acc, val) => ElectrumBalance(
confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0),
unconfirmed:
(val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0)));
return balance;
}
Future<void> _updateBalance() async {
balance = await _fetchBalances();
await save();
}
}

View file

@ -0,0 +1,42 @@
import 'dart:convert';
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
import 'package:cake_wallet/bitcoin/electrum_balance.dart';
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/entities/pathForWallet.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
class ElectrumWallletSnapshot {
ElectrumWallletSnapshot(this.name, this.type, this.password);
final String name;
final String password;
final WalletType type;
String mnemonic;
List<BitcoinAddressRecord> addresses;
ElectrumBalance balance;
int accountIndex;
Future<void> load() async {
try {
final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List ?? <Object>[];
mnemonic = data['mnemonic'] as String;
addresses = addressesTmp
.whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr))
.toList();
balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
ElectrumBalance(confirmed: 0, unconfirmed: 0);
accountIndex = 0;
try {
accountIndex = int.parse(data['account_index'] as String);
} catch (_) {}
} catch (e) {
print(e);
}
}
}

View file

@ -0,0 +1,9 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
final litecoinNetwork = NetworkType(
messagePrefix: '\x19Litecoin Signed Message:\n',
bech32: 'ltc',
bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4),
pubKeyHash: 0x30,
scriptHash: 0x32,
wif: 0xb0);

View file

@ -0,0 +1,88 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/wallet_info.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
import 'package:cake_wallet/bitcoin/electrum_balance.dart';
import 'package:cake_wallet/bitcoin/litecoin_network.dart';
import 'package:cake_wallet/bitcoin/utils.dart';
part 'litecoin_wallet.g.dart';
class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet;
abstract class LitecoinWalletBase extends ElectrumWallet with Store {
LitecoinWalletBase(
{@required String mnemonic,
@required String password,
@required WalletInfo walletInfo,
List<BitcoinAddressRecord> initialAddresses,
ElectrumBalance initialBalance,
int accountIndex = 0})
: super(
mnemonic: mnemonic,
password: password,
walletInfo: walletInfo,
networkType: litecoinNetwork,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
accountIndex: accountIndex);
static Future<LitecoinWallet> open({
@required String name,
@required WalletInfo walletInfo,
@required String password,
}) async {
final snp = ElectrumWallletSnapshot(name, walletInfo.type, password);
await snp.load();
return LitecoinWallet(
mnemonic: snp.mnemonic,
password: password,
walletInfo: walletInfo,
initialAddresses: snp.addresses,
initialBalance: snp.balance,
accountIndex: snp.accountIndex);
}
@override
String getAddress({@required int index, @required bitcoin.HDWallet hd}) =>
generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
@override
Future<void> generateAddresses() async {
if (addresses.length < 33) {
final addressesCount = 22 - addresses.length;
await generateNewAddresses(addressesCount,
hd: hd, startIndex: addresses.length);
final changeRoot = bitcoin.HDWallet.fromSeed(
mnemonicToSeedBytes(mnemonic),
network: networkType)
.derivePath("m/0'/1");
await generateNewAddresses(11,
startIndex: 0, hd: changeRoot, isHidden: true);
}
}
@override
int feeRate(TransactionPriority priority) {
if (priority is BitcoinTransactionPriority) {
switch (priority) {
case BitcoinTransactionPriority.slow:
return 1;
case BitcoinTransactionPriority.medium:
return 2;
case BitcoinTransactionPriority.fast:
return 3;
}
}
return 0;
}
}

View file

@ -0,0 +1,76 @@
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cake_wallet/bitcoin/litecoin_wallet.dart';
import 'package:cake_wallet/core/wallet_service.dart';
import 'package:cake_wallet/entities/pathForWallet.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
import 'package:cake_wallet/entities/wallet_info.dart';
import 'package:cake_wallet/core/wallet_base.dart';
class LitecoinWalletService extends WalletService<
BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials> {
LitecoinWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource;
@override
WalletType getType() => WalletType.litecoin;
@override
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials) async {
final wallet = LitecoinWallet(
mnemonic: generateMnemonic(),
password: credentials.password,
walletInfo: credentials.walletInfo);
await wallet.save();
await wallet.init();
return wallet;
}
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<LitecoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values.firstWhere(
(info) => info.id == WalletBase.idFor(name, getType()),
orElse: () => null);
final wallet = await LitecoinWalletBase.open(
password: password, name: name, walletInfo: walletInfo);
await wallet.init();
return wallet;
}
@override
Future<void> remove(String wallet) async =>
File(await pathForWalletDir(name: wallet, type: getType()))
.delete(recursive: true);
@override
Future<LitecoinWallet> restoreFromKeys(
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
throw UnimplementedError();
@override
Future<LitecoinWallet> restoreFromSeed(
BitcoinRestoreWalletFromSeedCredentials credentials) async {
if (!validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException();
}
final wallet = LitecoinWallet(
password: credentials.password,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo);
await wallet.save();
await wallet.init();
return wallet;
}
}

View file

@ -1,18 +1,22 @@
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/entities/transaction_direction.dart';
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/core/pending_transaction.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
import 'package:cake_wallet/entities/transaction_direction.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(this._tx,
{@required this.eclient, @required this.amount, @required this.fee})
: _listeners = <void Function(BitcoinTransactionInfo transaction)>[];
PendingBitcoinTransaction(this._tx, this.type,
{@required this.electrumClient,
@required this.amount,
@required this.fee})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
final bitcoin.Transaction _tx;
final ElectrumClient eclient;
final ElectrumClient electrumClient;
final int amount;
final int fee;
@ -25,24 +29,25 @@ class PendingBitcoinTransaction with PendingTransaction {
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
final List<void Function(BitcoinTransactionInfo transaction)> _listeners;
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
await eclient.broadcastTransaction(transactionRaw: _tx.toHex());
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
_listeners?.forEach((listener) => listener(transactionInfo()));
}
void addListener(
void Function(BitcoinTransactionInfo transaction) listener) =>
void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo(
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
id: id,
height: 0,
amount: amount,
direction: TransactionDirection.outgoing,
date: DateTime.now(),
isPending: true,
confirmations: 0);
confirmations: 0,
fee: fee);
}

View file

@ -1,15 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:crypto/crypto.dart';
String scriptHash(String address) {
final outputScript = bitcoin.Address.addressToOutputScript(address);
final splitted = sha256.convert(outputScript).toString().split('');
String scriptHash(String address, {@required bitcoin.NetworkType networkType}) {
final outputScript =
bitcoin.Address.addressToOutputScript(address, networkType);
final parts = sha256.convert(outputScript).toString().split('');
var res = '';
for (var i = splitted.length - 1; i >= 0; i--) {
final char = splitted[i];
for (var i = parts.length - 1; i >= 0; i--) {
final char = parts[i];
i--;
final nextChar = splitted[i];
final nextChar = parts[i];
res += nextChar;
res += char;
}

View file

@ -13,14 +13,43 @@ bitcoin.ECPair generateKeyPair(
{@required bitcoin.HDWallet hd,
@required int index,
bitcoin.NetworkType network}) =>
bitcoin.ECPair.fromWIF(hd.derive(index).wif,
network: network ?? bitcoin.bitcoin);
bitcoin.ECPair.fromWIF(hd.derive(index).wif, network: network);
String generateAddress({@required bitcoin.HDWallet hd, @required int index}) =>
String generateP2WPKHAddress(
{@required bitcoin.HDWallet hd,
@required int index,
bitcoin.NetworkType networkType}) =>
bitcoin
.P2WPKH(
data: PaymentData(
pubkey:
Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))))
Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))),
network: networkType)
.data
.address;
String generateP2WPKHAddressByPath(
{@required bitcoin.HDWallet hd,
@required String path,
bitcoin.NetworkType networkType}) =>
bitcoin
.P2WPKH(
data: PaymentData(
pubkey:
Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey))),
network: networkType)
.data
.address;
String generateP2PKHAddress(
{@required bitcoin.HDWallet hd,
@required int index,
bitcoin.NetworkType networkType}) =>
bitcoin
.P2PKH(
data: PaymentData(
pubkey:
Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))),
network: networkType)
.data
.address;

View file

@ -72,7 +72,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.eth:
return [42];
case CryptoCurrency.ltc:
return [34];
return [34, 43];
case CryptoCurrency.nano:
return [64, 65];
case CryptoCurrency.trx:

View file

@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:cryptography/cryptography.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:archive/archive_io.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/entities/encrypt.dart';
import 'package:cake_wallet/entities/preferences_key.dart';

View file

@ -3,7 +3,6 @@ import 'package:cake_wallet/entities/fiat_currency.dart';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/entities/currency_formatter.dart';
const fiatApiAuthority = 'fiat-api.cakewallet.com';
const fiatApiPath = '/v1/rates';
@ -15,8 +14,7 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
try {
final fiatStringified = fiat.toString();
final uri =
Uri.https(fiatApiAuthority, fiatApiPath,
final uri = Uri.https(fiatApiAuthority, fiatApiPath,
<String, String>{'convert': fiatStringified});
final response = await get(uri.toString());
@ -28,7 +26,7 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
final data = responseJSON['data'] as List<dynamic>;
for (final item in data) {
if (item['symbol'] == cryptoToString(crypto)) {
if (item['symbol'] == crypto.title) {
price = item['quote'][fiatStringified]['price'] as double;
break;
}
@ -45,6 +43,7 @@ Future<double> _fetchPriceAsync(
compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto});
class FiatConversionService {
static Future<double> fetchPrice(CryptoCurrency crypto, FiatCurrency fiat) async =>
static Future<double> fetchPrice(
CryptoCurrency crypto, FiatCurrency fiat) async =>
await _fetchPriceAsync(crypto, fiat);
}

View file

@ -4,9 +4,9 @@ import 'package:cake_wallet/entities/wallet_type.dart';
String generateWalletPassword(WalletType type) {
switch (type) {
case WalletType.bitcoin:
return generateKey();
default:
case WalletType.monero:
return Uuid().v4();
default:
return generateKey();
}
}

View file

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/validator.dart';

View file

@ -3,43 +3,50 @@ import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
TransactionHistoryBase() : _isUpdating = false;
TransactionHistoryBase();
// : _isUpdating = false;
@observable
ObservableMap<String, TransactionType> transactions;
bool _isUpdating;
Future<void> save();
@action
Future<void> update() async {
if (_isUpdating) {
return;
}
void addOne(TransactionType transaction);
try {
_isUpdating = true;
final _transactions = await fetchTransactions();
transactions.keys
.toSet()
.difference(_transactions.keys.toSet())
.forEach((k) => transactions.remove(k));
_transactions.forEach((key, value) => transactions[key] = value);
_isUpdating = false;
} catch (e) {
_isUpdating = false;
rethrow;
}
}
void addMany(Map<String, TransactionType> transactions);
void updateAsync({void Function() onFinished}) {
fetchTransactionsAsync(
(transaction) => transactions[transaction.id] = transaction,
onFinished: onFinished);
}
// bool _isUpdating;
void fetchTransactionsAsync(
void Function(TransactionType transaction) onTransactionLoaded,
{void Function() onFinished});
// @action
// Future<void> update() async {
// if (_isUpdating) {
// return;
// }
Future<Map<String, TransactionType>> fetchTransactions();
// try {
// _isUpdating = true;
// final _transactions = await fetchTransactions();
// transactions.keys
// .toSet()
// .difference(_transactions.keys.toSet())
// .forEach((k) => transactions.remove(k));
// _transactions.forEach((key, value) => transactions[key] = value);
// _isUpdating = false;
// } catch (e) {
// _isUpdating = false;
// rethrow;
// }
// }
// void updateAsync({void Function() onFinished}) {
// fetchTransactionsAsync(
// (transaction) => transactions[transaction.id] = transaction,
// onFinished: onFinished);
// }
// void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded,
// {void Function() onFinished});
// Future<Map<String, TransactionType>> fetchTransactions();
}

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/entities/wallet_info.dart';
@ -11,7 +12,10 @@ import 'package:cake_wallet/entities/sync_status.dart';
import 'package:cake_wallet/entities/node.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
abstract class WalletBase<BalanceType extends Balance> {
abstract class WalletBase<
BalanceType extends Balance,
HistoryType extends TransactionHistoryBase,
TransactionType extends TransactionInfo> {
WalletBase(this.walletInfo);
static String idFor(String name, WalletType type) =>
@ -41,7 +45,7 @@ abstract class WalletBase<BalanceType extends Balance> {
Object get keys;
TransactionHistoryBase transactionHistory;
HistoryType transactionHistory;
Future<void> connectToNode({@required Node node});
@ -51,6 +55,12 @@ abstract class WalletBase<BalanceType extends Balance> {
int calculateEstimatedFee(TransactionPriority priority, int amount);
// void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded,
// {void Function() onFinished});
Future<Map<String, TransactionType>> fetchTransactions();
Future<void> save();
Future<void> rescan({int height});

View file

@ -1,8 +1,11 @@
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/core/wallet_credentials.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
abstract class WalletService<N extends WalletCredentials,
RFS extends WalletCredentials, RFK extends WalletCredentials> {
WalletType getType();
Future<WalletBase> create(N credentials);
Future<WalletBase> restoreFromSeed(RFS credentials);

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart';
import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart';
import 'package:cake_wallet/core/backup_service.dart';
import 'package:cake_wallet/core/wallet_service.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
@ -443,6 +444,8 @@ Future setup(
getIt.registerFactory(() => BitcoinWalletService(_walletInfoSource));
getIt.registerFactory(() => LitecoinWalletService(_walletInfoSource));
getIt.registerFactoryParam<WalletService, WalletType, void>(
(WalletType param1, __) {
switch (param1) {
@ -450,6 +453,8 @@ Future setup(
return getIt.get<MoneroWalletService>();
case WalletType.bitcoin:
return getIt.get<BitcoinWalletService>();
case WalletType.litecoin:
return getIt.get<LitecoinWalletService>();
default:
return null;
}

View file

@ -7,6 +7,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
return CryptoCurrency.btc;
case WalletType.monero:
return CryptoCurrency.xmr;
case WalletType.litecoin:
return CryptoCurrency.ltc;
default:
return null;
}

View file

@ -1,12 +0,0 @@
import 'package:cake_wallet/entities/crypto_currency.dart';
String cryptoToString(CryptoCurrency crypto) {
switch (crypto) {
case CryptoCurrency.xmr:
return 'XMR';
case CryptoCurrency.btc:
return 'BTC';
default:
return '';
}
}

View file

@ -1,11 +1,7 @@
import 'dart:io' show File, Platform;
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/core/generate_wallet_password.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/pathForWallet.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/monero/monero_wallet_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
@ -24,7 +20,8 @@ import 'package:cake_wallet/exchange/trade.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
const cakeWalletElectrumUri = 'electrum.cakewallet.com:50002';
const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
const cakeWalletLitecoinElectrumUri = '128.199.34.116:50002';
Future defaultSettingsMigration(
{@required int version,
@ -68,6 +65,8 @@ Future defaultSettingsMigration(
sharedPreferences: sharedPreferences, nodes: nodes);
await changeBitcoinCurrentElectrumServerToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
await changeLitecoinCurrentElectrumServerToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
break;
case 2:
@ -97,6 +96,7 @@ Future defaultSettingsMigration(
case 9:
await generateBackupPassword(secureStorage);
break;
case 10:
await changeTransactionPriorityAndFeeRateKeys(sharedPreferences);
break;
@ -110,7 +110,14 @@ Future defaultSettingsMigration(
break;
case 13:
await resetElectrumServer(nodes, sharedPreferences);
await resetBitcoinElectrumServer(nodes, sharedPreferences);
break;
case 15:
await addLitecoinElectrumServerList(nodes: nodes);
await changeLitecoinCurrentElectrumServerToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
await checkCurrentNodes(nodes, sharedPreferences);
break;
default:
@ -160,12 +167,21 @@ Future<void> changeMoneroCurrentNodeToDefault(
}
Node getBitcoinDefaultElectrumServer({@required Box<Node> nodes}) {
return nodes.values
.firstWhere((Node node) => node.uri == cakeWalletElectrumUri, orElse: () => null) ??
return nodes.values.firstWhere(
(Node node) => node.uri == cakeWalletBitcoinElectrumUri,
orElse: () => null) ??
nodes.values.firstWhere((node) => node.type == WalletType.bitcoin,
orElse: () => null);
}
Node getLitecoinDefaultElectrumServer({@required Box<Node> nodes}) {
return nodes.values.firstWhere(
(Node node) => node.uri == cakeWalletLitecoinElectrumUri,
orElse: () => null) ??
nodes.values.firstWhere((node) => node.type == WalletType.litecoin,
orElse: () => null);
}
Node getMoneroDefaultNode({@required Box<Node> nodes}) {
final timeZone = DateTime.now().timeZoneOffset.inHours;
var nodeUri = '';
@ -192,6 +208,15 @@ Future<void> changeBitcoinCurrentElectrumServerToDefault(
await sharedPreferences.setInt('current_node_id_btc', serverId);
}
Future<void> changeLitecoinCurrentElectrumServerToDefault(
{@required SharedPreferences sharedPreferences,
@required Box<Node> nodes}) async {
final server = getLitecoinDefaultElectrumServer(nodes: nodes);
final serverId = server?.key as int ?? 0;
await sharedPreferences.setInt('current_node_id_ltc', serverId);
}
Future<void> replaceDefaultNode(
{@required SharedPreferences sharedPreferences,
@required Box<Node> nodes}) async {
@ -224,7 +249,12 @@ Future<void> updateNodeTypes({@required Box<Node> nodes}) async {
}
Future<void> addBitcoinElectrumServerList({@required Box<Node> nodes}) async {
final serverList = await loadElectrumServerList();
final serverList = await loadBitcoinElectrumServerList();
await nodes.addAll(serverList);
}
Future<void> addLitecoinElectrumServerList({@required Box<Node> nodes}) async {
final serverList = await loadLitecoinElectrumServerList();
await nodes.addAll(serverList);
}
@ -284,57 +314,97 @@ Future<void> changeTransactionPriorityAndFeeRateKeys(
Future<void> changeDefaultMoneroNode(
Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
const cakeWalletMoneroNodeUriPattern = '.cakewallet.com';
final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId);
final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.contains(cakeWalletMoneroNodeUriPattern);
final currentMoneroNodeId =
sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final currentMoneroNode =
nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId);
final needToReplaceCurrentMoneroNode =
currentMoneroNode.uri.contains(cakeWalletMoneroNodeUriPattern);
nodeSource.values.forEach((node) async {
if (node.type == WalletType.monero && node.uri.contains(cakeWalletMoneroNodeUriPattern)) {
if (node.type == WalletType.monero &&
node.uri.contains(cakeWalletMoneroNodeUriPattern)) {
await node.delete();
}
});
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
final newCakeWalletNode =
Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
await nodeSource.add(newCakeWalletNode);
if (needToReplaceCurrentMoneroNode) {
await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
await sharedPreferences.setInt(
PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
}
}
Future<void> checkCurrentNodes(Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final currentElectrumSeverId = await sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId, orElse: () => null);
final currentElectrumServer = nodeSource.values.firstWhere((node) => node.key == currentElectrumSeverId, orElse: () => null);
Future<void> checkCurrentNodes(
Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
final currentMoneroNodeId =
sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final currentBitcoinElectrumSeverId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final currentLitecoinElectrumSeverId = sharedPreferences
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final currentMoneroNode = nodeSource.values.firstWhere(
(node) => node.key == currentMoneroNodeId,
orElse: () => null);
final currentBitcoinElectrumServer = nodeSource.values.firstWhere(
(node) => node.key == currentBitcoinElectrumSeverId,
orElse: () => null);
final currentLitecoinElectrumServer = nodeSource.values.firstWhere(
(node) => node.key == currentLitecoinElectrumSeverId,
orElse: () => null);
if (currentMoneroNode == null) {
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
final newCakeWalletNode =
Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
await nodeSource.add(newCakeWalletNode);
await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
await sharedPreferences.setInt(
PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
}
if (currentElectrumServer == null) {
final cakeWalletElectrum = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin);
if (currentBitcoinElectrumServer == null) {
final cakeWalletElectrum =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin);
await nodeSource.add(cakeWalletElectrum);
await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int);
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey,
cakeWalletElectrum.key as int);
}
if (currentLitecoinElectrumServer == null) {
final cakeWalletElectrum =
Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin);
await nodeSource.add(cakeWalletElectrum);
await sharedPreferences.setInt(
PreferencesKey.currentLitecoinElectrumSererIdKey,
cakeWalletElectrum.key as int);
}
}
Future<void> resetElectrumServer(Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
final currentElectrumSeverId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final oldElectrumServer = nodeSource.values.firstWhere((node) => node.uri.contains('electrumx.cakewallet.com'), orElse: () => null);
var cakeWalletNode = nodeSource.values.firstWhere((node) => node.uri == cakeWalletElectrumUri, orElse: () => null);
Future<void> resetBitcoinElectrumServer(
Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
final currentElectrumSeverId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final oldElectrumServer = nodeSource.values.firstWhere(
(node) => node.uri.contains('electrumx.cakewallet.com'),
orElse: () => null);
var cakeWalletNode = nodeSource.values.firstWhere(
(node) => node.uri == cakeWalletBitcoinElectrumUri,
orElse: () => null);
if (cakeWalletNode == null) {
cakeWalletNode = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin);
cakeWalletNode =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin);
await nodeSource.add(cakeWalletNode);
}
if (currentElectrumSeverId == oldElectrumServer?.key) {
await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletNode.key as int);
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey,
cakeWalletNode.key as int);
}
await oldElectrumServer?.delete();

View file

@ -1,7 +1,10 @@
import 'dart:io';
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/crypto_currency.dart';
import 'package:cake_wallet/entities/encrypt.dart';
@ -14,11 +17,6 @@ import 'package:cake_wallet/entities/wallet_info.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
const reservedNames = ["flutter_assets", "wallets", "db"];

View file

@ -20,9 +20,9 @@ Future<List<Node>> loadDefaultNodes() async {
}).toList();
}
Future<List<Node>> loadElectrumServerList() async {
Future<List<Node>> loadBitcoinElectrumServerList() async {
final serverListRaw =
await rootBundle.loadString('assets/electrum_server_list.yml');
await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml');
final serverList = loadYaml(serverListRaw) as YamlList;
return serverList.map((dynamic raw) {
@ -37,10 +37,29 @@ Future<List<Node>> loadElectrumServerList() async {
}).toList();
}
Future<List<Node>> loadLitecoinElectrumServerList() async {
final serverListRaw =
await rootBundle.loadString('assets/litecoin_electrum_server_list.yml');
final serverList = loadYaml(serverListRaw) as YamlList;
return serverList.map((dynamic raw) {
if (raw is Map) {
final node = Node.fromMap(raw);
node?.type = WalletType.litecoin;
return node;
}
return null;
}).toList();
}
Future resetToDefault(Box<Node> nodeSource) async {
final moneroNodes = await loadDefaultNodes();
final bitcoinElectrumServerList = await loadElectrumServerList();
final nodes = moneroNodes + bitcoinElectrumServerList;
final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
final litecoinElectrumServerList = await loadLitecoinElectrumServerList();
final nodes =
moneroNodes + bitcoinElectrumServerList + litecoinElectrumServerList;
await nodeSource.clear();
await nodeSource.addAll(nodes);

View file

@ -3,6 +3,7 @@ class PreferencesKey {
static const currentWalletName = 'current_wallet_name';
static const currentNodeIdKey = 'current_node_id';
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
static const currentFiatCurrencyKey = 'current_fiat_currency';
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
@ -14,7 +15,8 @@ class PreferencesKey {
static const displayActionListModeKey = 'display_list_mode';
static const currentPinLength = 'current_pin_length';
static const currentLanguageCode = 'language_code';
static const currentDefaultSettingsMigrationVersion = 'current_default_settings_migration_version';
static const currentDefaultSettingsMigrationVersion =
'current_default_settings_migration_version';
static const moneroTransactionPriority = 'current_fee_priority_monero';
static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin';
}

View file

@ -3,7 +3,11 @@ import 'package:hive/hive.dart';
part 'wallet_type.g.dart';
const walletTypes = [WalletType.monero, WalletType.bitcoin];
const walletTypes = [
WalletType.monero,
WalletType.bitcoin,
WalletType.litecoin
];
const walletTypeTypeId = 5;
@HiveType(typeId: walletTypeTypeId)
@ -15,7 +19,10 @@ enum WalletType {
none,
@HiveField(2)
bitcoin
bitcoin,
@HiveField(3)
litecoin
}
int serializeToInt(WalletType type) {
@ -24,6 +31,8 @@ int serializeToInt(WalletType type) {
return 0;
case WalletType.bitcoin:
return 1;
case WalletType.litecoin:
return 2;
default:
return -1;
}
@ -35,6 +44,8 @@ WalletType deserializeFromInt(int raw) {
return WalletType.monero;
case 1:
return WalletType.bitcoin;
case 2:
return WalletType.litecoin;
default:
return null;
}
@ -46,6 +57,8 @@ String walletTypeToString(WalletType type) {
return 'Monero';
case WalletType.bitcoin:
return 'Bitcoin';
case WalletType.litecoin:
return 'Litecoin';
default:
return '';
}
@ -57,6 +70,8 @@ String walletTypeToDisplayName(WalletType type) {
return 'Monero';
case WalletType.bitcoin:
return 'Bitcoin (Electrum)';
case WalletType.litecoin:
return 'Litecoin';
default:
return '';
}
@ -68,6 +83,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
return CryptoCurrency.xmr;
case WalletType.bitcoin:
return CryptoCurrency.btc;
case WalletType.litecoin:
return CryptoCurrency.ltc;
default:
return null;
}

View file

@ -107,7 +107,7 @@ Future<void> main() async {
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
initialMigrationVersion: 13);
initialMigrationVersion: 15);
runApp(App());
} catch (e) {
runApp(MaterialApp(
@ -135,7 +135,7 @@ Future<void> initialSetup(
@required Box<ExchangeTemplate> exchangeTemplates,
@required Box<TransactionDescription> transactionDescriptions,
FlutterSecureStorage secureStorage,
int initialMigrationVersion = 13}) async {
int initialMigrationVersion = 15}) async {
LanguageService.loadLocaleList();
await defaultSettingsMigration(
secureStorage: secureStorage,

View file

@ -49,13 +49,13 @@ abstract class MoneroAccountListBase with Store {
Future addAccount({String label}) async {
await account_list.addAccount(label: label);
await update();
update();
}
Future setLabelAccount({int accountIndex, String label}) async {
await account_list.setLabelForAccount(
accountIndex: accountIndex, label: label);
await update();
update();
}
void refresh() {

View file

@ -1,18 +1,10 @@
import 'dart:core';
import 'package:mobx/mobx.dart';
import 'package:cw_monero/transaction_history.dart'
as monero_transaction_history;
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/monero/monero_transaction_info.dart';
part 'monero_transaction_history.g.dart';
List<MoneroTransactionInfo> _getAllTransactions(dynamic _) =>
monero_transaction_history
.getAllTransations()
.map((row) => MoneroTransactionInfo.fromRow(row))
.toList();
class MoneroTransactionHistory = MoneroTransactionHistoryBase
with _$MoneroTransactionHistory;
@ -23,30 +15,13 @@ abstract class MoneroTransactionHistoryBase
}
@override
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
monero_transaction_history.refreshTransactions();
return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
<String, MoneroTransactionInfo>{},
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
});
}
Future<void> save() async {}
@override
@action
void updateAsync({void Function() onFinished}) {
fetchTransactionsAsync(
(transaction) => transactions[transaction.id] = transaction,
onFinished: onFinished);
}
void addOne(MoneroTransactionInfo transaction) =>
transactions[transaction.id] = transaction;
@override
void fetchTransactionsAsync(
void Function(MoneroTransactionInfo transaction) onTransactionLoaded,
{void Function() onFinished}) async {
final transactions = await fetchTransactions();
transactions.values.forEach((tx) => onTransactionLoaded(tx));
onFinished?.call();
}
void addMany(Map<String, MoneroTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
}

View file

@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo {
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() =>
'${formatAmount(moneroAmountToString(amount: fee))} XMR';
}

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:cake_wallet/monero/monero_amount_format.dart';
import 'package:cake_wallet/monero/monero_transaction_creation_exception.dart';
import 'package:cake_wallet/monero/monero_transaction_info.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_monero/transaction_history.dart'
as monero_transaction_history;
import 'package:cw_monero/wallet.dart';
import 'package:cw_monero/wallet.dart' as monero_wallet;
import 'package:cw_monero/transaction_history.dart' as transaction_history;
@ -30,19 +32,21 @@ const moneroBlockSize = 1000;
class MoneroWallet = MoneroWalletBase with _$MoneroWallet;
abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
MoneroWalletBase({String filename, WalletInfo walletInfo})
: transactionHistory = MoneroTransactionHistory(),
accountList = MoneroAccountList(),
abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
MoneroTransactionHistory, MoneroTransactionInfo> with Store {
MoneroWalletBase({WalletInfo walletInfo})
: accountList = MoneroAccountList(),
subaddressList = MoneroSubaddressList(),
super(walletInfo) {
_filename = filename;
transactionHistory = MoneroTransactionHistory();
balance = MoneroBalance(
fullBalance: monero_wallet.getFullBalance(accountIndex: 0),
unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0));
_lastAutosaveTimestamp = 0;
_lastSaveTimestamp = 0;
_isSavingAfterSync = false;
_isSavingAfterNewTransaction = false;
_isTransactionUpdating = false;
_onAccountChangeReaction = reaction((_) => account, (Account account) {
balance = MoneroBalance(
fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
@ -56,9 +60,6 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
static const int _autoAfterSyncSaveInterval = 60000;
@override
final MoneroTransactionHistory transactionHistory;
@observable
Account account;
@ -91,12 +92,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
final MoneroAccountList accountList;
String _filename;
SyncListener _listener;
ReactionDisposer _onAccountChangeReaction;
int _lastAutosaveTimestamp;
bool _isSavingAfterSync;
bool _isSavingAfterNewTransaction;
bool _isTransactionUpdating;
int _lastSaveTimestamp;
Future<void> init() async {
accountList.update();
@ -111,7 +113,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
monero_wallet.getUnlockedBalance(accountIndex: account.id));
address = subaddress.address;
_setListeners();
await transactionHistory.update();
await updateTransactions();
if (walletInfo.isRecovery) {
monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
@ -238,6 +240,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
@override
Future<void> save() async {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastSaveTimestamp < Duration(seconds: 10).inMilliseconds) {
return;
}
_lastSaveTimestamp = now;
await monero_wallet.store();
}
@ -264,6 +273,40 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
await walletInfo.save();
}
@override
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
monero_transaction_history.refreshTransactions();
return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
<String, MoneroTransactionInfo>{},
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
});
}
Future<void> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (e) {
print(e);
_isTransactionUpdating = false;
}
}
List<MoneroTransactionInfo> _getAllTransactions(dynamic _) =>
monero_transaction_history
.getAllTransations()
.map((row) => MoneroTransactionInfo.fromRow(row))
.toList();
void _setListeners() {
_listener?.stop();
_listener = monero_wallet.setListeners(_onNewBlock, _onNewTransaction);
@ -315,7 +358,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
}
Future<void> _askForUpdateTransactionHistory() async =>
await transactionHistory.update();
await updateTransactions();
int _getFullBalance() =>
monero_wallet.getFullBalance(accountIndex: account.id);
@ -389,11 +432,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
}
}
void _onNewTransaction() {
void _onNewTransaction() async {
try {
_askForUpdateTransactionHistory();
await _askForUpdateTransactionHistory();
_askForUpdateBalance();
Timer(Duration(seconds: 1), () => _afterNewTransactionSave());
await Future<void>.delayed(Duration(seconds: 1));
await _afterNewTransactionSave();
} catch (e) {
print(e.toString());
}

View file

@ -68,18 +68,18 @@ class MoneroWalletService extends WalletService<
static bool walletFilesExist(String path) =>
!File(path).existsSync() && !File('$path.keys').existsSync();
@override
WalletType getType() => WalletType.monero;
@override
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials) async {
try {
final path =
await pathForWallet(name: credentials.name, type: WalletType.monero);
final path = await pathForWallet(name: credentials.name, type: getType());
await monero_wallet_manager.createWallet(
path: path,
password: credentials.password,
language: credentials.language);
final wallet = MoneroWallet(
filename: monero_wallet.getFilename(),
walletInfo: credentials.walletInfo);
final wallet = MoneroWallet(walletInfo: credentials.walletInfo);
await wallet.init();
return wallet;
@ -93,7 +93,7 @@ class MoneroWalletService extends WalletService<
@override
Future<bool> isWalletExit(String name) async {
try {
final path = await pathForWallet(name: name, type: WalletType.monero);
final path = await pathForWallet(name: name, type: getType());
return monero_wallet_manager.isWalletExist(path: path);
} catch (e) {
// TODO: Implement Exception for wallet list service.
@ -105,7 +105,7 @@ class MoneroWalletService extends WalletService<
@override
Future<MoneroWallet> openWallet(String name, String password) async {
try {
final path = await pathForWallet(name: name, type: WalletType.monero);
final path = await pathForWallet(name: name, type: getType());
if (walletFilesExist(path)) {
await repairOldAndroidWallet(name);
@ -114,10 +114,9 @@ class MoneroWalletService extends WalletService<
await monero_wallet_manager
.openWalletAsync({'path': path, 'password': password});
final walletInfo = walletInfoSource.values.firstWhere(
(info) => info.id == WalletBase.idFor(name, WalletType.monero),
(info) => info.id == WalletBase.idFor(name, getType()),
orElse: () => null);
final wallet = MoneroWallet(
filename: monero_wallet.getFilename(), walletInfo: walletInfo);
final wallet = MoneroWallet(walletInfo: walletInfo);
final isValid = wallet.validate();
if (!isValid) {
@ -146,7 +145,7 @@ class MoneroWalletService extends WalletService<
@override
Future<void> remove(String wallet) async {
final path = await pathForWalletDir(name: wallet, type: WalletType.monero);
final path = await pathForWalletDir(name: wallet, type: getType());
final file = Directory(path);
final isExist = file.existsSync();
@ -159,8 +158,7 @@ class MoneroWalletService extends WalletService<
Future<MoneroWallet> restoreFromKeys(
MoneroRestoreWalletFromKeysCredentials credentials) async {
try {
final path =
await pathForWallet(name: credentials.name, type: WalletType.monero);
final path = await pathForWallet(name: credentials.name, type: getType());
await monero_wallet_manager.restoreFromKeys(
path: path,
password: credentials.password,
@ -169,9 +167,7 @@ class MoneroWalletService extends WalletService<
address: credentials.address,
viewKey: credentials.viewKey,
spendKey: credentials.spendKey);
final wallet = MoneroWallet(
filename: monero_wallet.getFilename(),
walletInfo: credentials.walletInfo);
final wallet = MoneroWallet(walletInfo: credentials.walletInfo);
await wallet.init();
return wallet;
@ -186,16 +182,13 @@ class MoneroWalletService extends WalletService<
Future<MoneroWallet> restoreFromSeed(
MoneroRestoreWalletFromSeedCredentials credentials) async {
try {
final path =
await pathForWallet(name: credentials.name, type: WalletType.monero);
final path = await pathForWallet(name: credentials.name, type: getType());
await monero_wallet_manager.restoreFromSeed(
path: path,
password: credentials.password,
seed: credentials.mnemonic,
restoreHeight: credentials.height);
final wallet = MoneroWallet(
filename: monero_wallet.getFilename(),
walletInfo: credentials.walletInfo);
final wallet = MoneroWallet(walletInfo: credentials.walletInfo);
await wallet.init();
return wallet;
@ -221,7 +214,7 @@ class MoneroWalletService extends WalletService<
}
final newWalletDirPath =
await pathForWalletDir(name: name, type: WalletType.monero);
await pathForWalletDir(name: name, type: getType());
dir.listSync().forEach((f) {
final file = File(f.path);

View file

@ -1,4 +1,6 @@
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/di.dart';
@ -18,9 +20,11 @@ ReactionDisposer _onCurrentWalletChangeFiatRateUpdateReaction;
void startCurrentWalletChangeReaction(AppStore appStore,
SettingsStore settingsStore, FiatConversionStore fiatConversionStore) {
_onCurrentWalletChangeReaction?.reaction?.dispose();
_onCurrentWalletChangeFiatRateUpdateReaction?.reaction?.dispose();
_onCurrentWalletChangeReaction =
reaction((_) => appStore.wallet, (WalletBase<Balance> wallet) async {
_onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase<
Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet) async {
try {
final node = settingsStore.getCurrentNode(wallet.type);
startWalletSyncStatusChangeReaction(wallet);
@ -45,7 +49,9 @@ void startCurrentWalletChangeReaction(AppStore appStore,
});
_onCurrentWalletChangeFiatRateUpdateReaction =
reaction((_) => appStore.wallet, (WalletBase<Balance> wallet) async {
reaction((_) => appStore.wallet, (WalletBase<Balance,
TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet) async {
try {
fiatConversionStore.prices[wallet.currency] = 0;
fiatConversionStore.prices[wallet.currency] =

View file

@ -1,11 +1,16 @@
import 'package:cake_wallet/entities/balance.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/entities/sync_status.dart';
ReactionDisposer _onWalletSyncStatusChangeReaction;
void startWalletSyncStatusChangeReaction(WalletBase<Balance> wallet) {
void startWalletSyncStatusChangeReaction(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet) {
_onWalletSyncStatusChangeReaction?.reaction?.dispose();
_onWalletSyncStatusChangeReaction =
reaction((_) => wallet.syncStatus, (SyncStatus status) async {

View file

@ -21,6 +21,7 @@ class MenuWidget extends StatefulWidget {
class MenuWidgetState extends State<MenuWidget> {
Image moneroIcon;
Image bitcoinIcon;
Image litecoinIcon;
final largeScreen = 731;
double menuWidth;
@ -76,6 +77,7 @@ class MenuWidgetState extends State<MenuWidget> {
color: Theme.of(context).accentTextTheme.overline.decorationColor);
bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png',
color: Theme.of(context).accentTextTheme.overline.decorationColor);
litecoinIcon = Image.asset('assets/images/litecoin_menu.png');
return Row(
mainAxisSize: MainAxisSize.max,
@ -238,6 +240,8 @@ class MenuWidgetState extends State<MenuWidget> {
return moneroIcon;
case WalletType.bitcoin:
return bitcoinIcon;
case WalletType.litecoin:
return litecoinIcon;
default:
return null;
}

View file

@ -59,6 +59,8 @@ class WalletTypeFormState extends State<WalletTypeForm> {
Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
final bitcoinIcon =
Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
final litecoinIcon =
Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final walletTypeImage = Image.asset('assets/images/wallet_type.png');
final walletTypeLightImage =
Image.asset('assets/images/wallet_type_light.png');
@ -69,7 +71,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
@override
void initState() {
types = [WalletType.bitcoin, WalletType.monero];
types = [WalletType.bitcoin, WalletType.monero, WalletType.litecoin];
super.initState();
}
@ -84,8 +86,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
padding: EdgeInsets.only(left: 12, right: 12),
child: AspectRatio(
aspectRatio: aspectRatioImage,
child:
FittedBox(child: widget.walletImage, fit: BoxFit.fill)),
child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)),
),
Padding(
padding: EdgeInsets.only(top: 48),
@ -125,6 +126,8 @@ class WalletTypeFormState extends State<WalletTypeForm> {
return moneroIcon;
case WalletType.bitcoin:
return bitcoinIcon;
case WalletType.litecoin:
return litecoinIcon;
default:
return null;
}

View file

@ -40,6 +40,8 @@ class WalletListBodyState extends State<WalletListBody> {
Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
final bitcoinIcon =
Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
final litecoinIcon =
Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final scrollController = ScrollController();
final double tileHeight = 60;
Flushbar<void> _progressBar;
@ -193,6 +195,8 @@ class WalletListBodyState extends State<WalletListBody> {
return bitcoinIcon;
case WalletType.monero:
return moneroIcon;
case WalletType.litecoin:
return litecoinIcon;
default:
return null;
}

View file

@ -1,6 +1,8 @@
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -20,7 +22,8 @@ abstract class AppStoreBase with Store {
AuthenticationStore authenticationStore;
@observable
WalletBase<Balance> wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet;
WalletListStore walletList;
@ -29,7 +32,10 @@ abstract class AppStoreBase with Store {
NodeListStore nodeListStore;
@action
void changeCurrentWallet(WalletBase wallet) {
void changeCurrentWallet(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet) {
this.wallet?.close();
this.wallet = wallet;
}

View file

@ -205,15 +205,19 @@ abstract class SettingsStoreBase with Store {
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId = sharedPreferences
.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final litecoinElectrumServerId = sharedPreferences
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
final packageInfo = await PackageInfo.fromPlatform();
return SettingsStore(
sharedPreferences: sharedPreferences,
nodes: {
WalletType.monero: moneroNode,
WalletType.bitcoin: bitcoinElectrumServer
WalletType.bitcoin: bitcoinElectrumServer,
WalletType.litecoin: litecoinElectrumServer
},
appVersion: packageInfo.version,
isBitcoinBuyEnabled: isBitcoinBuyEnabled,
@ -263,6 +267,10 @@ abstract class SettingsStoreBase with Store {
await _sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, node.key as int);
break;
case WalletType.litecoin:
await _sharedPreferences.setInt(
PreferencesKey.currentLitecoinElectrumSererIdKey, node.key as int);
break;
case WalletType.monero:
await _sharedPreferences.setInt(
PreferencesKey.currentNodeIdKey, node.key as int);

View file

@ -1,7 +1,9 @@
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/crypto_currency.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero_wallet.dart';
@ -23,11 +25,7 @@ abstract class BalanceViewModelBase with Store {
@required this.settingsStore,
@required this.fiatConvertationStore}) {
isReversing = false;
wallet ??= appStore.wallet;
_reaction = reaction((_) => appStore.wallet, _onWalletChange);
final _wallet = wallet;
if (_wallet is MoneroWallet) {
@ -38,6 +36,8 @@ abstract class BalanceViewModelBase with Store {
balance = _wallet.balance;
}
reaction((_) => appStore.wallet, _onWalletChange);
_onCurrentWalletChangeReaction =
reaction<void>((_) => wallet.balance, (dynamic balance) {
if (balance is Balance) {
@ -59,7 +59,8 @@ abstract class BalanceViewModelBase with Store {
Balance balance;
@observable
WalletBase<Balance> wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet;
@computed
double get price => fiatConvertationStore.prices[appStore.wallet.currency];
@ -153,14 +154,14 @@ abstract class BalanceViewModelBase with Store {
CryptoCurrency get currency => appStore.wallet.currency;
ReactionDisposer _onCurrentWalletChangeReaction;
ReactionDisposer _reaction;
@action
void _onWalletChange(WalletBase<Balance> wallet) {
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet) {
this.wallet = wallet;
balance = wallet.balance;
_onCurrentWalletChangeReaction?.reaction?.dispose();
_onCurrentWalletChangeReaction = reaction<Balance>(
(_) => wallet.balance, (Balance balance) => this.balance = balance);

View file

@ -1,23 +1,13 @@
import 'dart:convert';
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/order.dart';
import 'package:cake_wallet/entities/transaction_history.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/monero/account.dart';
import 'package:cake_wallet/monero/monero_balance.dart';
import 'package:cake_wallet/monero/monero_transaction_history.dart';
import 'package:cake_wallet/monero/monero_transaction_info.dart';
import 'package:cake_wallet/monero/monero_wallet.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/crypto_currency.dart';
import 'package:cake_wallet/entities/transaction_direction.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/dashboard/orders_store.dart';
import 'package:cake_wallet/utils/mobx.dart';
@ -27,12 +17,8 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_display_mode.dart';
import 'package:cake_wallet/view_model/wyre_view_model.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/entities/sync_status.dart';
@ -43,8 +29,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:convert/convert.dart';
part 'dashboard_view_model.g.dart';
@ -100,9 +84,6 @@ abstract class DashboardViewModelBase with Store {
name = appStore.wallet?.name;
wallet ??= appStore.wallet;
type = wallet.type;
_reaction = reaction((_) => appStore.wallet, _onWalletChange);
final _wallet = wallet;
if (_wallet is MoneroWallet) {
@ -133,6 +114,8 @@ abstract class DashboardViewModelBase with Store {
settingsStore: appStore.settingsStore)));
}
reaction((_) => appStore.wallet, _onWalletChange);
connectMapToListWithTransform(
appStore.wallet.transactionHistory.transactions,
transactions,
@ -215,7 +198,8 @@ abstract class DashboardViewModelBase with Store {
}
@observable
WalletBase<Balance> wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet;
bool get hasRescan => wallet.type == WalletType.monero;
@ -241,8 +225,6 @@ abstract class DashboardViewModelBase with Store {
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
ReactionDisposer _reaction;
ReactionDisposer _onMoneroAccountChangeReaction;
ReactionDisposer _onMoneroBalanceChangeReaction;
@ -253,7 +235,10 @@ abstract class DashboardViewModelBase with Store {
}
@action
void _onWalletChange(WalletBase<Balance> wallet) {
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet) {
this.wallet = wallet;
type = wallet.type;
name = wallet.name;
@ -319,6 +304,4 @@ abstract class DashboardViewModelBase with Store {
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore)));
}
}

View file

@ -4,7 +4,7 @@ import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/mobx.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
import 'package:cake_wallet/monero/monero_transaction_info.dart';
import 'package:cake_wallet/monero/monero_amount_format.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
@ -42,7 +42,7 @@ class TransactionListItem extends ActionListItem with Keyable {
transaction.changeFiatAmount(amount);
}
if (transaction is BitcoinTransactionInfo) {
if (transaction is ElectrumTransactionInfo) {
final amount = calculateFiatAmountRaw(
cryptoAmount: bitcoinAmountToDouble(amount: transaction.amount),
price: price);

View file

@ -33,9 +33,7 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
abstract class ExchangeViewModelBase with Store {
ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
this.tradesStore, this._settingsStore) {
providerList = [
ChangeNowExchangeProvider()
];
providerList = [ChangeNowExchangeProvider()];
_initialPairBasedOnWallet();
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
@ -57,9 +55,11 @@ abstract class ExchangeViewModelBase with Store {
_onPairChange();
}
});
receiveCurrencies = CryptoCurrency.all.where((cryptoCurrency) =>
receiveCurrencies = CryptoCurrency.all
.where((cryptoCurrency) =>
(cryptoCurrency != CryptoCurrency.xlm) &&
(cryptoCurrency != CryptoCurrency.xrp)).toList();
(cryptoCurrency != CryptoCurrency.xrp))
.toList();
_defineIsReceiveAmountEditable();
isFixedRateMode = false;
isReceiveAmountEntered = false;
@ -218,8 +218,10 @@ abstract class ExchangeViewModelBase with Store {
limitsState = LimitsIsLoading();
try {
limits = await provider.fetchLimits(from: depositCurrency,
to: receiveCurrency, isFixedRateMode: isFixedRateMode);
limits = await provider.fetchLimits(
from: depositCurrency,
to: receiveCurrency,
isFixedRateMode: isFixedRateMode);
limitsState = LimitsLoadedSuccessfully(limits: limits);
} catch (e) {
limitsState = LimitsLoadedFailure(error: e.toString());
@ -283,8 +285,8 @@ abstract class ExchangeViewModelBase with Store {
} else {
try {
tradeState = TradeIsCreating();
final trade = await provider.createTrade(request: request,
isFixedRateMode: isFixedRateMode);
final trade = await provider.createTrade(
request: request, isFixedRateMode: isFixedRateMode);
trade.walletId = wallet.id;
tradesStore.setTrade(trade);
await trades.add(trade);
@ -320,7 +322,8 @@ abstract class ExchangeViewModelBase with Store {
void calculateDepositAllAmount() {
if (wallet is BitcoinWallet) {
final availableBalance = wallet.balance.available;
final priority = _settingsStore.priority[wallet.type] as BitcoinTransactionPriority;
final priority =
_settingsStore.priority[wallet.type] as BitcoinTransactionPriority;
final fee = wallet.calculateEstimatedFee(priority, null);
if (availableBalance < fee || availableBalance == 0) {
@ -402,6 +405,10 @@ abstract class ExchangeViewModelBase with Store {
depositCurrency = CryptoCurrency.btc;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.litecoin:
depositCurrency = CryptoCurrency.ltc;
receiveCurrency = CryptoCurrency.xmr;
break;
default:
break;
}

View file

@ -39,6 +39,9 @@ abstract class NodeListViewModelBase with Store {
case WalletType.monero:
node = getMoneroDefaultNode(nodes: _nodeSource);
break;
case WalletType.litecoin:
node = getLitecoinDefaultElectrumServer(nodes: _nodeSource);
break;
default:
break;
}

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
@ -90,6 +91,9 @@ abstract class SendViewModelBase with Store {
case WalletType.bitcoin:
_amount = stringDoubleToBitcoinAmount(_cryptoAmount);
break;
case WalletType.litecoin:
_amount = stringDoubleToBitcoinAmount(_cryptoAmount);
break;
default:
break;
}
@ -102,7 +106,7 @@ abstract class SendViewModelBase with Store {
final fee = _wallet.calculateEstimatedFee(
_settingsStore.priority[_wallet.type], amount);
if (_wallet is BitcoinWallet) {
if (_wallet is ElectrumWallet) {
return bitcoinAmountToDouble(amount: fee);
}
@ -304,6 +308,12 @@ abstract class SendViewModelBase with Store {
final amount = !sendAll ? _amount : null;
final priority = _settingsStore.priority[_wallet.type];
return BitcoinTransactionCredentials(
address, amount, priority as BitcoinTransactionPriority);
case WalletType.litecoin:
final amount = !sendAll ? _amount : null;
final priority = _settingsStore.priority[_wallet.type];
return BitcoinTransactionCredentials(
address, amount, priority as BitcoinTransactionPriority);
case WalletType.monero:
@ -330,6 +340,9 @@ abstract class SendViewModelBase with Store {
case WalletType.bitcoin:
maximumFractionDigits = 8;
break;
case WalletType.litecoin:
maximumFractionDigits = 8;
break;
default:
break;
}
@ -357,7 +370,7 @@ abstract class SendViewModelBase with Store {
final _priority = priority as TransactionPriority;
final wallet = _wallet;
if (wallet is BitcoinWallet) {
if (wallet is ElectrumWallet) {
final rate = wallet.feeRate(_priority);
return '${priority.toString()} ($rate sat/byte)';
}

View file

@ -1,10 +1,3 @@
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/themes/theme_list.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:flutter/cupertino.dart';
import 'package:mobx/mobx.dart';
import 'package:package_info/package_info.dart';
@ -25,6 +18,15 @@ import 'package:cake_wallet/view_model/settings/regular_list_item.dart';
import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
import 'package:cake_wallet/view_model/settings/switcher_list_item.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/entities/transaction_priority.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/themes/theme_list.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
part 'settings_view_model.g.dart';
@ -36,13 +38,19 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
return MoneroTransactionPriority.all;
case WalletType.bitcoin:
return BitcoinTransactionPriority.all;
case WalletType.litecoin:
return BitcoinTransactionPriority.all;
default:
return [];
}
}
abstract class SettingsViewModelBase with Store {
SettingsViewModelBase(this._settingsStore, WalletBase<Balance> wallet)
SettingsViewModelBase(
this._settingsStore,
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet)
: itemHeaders = {},
_walletType = wallet.type,
_biometricAuth = BiometricAuth() {
@ -77,7 +85,7 @@ abstract class SettingsViewModelBase with Store {
displayItem: (dynamic priority) {
final _priority = priority as TransactionPriority;
if (wallet is BitcoinWallet) {
if (wallet is ElectrumWallet) {
final rate = wallet.feeRate(_priority);
return '${priority.toString()} ($rate sat/byte)';
}

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
import 'package:cake_wallet/monero/monero_transaction_info.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart';
@ -43,12 +44,6 @@ abstract class TransactionDetailsViewModelBase with Store {
value: tx.amountFormatted()),
StandartListItem(
title: S.current.transaction_details_fee, value: tx.feeFormatted()),
BlockExplorerListItem(
title: "View in Block Explorer",
value: "View Transaction on XMRChain.net",
onTap: () {
launch("https://xmrchain.net/search?value=${tx.id}");
})
];
if (tx.key?.isNotEmpty ?? null) {
@ -59,7 +54,7 @@ abstract class TransactionDetailsViewModelBase with Store {
items.addAll(_items);
}
if (tx is BitcoinTransactionInfo) {
if (tx is ElectrumTransactionInfo) {
final _items = [
StandartListItem(
title: S.current.transaction_details_transaction_id, value: tx.id),
@ -78,12 +73,6 @@ abstract class TransactionDetailsViewModelBase with Store {
StandartListItem(
title: S.current.transaction_details_fee,
value: tx.feeFormatted()),
BlockExplorerListItem(
title: "View in Block Explorer",
value: "View Transaction on Blockchain.com",
onTap: () {
launch("https://www.blockchain.com/btc/tx/${tx.id}");
})
];
items.addAll(_items);
@ -101,6 +90,19 @@ abstract class TransactionDetailsViewModelBase with Store {
}
}
WalletType type;
if (tx is MoneroTransactionInfo) {
type = WalletType.monero;
} else if (tx is ElectrumTransactionInfo) {
type = tx.type;
}
items.add(BlockExplorerListItem(
title: "View in Block Explorer",
value: _explorerDescription(type),
onTap: () => launch(_explorerUrl(type, tx.id))));
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == transactionInfo.id,
orElse: () => TransactionDescription(id: transactionInfo.id));
@ -125,4 +127,30 @@ abstract class TransactionDetailsViewModelBase with Store {
final List<TransactionDetailsListItem> items;
bool showRecipientAddress;
String _explorerUrl(WalletType type, String txId) {
switch (type) {
case WalletType.monero:
return 'https://xmrchain.net/search?value=${txId}';
case WalletType.bitcoin:
return 'https://www.blockchain.com/btc/tx/${txId}';
case WalletType.litecoin:
return 'https://blockchair.com/litecoin/transaction/${txId}';
default:
return '';
}
}
String _explorerDescription(WalletType type) {
switch (type) {
case WalletType.monero:
return 'View Transaction on XMRChain.net';
case WalletType.bitcoin:
return 'View Transaction on Blockchain.com';
case WalletType.litecoin:
return 'View Transaction on Blockchair.com';
default:
return '';
}
}
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/monero/monero_wallet.dart';
@ -62,7 +63,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
Future<void> _createNew() async {
final wallet = _wallet;
if (wallet is BitcoinWallet) {
if (wallet is ElectrumWallet) {
await wallet.generateNewAddress();
}

View file

@ -1,4 +1,6 @@
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
@ -56,12 +58,13 @@ class BitcoinURI extends PaymentURI {
}
abstract class WalletAddressListViewModelBase with Store {
WalletAddressListViewModelBase(
{@required AppStore appStore}) {
WalletAddressListViewModelBase({@required AppStore appStore}) {
_appStore = appStore;
_wallet = _appStore.wallet;
hasAccounts = _wallet?.type == WalletType.monero;
_onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase<Balance> wallet) {
_onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase<
Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet) {
_wallet = wallet;
hasAccounts = _wallet.type == WalletType.monero;
});
@ -119,9 +122,7 @@ abstract class WalletAddressListViewModelBase with Store {
final isPrimary = addr == primaryAddress;
return WalletAddressListItem(
isPrimary: isPrimary,
name: null,
address: addr.address);
isPrimary: isPrimary, name: null, address: addr.address);
});
addressList.addAll(bitcoinAddresses);
}
@ -147,7 +148,8 @@ abstract class WalletAddressListViewModelBase with Store {
bool get hasAddressList => _wallet.type == WalletType.monero;
@observable
WalletBase<Balance> _wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
_wallet;
List<ListItem> _baseItems;
@ -155,7 +157,6 @@ abstract class WalletAddressListViewModelBase with Store {
ReactionDisposer _onWalletChangeReaction;
@action
void setAddress(WalletAddressListItem address) =>
_wallet.address = address.address;

View file

@ -2,7 +2,7 @@ import 'package:mobx/mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/monero/monero_wallet.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
part 'wallet_keys_view_model.g.dart';
@ -24,12 +24,11 @@ abstract class WalletKeysViewModelBase with Store {
title: S.current.view_key_public, value: keys.publicViewKey),
StandartListItem(
title: S.current.view_key_private, value: keys.privateViewKey),
StandartListItem(
title: S.current.wallet_seed, value: wallet.seed),
StandartListItem(title: S.current.wallet_seed, value: wallet.seed),
]);
}
if (wallet is BitcoinWallet) {
if (wallet is ElectrumWallet) {
final keys = wallet.keys;
items.addAll([

View file

@ -37,6 +37,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
name: name, language: options as String);
case WalletType.bitcoin:
return BitcoinNewWalletCredentials(name: name);
case WalletType.litecoin:
return BitcoinNewWalletCredentials(name: name);
default:
return null;
}

View file

@ -67,10 +67,12 @@ packages:
bech32:
dependency: transitive
description:
name: bech32
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
path: "."
ref: cake
resolved-ref: "02fef082f20af13de00b4e64efb93a2c1e5e1cf2"
url: "git@github.com:cake-tech/bech32.git"
source: git
version: "0.2.0"
bip32:
dependency: transitive
description:
@ -88,9 +90,11 @@ packages:
bitcoin_flutter:
dependency: "direct main"
description:
name: bitcoin_flutter
url: "https://pub.dartlang.org"
source: hosted
path: "."
ref: cake
resolved-ref: b3ab2926c665f0e68b74a4a5f31059f7fcd817b7
url: "git@github.com:cake-tech/bitcoin_flutter.git"
source: git
version: "2.0.2"
boolean_selector:
dependency: transitive

View file

@ -12,6 +12,7 @@ description: Cake Wallet.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 4.1.4+43
publish_to: none
environment:
sdk: ">=2.7.0 <3.0.0"
@ -65,7 +66,10 @@ dependencies:
crypto: ^2.1.5
password: ^1.0.0
basic_utils: ^2.0.3
bitcoin_flutter: ^2.0.0
bitcoin_flutter:
git:
url: git@github.com:cake-tech/bitcoin_flutter.git
ref: cake
get_it: ^6.0.0
connectivity: ^3.0.3
keyboard_actions: ^3.3.0
@ -105,7 +109,8 @@ flutter:
assets:
- assets/images/
- assets/node_list.yml
- assets/electrum_server_list.yml
- assets/bitcoin_electrum_server_list.yml
- assets/litecoin_electrum_server_list.yml
- assets/text/
- assets/faq/