mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
Reworked electrum wallet. Added Litecoin wallet.
This commit is contained in:
parent
c8a521a29f
commit
1330966794
71 changed files with 1503 additions and 1071 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -96,3 +96,5 @@ vendor/
|
|||
android/app/.cxx/**
|
||||
ios/Flutter/.last_build_id
|
||||
/lib/generated/**
|
||||
#**#
|
||||
/**/#**#
|
|
@ -25,7 +25,6 @@ linter:
|
|||
- empty_constructor_bodies
|
||||
- empty_statements
|
||||
- hash_and_equals
|
||||
- implementation_imports
|
||||
- invariant_booleans
|
||||
- iterable_contains_unrelated_type
|
||||
- library_names
|
||||
|
|
BIN
assets/images/litecoin_icon.png
Normal file
BIN
assets/images/litecoin_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/images/litecoin_menu.png
Normal file
BIN
assets/images/litecoin_menu.png
Normal file
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 |
2
assets/litecoin_electrum_server_list.yml
Normal file
2
assets/litecoin_electrum_server_list.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
-
|
||||
uri: 128.199.34.116:50002
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 =>
|
98
lib/bitcoin/electrum_transaction_history.dart
Normal file
98
lib/bitcoin/electrum_transaction_history.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
467
lib/bitcoin/electrum_wallet.dart
Normal file
467
lib/bitcoin/electrum_wallet.dart
Normal 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();
|
||||
}
|
||||
}
|
42
lib/bitcoin/electrum_wallet_snapshot.dart
Normal file
42
lib/bitcoin/electrum_wallet_snapshot.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
9
lib/bitcoin/litecoin_network.dart
Normal file
9
lib/bitcoin/litecoin_network.dart
Normal 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);
|
88
lib/bitcoin/litecoin_wallet.dart
Normal file
88
lib/bitcoin/litecoin_wallet.dart
Normal 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;
|
||||
}
|
||||
}
|
76
lib/bitcoin/litecoin_wallet_service.dart
Normal file
76
lib/bitcoin/litecoin_wallet_service.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/core/validator.dart';
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo {
|
|||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() =>
|
||||
'${formatAmount(moneroAmountToString(amount: fee))} XMR';
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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] =
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)';
|
||||
}
|
||||
|
|
|
@ -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)';
|
||||
}
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
18
pubspec.lock
18
pubspec.lock
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
||||
|
|
Loading…
Reference in a new issue