This commit is contained in:
M 2020-05-12 15:04:54 +03:00
parent 2673ecd45e
commit 1d793ab284
44 changed files with 1503 additions and 225 deletions

17
android/.project Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>android</name>
<comment>Project android_ created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,2 @@
connection.project.dir=
eclipse.preferences.version=1

6
android/app/.classpath Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
android/app/.project Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,2 @@
connection.project.dir=..
eclipse.preferences.version=1

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>cw_monero</name>
<comment>Project android created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,13 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.0-20191016123526+0000))
connection.project.dir=../../android
eclipse.preferences.version=1
gradle.user.home=
java.home=
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

View file

@ -48,4 +48,9 @@ A new flutter plugin project.
sodium.libraries = 'sodium'
sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/libs/sodium/include/**" }
end
s.subspec 'lmdb' do |lmdb|
lmdb.vendored_libraries = 'External/ios/libs/lmdb/liblmdb.a'
lmdb.libraries = 'lmdb'
end
end

View file

@ -4,24 +4,33 @@ PODS:
- MTBBarcodeScanner
- cw_monero (0.0.2):
- cw_monero/Boost (= 0.0.2)
- cw_monero/lmdb (= 0.0.2)
- cw_monero/Monero (= 0.0.2)
- cw_monero/OpenSSL (= 0.0.2)
- cw_monero/Sodium (= 0.0.2)
- Flutter
- cw_monero/Boost (0.0.2):
- Flutter
- cw_monero/lmdb (0.0.2):
- Flutter
- cw_monero/Monero (0.0.2):
- Flutter
- cw_monero/OpenSSL (0.0.2):
- Flutter
- cw_monero/Sodium (0.0.2):
- Flutter
- devicelocale (0.0.1):
- Flutter
- esys_flutter_share (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_secure_storage (3.3.1):
- Flutter
- local_auth (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- package_info (0.0.1):
- Flutter
- path_provider (0.0.1):
- Flutter
- share (0.5.2):
@ -36,9 +45,12 @@ PODS:
DEPENDENCIES:
- barcode_scan (from `.symlinks/plugins/barcode_scan/ios`)
- cw_monero (from `.symlinks/plugins/cw_monero/ios`)
- devicelocale (from `.symlinks/plugins/devicelocale/ios`)
- esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`)
- Flutter (from `.symlinks/flutter/ios-release`)
- Flutter (from `.symlinks/flutter/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- local_auth (from `.symlinks/plugins/local_auth/ios`)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- share (from `.symlinks/plugins/share/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
@ -54,12 +66,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/barcode_scan/ios"
cw_monero:
:path: ".symlinks/plugins/cw_monero/ios"
devicelocale:
:path: ".symlinks/plugins/devicelocale/ios"
esys_flutter_share:
:path: ".symlinks/plugins/esys_flutter_share/ios"
Flutter:
:path: ".symlinks/flutter/ios-release"
:path: ".symlinks/flutter/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
local_auth:
:path: ".symlinks/plugins/local_auth/ios"
package_info:
:path: ".symlinks/plugins/package_info/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
share:
@ -73,11 +91,14 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
barcode_scan: 33f586d02270046fc6559135038b34b5754eaa4f
cw_monero: ca40a57b99f7753ed93d3b50af671a637277eb89
cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649
devicelocale: feebbe5e7a30adb8c4f83185de1b50ff19b44f00
esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
local_auth: 2571c49920ae469f46d5557435fad8fa473a5e88
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info: 48b108e75b8802c2d5e126f208ef540561c98aef
path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
share: bae0a282aab4483288913fc4dc0b935d4b491f2e
shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01
@ -86,4 +107,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: f1916a43bb28badbd408be80e8e4b8652a74e93e
COCOAPODS: 1.8.4
COCOAPODS: 1.9.1

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

21
lib/bitcoin/api.dart Normal file
View file

@ -0,0 +1,21 @@
import 'dart:convert';
import 'package:http/http.dart';
const blockchainInfoBaseURI = 'https://blockchain.info';
const multiAddressURI = '$blockchainInfoBaseURI/multiaddr';
Future<List<String>> fetchAllAddresses({String xpub}) async {
final uri = '$multiAddressURI?active=$xpub';
final response = await get(uri);
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
print(responseJSON);
return (responseJSON['addresses'] as List<dynamic>).map((dynamic row) {
if (row is Map<String, Object>) {
return row['address'] as String;
}
return '';
}).toList();
}

View file

@ -0,0 +1,13 @@
import 'package:intl/intl.dart';
import 'package:cake_wallet/src/domain/common/crypto_amount_format.dart';
const bitcoinAmountLength = 8;
const bitcoinAmountDivider = 100000000;
final bitcoinAmountFormat = NumberFormat()
..maximumFractionDigits = bitcoinAmountLength
..minimumFractionDigits = 1;
String bitcoinAmountToString({int amount}) =>
bitcoinAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider));
double bitcoinAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider);

View file

@ -0,0 +1,14 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/src/domain/common/balance.dart';
class BitcoinBalance extends Balance {
BitcoinBalance({@required this.confirmed, @required this.unconfirmed});
final int confirmed;
final int unconfirmed;
int get total => confirmed + unconfirmed;
String get confirmedFormatted => bitcoinAmountToString(amount: confirmed);
String get unconfirmedFormatted => bitcoinAmountToString(amount: unconfirmed);
String get totalFormatted => bitcoinAmountToString(amount: total);
}

View file

@ -0,0 +1,145 @@
import 'dart:convert';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
import 'package:cake_wallet/bitcoin/file.dart';
class BitcoinTransactionHistory extends TransactionHistory {
BitcoinTransactionHistory(
{@required this.eclient,
@required this.path,
@required String password,
@required this.wallet})
: _transactions = BehaviorSubject<List<TransactionInfo>>.seeded([]),
_password = password,
_height = 0;
final BitcoinWallet wallet;
final ElectrumClient eclient;
final String path;
final String _password;
int _height;
@override
Observable<List<TransactionInfo>> get transactions => _transactions.stream;
List<TransactionInfo> get transactionsAll => _transactions.value;
final BehaviorSubject<List<TransactionInfo>> _transactions;
bool _isUpdating = false;
Future<void> init() async {
final info = await _read();
_height = (info['height'] as int) ?? _height;
_transactions.value = info['transactions'] as List<TransactionInfo>;
}
@override
Future<List<TransactionInfo>> getAll() async => _transactions.value;
@override
Future update() async {
if (_isUpdating) {
return;
}
try {
_isUpdating = true;
final newTransasctions = await fetchTransactions();
_transactions.value = _transactions.value + newTransasctions;
_updateHeight();
await save();
_isUpdating = false;
} catch (e) {
_isUpdating = false;
rethrow;
}
}
Future<Map<String, Object>> fetchTransactionInfo(
{@required String hash, @required int height}) async {
final rawFetching = eclient.getTransactionRaw(hash: hash);
final headerFetching = eclient.getHeader(height: height);
final result = await Future.wait([rawFetching, headerFetching]);
final raw = result.first as String;
final header = result[1] as Map<String, Object>;
return {'raw': raw, 'header': header};
}
Future<List<BitcoinTransactionInfo>> fetchTransactions() async {
final addresses = wallet.getAddresses();
final histories =
addresses.map((address) => eclient.getHistory(address: address));
final _historiesWithDetails = await Future.wait(histories)
.then((histories) => histories
.map((h) => h.where((tx) => (tx['height'] as int) > _height))
.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
.map((info) => BitcoinTransactionInfo.fromHexAndHeader(
info['raw'] as String, info['header'] as Map<String, Object>,
addresses: addresses))
.toList();
}
Future<void> add(List<BitcoinTransactionInfo> transactions) async {
final txs = await getAll()
..addAll(transactions);
await writeData(
path: path,
password: _password,
data: json
.encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson())));
}
Future<void> addOne(BitcoinTransactionInfo tx) async {
final txs = await getAll()
..add(tx);
await writeData(
path: path,
password: _password,
data: json
.encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson())));
}
Future<void> save() async => writeData(
path: path,
password: _password,
data: json
.encode({'height': _height, 'transactions': _transactions.value}));
Future<Map<String, Object>> _read() async {
try {
final content = await read(path: path, password: _password);
final jsoned = json.decode(content) as Map<String, Object>;
final height = jsoned['height'] as int;
final transactions = (jsoned['transactions'] as List<dynamic>)
.map((dynamic row) {
if (row is Map<String, Object>) {
return BitcoinTransactionInfo.fromJson(row);
}
return null;
})
.where((el) => el != null)
.toList();
return {'transactions': transactions, 'height': height};
} catch (_) {
return {'transactions': List<TransactionInfo>(), 'height': 0};
}
}
void _updateHeight() {
final int newHeight = _transactions.value
.fold(0, (acc, val) => val.height > acc ? val.height : acc);
_height = newHeight > _height ? newHeight : _height;
}
}

View file

@ -0,0 +1,81 @@
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
class BitcoinTransactionInfo extends TransactionInfo {
BitcoinTransactionInfo(
{@required this.id,
@required int height,
@required int amount,
@required TransactionDirection direction,
@required bool isPending,
@required DateTime date}) {
this.height = height;
this.amount = amount;
this.direction = direction;
this.date = date;
this.isPending = isPending;
}
factory BitcoinTransactionInfo.fromHexAndHeader(
String hex, Map<String, Object> header,
{List<String> addresses}) {
final tx = bitcoin.Transaction.fromHex(hex);
var exist = false;
var amount = 0;
tx.outs.forEach((out) {
try {
final p2pkh = bitcoin.P2PKH(
data: PaymentData(output: out.script), network: bitcoin.bitcoin);
exist = addresses.contains(p2pkh.data.address);
if (exist) {
amount += out.value;
}
} catch (_) {}
});
// FIXME: Get transaction is pending
return BitcoinTransactionInfo(
id: tx.getId(),
height: header['block_height'] as int,
isPending: false,
direction: TransactionDirection.incoming,
amount: amount,
date: DateTime.fromMillisecondsSinceEpoch(
(header['timestamp'] as int) * 1000));
}
factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
return BitcoinTransactionInfo(
id: data['id'] as String,
height: data['height'] as int,
amount: data['amount'] as int,
direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool);
}
final String id;
@override
String amountFormatted() => bitcoinAmountToString(amount: amount);
@override
String fiatAmount() => '';
Map<String, dynamic> toJson() {
final m = Map<String, dynamic>();
m['id'] = id;
m['height'] = height;
m['amount'] = amount;
m['direction'] = direction.index;
m['date'] = date.millisecondsSinceEpoch;
m['isPending'] = isPending;
return m;
}
}

View file

@ -0,0 +1,274 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart';
import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/src/domain/common/pending_transaction.dart';
import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart';
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
class BitcoinWallet extends Wallet {
BitcoinWallet(
{@required this.hdwallet,
@required this.eclient,
@required this.path,
@required String password,
int accountIndex = 0,
this.mnemonic})
: _accountIndex = accountIndex,
_password = password,
_syncStatus = BehaviorSubject<SyncStatus>(),
_onBalanceChange = BehaviorSubject<BitcoinBalance>(),
_onAddressChange = BehaviorSubject<String>(),
_onNameChange = BehaviorSubject<String>();
@override
Observable<BitcoinBalance> get onBalanceChange => _onBalanceChange.stream;
@override
Observable<SyncStatus> get syncStatus => _syncStatus.stream;
@override
String get name => path.split('/').last ?? '';
@override
String get address => hdwallet.address;
String get xpub => hdwallet.base58;
final String path;
final bitcoin.HDWallet hdwallet;
final ElectrumClient eclient;
final String mnemonic;
BitcoinTransactionHistory history;
final BehaviorSubject<SyncStatus> _syncStatus;
final BehaviorSubject<BitcoinBalance> _onBalanceChange;
final BehaviorSubject<String> _onAddressChange;
final BehaviorSubject<String> _onNameChange;
BehaviorSubject<Object> _addressUpdatesSubject;
StreamSubscription<Object> _addressUpdatesSubscription;
final String _password;
int _accountIndex;
static Future<BitcoinWallet> load(
{@required String name, @required 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 jsoned = json.decode(walletJSONRaw) as Map<String, Object>;
final mnemonic = jsoned['mnemonic'] as String;
final accountIndex =
(jsoned['account_index'] == "null" || jsoned['account_index'] == null)
? 0
: int.parse(jsoned['account_index'] as String);
return await build(
mnemonic: mnemonic,
password: password,
name: name,
accountIndex: accountIndex);
}
static Future<BitcoinWallet> build(
{@required String mnemonic,
@required String password,
@required String name,
int accountIndex = 0}) async {
final hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
network: bitcoin.bitcoin);
final walletDirPath =
await pathForWalletDir(name: name, type: WalletType.bitcoin);
final walletPath = '$walletDirPath/$name';
final historyPath = '$walletDirPath/transactions.json';
final eclient = ElectrumClient();
final wallet = BitcoinWallet(
hdwallet: hd,
eclient: eclient,
path: walletPath,
mnemonic: mnemonic,
password: password,
accountIndex: accountIndex);
final history = BitcoinTransactionHistory(
eclient: eclient,
path: historyPath,
password: password,
wallet: wallet);
wallet.history = history;
await history.init();
// await wallet.connectToNode(
// node: Node(uri: 'https://electrum2.hodlister.co:50002'));
// final transactions = await history.fetchTransactions();
// final balance = await wallet.fetchBalance();
// print('balance\n$balance');
// transactions.forEach((tx) => print(tx.id));
await wallet.updateInfo();
return wallet;
}
List<String> getAddresses() => _accountIndex == 0
? [address]
: List<String>.generate(
_accountIndex, (i) => _getAddress(hd: hdwallet, index: i));
Future<String> newAddress() async {
_accountIndex += 1;
final address = _getAddress(hd: hdwallet, index: _accountIndex);
await save();
return address;
}
@override
Future close() async {
await _addressUpdatesSubscription?.cancel();
}
@override
Future connectToNode(
{Node node, bool useSSL = false, bool isLightWallet = false}) async {
try {
// FIXME: Hardcoded server address
// final uri = Uri.parse(node.uri);
// https://electrum2.hodlister.co:50002
await eclient.connect(host: 'electrum2.hodlister.co', port: 50002);
_syncStatus.value = ConnectedSyncStatus();
} catch (e) {
print(e.toString());
_syncStatus.value = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(
TransactionCreationCredentials credentials) {
final txb = TransactionBuilder(network: bitcoin.bitcoin);
// TODO: implement createTransaction
return null;
}
@override
Future<String> getAddress() async => address;
@override
Future<int> getCurrentHeight() async => 0;
@override
Future<String> getFilename() async => path.split('/').last ?? '';
@override
Future<String> getFullBalance() async =>
bitcoinAmountToString(amount: _onBalanceChange.value.total);
@override
TransactionHistory getHistory() => history;
@override
Future<Map<String, String>> getKeys() async =>
{'publicKey': hdwallet.pubKey, 'privateKey': hdwallet.privKey};
@override
Future<String> getName() async => path.split('/').last ?? '';
@override
Future<int> getNodeHeight() async => 0;
@override
Future<String> getSeed() async => mnemonic;
@override
WalletType getType() => WalletType.bitcoin;
@override
Future<String> getUnlockedBalance() async =>
bitcoinAmountToString(amount: _onBalanceChange.value.total);
@override
Future<bool> isConnected() async => eclient.isConnected;
@override
Observable<String> get onAddressChange => _onAddressChange.stream;
@override
Observable<String> get onNameChange => _onNameChange.stream;
@override
Future rescan({int restoreHeight = 0}) {
// TODO: implement rescan
return null;
}
@override
Future startSync() async {
_addressUpdatesSubject = eclient.addressUpdate(address: address);
_addressUpdatesSubscription =
_addressUpdatesSubject.listen((obj) => print('new obj: $obj'));
_onBalanceChange.value = await fetchBalance();
getHistory().update();
}
@override
Future updateInfo() async {
_onNameChange.value = await getName();
// _addressUpdatesSubject = eclient.addressUpdate(address: address);
// _addressUpdatesSubscription =
// _addressUpdatesSubject.listen((obj) => print('new obj: $obj'));
_onBalanceChange.value = BitcoinBalance(confirmed: 0, unconfirmed: 0);
print(await getKeys());
}
Future<BitcoinBalance> fetchBalance() async {
final balance = await _fetchBalances();
return BitcoinBalance(
confirmed: balance['confirmed'], unconfirmed: balance['unconfirmed']);
}
Future<void> save() async => await write(
path: path,
password: _password,
obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()});
String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin
.P2PKH(
data: PaymentData(
pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits)))
.data
.address;
Future<Map<String, int>> _fetchBalances() async {
final balances = await Future.wait(
getAddresses().map((address) => eclient.getBalance(address: address)));
final balance =
balances.fold(Map<String, int>(), (Map<String, int> acc, val) {
acc['confirmed'] =
(val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0);
acc['unconfirmed'] =
(val['unconfirmed'] as int ?? 0) + (acc['unconfirmed'] ?? 0);
return acc;
});
return balance;
}
}

View file

@ -0,0 +1,59 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/common/wallet_description.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/domain/common/wallets_manager.dart';
class BitcoinWalletManager extends WalletsManager {
@override
Future<Wallet> create(String name, String password, String language) async {
final wallet = await BitcoinWallet.build(
mnemonic: bip39.generateMnemonic(), password: password, name: name);
await wallet.save();
return wallet;
}
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: WalletType.bitcoin))
.existsSync();
@override
Future<Wallet> openWallet(String name, String password) async {
return BitcoinWallet.load(
name: name, password: password);
}
@override
Future remove(WalletDescription wallet) async {
final path = await pathForWalletDir(name: wallet.name, type: wallet.type);
final f = File(path);
if (!f.existsSync()) {
return;
}
f.deleteSync();
}
@override
Future<Wallet> restoreFromKeys(String name, String password, String language,
int restoreHeight, String address, String viewKey, String spendKey) {
// TODO: implement restoreFromKeys
return null;
}
@override
Future<Wallet> restoreFromSeed(
String name, String password, String seed, int restoreHeight) async {
final wallet = await BitcoinWallet.build(
name: name, password: password, mnemonic: seed);
await wallet.save();
return wallet;
}
}

218
lib/bitcoin/electrum.dart Normal file
View file

@ -0,0 +1,218 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
String jsonrpcparams(List<Object> params) {
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
return "[$_params]";
}
String jsonrpc(
{String method, List<Object> params, int id, double version = 2.0}) =>
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n';
class SocketTask {
SocketTask({this.completer, this.isSubscription, this.subject});
final Completer completer;
final BehaviorSubject subject;
final bool isSubscription;
}
class ElectrumClient {
ElectrumClient()
: _id = 0,
_isConnected = false,
_tasks = {};
static const connectionTimeout = Duration(seconds: 5);
bool get isConnected => _isConnected;
Socket socket;
int _id;
final Map<String, SocketTask> _tasks;
bool _isConnected;
Future<void> connect({@required String host, @required int port}) async {
if (socket != null) {
await socket.close();
}
final start = DateTime.now();
socket = await SecureSocket.connect(host, port, timeout: connectionTimeout);
_isConnected = true;
socket.listen((List<int> event) {
try {
final Map<String, Object> jsoned =
json.decode(utf8.decode(event)) as Map<String, Object>;
final method = jsoned['method'];
if (method is String) {
_methodHandler(method: method, request: jsoned);
return;
}
final id = jsoned['id'] as String;
final params = jsoned['result'];
_finish(id, params);
} catch (e) {
print(e);
}
}, onError: (Object error) {
print('ElectrumClient error: ${error.toString()}');
}, onDone: () {
final end = DateTime.now();
final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
print('On done: $diff');
});
print('Connected to ${socket.remoteAddress}');
}
Future<void> ping() => call(method: 'server.ping');
Future<List<String>> version() =>
call(method: 'server.version').then((dynamic result) {
if (result is List) {
return result.map((dynamic val) => val.toString()).toList();
}
return [];
});
Future<Map<String, Object>> getBalance({String address}) =>
call(method: 'blockchain.address.get_balance', params: [address])
.then((dynamic result) {
if (result is Map<String, Object>) {
return result;
}
return Map<String, Object>();
});
Future<List<Map<String, dynamic>>> getHistory({String address}) =>
call(method: 'blockchain.address.get_history', params: [address])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, Object>) {
return val;
}
return Map<String, Object>();
}).toList();
}
return [];
});
Future<String> getTransactionRaw({@required String hash}) async =>
call(method: 'blockchain.transaction.get', params: [hash])
.then((dynamic result) {
if (result is String) {
return result;
}
return '';
});
Future<Map<String, dynamic>> getMerkle(
{@required String hash, @required int height}) async =>
await call(
method: 'blockchain.transaction.get_merkle',
params: [hash, height]) as Map<String, dynamic>;
Future<Map<String, dynamic>> getHeader({@required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height])
as Map<String, dynamic>;
Future<double> estimatefee({@required int p}) =>
call(method: 'blockchain.estimatefee', params: [p])
.then((dynamic result) {
if (result is double) {
return result;
}
if (result is String) {
return double.parse(result);
}
return 0;
});
BehaviorSubject<Object> addressUpdate({@required String address}) =>
subscribe<Object>(
id: 'blockchain.address.subscribe:$address',
method: 'blockchain.address.subscribe',
params: [address]);
BehaviorSubject<T> subscribe<T>(
{@required String id,
@required String method,
List<Object> params = const []}) {
final subscription = BehaviorSubject<T>();
_regisrySubscription(id, subscription);
socket.write(jsonrpc(method: method, id: _id, params: params));
return subscription;
}
Future<dynamic> call({String method, List<Object> params = const []}) {
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
_regisryTask(id, completer);
socket.write(jsonrpc(method: method, id: _id, params: params));
return completer.future;
}
void request({String method, List<Object> params = const []}) {
_id += 1;
socket.write(jsonrpc(method: method, id: _id, params: params));
}
void _regisryTask(int id, Completer completer) => _tasks[id.toString()] =
SocketTask(completer: completer, isSubscription: false);
void _regisrySubscription(String id, BehaviorSubject subject) =>
_tasks[id] = SocketTask(subject: subject, isSubscription: true);
void _finish(String id, Object data) {
if (_tasks[id] == null) {
return;
}
_tasks[id]?.completer?.complete(data);
if (!(_tasks[id]?.isSubscription ?? false)) {
_tasks[id] = null;
} else {
_tasks[id].subject.add(data);
}
}
void _methodHandler(
{@required String method, @required Map<String, Object> request}) {
switch (method) {
case 'blockchain.address.subscribe':
final params = request['params'] as List<dynamic>;
final address = params.first as String;
final id = 'blockchain.address.subscribe:$address';
if (_tasks[id] != null) {
_tasks[id].subject.add(params.last);
}
break;
default:
break;
}
}
}

43
lib/bitcoin/file.dart Normal file
View file

@ -0,0 +1,43 @@
import 'dart:io';
import 'dart:convert';
import 'package:cake_wallet/bitcoin/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/foundation.dart';
Future<void> write(
{@required String path,
@required String password,
@required Map<String, String> obj}) async {
final jsoned = json.encode(obj);
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: jsoned);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<void> writeData(
{@required String path,
@required String password,
@required String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: data);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<String> read(
{@required String path, @required String password}) async {
final file = File(path);
if (!file.existsSync()) {
file.createSync();
}
final encrypted = file.readAsStringSync();
return decode(password: password, data: encrypted);
}

34
lib/bitcoin/key.dart Normal file
View file

@ -0,0 +1,34 @@
import 'package:encrypt/encrypt.dart' as encrypt;
const ivEncodedStringLength = 12;
String generateKey() {
final key = encrypt.Key.fromSecureRandom(512);
final iv = encrypt.IV.fromSecureRandom(8);
return key.base64 + iv.base64;
}
List<String> extractKeys(String key) {
final _key = key.substring(0, key.length - ivEncodedStringLength);
final iv = key.substring(key.length - ivEncodedStringLength);
return [_key, iv];
}
Future<String> encode({encrypt.Key key, encrypt.IV iv, String data}) async {
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
final encrypted = encrypter.encrypt(data, iv: iv);
return encrypted.base64;
}
Future<String> decode({String password, String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
final encrypted = encrypter.decrypt64(data, iv: iv);
return encrypted;
}

View file

@ -1,3 +1,7 @@
import 'package:cake_wallet/bitcoin/api.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart';
import 'package:cake_wallet/bitcoin/key.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -36,6 +40,8 @@ import 'package:cake_wallet/src/domain/services/wallet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/language.dart';
import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -111,6 +117,42 @@ void main() async {
authenticationStore: authenticationStore,
loginStore: loginStore);
// final addresses = await fetchAllAddresses(xpub: '6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz');
// print(addresses);
final eclient = ElectrumClient();
await eclient.connect(host: "electrum2.hodlister.co", port: 50002);
await eclient.getMerkle(hash: '3780302c523831311afaccd883f04c814bc13c3ad7c2c13810bb5bfe2c3fa621', height: 629341);
print(await eclient.getHeader(height: 629341));
// final version = await eclient.version();
// print('version $version');
// eclient.banner();
// eclient.headersSubscribe();
// final history = await eclient.getHistory(address: '1QEWnc4mSxUoP1fWPs32eZNRtc9zX55TwE');
// print('history $history');
// final balance = await eclient.getBalance(address: '1QEWnc4mSxUoP1fWPs32eZNRtc9zX55TwE');
// print('balance $balance');
// final estimateFee = await eclient.estimatefee(p: 6);
// print('estimateFee $estimateFee');
// final walletManager = BitcoinWalletManager();
// final key = "zLEjJ96r4WPzlc8rWbuaP2HgxoNfec6sbWjKAcNwqTjzBfiE62A8/0Wp5P3a8Ryo3GUIs/GDG7KfwkoI1FpyuhzWZNU1P8sMN/fp88sB9ktffU5V4B9GZJU5ufSblQOKsvZxqxLJWA8nhL7iaUGcifr9TkwbpqHxBTZDlQxlZXAf/DlRUFEF2LLwo8EJ0HcCn+iPVsnqeGtgtjmOG6l7puP31AErKzaLX4yEgXaKxrdqo0ljS4g7fn4UUXpipv7ry83ZId9ZhpkcdqMRnzu84Msyg/UGGg3BX7VTtbO/ko7ojIBoyzEaF355Tg+sgbfwYAY0CNvOJqPpIhDwu+sq4mhb5H592JP426rDTcy9KV1JbZWbnbbWcqcb04vE2zvXN0x37bd4WfO77qkdoGN5m1XZB2+F2wzNUxvf25WPp5L/nvPZFk/rJGGFoy6X8mASnmIXcq5bRzwC+F2zkZSbXoRFx3yXxlaRnzltVDjWlLUrh8S01TV2llUJEFQhefzR3Xz7mgmHRXANIqRztb1AmjD7eVZid84OfedhD2Lfg9rzFcXeMTcBlaKR36ChIY5zw+ljpnqAm86pSwcJXOAJVKcQ0fJLT6dYbHYkOQqdiSs4cJQMdr/xshrkFd1raVDyL8CTNznfxSvWqSrCUqxbuvylGfrWgHzJfK5CB0oLZnA=WBZ/TzHfP4A=";
// await walletManager.openWallet('name', password)
// print(await walletManager.isWalletExit('qwerty'));
// final wallet = await walletManager.openWallet('green', key);
// final keys = await wallet.getKeys();
// final seed = await wallet.getSeed();
// final address = await wallet.getAddress();
// print('key $key');
// print('keys $keys');
// print('seed $seed');
// print('address $address');
runApp(MultiProvider(providers: [
Provider(create: (_) => sharedPreferences),
Provider(create: (_) => walletService),
@ -136,7 +178,7 @@ Future<void> initialSetup(
Box<Node> nodes,
AuthenticationStore authStore,
int initialMigrationVersion = 1,
WalletType initialWalletType = WalletType.monero}) async {
WalletType initialWalletType = WalletType.bitcoin}) async {
await walletListService.changeWalletManger(walletType: initialWalletType);
await defaultSettingsMigration(
version: initialMigrationVersion,

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -109,6 +110,9 @@ class Router {
return MaterialPageRoute<void>(builder: (_) => createWelcomePage());
case Routes.newWalletFromWelcome:
final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type);
return CupertinoPageRoute<void>(
builder: (_) => Provider(
create: (_) => UserStore(
@ -116,10 +120,14 @@ class Router {
secureStorage: FlutterSecureStorage(),
sharedPreferences: sharedPreferences)),
child: SetupPinCodePage(
onPinCodeSetup: (context, _) =>
Navigator.pushNamed(context, Routes.newWallet))));
onPinCodeSetup: (context, _) => Navigator.pushNamed(
context, Routes.newWallet,
arguments: type))));
case Routes.newWallet:
final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type);
return CupertinoPageRoute<void>(
builder:
(_) =>
@ -152,11 +160,18 @@ class Router {
fullscreenDialog: true);
case Routes.restoreOptions:
return CupertinoPageRoute<void>(builder: (_) => RestoreOptionsPage());
final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type);
return CupertinoPageRoute<void>(
builder: (_) => RestoreOptionsPage(type: type));
case Routes.restoreWalletOptions:
final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type);
return CupertinoPageRoute<void>(
builder: (_) => RestoreWalletOptionsPage());
builder: (_) => RestoreWalletOptionsPage(type: type));
case Routes.restoreWalletOptionsFromWelcome:
return CupertinoPageRoute<void>(
@ -177,6 +192,9 @@ class Router {
callback: settings.arguments as void Function()));
case Routes.restoreWalletFromSeed:
final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type);
return CupertinoPageRoute<void>(
builder: (_) =>
ProxyProvider<AuthenticationStore, WalletRestorationStore>(
@ -235,11 +253,15 @@ class Router {
case Routes.receive:
return CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) => MultiProvider(providers: [
Provider(
create: (_) =>
SubaddressListStore(walletService: walletService))
], child: ReceivePage()));
builder: (_) => MultiProvider(
providers: walletService.getType() == WalletType.monero
? [
Provider(
create: (_) => SubaddressListStore(
walletService: walletService))
]
: [],
child: ReceivePage()));
case Routes.transactionDetails:
return CupertinoPageRoute<void>(

View file

@ -0,0 +1,21 @@
import 'dart:io';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
Future<String> pathForWalletDir({@required String name, @required WalletType type}) async {
final root = await getApplicationDocumentsDirectory();
final prefix = walletTypeToString(type).toLowerCase();
final walletsDir = Directory('${root.path}/wallets');
final walletDire = Directory('${walletsDir.path}/$prefix/$name');
if (!walletDire.existsSync()) {
walletDire.createSync(recursive: true);
}
return walletDire.path;
}
Future<String> pathForWallet({@required String name, @required WalletType type}) async =>
await pathForWalletDir(name: name, type: type)
.then((path) => path + '/$name');

View file

@ -4,7 +4,5 @@ import 'package:cake_wallet/src/domain/common/transaction_info.dart';
abstract class TransactionHistory {
Observable<List<TransactionInfo>> transactions;
Future<List<TransactionInfo>> getAll();
Future<int> count();
Future refresh();
Future update();
}

View file

@ -1,49 +1,11 @@
import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
import 'package:cw_monero/structs/transaction_info_row.dart';
import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:cake_wallet/src/domain/common/format_amount.dart';
class TransactionInfo {
TransactionInfo(this.id, this.height, this.direction, this.date,
this.isPending, this.amount, this.accountIndex);
TransactionInfo.fromMap(Map map)
: id = (map['hash'] ?? '') as String,
height = (map['height'] ?? 0) as int,
direction =
parseTransactionDirectionFromNumber(map['direction'] as String) ??
TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(
(int.parse(map['timestamp'] as String) ?? 0) * 1000),
isPending = parseBoolFromString(map['isPending'] as String),
amount = map['amount'] as int,
accountIndex = int.parse(map['accountIndex'] as String);
TransactionInfo.fromRow(TransactionInfoRow row)
: id = row.getHash(),
height = row.blockHeight,
direction = parseTransactionDirectionFromInt(row.direction) ??
TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000),
isPending = row.isPending != 0,
amount = row.getAmount(),
accountIndex = row.subaddrAccount;
final String id;
final int height;
final TransactionDirection direction;
final DateTime date;
final int accountIndex;
final bool isPending;
final int amount;
String recipientAddress;
String _fiatAmount;
String amountFormatted() => '${formatAmount(moneroAmountToString(amount: amount))} XMR';
String fiatAmount() => _fiatAmount ?? '';
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
}
abstract class TransactionInfo extends Object {
int amount;
TransactionDirection direction;
bool isPending;
DateTime date;
int height;
String amountFormatted();
String fiatAmount();
}

View file

@ -2,13 +2,18 @@ import 'package:hive/hive.dart';
part 'wallet_type.g.dart';
const walletTypes = [WalletType.monero, WalletType.bitcoin];
@HiveType()
enum WalletType {
@HiveField(0)
monero,
@HiveField(1)
none
none,
@HiveField(2)
bitcoin
}
int serializeToInt(WalletType type) {
@ -33,7 +38,9 @@ String walletTypeToString(WalletType type) {
switch (type) {
case WalletType.monero:
return 'Monero';
case WalletType.bitcoin:
return 'Bitcoin';
default:
return '';
}
}
}

View file

@ -1,4 +1,5 @@
import 'dart:core';
import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';
import 'package:cw_monero/transaction_history.dart'
@ -6,10 +7,11 @@ import 'package:cw_monero/transaction_history.dart'
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
List<TransactionInfo> _getAllTransactions(dynamic _) => monero_transaction_history
.getAllTransations()
.map((row) => TransactionInfo.fromRow(row))
.toList();
List<TransactionInfo> _getAllTransactions(dynamic _) =>
monero_transaction_history
.getAllTransations()
.map((row) => MoneroTransactionInfo.fromRow(row))
.toList();
class MoneroTransactionHistory extends TransactionHistory {
MoneroTransactionHistory()
@ -31,7 +33,6 @@ class MoneroTransactionHistory extends TransactionHistory {
try {
_isUpdating = true;
await refresh();
_transactions.value = await getAll(force: true);
_isUpdating = false;
@ -46,13 +47,11 @@ class MoneroTransactionHistory extends TransactionHistory {
}
@override
Future<List<TransactionInfo>> getAll({bool force = false}) async =>
_getAllTransactions(null);
Future<List<TransactionInfo>> getAll({bool force = false}) async {
await refresh();
return _getAllTransactions(null);
}
@override
Future<int> count() async => monero_transaction_history.countOfTransactions();
@override
Future refresh() async {
if (_isRefreshing) {
return;

View file

@ -0,0 +1,50 @@
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
import 'package:cw_monero/structs/transaction_info_row.dart';
import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:cake_wallet/src/domain/common/format_amount.dart';
class MoneroTransactionInfo extends TransactionInfo {
MoneroTransactionInfo(this.id, this.height, this.direction, this.date,
this.isPending, this.amount, this.accountIndex);
MoneroTransactionInfo.fromMap(Map map)
: id = (map['hash'] ?? '') as String,
height = (map['height'] ?? 0) as int,
direction =
parseTransactionDirectionFromNumber(map['direction'] as String) ??
TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(
(int.parse(map['timestamp'] as String) ?? 0) * 1000),
isPending = parseBoolFromString(map['isPending'] as String),
amount = map['amount'] as int,
accountIndex = int.parse(map['accountIndex'] as String);
MoneroTransactionInfo.fromRow(TransactionInfoRow row)
: id = row.getHash(),
height = row.blockHeight,
direction = parseTransactionDirectionFromInt(row.direction) ??
TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000),
isPending = row.isPending != 0,
amount = row.getAmount(),
accountIndex = row.subaddrAccount;
final String id;
final int height;
final TransactionDirection direction;
final DateTime date;
final int accountIndex;
final bool isPending;
final int amount;
String recipientAddress;
String _fiatAmount;
String amountFormatted() => '${formatAmount(moneroAmountToString(amount: amount))} XMR';
String fiatAmount() => _fiatAmount ?? '';
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
}

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
@ -11,17 +12,7 @@ import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/monero/monero_wallet.dart';
import 'package:cake_wallet/src/domain/common/wallet_description.dart';
Future<String> pathForWallet({String name}) async {
final directory = await getApplicationDocumentsDirectory();
final pathDir = directory.path + '/$name';
final dir = Directory(pathDir);
if (!await dir.exists()) {
await dir.create();
}
return pathDir + '/$name';
}
class MoneroWalletsManager extends WalletsManager {
MoneroWalletsManager({@required this.walletInfoSource});
@ -34,7 +25,7 @@ class MoneroWalletsManager extends WalletsManager {
Future<Wallet> create(String name, String password, String language) async {
try {
const isRecovery = false;
final path = await pathForWallet(name: name);
final path = await pathForWallet(name: name, type: WalletType.monero);
await monero_wallet_manager.createWallet(path: path, password: password, language: language);

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart';
import 'package:cake_wallet/bitcoin/key.dart';
import 'package:cake_wallet/src/domain/common/wallet_info.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
@ -29,13 +31,15 @@ class WalletListService {
this.walletInfoSource,
this.walletsManager,
@required this.walletService,
@required this.sharedPreferences});
@required this.sharedPreferences})
: _type = WalletType.monero;
final FlutterSecureStorage secureStorage;
final WalletService walletService;
final Box<WalletInfo> walletInfoSource;
final SharedPreferences sharedPreferences;
WalletsManager walletsManager;
WalletType _type;
Future<List<WalletDescription>> getAll() async => walletInfoSource.values
.map((info) => WalletDescription(name: info.name, type: info.type))
@ -50,7 +54,7 @@ class WalletListService {
await walletService.close();
}
final password = Uuid().v4();
final password = _generatePassword();
await saveWalletPassword(password: password, walletName: name);
final wallet = await walletsManager.create(name, password, language);
@ -67,7 +71,7 @@ class WalletListService {
await walletService.close();
}
final password = Uuid().v4();
final password = _generatePassword();
await saveWalletPassword(password: password, walletName: name);
final wallet = await walletsManager.restoreFromSeed(
@ -76,8 +80,8 @@ class WalletListService {
await onWalletChange(wallet);
}
Future restoreFromKeys(String name, String language, int restoreHeight, String address,
String viewKey, String spendKey) async {
Future restoreFromKeys(String name, String language, int restoreHeight,
String address, String viewKey, String spendKey) async {
if (await walletsManager.isWalletExit(name)) {
throw WalletIsExistException(name);
}
@ -86,7 +90,7 @@ class WalletListService {
await walletService.close();
}
final password = Uuid().v4();
final password = _generatePassword();
await saveWalletPassword(password: password, walletName: name);
final wallet = await walletsManager.restoreFromKeys(
@ -107,11 +111,16 @@ class WalletListService {
}
Future changeWalletManger({WalletType walletType}) async {
_type = walletType;
switch (walletType) {
case WalletType.monero:
walletsManager =
MoneroWalletsManager(walletInfoSource: walletInfoSource);
break;
case WalletType.bitcoin:
walletsManager = BitcoinWalletManager();
break;
case WalletType.none:
walletsManager = null;
break;
@ -121,6 +130,7 @@ class WalletListService {
Future onWalletChange(Wallet wallet) async {
walletService.currentWallet = wallet;
final walletName = await wallet.getName();
print('walletName $walletName ');
await sharedPreferences.setString('current_wallet_name', walletName);
}
@ -142,4 +152,13 @@ class WalletListService {
await secureStorage.write(key: key, value: encodedPassword);
}
String _generatePassword() {
switch (_type) {
case WalletType.bitcoin:
return generateKey();
default:
return Uuid().v4();
}
}
}

View file

@ -70,7 +70,7 @@ class WalletService extends Wallet {
WalletDescription description;
@override
WalletType getType() => WalletType.monero;
WalletType getType() => _currentWallet.getType();
@override
Future<String> getFilename() => _currentWallet.getFilename();

View file

@ -33,7 +33,7 @@ class ReceivePage extends BasePage {
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: () => Share.text(
'Share address', walletStore.subaddress.address, 'text/plain'),
'Share address', walletStore.getAddress, 'text/plain'),
child: Icon(
Icons.share,
size: 30.0,
@ -65,7 +65,11 @@ class ReceiveBodyState extends State<ReceiveBody> {
@override
Widget build(BuildContext context) {
final walletStore = Provider.of<WalletStore>(context);
final subaddressListStore = Provider.of<SubaddressListStore>(context);
SubaddressListStore subaddressListStore;
try {
subaddressListStore = Provider.of<SubaddressListStore>(context);
} catch (_) {}
final currentColor = Theme.of(context).selectedRowColor;
final notCurrentColor = Theme.of(context).scaffoldBackgroundColor;
@ -101,7 +105,7 @@ class ReceiveBodyState extends State<ReceiveBody> {
padding: EdgeInsets.all(5),
color: Colors.white,
child: QrImage(
data: walletStore.subaddress.address +
data: walletStore.getAddress +
walletStore.amountValue,
backgroundColor: Colors.transparent,
),
@ -122,8 +126,8 @@ class ReceiveBodyState extends State<ReceiveBody> {
child: Center(
child: GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(
text: walletStore.subaddress.address));
Clipboard.setData(
ClipboardData(text: walletStore.getAddress));
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(
S.of(context).copied_to_clipboard,
@ -133,7 +137,7 @@ class ReceiveBodyState extends State<ReceiveBody> {
));
},
child: Text(
walletStore.subaddress.address,
walletStore.getAddress,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
@ -187,78 +191,17 @@ class ReceiveBodyState extends State<ReceiveBody> {
],
),
),
Row(
children: <Widget>[
Expanded(
child: Container(
color: Theme.of(context).accentTextTheme.headline.color,
child: Column(
subaddressListStore != null
? Row(
children: <Widget>[
ListTile(
title: Text(
S.of(context).subaddresses,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.headline
.color),
),
trailing: Container(
width: 28.0,
height: 28.0,
decoration: BoxDecoration(
color: Theme.of(context).selectedRowColor,
shape: BoxShape.circle),
child: InkWell(
onTap: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress),
borderRadius: BorderRadius.all(Radius.circular(14.0)),
child: Icon(
Icons.add,
color: Palette.violet,
size: 22.0,
),
),
),
),
Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
)
],
),
))
],
),
Observer(builder: (_) {
return ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: subaddressListStore.subaddresses.length,
separatorBuilder: (context, i) {
return Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
);
},
itemBuilder: (context, i) {
return Observer(builder: (_) {
final subaddress = subaddressListStore.subaddresses[i];
final isCurrent =
walletStore.subaddress.address == subaddress.address;
final label = subaddress.label.isNotEmpty
? subaddress.label
: subaddress.address;
return InkWell(
onTap: () => walletStore.setSubaddress(subaddress),
child: Container(
color: isCurrent ? currentColor : notCurrentColor,
child: Column(children: <Widget>[
Expanded(
child: Container(
color: Theme.of(context).accentTextTheme.headline.color,
child: Column(
children: <Widget>[
ListTile(
title: Text(
label,
S.of(context).subaddresses,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
@ -266,13 +209,79 @@ class ReceiveBodyState extends State<ReceiveBody> {
.headline
.color),
),
trailing: Container(
width: 28.0,
height: 28.0,
decoration: BoxDecoration(
color: Theme.of(context).selectedRowColor,
shape: BoxShape.circle),
child: InkWell(
onTap: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress),
borderRadius:
BorderRadius.all(Radius.circular(14.0)),
child: Icon(
Icons.add,
color: Palette.violet,
size: 22.0,
),
),
),
),
Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
)
]),
],
),
);
});
});
})
))
],
)
: Container(),
subaddressListStore != null
? Observer(builder: (_) {
return ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: subaddressListStore.subaddresses.length,
separatorBuilder: (context, i) {
return Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
);
},
itemBuilder: (context, i) {
return Observer(builder: (_) {
final subaddress = subaddressListStore.subaddresses[i];
final isCurrent =
walletStore.getAddress == subaddress.address;
final label = subaddress.label.isNotEmpty
? subaddress.label
: subaddress.address;
return InkWell(
onTap: () => walletStore.setSubaddress(subaddress),
child: Container(
color: isCurrent ? currentColor : notCurrentColor,
child: Column(children: <Widget>[
ListTile(
title: Text(
label,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.headline
.color),
),
)
]),
),
);
});
});
})
: Container()
],
)));
}

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
@ -9,7 +10,10 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart';
class RestoreOptionsPage extends BasePage {
RestoreOptionsPage({@required this.type});
static const _aspectRatioImage = 2.086;
final WalletType type;
@override
String get title => S.current.restore_restore_wallet;

View file

@ -6,12 +6,16 @@ import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart';
import 'package:cake_wallet/src/screens/restore/widgets/image_widget.dart';
import 'package:cake_wallet/src/screens/restore/widgets/base_restore_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart';
import 'package:provider/provider.dart';
class RestoreWalletOptionsPage extends BasePage {
RestoreWalletOptionsPage({@required this.type});
static const _aspectRatioImage = 2.086;
final WalletType type;
@override
String get title => S.current.restore_seed_keys_restore;
@ -31,7 +35,7 @@ class RestoreWalletOptionsPage extends BasePage {
firstRestoreButton: RestoreButton(
onPressed: () {
seedLanguageStore.setCurrentRoute(Routes.restoreWalletFromSeed);
Navigator.pushNamed(context, Routes.seedLanguage);
Navigator.pushNamed(context, Routes.seedLanguage, arguments: type);
},
imageWidget: ImageWidget(
image: _imageSeed,
@ -46,7 +50,7 @@ class RestoreWalletOptionsPage extends BasePage {
secondRestoreButton: RestoreButton(
onPressed: () {
seedLanguageStore.setCurrentRoute(Routes.restoreWalletFromKeys);
Navigator.pushNamed(context, Routes.seedLanguage);
Navigator.pushNamed(context, Routes.seedLanguage, arguments: type);
},
imageWidget: ImageWidget(
image: _imageKeys,

View file

@ -25,11 +25,19 @@ class ShowKeysPage extends BasePage {
builder: (_) {
final keysMap = {
S.of(context).view_key_public: walletKeysStore.publicViewKey,
S.of(context).view_key_private: walletKeysStore.privateViewKey,
S.of(context).spend_key_public: walletKeysStore.publicSpendKey,
S.of(context).spend_key_private: walletKeysStore.privateSpendKey
};
if (walletKeysStore.privateViewKey.isNotEmpty) {
keysMap[S.of(context).view_key_private] =
walletKeysStore.privateViewKey;
}
if (walletKeysStore.publicSpendKey.isNotEmpty) {
keysMap[S.of(context).spend_key_public] =
walletKeysStore.publicSpendKey;
}
return ListView.separated(
separatorBuilder: (_, __) => Container(
padding: EdgeInsets.only(left: 30.0, right: 20.0),

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
@ -46,31 +47,33 @@ class TransactionDetailsFormState extends State<TransactionDetailsForm> {
@override
void initState() {
final _dateFormat = widget.settingsStore.getCurrentDateFormat(
formatUSA: "yyyy.MM.dd, HH:mm",
formatDefault: "dd.MM.yyyy, HH:mm");
final items = [
StandartListItem(
title: S.current.transaction_details_transaction_id,
value: widget.transactionInfo.id),
StandartListItem(
title: S.current.transaction_details_date,
value: _dateFormat.format(widget.transactionInfo.date)),
StandartListItem(
title: S.current.transaction_details_height,
value: '${widget.transactionInfo.height}'),
StandartListItem(
title: S.current.transaction_details_amount,
value: widget.transactionInfo.amountFormatted())
];
formatUSA: "yyyy.MM.dd, HH:mm", formatDefault: "dd.MM.yyyy, HH:mm");
final tx = widget.transactionInfo;
if (widget.settingsStore.shouldSaveRecipientAddress &&
widget.transactionInfo.recipientAddress != null) {
items.add(StandartListItem(
title: S.current.transaction_details_recipient_address,
value: widget.transactionInfo.recipientAddress));
if (tx is MoneroTransactionInfo) {
final items = [
StandartListItem(
title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date,
value: _dateFormat.format(tx.date)),
StandartListItem(
title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(
title: S.current.transaction_details_amount,
value: tx.amountFormatted())
];
if (widget.settingsStore.shouldSaveRecipientAddress &&
tx.recipientAddress != null) {
items.add(StandartListItem(
title: S.current.transaction_details_recipient_address,
value: tx.recipientAddress));
}
_items.addAll(items);
}
_items.addAll(items);
super.initState();
}

View file

@ -1,3 +1,5 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
@ -5,14 +7,38 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart';
WalletType _selectedType;
class WelcomePage extends BasePage {
static const _aspectRatioImage = 1.26;
static const _baseWidth = 411.43;
final _image = Image.asset('assets/images/welcomeImg.png');
final _cakeLogo = Image.asset('assets/images/cake_logo.png');
final Map<String, WalletType> _picker =
walletTypes.fold(Map<String, WalletType>(), (acc, item) {
acc[walletTypeToString(item)] = item;
return acc;
});
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _selectedType == null
? showDialog<void>(
builder: (_) => Picker(
items: walletTypes
.map((item) => walletTypeToString(item))
.toList(),
selectedAtIndex: -1,
title: 'Select wallet type',
pickerHeight: 510,
onItemSelected: (String item) {
print('before $_selectedType');
_selectedType = _picker[item];
print('after $_selectedType');
}),
context: context)
: null);
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
resizeToAvoidBottomPadding: false,
@ -72,7 +98,8 @@ class WelcomePage extends BasePage {
child: Column(children: <Widget>[
PrimaryButton(
onPressed: () {
Navigator.pushNamed(context, Routes.newWalletFromWelcome);
Navigator.pushNamed(context, Routes.newWalletFromWelcome,
arguments: _selectedType);
},
text: S.of(context).create_new,
color:
@ -82,7 +109,8 @@ class WelcomePage extends BasePage {
SizedBox(height: 10),
PrimaryButton(
onPressed: () {
Navigator.pushNamed(context, Routes.restoreOptions);
Navigator.pushNamed(context, Routes.restoreOptions,
arguments: _selectedType);
},
color: Theme.of(context).accentTextTheme.caption.backgroundColor,
borderColor:

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/src/domain/exchange/trade.dart';
import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart';
@ -98,10 +99,14 @@ abstract class ActionListBase with Store {
final price = _priceStore.prices[symbol];
_transactions.forEach((item) {
final amount = calculateFiatAmountRaw(
cryptoAmount: moneroAmountToDouble(amount: item.transaction.amount),
price: price);
item.transaction.changeFiatAmount(amount);
final tx = item.transaction;
if (tx is MoneroTransactionInfo) {
final amount = calculateFiatAmountRaw(
cryptoAmount: moneroAmountToDouble(amount: tx.amount),
price: price);
tx.changeFiatAmount(amount);
}
});
return _transactions;
@ -171,7 +176,6 @@ abstract class ActionListBase with Store {
tradesSource.values.map((trade) => TradeListItem(trade: trade)).toList();
Future _updateTransactionsList() async {
await _history.refresh();
final _transactions = await _history.getAll();
await _setTransactions(_transactions);
}
@ -203,22 +207,29 @@ abstract class ActionListBase with Store {
Future _setTransactions(List<TransactionInfo> transactions) async {
final wallet = _walletService.currentWallet;
List<TransactionInfo> sortedTransactions = transactions.map((transaction) {
if (transactionDescriptions.values.isNotEmpty) {
final description = transactionDescriptions.values.firstWhere(
(desc) => desc.id == transaction.id,
orElse: () => null);
if (transaction is MoneroTransactionInfo) {
if (transactionDescriptions.values.isNotEmpty) {
final description = transactionDescriptions.values.firstWhere(
(desc) => desc.id == transaction.id,
orElse: () => null);
if (description != null && description.recipientAddress != null) {
transaction.recipientAddress = description.recipientAddress;
if (description != null && description.recipientAddress != null) {
transaction.recipientAddress = description.recipientAddress;
}
}
return transaction;
}
return transaction;
}).toList();
if (wallet is MoneroWallet) {
sortedTransactions =
transactions.where((tx) => tx.accountIndex == _account.id).toList();
sortedTransactions = transactions
.where((tx) => tx is MoneroTransactionInfo
? tx.accountIndex == _account.id
: false)
.toList();
}
this._transactions = sortedTransactions

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
@ -87,17 +88,30 @@ abstract class BalanceStoreBase with Store {
}
Future _onBalanceChange(Balance balance) async {
final _balance = balance as MoneroBalance;
if (this.fullBalance != _balance.fullBalance) {
this.fullBalance = _balance.fullBalance;
if (balance is MoneroBalance) {
await _onMoneroBalanceChange(balance);
}
if (this.unlockedBalance != _balance.unlockedBalance) {
this.unlockedBalance = _balance.unlockedBalance;
if (balance is BitcoinBalance) {
await _onBitcoinBalanceChange(balance);
}
}
Future _onMoneroBalanceChange(MoneroBalance balance) async {
if (this.fullBalance != balance.fullBalance) {
this.fullBalance = balance.fullBalance;
}
if (this.unlockedBalance != balance.unlockedBalance) {
this.unlockedBalance = balance.unlockedBalance;
}
}
Future _onBitcoinBalanceChange(BitcoinBalance balance) async {
fullBalance = balance.totalFormatted;
unlockedBalance = balance.totalFormatted;
}
Future _onWalletChanged(Wallet wallet) async {
if (_onBalanceChangeSubscription != null) {
await _onBalanceChangeSubscription.cancel();

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
@ -15,10 +16,17 @@ abstract class WalletKeysStoreBase with Store {
if (walletService.currentWallet != null) {
walletService.getKeys().then((keys) {
publicViewKey = keys['publicViewKey'];
privateViewKey = keys['privateViewKey'];
publicSpendKey = keys['publicSpendKey'];
privateSpendKey = keys['privateSpendKey'];
if (walletService.getType() == WalletType.monero) {
publicViewKey = keys['publicViewKey'];
privateViewKey = keys['privateViewKey'];
publicSpendKey = keys['publicSpendKey'];
privateSpendKey = keys['privateSpendKey'];
}
if (walletService.getType() == WalletType.bitcoin) {
publicViewKey = keys['publicKey'];
privateSpendKey = keys['privateKey'];
}
});
}
}

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/monero/account.dart';
@ -119,6 +120,7 @@ abstract class WalletStoreBase with Store {
return;
}
address = await wallet.getAddress();
wallet.onNameChange.listen((name) => this.name = name);
wallet.onAddressChange.listen((address) => this.address = address);
@ -160,4 +162,9 @@ abstract class WalletStoreBase with Store {
}
Future<bool> isConnected() async => await _walletService.isConnected();
WalletType getType() => _walletService.getType();
String get getAddress =>
getType() == WalletType.monero ? subaddress.address : address;
}

View file

@ -57,6 +57,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
bech32:
dependency: transitive
description:
name: bech32
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
bip32:
dependency: transitive
description:
name: bip32
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
bip39:
dependency: transitive
description:
name: bip39
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
bitcoin_flutter:
dependency: "direct main"
description:
name: bitcoin_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
boolean_selector:
dependency: transitive
description:
@ -64,6 +92,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
bs58check:
dependency: transitive
description:
name: bs58check
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
build:
dependency: transitive
description:
@ -336,6 +371,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
hex:
dependency: transitive
description:
name: hex
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
hive:
dependency: "direct main"
description:

View file

@ -56,6 +56,7 @@ dependencies:
encrypt: ^4.0.0
password: ^1.0.0
basic_utils: ^1.0.8
bitcoin_flutter: ^2.0.0
dev_dependencies:
flutter_test: