parent
2673ecd45e
commit
1d793ab284
@ -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>
|
@ -0,0 +1,2 @@
|
|||||||
|
connection.project.dir=
|
||||||
|
eclipse.preferences.version=1
|
@ -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>
|
@ -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>
|
@ -0,0 +1,2 @@
|
|||||||
|
connection.project.dir=..
|
||||||
|
eclipse.preferences.version=1
|
@ -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>
|
@ -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>
|
@ -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
|
@ -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>
|
@ -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();
|
||||||
|
}
|
@ -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);
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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');
|
@ -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/transaction_direction.dart';
|
||||||
import 'package:cake_wallet/src/domain/common/format_amount.dart';
|
|
||||||
|
|
||||||
class TransactionInfo {
|
abstract class TransactionInfo extends Object {
|
||||||
TransactionInfo(this.id, this.height, this.direction, this.date,
|
int amount;
|
||||||
this.isPending, this.amount, this.accountIndex);
|
TransactionDirection direction;
|
||||||
|
bool isPending;
|
||||||
TransactionInfo.fromMap(Map map)
|
DateTime date;
|
||||||
: id = (map['hash'] ?? '') as String,
|
int height;
|
||||||
height = (map['height'] ?? 0) as int,
|
String amountFormatted();
|
||||||
direction =
|
String fiatAmount();
|
||||||
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);
|
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in new issue