mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-13 09:04:09 +01:00
CAKE-234
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import 'package:cake_wallet/entities/transaction_priority.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
|
||||
|
||||
class BitcoinTransactionCredentials {
|
||||
BitcoinTransactionCredentials(this.address, this.amount, this.priority);
|
||||
|
||||
final String address;
|
||||
final String amount;
|
||||
TransactionPriority priority;
|
||||
BitcoinTransactionPriority priority;
|
||||
}
|
||||
|
||||
@@ -102,8 +102,12 @@ abstract class BitcoinTransactionHistoryBase
|
||||
BitcoinTransactionInfo get(String id) => transactions[id];
|
||||
|
||||
Future<void> save() async {
|
||||
final data = json.encode({'height': _height, 'transactions': transactions});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
try {
|
||||
final data = json.encode({'height': _height, 'transactions': transactions});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
} catch(e) {
|
||||
print('Error while save bitcoin transaction history: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -170,6 +174,10 @@ abstract class BitcoinTransactionHistoryBase
|
||||
}
|
||||
|
||||
void _updateOrInsert(BitcoinTransactionInfo transaction) {
|
||||
if (transaction.id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactions[transaction.id] == null) {
|
||||
transactions[transaction.id] = transaction;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:cake_wallet/entities/transaction_priority.dart';
|
||||
|
||||
class BitcoinTransactionPriority extends TransactionPriority {
|
||||
const BitcoinTransactionPriority(this.rate, {String title, int raw})
|
||||
: super(title: title, raw: raw);
|
||||
|
||||
static const List<BitcoinTransactionPriority> all = [slow, medium, fast];
|
||||
static const BitcoinTransactionPriority slow = BitcoinTransactionPriority(11, title: 'Slow', raw: 0);
|
||||
static const BitcoinTransactionPriority medium = BitcoinTransactionPriority(90, title: 'Medium', raw: 1);
|
||||
static const BitcoinTransactionPriority fast = BitcoinTransactionPriority(98, title: 'Fast', raw: 2);
|
||||
|
||||
static BitcoinTransactionPriority deserialize({int raw}) {
|
||||
switch (raw) {
|
||||
case 0:
|
||||
return slow;
|
||||
case 2:
|
||||
return medium;
|
||||
case 3:
|
||||
return fast;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final int rate;
|
||||
|
||||
@override
|
||||
String toString() => '$rate sat/byte';
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:cake_wallet/bitcoin/address_to_output_script.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cake_wallet/entities/transaction_priority.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
@@ -17,7 +19,6 @@ import 'package:cake_wallet/bitcoin/script_hash.dart';
|
||||
import 'package:cake_wallet/bitcoin/utils.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cake_wallet/entities/sync_status.dart';
|
||||
import 'package:cake_wallet/entities/transaction_priority.dart';
|
||||
import 'package:cake_wallet/entities/wallet_info.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
|
||||
@@ -53,6 +54,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
_password = password,
|
||||
_accountIndex = accountIndex,
|
||||
super(walletInfo) {
|
||||
_unspent = [];
|
||||
_scripthashesUpdateSubject = {};
|
||||
}
|
||||
|
||||
@@ -116,18 +118,12 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
walletInfo: walletInfo);
|
||||
}
|
||||
|
||||
static int feeAmountForPriority(TransactionPriority priority) {
|
||||
switch (priority) {
|
||||
case TransactionPriority.slow:
|
||||
return 6000;
|
||||
case TransactionPriority.regular:
|
||||
return 22080;
|
||||
case TransactionPriority.fast:
|
||||
return 24000;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
static int feeAmountForPriority(BitcoinTransactionPriority priority,
|
||||
int inputsCount, int outputsCount) =>
|
||||
priority.rate * estimatedTransactionSize(inputsCount, outputsCount);
|
||||
|
||||
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
|
||||
inputsCount * 146 + outputsCounts * 33 + 8;
|
||||
|
||||
@override
|
||||
final BitcoinTransactionHistory transactionHistory;
|
||||
@@ -136,6 +132,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
final ElectrumClient eclient;
|
||||
final String mnemonic;
|
||||
|
||||
List<BitcoinUnspent> _unspent;
|
||||
|
||||
@override
|
||||
@observable
|
||||
String address;
|
||||
@@ -234,6 +232,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
});
|
||||
_subscribeForUpdates();
|
||||
await _updateBalance();
|
||||
await _updateUnspent();
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
@@ -264,37 +263,25 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
Object credentials) async {
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final inputs = <BitcoinUnspent>[];
|
||||
final fee = feeAmountForPriority(transactionCredentials.priority);
|
||||
final allAmountFee =
|
||||
calculateEstimatedFee(transactionCredentials.priority, null);
|
||||
var fee = 0;
|
||||
final amount = transactionCredentials.amount != null
|
||||
? stringDoubleToBitcoinAmount(transactionCredentials.amount)
|
||||
: balance.confirmed - fee;
|
||||
final totalAmount = amount + fee;
|
||||
: balance.confirmed - allAmountFee;
|
||||
final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin);
|
||||
final changeAddress = address;
|
||||
var leftAmount = totalAmount;
|
||||
var leftAmount = amount;
|
||||
var totalInputAmount = 0;
|
||||
|
||||
if (totalAmount > balance.confirmed) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
if (_unspent.isEmpty) {
|
||||
await _updateUnspent();
|
||||
}
|
||||
|
||||
final unspent = addresses.map((address) => eclient
|
||||
.getListUnspentWithAddress(address.address)
|
||||
.then((unspent) => unspent
|
||||
.map((unspent) => BitcoinUnspent.fromJSON(address, unspent))));
|
||||
|
||||
for (final unptsFutures in unspent) {
|
||||
final utxs = await unptsFutures;
|
||||
|
||||
for (final utx in utxs) {
|
||||
leftAmount = leftAmount - utx.value;
|
||||
totalInputAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
|
||||
if (leftAmount <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (final utx in _unspent) {
|
||||
leftAmount = leftAmount - utx.value;
|
||||
totalInputAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
|
||||
if (leftAmount <= 0) {
|
||||
break;
|
||||
@@ -305,11 +292,19 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
throw BitcoinTransactionNoInputsException();
|
||||
}
|
||||
|
||||
if (amount <= 0 || totalInputAmount < amount) {
|
||||
final totalAmount = amount + fee;
|
||||
fee = transactionCredentials.amount != null
|
||||
? feeAmountForPriority(
|
||||
transactionCredentials.priority, inputs.length, 2)
|
||||
: allAmountFee;
|
||||
|
||||
if (totalAmount > balance.confirmed) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
final changeValue = totalInputAmount - amount - fee;
|
||||
if (amount <= 0 || totalInputAmount < amount) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
txb.setVersion(1);
|
||||
|
||||
@@ -330,6 +325,10 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
txb.addOutput(
|
||||
addressToOutputScript(transactionCredentials.address), amount);
|
||||
|
||||
final estimatedSize = estimatedTransactionSize(inputs.length, 2);
|
||||
final feeAmount = transactionCredentials.priority.rate * estimatedSize;
|
||||
final changeValue = totalInputAmount - amount - feeAmount;
|
||||
|
||||
if (changeValue > 0) {
|
||||
txb.addOutput(changeAddress, changeValue);
|
||||
}
|
||||
@@ -358,8 +357,30 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
});
|
||||
|
||||
@override
|
||||
double calculateEstimatedFee(TransactionPriority priority) =>
|
||||
bitcoinAmountToDouble(amount: feeAmountForPriority(priority));
|
||||
int calculateEstimatedFee(TransactionPriority priority, int amount) {
|
||||
if (priority is BitcoinTransactionPriority) {
|
||||
int inputsCount = 0;
|
||||
|
||||
if (amount != null) {
|
||||
int totalValue = 0;
|
||||
|
||||
for (final input in _unspent) {
|
||||
if (totalValue >= amount) {
|
||||
break;
|
||||
}
|
||||
|
||||
totalValue += input.value;
|
||||
inputsCount += 1;
|
||||
}
|
||||
} else {
|
||||
inputsCount = _unspent.length;
|
||||
}
|
||||
|
||||
return feeAmountForPriority(priority, inputsCount, 2);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
@@ -380,13 +401,26 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||
await eclient.close();
|
||||
}
|
||||
|
||||
Future<void> _updateUnspent() async {
|
||||
final unspent = await Future.wait(addresses.map((address) => eclient
|
||||
.getListUnspentWithAddress(address.address)
|
||||
.then((unspent) => unspent
|
||||
.map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))));
|
||||
_unspent = unspent.expand((e) => e).toList();
|
||||
}
|
||||
|
||||
void _subscribeForUpdates() {
|
||||
scriptHashes.forEach((sh) async {
|
||||
await _scripthashesUpdateSubject[sh]?.close();
|
||||
_scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh);
|
||||
_scripthashesUpdateSubject[sh].listen((event) async {
|
||||
await _updateBalance();
|
||||
transactionHistory.updateAsync();
|
||||
try {
|
||||
await _updateBalance();
|
||||
await _updateUnspent();
|
||||
transactionHistory.updateAsync();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+25
-31
@@ -22,8 +22,9 @@ String jsonrpcparams(List<Object> params) {
|
||||
}
|
||||
|
||||
String jsonrpc(
|
||||
{String method, List<Object> params, int id, double version = 2.0}) =>
|
||||
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
|
||||
{String method, List<Object> params, int id, double version = 2.0}) =>
|
||||
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json
|
||||
.encode(params)}}\n';
|
||||
|
||||
class SocketTask {
|
||||
SocketTask({this.completer, this.isSubscription, this.subject});
|
||||
@@ -76,7 +77,7 @@ class ElectrumClient {
|
||||
socket.listen((Uint8List event) {
|
||||
try {
|
||||
final response =
|
||||
json.decode(utf8.decode(event.toList())) as Map<String, Object>;
|
||||
json.decode(utf8.decode(event.toList())) as Map<String, Object>;
|
||||
_handleResponse(response);
|
||||
} on FormatException catch (e) {
|
||||
final msg = e.message.toLowerCase();
|
||||
@@ -92,7 +93,7 @@ class ElectrumClient {
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response =
|
||||
json.decode(unterminatedString) as Map<String, Object>;
|
||||
json.decode(unterminatedString) as Map<String, Object>;
|
||||
_handleResponse(response);
|
||||
unterminatedString = '';
|
||||
}
|
||||
@@ -106,7 +107,7 @@ class ElectrumClient {
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response =
|
||||
json.decode(unterminatedString) as Map<String, Object>;
|
||||
json.decode(unterminatedString) as Map<String, Object>;
|
||||
_handleResponse(response);
|
||||
unterminatedString = null;
|
||||
}
|
||||
@@ -172,7 +173,7 @@ class ElectrumClient {
|
||||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
|
||||
String address) =>
|
||||
String address) =>
|
||||
call(
|
||||
method: 'blockchain.scripthash.listunspent',
|
||||
params: [scriptHash(address)]).then((dynamic result) {
|
||||
@@ -252,7 +253,7 @@ class ElectrumClient {
|
||||
}
|
||||
|
||||
Future<String> broadcastTransaction(
|
||||
{@required String transactionRaw}) async =>
|
||||
{@required String transactionRaw}) async =>
|
||||
call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
|
||||
.then((dynamic result) {
|
||||
if (result is String) {
|
||||
@@ -263,14 +264,14 @@ class ElectrumClient {
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> getMerkle(
|
||||
{@required String hash, @required int height}) async =>
|
||||
{@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>;
|
||||
as Map<String, dynamic>;
|
||||
|
||||
Future<double> estimatefee({@required int p}) =>
|
||||
call(method: 'blockchain.estimatefee', params: [p])
|
||||
@@ -294,10 +295,9 @@ class ElectrumClient {
|
||||
params: [scripthash]);
|
||||
}
|
||||
|
||||
BehaviorSubject<T> subscribe<T>(
|
||||
{@required String id,
|
||||
@required String method,
|
||||
List<Object> params = const []}) {
|
||||
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));
|
||||
@@ -306,38 +306,31 @@ class ElectrumClient {
|
||||
}
|
||||
|
||||
Future<dynamic> call({String method, List<Object> params = const []}) async {
|
||||
await Future<void>.delayed(Duration(milliseconds: 100));
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
_regisryTask(id, completer);
|
||||
socket.write(jsonrpc(method: method, id: _id, params: params));
|
||||
_registryTask(id, completer);
|
||||
socket.write(jsonrpc(method: method, id: id, params: params));
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<dynamic> callWithTimeout(
|
||||
{String method,
|
||||
List<Object> params = const [],
|
||||
int timeout = 2000}) async {
|
||||
Future<dynamic> callWithTimeout({String method,
|
||||
List<Object> params = const [],
|
||||
int timeout = 2000}) async {
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
_regisryTask(id, completer);
|
||||
socket.write(jsonrpc(method: method, id: _id, params: params));
|
||||
|
||||
_registryTask(id, completer);
|
||||
socket.write(jsonrpc(method: method, id: id, params: params));
|
||||
Timer(Duration(milliseconds: timeout), () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(RequestFailedTimeoutException(method, _id));
|
||||
completer.completeError(RequestFailedTimeoutException(method, id));
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void request({String method, List<Object> params = const []}) {
|
||||
_id += 1;
|
||||
socket.write(jsonrpc(method: method, id: _id, params: params));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
@@ -346,8 +339,9 @@ class ElectrumClient {
|
||||
onConnectionStatusChange = null;
|
||||
}
|
||||
|
||||
void _regisryTask(int id, Completer completer) => _tasks[id.toString()] =
|
||||
SocketTask(completer: completer, isSubscription: false);
|
||||
void _registryTask(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);
|
||||
|
||||
Reference in New Issue
Block a user