mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
BTC
This commit is contained in:
parent
2673ecd45e
commit
1d793ab284
44 changed files with 1503 additions and 225 deletions
17
android/.project
Normal file
17
android/.project
Normal 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>
|
2
android/.settings/org.eclipse.buildship.core.prefs
Normal file
2
android/.settings/org.eclipse.buildship.core.prefs
Normal file
|
@ -0,0 +1,2 @@
|
|||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
6
android/app/.classpath
Normal file
6
android/app/.classpath
Normal 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
23
android/app/.project
Normal 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>
|
2
android/app/.settings/org.eclipse.buildship.core.prefs
Normal file
2
android/app/.settings/org.eclipse.buildship.core.prefs
Normal file
|
@ -0,0 +1,2 @@
|
|||
connection.project.dir=..
|
||||
eclipse.preferences.version=1
|
6
cw_monero/android/.classpath
Normal file
6
cw_monero/android/.classpath
Normal 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
cw_monero/android/.project
Normal file
23
cw_monero/android/.project
Normal 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>
|
13
cw_monero/android/.settings/org.eclipse.buildship.core.prefs
Normal file
13
cw_monero/android/.settings/org.eclipse.buildship.core.prefs
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
21
lib/bitcoin/api.dart
Normal 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();
|
||||
}
|
13
lib/bitcoin/bitcoin_amount_format.dart
Normal file
13
lib/bitcoin/bitcoin_amount_format.dart
Normal 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);
|
14
lib/bitcoin/bitcoin_balance.dart
Normal file
14
lib/bitcoin/bitcoin_balance.dart
Normal 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);
|
||||
}
|
145
lib/bitcoin/bitcoin_transaction_history.dart
Normal file
145
lib/bitcoin/bitcoin_transaction_history.dart
Normal 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;
|
||||
}
|
||||
}
|
81
lib/bitcoin/bitcoin_transaction_info.dart
Normal file
81
lib/bitcoin/bitcoin_transaction_info.dart
Normal 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;
|
||||
}
|
||||
}
|
274
lib/bitcoin/bitcoin_wallet.dart
Normal file
274
lib/bitcoin/bitcoin_wallet.dart
Normal 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;
|
||||
}
|
||||
}
|
59
lib/bitcoin/bitcoin_wallet.manager.dart
Normal file
59
lib/bitcoin/bitcoin_wallet.manager.dart
Normal 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
218
lib/bitcoin/electrum.dart
Normal 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
43
lib/bitcoin/file.dart
Normal 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
34
lib/bitcoin/key.dart
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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: [
|
||||
builder: (_) => MultiProvider(
|
||||
providers: walletService.getType() == WalletType.monero
|
||||
? [
|
||||
Provider(
|
||||
create: (_) =>
|
||||
SubaddressListStore(walletService: walletService))
|
||||
], child: ReceivePage()));
|
||||
create: (_) => SubaddressListStore(
|
||||
walletService: walletService))
|
||||
]
|
||||
: [],
|
||||
child: ReceivePage()));
|
||||
|
||||
case Routes.transactionDetails:
|
||||
return CupertinoPageRoute<void>(
|
||||
|
|
21
lib/src/domain/common/pathForWallet.dart
Normal file
21
lib/src/domain/common/pathForWallet.dart
Normal 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');
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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,6 +38,8 @@ String walletTypeToString(WalletType type) {
|
|||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return 'Monero';
|
||||
case WalletType.bitcoin:
|
||||
return 'Bitcoin';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -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,9 +7,10 @@ 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
|
||||
List<TransactionInfo> _getAllTransactions(dynamic _) =>
|
||||
monero_transaction_history
|
||||
.getAllTransations()
|
||||
.map((row) => TransactionInfo.fromRow(row))
|
||||
.map((row) => MoneroTransactionInfo.fromRow(row))
|
||||
.toList();
|
||||
|
||||
class MoneroTransactionHistory extends TransactionHistory {
|
||||
|
@ -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;
|
||||
|
|
50
lib/src/domain/monero/monero_transaction_info.dart
Normal file
50
lib/src/domain/monero/monero_transaction_info.dart
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,7 +191,8 @@ class ReceiveBodyState extends State<ReceiveBody> {
|
|||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
subaddressListStore != null
|
||||
? Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
@ -213,7 +218,8 @@ class ReceiveBodyState extends State<ReceiveBody> {
|
|||
child: InkWell(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.newSubaddress),
|
||||
borderRadius: BorderRadius.all(Radius.circular(14.0)),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(14.0)),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Palette.violet,
|
||||
|
@ -230,8 +236,10 @@ class ReceiveBodyState extends State<ReceiveBody> {
|
|||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
Observer(builder: (_) {
|
||||
)
|
||||
: Container(),
|
||||
subaddressListStore != null
|
||||
? Observer(builder: (_) {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
|
@ -246,7 +254,7 @@ class ReceiveBodyState extends State<ReceiveBody> {
|
|||
return Observer(builder: (_) {
|
||||
final subaddress = subaddressListStore.subaddresses[i];
|
||||
final isCurrent =
|
||||
walletStore.subaddress.address == subaddress.address;
|
||||
walletStore.getAddress == subaddress.address;
|
||||
final label = subaddress.label.isNotEmpty
|
||||
? subaddress.label
|
||||
: subaddress.address;
|
||||
|
@ -273,6 +281,7 @@ class ReceiveBodyState extends State<ReceiveBody> {
|
|||
});
|
||||
});
|
||||
})
|
||||
: Container()
|
||||
],
|
||||
)));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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");
|
||||
formatUSA: "yyyy.MM.dd, HH:mm", formatDefault: "dd.MM.yyyy, HH:mm");
|
||||
final tx = widget.transactionInfo;
|
||||
|
||||
if (tx is MoneroTransactionInfo) {
|
||||
final items = [
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_transaction_id,
|
||||
value: widget.transactionInfo.id),
|
||||
title: S.current.transaction_details_transaction_id, value: tx.id),
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_date,
|
||||
value: _dateFormat.format(widget.transactionInfo.date)),
|
||||
value: _dateFormat.format(tx.date)),
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_height,
|
||||
value: '${widget.transactionInfo.height}'),
|
||||
title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_amount,
|
||||
value: widget.transactionInfo.amountFormatted())
|
||||
value: tx.amountFormatted())
|
||||
];
|
||||
|
||||
if (widget.settingsStore.shouldSaveRecipientAddress &&
|
||||
widget.transactionInfo.recipientAddress != null) {
|
||||
tx.recipientAddress != null) {
|
||||
items.add(StandartListItem(
|
||||
title: S.current.transaction_details_recipient_address,
|
||||
value: widget.transactionInfo.recipientAddress));
|
||||
value: tx.recipientAddress));
|
||||
}
|
||||
|
||||
_items.addAll(items);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 tx = item.transaction;
|
||||
|
||||
if (tx is MoneroTransactionInfo) {
|
||||
final amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: moneroAmountToDouble(amount: item.transaction.amount),
|
||||
cryptoAmount: moneroAmountToDouble(amount: tx.amount),
|
||||
price: price);
|
||||
item.transaction.changeFiatAmount(amount);
|
||||
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,6 +207,7 @@ abstract class ActionListBase with Store {
|
|||
Future _setTransactions(List<TransactionInfo> transactions) async {
|
||||
final wallet = _walletService.currentWallet;
|
||||
List<TransactionInfo> sortedTransactions = transactions.map((transaction) {
|
||||
if (transaction is MoneroTransactionInfo) {
|
||||
if (transactionDescriptions.values.isNotEmpty) {
|
||||
final description = transactionDescriptions.values.firstWhere(
|
||||
(desc) => desc.id == transaction.id,
|
||||
|
@ -213,12 +218,18 @@ abstract class ActionListBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
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'];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
42
pubspec.lock
42
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue