mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
feat: rebase btc-addr-types, migrate to bitcoin_base
This commit is contained in:
parent
d6af37df06
commit
e4703a9ace
29 changed files with 1438 additions and 309 deletions
|
@ -10,21 +10,36 @@ class BitcoinAddressRecord {
|
|||
int balance = 0,
|
||||
String name = '',
|
||||
bool isUsed = false,
|
||||
required this.type,
|
||||
String? scriptHash,
|
||||
required this.network,
|
||||
this.silentPaymentTweak,
|
||||
}) : _txCount = txCount,
|
||||
_balance = balance,
|
||||
_name = name,
|
||||
_isUsed = isUsed;
|
||||
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
return BitcoinAddressRecord(decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
name: decoded['name'] as String? ?? '',
|
||||
balance: decoded['balance'] as int? ?? 0);
|
||||
return BitcoinAddressRecord(
|
||||
decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
name: decoded['name'] as String? ?? '',
|
||||
balance: decoded['balance'] as int? ?? 0,
|
||||
type: decoded['type'] != null && decoded['type'] != ''
|
||||
? BitcoinAddressType.values
|
||||
.firstWhere((type) => type.toString() == decoded['type'] as String)
|
||||
: SegwitAddresType.p2wpkh,
|
||||
scriptHash: decoded['scriptHash'] as String?,
|
||||
network: (decoded['network'] as String?) == null
|
||||
? network
|
||||
: BasedUtxoNetwork.fromName(decoded['network'] as String),
|
||||
silentPaymentTweak: decoded['silentPaymentTweak'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
final String address;
|
||||
|
@ -34,6 +49,9 @@ class BitcoinAddressRecord {
|
|||
int _balance;
|
||||
String _name;
|
||||
bool _isUsed;
|
||||
String? scriptHash;
|
||||
BasedUtxoNetwork? network;
|
||||
final String? silentPaymentTweak;
|
||||
|
||||
int get txCount => _txCount;
|
||||
|
||||
|
@ -66,5 +84,9 @@ class BitcoinAddressRecord {
|
|||
'name': name,
|
||||
'isUsed': isUsed,
|
||||
'balance': balance,
|
||||
'type': type.toString(),
|
||||
'scriptHash': scriptHash,
|
||||
'network': network?.value,
|
||||
'silentPaymentTweak': silentPaymentTweak,
|
||||
});
|
||||
}
|
||||
|
|
46
cw_bitcoin/lib/bitcoin_receive_page_option.dart
Normal file
46
cw_bitcoin/lib/bitcoin_receive_page_option.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
|
||||
class BitcoinReceivePageOption implements ReceivePageOption {
|
||||
static const p2wpkh = BitcoinReceivePageOption._('Segwit (P2WPKH)');
|
||||
static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible (P2SH)');
|
||||
static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)');
|
||||
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
|
||||
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
|
||||
|
||||
static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
|
||||
|
||||
const BitcoinReceivePageOption._(this.value);
|
||||
|
||||
final String value;
|
||||
|
||||
String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static const all = [
|
||||
BitcoinReceivePageOption.p2wpkh,
|
||||
BitcoinReceivePageOption.p2sh,
|
||||
BitcoinReceivePageOption.p2tr,
|
||||
BitcoinReceivePageOption.p2wsh,
|
||||
BitcoinReceivePageOption.p2pkh
|
||||
];
|
||||
|
||||
factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) {
|
||||
switch (type) {
|
||||
case SegwitAddresType.p2tr:
|
||||
return BitcoinReceivePageOption.p2tr;
|
||||
case SegwitAddresType.p2wsh:
|
||||
return BitcoinReceivePageOption.p2wsh;
|
||||
case P2pkhAddressType.p2pkh:
|
||||
return BitcoinReceivePageOption.p2pkh;
|
||||
case P2shAddressType.p2wpkhInP2sh:
|
||||
return BitcoinReceivePageOption.p2sh;
|
||||
case SilentPaymentsAddresType.p2sp:
|
||||
return BitcoinReceivePageOption.silent_payments;
|
||||
case SegwitAddresType.p2wpkh:
|
||||
default:
|
||||
return BitcoinReceivePageOption.p2wpkh;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,38 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_core/unspent_transaction_output.dart';
|
||||
|
||||
class BitcoinUnspent extends Unspent {
|
||||
BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout)
|
||||
BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout,
|
||||
{this.silentPaymentTweak, this.type})
|
||||
: bitcoinAddressRecord = addressRecord,
|
||||
super(addressRecord.address, hash, value, vout, null);
|
||||
|
||||
factory BitcoinUnspent.fromJSON(
|
||||
BitcoinAddressRecord address, Map<String, dynamic> json) =>
|
||||
BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int,
|
||||
json['tx_pos'] as int);
|
||||
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) =>
|
||||
BitcoinUnspent(
|
||||
address,
|
||||
json['tx_hash'] as String,
|
||||
json['value'] as int,
|
||||
json['tx_pos'] as int,
|
||||
silentPaymentTweak: json['silent_payment_tweak'] as String?,
|
||||
type: json['type'] == null
|
||||
? null
|
||||
: BitcoinAddressType.values.firstWhere((e) => e.toString() == json['type']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{
|
||||
'address_record': bitcoinAddressRecord.toJSON(),
|
||||
'tx_hash': hash,
|
||||
'value': value,
|
||||
'tx_pos': vout,
|
||||
'silent_payment_tweak': silentPaymentTweak,
|
||||
'type': type.toString(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
||||
final BitcoinAddressRecord bitcoinAddressRecord;
|
||||
String? silentPaymentTweak;
|
||||
BitcoinAddressType? type;
|
||||
}
|
||||
|
|
|
@ -17,17 +17,22 @@ part 'bitcoin_wallet.g.dart';
|
|||
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
|
||||
|
||||
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
BitcoinWalletBase(
|
||||
{required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(
|
||||
BitcoinWalletBase({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
String? addressPageType,
|
||||
BasedUtxoNetwork? networkParam,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
List<BitcoinAddressRecord>? initialSilentAddresses,
|
||||
int initialSilentAddressIndex = 0,
|
||||
SilentPaymentOwner? silentAddress,
|
||||
}) : super(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
|
@ -38,15 +43,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.btc) {
|
||||
walletAddresses = BitcoinWalletAddresses(
|
||||
walletInfo,
|
||||
electrumClient: electrumClient,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
|
||||
.derivePath("m/0'/1"),
|
||||
networkType: networkType);
|
||||
walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
initialSilentAddresses: initialSilentAddresses,
|
||||
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||
silentAddress: silentAddress,
|
||||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
|
||||
network: networkParam ?? network,
|
||||
);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
|
@ -58,20 +65,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
List<BitcoinAddressRecord>? initialSilentAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
int initialSilentAddressIndex = 0,
|
||||
}) async {
|
||||
return BitcoinWallet(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex);
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialSilentAddresses: initialSilentAddresses,
|
||||
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||
silentAddress: await SilentPaymentOwner.fromMnemonic(mnemonic,
|
||||
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
addressPageType: addressPageType,
|
||||
networkParam: network,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<BitcoinWallet> open({
|
||||
|
@ -82,14 +98,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
}) async {
|
||||
final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password);
|
||||
return BitcoinWallet(
|
||||
mnemonic: snp.mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex);
|
||||
mnemonic: snp.mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialSilentAddresses: snp.silentAddresses,
|
||||
initialSilentAddressIndex: snp.silentAddressIndex,
|
||||
silentAddress: await SilentPaymentOwner.fromMnemonic(snp.mnemonic,
|
||||
hrp: snp.network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex,
|
||||
addressPageType: snp.addressPageType,
|
||||
networkParam: snp.network,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,22 +11,18 @@ part 'bitcoin_wallet_addresses.g.dart';
|
|||
class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses;
|
||||
|
||||
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||
BitcoinWalletAddressesBase(WalletInfo walletInfo,
|
||||
{required bitcoin.HDWallet mainHd,
|
||||
required bitcoin.HDWallet sideHd,
|
||||
required bitcoin.NetworkType networkType,
|
||||
required ElectrumClient electrumClient,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: super(walletInfo,
|
||||
initialAddresses: initialAddresses,
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
mainHd: mainHd,
|
||||
sideHd: sideHd,
|
||||
electrumClient: electrumClient,
|
||||
networkType: networkType);
|
||||
BitcoinWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required super.mainHd,
|
||||
required super.sideHd,
|
||||
required super.network,
|
||||
super.initialAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
super.initialSilentAddresses,
|
||||
super.initialSilentAddressIndex = 0,
|
||||
super.silentAddress,
|
||||
}) : super(walletInfo);
|
||||
|
||||
@override
|
||||
String getAddress({required int index, required bitcoin.HDWallet hd}) =>
|
||||
|
|
|
@ -335,6 +335,27 @@ class ElectrumClient {
|
|||
}
|
||||
}
|
||||
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
|
||||
// example response:
|
||||
// {
|
||||
// "height": 520481,
|
||||
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
|
||||
// }
|
||||
Future<int?> getCurrentBlockChainTip() =>
|
||||
call(method: 'blockchain.headers.subscribe').then((result) {
|
||||
if (result is Map<String, dynamic>) {
|
||||
return result["height"] as int;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
BehaviorSubject<Object>? chainTipUpdate() {
|
||||
_id += 1;
|
||||
return subscribe<Object>(
|
||||
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
|
||||
}
|
||||
|
||||
BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
|
||||
_id += 1;
|
||||
return subscribe<Object>(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
|
@ -20,6 +20,8 @@ class ElectrumTransactionBundle {
|
|||
}
|
||||
|
||||
class ElectrumTransactionInfo extends TransactionInfo {
|
||||
BitcoinUnspent? unspent;
|
||||
|
||||
ElectrumTransactionInfo(this.type,
|
||||
{required String id,
|
||||
required int height,
|
||||
|
@ -28,7 +30,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
required TransactionDirection direction,
|
||||
required bool isPending,
|
||||
required DateTime date,
|
||||
required int confirmations}) {
|
||||
required int confirmations,
|
||||
String? to,
|
||||
this.unspent}) {
|
||||
this.id = id;
|
||||
this.height = height;
|
||||
this.amount = amount;
|
||||
|
@ -37,6 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
this.date = date;
|
||||
this.isPending = isPending;
|
||||
this.confirmations = confirmations;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
factory ElectrumTransactionInfo.fromElectrumVerbose(
|
||||
|
@ -143,52 +148,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
confirmations: bundle.confirmations);
|
||||
}
|
||||
|
||||
factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex,
|
||||
{List<String>? addresses, required int height, int? timestamp, required int confirmations}) {
|
||||
final tx = bitcoin.Transaction.fromHex(hex);
|
||||
var exist = false;
|
||||
var amount = 0;
|
||||
|
||||
if (addresses != null) {
|
||||
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 (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
final date = timestamp != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
|
||||
: DateTime.now();
|
||||
|
||||
return ElectrumTransactionInfo(type,
|
||||
id: tx.getId(),
|
||||
height: height,
|
||||
isPending: false,
|
||||
fee: null,
|
||||
direction: TransactionDirection.incoming,
|
||||
amount: amount,
|
||||
date: date,
|
||||
confirmations: confirmations);
|
||||
}
|
||||
|
||||
factory ElectrumTransactionInfo.fromJson(
|
||||
Map<String, dynamic> data, WalletType type) {
|
||||
return ElectrumTransactionInfo(type,
|
||||
id: data['id'] as String,
|
||||
height: data['height'] as int,
|
||||
amount: data['amount'] as int,
|
||||
fee: data['fee'] as int,
|
||||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
confirmations: data['confirmations'] as int);
|
||||
factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
|
||||
return ElectrumTransactionInfo(
|
||||
type,
|
||||
id: data['id'] as String,
|
||||
height: data['height'] as int,
|
||||
amount: data['amount'] as int,
|
||||
fee: data['fee'] as int,
|
||||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
confirmations: data['confirmations'] as int,
|
||||
to: data['to'] as String?,
|
||||
unspent: data['unspent'] != null
|
||||
? BitcoinUnspent.fromJSON(
|
||||
BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String),
|
||||
data['unspent'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
final WalletType type;
|
||||
|
@ -232,6 +209,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
m['isPending'] = isPending;
|
||||
m['confirmations'] = confirmations;
|
||||
m['fee'] = fee;
|
||||
m['to'] = to;
|
||||
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
|
||||
return m;
|
||||
}
|
||||
|
||||
String toString() {
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspent)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
|
@ -130,22 +131,93 @@ abstract class ElectrumWalletBase
|
|||
List<BitcoinUnspent> unspentCoins;
|
||||
List<int> _feeRates;
|
||||
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
|
||||
BehaviorSubject<Object>? _chainTipUpdateSubject;
|
||||
bool _isTransactionUpdating;
|
||||
// Future<Isolate>? _isolate;
|
||||
|
||||
void Function(FlutterErrorDetails)? _onError;
|
||||
Timer? _autoSaveTimer;
|
||||
static const int _autoSaveInterval = 30;
|
||||
|
||||
Future<void> init() async {
|
||||
await walletAddresses.init();
|
||||
await transactionHistory.init();
|
||||
await save();
|
||||
|
||||
_autoSaveTimer =
|
||||
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
|
||||
}
|
||||
|
||||
// @action
|
||||
// Future<void> _setListeners(int height, {int? chainTip}) async {
|
||||
// final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0;
|
||||
// syncStatus = AttemptingSyncStatus();
|
||||
|
||||
// if (_isolate != null) {
|
||||
// final runningIsolate = await _isolate!;
|
||||
// runningIsolate.kill(priority: Isolate.immediate);
|
||||
// }
|
||||
|
||||
// final receivePort = ReceivePort();
|
||||
// _isolate = Isolate.spawn(
|
||||
// startRefresh,
|
||||
// ScanData(
|
||||
// sendPort: receivePort.sendPort,
|
||||
// primarySilentAddress: walletAddresses.primarySilentAddress!,
|
||||
// networkType: networkType,
|
||||
// height: height,
|
||||
// chainTip: currentChainTip,
|
||||
// electrumClient: ElectrumClient(),
|
||||
// transactionHistoryIds: transactionHistory.transactions.keys.toList(),
|
||||
// node: electrumClient.uri.toString(),
|
||||
// labels: walletAddresses.labels,
|
||||
// ));
|
||||
|
||||
// await for (var message in receivePort) {
|
||||
// if (message is BitcoinUnspent) {
|
||||
// if (!unspentCoins.any((utx) =>
|
||||
// utx.hash.contains(message.hash) &&
|
||||
// utx.vout == message.vout &&
|
||||
// utx.address.contains(message.address))) {
|
||||
// unspentCoins.add(message);
|
||||
|
||||
// if (unspentCoinsInfo.values.any((element) =>
|
||||
// element.walletId.contains(id) &&
|
||||
// element.hash.contains(message.hash) &&
|
||||
// element.address.contains(message.address))) {
|
||||
// _addCoinInfo(message);
|
||||
|
||||
// await walletInfo.save();
|
||||
// await save();
|
||||
// }
|
||||
|
||||
// balance[currency] = await _fetchBalances();
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (message is Map<String, ElectrumTransactionInfo>) {
|
||||
// transactionHistory.addMany(message);
|
||||
// await transactionHistory.save();
|
||||
// }
|
||||
|
||||
// // check if is a SyncStatus type since "is SyncStatus" doesn't work here
|
||||
// if (message is SyncResponse) {
|
||||
// syncStatus = message.syncStatus;
|
||||
// walletInfo.restoreHeight = message.height;
|
||||
// await walletInfo.save();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await walletAddresses.discoverAddresses();
|
||||
await _setInitialHeight();
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
rescan(height: walletInfo.restoreHeight);
|
||||
|
||||
await updateTransactions();
|
||||
_subscribeForUpdates();
|
||||
await updateUnspent();
|
||||
|
@ -175,6 +247,12 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
};
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
|
||||
// final currentChainTip = await electrumClient.getCurrentBlockChainTip();
|
||||
|
||||
// if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
|
||||
// _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
|
||||
// }
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
syncStatus = FailedSyncStatus();
|
||||
|
@ -197,7 +275,47 @@ abstract class ElectrumWalletBase
|
|||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
allInputsAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
leftAmount = leftAmount - utx.value;
|
||||
|
||||
if (utx.bitcoinAddressRecord.silentPaymentTweak != null) {
|
||||
// final d = ECPrivate.fromHex(walletAddresses.primarySilentAddress!.spendPrivkey.toHex())
|
||||
// .tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!)!;
|
||||
|
||||
// inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true));
|
||||
// address = bitcoin.P2trAddress(address: utx.address, networkType: networkType);
|
||||
// keyPairs.add(bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex,
|
||||
// compressed: true, network: networkType));
|
||||
// scriptType = bitcoin.AddressType.p2tr;
|
||||
// script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType)
|
||||
// .scriptPubkey
|
||||
// .toBytes();
|
||||
}
|
||||
|
||||
final address = _addressTypeFromStr(utx.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: utx.bitcoinAddressRecord.index,
|
||||
network: network);
|
||||
|
||||
privateKeys.add(privkey);
|
||||
|
||||
utxos.add(
|
||||
UtxoWithAddress(
|
||||
utxo: BitcoinUtxo(
|
||||
txHash: utx.hash,
|
||||
value: BigInt.from(utx.value),
|
||||
vout: utx.vout,
|
||||
scriptType: _getScriptType(address),
|
||||
),
|
||||
ownerDetails:
|
||||
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
|
||||
),
|
||||
);
|
||||
|
||||
bool amountIsAcquired = !sendAll && leftAmount <= 0;
|
||||
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,20 +462,83 @@ abstract class ElectrumWalletBase
|
|||
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
|
||||
}
|
||||
|
||||
return PendingBitcoinTransaction(txb.build(), type,
|
||||
electrumClient: electrumClient, amount: amount, fee: fee)
|
||||
..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
if (SilentPaymentAddress.regex.hasMatch(outputAddress)) {
|
||||
// final outpointsHash = SilentPayment.hashOutpoints(outpoints);
|
||||
// final generatedOutputs = SilentPayment.generateMultipleRecipientPubkeys(inputPrivKeys,
|
||||
// outpointsHash, SilentPaymentDestination.fromAddress(outputAddress, outputAmount!));
|
||||
|
||||
// generatedOutputs.forEach((recipientSilentAddress, generatedOutput) {
|
||||
// generatedOutput.forEach((output) {
|
||||
// outputs.add(BitcoinOutputDetails(
|
||||
// address: P2trAddress(
|
||||
// program: ECPublic.fromHex(output.$1.toHex()).toTapPoint(),
|
||||
// networkType: networkType),
|
||||
// value: BigInt.from(output.$2),
|
||||
// ));
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
outputAddresses.add(address);
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (out.sendAll || out.formattedCryptoAmount! <= 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
credentialsAmount += outputAmount;
|
||||
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
|
||||
} else {
|
||||
if (!sendAll) {
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
credentialsAmount += outputAmount;
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
|
||||
} else {
|
||||
// The value will be changed after estimating the Tx size and deducting the fee from the total
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final estimatedTx = await _estimateTxFeeAndInputsToUse(
|
||||
credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials);
|
||||
|
||||
final txb = BitcoinTransactionBuilder(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: outputs,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network);
|
||||
|
||||
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
|
||||
final key = estimatedTx.privateKeys
|
||||
.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
|
||||
|
||||
if (key == null) {
|
||||
throw Exception("Cannot find private key");
|
||||
}
|
||||
|
||||
if (utxo.utxo.isP2tr()) {
|
||||
return key.signTapRoot(txDigest, sighash: sighash);
|
||||
} else {
|
||||
return key.signInput(txDigest, sigHash: sighash);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': mnemonic,
|
||||
'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
|
||||
'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
|
||||
'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
|
||||
'balance': balance[currency]?.toJSON()
|
||||
'account_index': walletAddresses.currentReceiveAddressIndexByType,
|
||||
'change_address_index': walletAddresses.currentChangeAddressIndexByType,
|
||||
'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
|
||||
'address_page_type': walletInfo.addressPageType == null
|
||||
? SegwitAddresType.p2wpkh.toString()
|
||||
: walletInfo.addressPageType.toString(),
|
||||
'balance': balance[currency]?.toJSON(),
|
||||
'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(),
|
||||
'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(),
|
||||
'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
|
||||
});
|
||||
|
||||
int feeRate(TransactionPriority priority) {
|
||||
|
@ -461,21 +642,38 @@ abstract class ElectrumWalletBase
|
|||
generateKeyPair(hd: hd, index: index, network: networkType);
|
||||
|
||||
@override
|
||||
Future<void> rescan({required int height}) async => throw UnimplementedError();
|
||||
Future<void> rescan({required int height, int? chainTip, ScanData? scanData}) async {
|
||||
// _setListeners(height);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
try {
|
||||
await electrumClient.close();
|
||||
} catch (_) {}
|
||||
_autoSaveTimer?.cancel();
|
||||
}
|
||||
|
||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
Future<void> updateUnspent() async {
|
||||
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
|
||||
.getListUnspentWithAddress(address.address, networkType)
|
||||
.then((unspent) => unspent.map((unspent) {
|
||||
// Update unspents stored from scanned silent payment transactions
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.unspent != null) {
|
||||
if (!unspentCoins
|
||||
.any((utx) => utx.hash.contains(tx.unspent!.hash) && utx.vout == tx.unspent!.vout)) {
|
||||
unspentCoins.add(tx.unspent!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||
|
||||
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
||||
|
||||
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
|
||||
.getListUnspentWithAddress(address.address, network)
|
||||
.then((unspent) => Future.forEach<Map<String, dynamic>>(unspent, (unspent) async {
|
||||
try {
|
||||
return BitcoinUnspent.fromJSON(address, unspent);
|
||||
} catch (_) {
|
||||
|
@ -495,8 +693,10 @@ abstract class ElectrumWalletBase
|
|||
|
||||
if (unspentCoins.isNotEmpty) {
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values
|
||||
.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
|
||||
final coinInfoList = unspentCoinsInfo.values.where((element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.hash.contains(coin.hash) &&
|
||||
element.address.contains(coin.address));
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
|
@ -513,6 +713,7 @@ abstract class ElectrumWalletBase
|
|||
await _refreshUnspentCoinsInfo();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
|
||||
final newInfo = UnspentCoinsInfo(
|
||||
walletId: id,
|
||||
|
@ -569,7 +770,22 @@ abstract class ElectrumWalletBase
|
|||
ins.add(tx);
|
||||
}
|
||||
|
||||
return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations);
|
||||
final original = BtcTransaction.fromRaw(transactionHex);
|
||||
final ins = <BtcTransaction>[];
|
||||
|
||||
for (final vin in original.inputs) {
|
||||
try {
|
||||
final id = HEX.encode(HEX.decode(vin.txId).reversed.toList());
|
||||
final txHex = await electrumClient.getTransactionHex(hash: id);
|
||||
final tx = BtcTransaction.fromRaw(txHex);
|
||||
ins.add(tx);
|
||||
} catch (_) {
|
||||
ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId)));
|
||||
}
|
||||
}
|
||||
|
||||
return ElectrumTransactionBundle(original,
|
||||
ins: ins, time: time, confirmations: confirmations, height: height);
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
|
||||
|
@ -666,7 +882,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
void _subscribeForUpdates() {
|
||||
void _subscribeForUpdates() async {
|
||||
scriptHashes.forEach((sh) async {
|
||||
await _scripthashesUpdateSubject[sh]?.close();
|
||||
_scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh);
|
||||
|
@ -685,6 +901,23 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
await _chainTipUpdateSubject?.close();
|
||||
_chainTipUpdateSubject = electrumClient.chainTipUpdate();
|
||||
_chainTipUpdateSubject?.listen((_) async {
|
||||
try {
|
||||
final currentHeight = await electrumClient.getCurrentBlockChainTip();
|
||||
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
|
||||
// _setListeners(walletInfo.restoreHeight, chainTip: currentHeight);
|
||||
} catch (e, s) {
|
||||
print(e.toString());
|
||||
_onError?.call(FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: s,
|
||||
library: this.runtimeType.toString(),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<ElectrumBalance> _fetchBalances() async {
|
||||
|
@ -698,20 +931,25 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
var totalFrozen = 0;
|
||||
var totalConfirmed = 0;
|
||||
var totalUnconfirmed = 0;
|
||||
|
||||
// Add values from unspent coins that are not fetched by the address list
|
||||
// i.e. scanned silent payments
|
||||
unspentCoinsInfo.values.forEach((info) {
|
||||
unspentCoins.forEach((element) {
|
||||
if (element.hash == info.hash &&
|
||||
info.isFrozen &&
|
||||
element.bitcoinAddressRecord.address == info.address &&
|
||||
element.value == info.value) {
|
||||
totalFrozen += element.value;
|
||||
if (info.isFrozen) totalFrozen += element.value;
|
||||
if (element.bitcoinAddressRecord.silentPaymentTweak != null) {
|
||||
totalConfirmed += element.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
final balances = await Future.wait(balanceFutures);
|
||||
var totalConfirmed = 0;
|
||||
var totalUnconfirmed = 0;
|
||||
|
||||
for (var i = 0; i < balances.length; i++) {
|
||||
final addressRecord = addresses[i];
|
||||
|
@ -758,4 +996,426 @@ abstract class ElectrumWalletBase
|
|||
final HD = index == null ? hd : hd.derive(index);
|
||||
return base64Encode(HD.signMessage(message));
|
||||
}
|
||||
|
||||
Future<void> _setInitialHeight() async {
|
||||
if (walletInfo.isRecovery) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (walletInfo.restoreHeight == 0) {
|
||||
final currentHeight = await electrumClient.getCurrentBlockChainTip();
|
||||
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScanData {
|
||||
final SendPort sendPort;
|
||||
final SilentPaymentReceiver primarySilentAddress;
|
||||
final int height;
|
||||
final String node;
|
||||
final bitcoin.NetworkType networkType;
|
||||
final int chainTip;
|
||||
final ElectrumClient electrumClient;
|
||||
final List<String> transactionHistoryIds;
|
||||
final Map<String, String> labels;
|
||||
|
||||
ScanData({
|
||||
required this.sendPort,
|
||||
required this.primarySilentAddress,
|
||||
required this.height,
|
||||
required this.node,
|
||||
required this.networkType,
|
||||
required this.chainTip,
|
||||
required this.electrumClient,
|
||||
required this.transactionHistoryIds,
|
||||
required this.labels,
|
||||
});
|
||||
|
||||
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
|
||||
return ScanData(
|
||||
sendPort: scanData.sendPort,
|
||||
primarySilentAddress: scanData.primarySilentAddress,
|
||||
height: newHeight,
|
||||
node: scanData.node,
|
||||
networkType: scanData.networkType,
|
||||
chainTip: scanData.chainTip,
|
||||
transactionHistoryIds: scanData.transactionHistoryIds,
|
||||
electrumClient: scanData.electrumClient,
|
||||
labels: scanData.labels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResponse {
|
||||
final int height;
|
||||
final SyncStatus syncStatus;
|
||||
|
||||
SyncResponse(this.height, this.syncStatus);
|
||||
}
|
||||
|
||||
// Future<void> startRefresh(ScanData scanData) async {
|
||||
// var cachedBlockchainHeight = scanData.chainTip;
|
||||
|
||||
// Future<int> getNodeHeightOrUpdate(int baseHeight) async {
|
||||
// if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) {
|
||||
// final electrumClient = scanData.electrumClient;
|
||||
// if (!electrumClient.isConnected) {
|
||||
// final node = scanData.node;
|
||||
// await electrumClient.connectToUri(Uri.parse(node));
|
||||
// }
|
||||
|
||||
// cachedBlockchainHeight =
|
||||
// await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight;
|
||||
// }
|
||||
|
||||
// return cachedBlockchainHeight;
|
||||
// }
|
||||
|
||||
// var lastKnownBlockHeight = 0;
|
||||
// var initialSyncHeight = 0;
|
||||
|
||||
// var syncHeight = scanData.height;
|
||||
// var currentChainTip = scanData.chainTip;
|
||||
|
||||
// if (syncHeight <= 0) {
|
||||
// syncHeight = currentChainTip;
|
||||
// }
|
||||
|
||||
// if (initialSyncHeight <= 0) {
|
||||
// initialSyncHeight = syncHeight;
|
||||
// }
|
||||
|
||||
// if (lastKnownBlockHeight == syncHeight) {
|
||||
// scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus()));
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Run this until no more blocks left to scan txs. At first this was recursive
|
||||
// // i.e. re-calling the startRefresh function but this was easier for the above values to retain
|
||||
// // their initial values
|
||||
// while (true) {
|
||||
// lastKnownBlockHeight = syncHeight;
|
||||
|
||||
// final syncingStatus =
|
||||
// SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight);
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
|
||||
|
||||
// if (syncingStatus.blocksLeft <= 0) {
|
||||
// scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus()));
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // print(["Scanning from height:", syncHeight]);
|
||||
|
||||
// try {
|
||||
// final networkPath =
|
||||
// scanData.networkType.network == bitcoin.BtcNetwork.mainnet ? "" : "/testnet";
|
||||
|
||||
// // This endpoint gets up to 10 latest blocks from the given height
|
||||
// final tenNewestBlocks =
|
||||
// (await http.get(Uri.parse("https://blockstream.info$networkPath/api/blocks/$syncHeight")))
|
||||
// .body;
|
||||
// var decodedBlocks = json.decode(tenNewestBlocks) as List<dynamic>;
|
||||
|
||||
// decodedBlocks.sort((a, b) => (a["height"] as int).compareTo(b["height"] as int));
|
||||
// decodedBlocks =
|
||||
// decodedBlocks.where((element) => (element["height"] as int) >= syncHeight).toList();
|
||||
|
||||
// // for each block, get up to 25 txs
|
||||
// for (var i = 0; i < decodedBlocks.length; i++) {
|
||||
// final blockJson = decodedBlocks[i];
|
||||
// final blockHash = blockJson["id"];
|
||||
// final txCount = blockJson["tx_count"] as int;
|
||||
|
||||
// // print(["Scanning block index:", i, "with tx count:", txCount]);
|
||||
|
||||
// int startIndex = 0;
|
||||
// // go through each tx in block until no more txs are left
|
||||
// while (startIndex < txCount) {
|
||||
// // This endpoint gets up to 25 txs from the given block hash and start index
|
||||
// final twentyFiveTxs = json.decode((await http.get(Uri.parse(
|
||||
// "https://blockstream.info$networkPath/api/block/$blockHash/txs/$startIndex")))
|
||||
// .body) as List<dynamic>;
|
||||
|
||||
// // print(["Scanning txs index:", startIndex]);
|
||||
|
||||
// // For each tx, apply silent payment filtering and do shared secret calculation when applied
|
||||
// for (var i = 0; i < twentyFiveTxs.length; i++) {
|
||||
// try {
|
||||
// final tx = twentyFiveTxs[i];
|
||||
// final txid = tx["txid"] as String;
|
||||
|
||||
// // print(["Scanning tx:", txid]);
|
||||
|
||||
// // TODO: if tx already scanned & stored skip
|
||||
// // if (scanData.transactionHistoryIds.contains(txid)) {
|
||||
// // // already scanned tx, continue to next tx
|
||||
// // pos++;
|
||||
// // continue;
|
||||
// // }
|
||||
|
||||
// List<String> pubkeys = [];
|
||||
// List<bitcoin.Outpoint> outpoints = [];
|
||||
|
||||
// bool skip = false;
|
||||
|
||||
// for (var i = 0; i < (tx["vin"] as List<dynamic>).length; i++) {
|
||||
// final input = tx["vin"][i];
|
||||
// final prevout = input["prevout"];
|
||||
// final scriptPubkeyType = prevout["scriptpubkey_type"];
|
||||
// String? pubkey;
|
||||
|
||||
// if (scriptPubkeyType == "v0_p2wpkh" || scriptPubkeyType == "v1_p2tr") {
|
||||
// final witness = input["witness"];
|
||||
// if (witness == null) {
|
||||
// skip = true;
|
||||
// // print("Skipping, no witness");
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if (witness.length == 2) {
|
||||
// pubkey = witness[1] as String;
|
||||
// } else if (witness.length == 1) {
|
||||
// pubkey = "02" + (prevout["scriptpubkey"] as String).fromHex.sublist(2).hex;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (scriptPubkeyType == "p2pkh") {
|
||||
// pubkey = bitcoin.P2pkhAddress(
|
||||
// scriptSig: bitcoin.Script.fromRaw(hexData: input["scriptsig"] as String))
|
||||
// .pubkey;
|
||||
// }
|
||||
|
||||
// if (pubkey == null) {
|
||||
// skip = true;
|
||||
// // print("Skipping, invalid witness");
|
||||
// break;
|
||||
// }
|
||||
|
||||
// pubkeys.add(pubkey);
|
||||
// outpoints.add(
|
||||
// bitcoin.Outpoint(txid: input["txid"] as String, index: input["vout"] as int));
|
||||
// }
|
||||
|
||||
// if (skip) {
|
||||
// // skipped tx, continue to next tx
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// Map<String, bitcoin.Outpoint> outpointsByP2TRpubkey = {};
|
||||
// for (var i = 0; i < (tx["vout"] as List<dynamic>).length; i++) {
|
||||
// final output = tx["vout"][i];
|
||||
// if (output["scriptpubkey_type"] != "v1_p2tr") {
|
||||
// // print("Skipping, not a v1_p2tr output");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// final script = (output["scriptpubkey"] as String).fromHex;
|
||||
|
||||
// // final alreadySpentOutput = (await electrumClient.getHistory(
|
||||
// // scriptHashFromScript(script, networkType: scanData.networkType)))
|
||||
// // .length >
|
||||
// // 1;
|
||||
|
||||
// // if (alreadySpentOutput) {
|
||||
// // print("Skipping, invalid witness");
|
||||
// // break;
|
||||
// // }
|
||||
|
||||
// final p2tr = bitcoin.P2trAddress(
|
||||
// program: script.sublist(2).hex, networkType: scanData.networkType);
|
||||
// final address = p2tr.address;
|
||||
|
||||
// print(["Verifying taproot address:", address]);
|
||||
|
||||
// outpointsByP2TRpubkey[script.sublist(2).hex] =
|
||||
// bitcoin.Outpoint(txid: txid, index: i, value: output["value"] as int);
|
||||
// }
|
||||
|
||||
// if (pubkeys.isEmpty || outpoints.isEmpty || outpointsByP2TRpubkey.isEmpty) {
|
||||
// // skipped tx, continue to next tx
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
|
||||
|
||||
// final result = bitcoin.scanOutputs(
|
||||
// scanData.primarySilentAddress.scanPrivkey,
|
||||
// scanData.primarySilentAddress.spendPubkey,
|
||||
// bitcoin.getSumInputPubKeys(pubkeys),
|
||||
// outpointHash,
|
||||
// outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(),
|
||||
// labels: scanData.labels,
|
||||
// );
|
||||
|
||||
// if (result.isEmpty) {
|
||||
// // no results tx, continue to next tx
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if (result.length > 1) {
|
||||
// print("MULTIPLE UNSPENT COINS FOUND!");
|
||||
// } else {
|
||||
// print("UNSPENT COIN FOUND!");
|
||||
// }
|
||||
|
||||
// result.forEach((key, value) async {
|
||||
// final outpoint = outpointsByP2TRpubkey[key];
|
||||
|
||||
// if (outpoint == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// final tweak = value[0];
|
||||
// String? label;
|
||||
// if (value.length > 1) label = value[1];
|
||||
|
||||
// final txInfo = ElectrumTransactionInfo(
|
||||
// WalletType.bitcoin,
|
||||
// id: txid,
|
||||
// height: syncHeight,
|
||||
// amount: outpoint.value!,
|
||||
// fee: 0,
|
||||
// direction: TransactionDirection.incoming,
|
||||
// isPending: false,
|
||||
// date: DateTime.fromMillisecondsSinceEpoch((blockJson["timestamp"] as int) * 1000),
|
||||
// confirmations: currentChainTip - syncHeight,
|
||||
// to: bitcoin.SilentPaymentAddress.createLabeledSilentPaymentAddress(
|
||||
// scanData.primarySilentAddress.scanPubkey,
|
||||
// scanData.primarySilentAddress.spendPubkey,
|
||||
// label != null ? label.fromHex : "0".fromHex,
|
||||
// hrp: scanData.primarySilentAddress.hrp,
|
||||
// version: scanData.primarySilentAddress.version)
|
||||
// .toString(),
|
||||
// unspent: null,
|
||||
// );
|
||||
|
||||
// final status = json.decode((await http
|
||||
// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$txid/outspends")))
|
||||
// .body) as List<dynamic>;
|
||||
|
||||
// bool spent = false;
|
||||
// for (final s in status) {
|
||||
// if ((s["spent"] as bool) == true) {
|
||||
// spent = true;
|
||||
|
||||
// scanData.sendPort.send({txid: txInfo});
|
||||
|
||||
// final sentTxId = s["txid"] as String;
|
||||
// final sentTx = json.decode((await http
|
||||
// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$sentTxId")))
|
||||
// .body);
|
||||
|
||||
// int amount = 0;
|
||||
// for (final out in (sentTx["vout"] as List<dynamic>)) {
|
||||
// amount += out["value"] as int;
|
||||
// }
|
||||
|
||||
// final height = s["status"]["block_height"] as int;
|
||||
|
||||
// scanData.sendPort.send({
|
||||
// sentTxId: ElectrumTransactionInfo(
|
||||
// WalletType.bitcoin,
|
||||
// id: sentTxId,
|
||||
// height: height,
|
||||
// amount: amount,
|
||||
// fee: 0,
|
||||
// direction: TransactionDirection.outgoing,
|
||||
// isPending: false,
|
||||
// date: DateTime.fromMillisecondsSinceEpoch(
|
||||
// (s["status"]["block_time"] as int) * 1000),
|
||||
// confirmations: currentChainTip - height,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (spent) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// final unspent = BitcoinUnspent(
|
||||
// BitcoinAddressRecord(
|
||||
// bitcoin.P2trAddress(program: key, networkType: scanData.networkType).address,
|
||||
// index: 0,
|
||||
// isHidden: true,
|
||||
// isUsed: true,
|
||||
// silentAddressLabel: null,
|
||||
// silentPaymentTweak: tweak,
|
||||
// type: bitcoin.AddressType.p2tr,
|
||||
// ),
|
||||
// txid,
|
||||
// outpoint.value!,
|
||||
// outpoint.index,
|
||||
// silentPaymentTweak: tweak,
|
||||
// type: bitcoin.AddressType.p2tr,
|
||||
// );
|
||||
|
||||
// // found utxo for tx, send unspent coin to main isolate
|
||||
// scanData.sendPort.send(unspent);
|
||||
|
||||
// // also send tx data for tx history
|
||||
// txInfo.unspent = unspent;
|
||||
// scanData.sendPort.send({txid: txInfo});
|
||||
// });
|
||||
// } catch (_) {}
|
||||
// }
|
||||
|
||||
// // Finished scanning batch of txs in block, add 25 to start index and continue to next block in loop
|
||||
// startIndex += 25;
|
||||
// }
|
||||
|
||||
// // Finished scanning block, add 1 to height and continue to next block in loop
|
||||
// syncHeight += 1;
|
||||
// currentChainTip = await getNodeHeightOrUpdate(syncHeight);
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight,
|
||||
// SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight)));
|
||||
// }
|
||||
// } catch (e, stacktrace) {
|
||||
// print(stacktrace);
|
||||
// print(e.toString());
|
||||
|
||||
// scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus()));
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
class EstimatedTxResult {
|
||||
EstimatedTxResult(
|
||||
{required this.utxos, required this.privateKeys, required this.fee, required this.amount});
|
||||
|
||||
final List<UtxoWithAddress> utxos;
|
||||
final List<ECPrivate> privateKeys;
|
||||
final int fee;
|
||||
final int amount;
|
||||
}
|
||||
|
||||
BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) {
|
||||
if (P2pkhAddress.regex.hasMatch(address)) {
|
||||
return P2pkhAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2shAddress.regex.hasMatch(address)) {
|
||||
return P2shAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2wshAddress.regex.hasMatch(address)) {
|
||||
return P2wshAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2trAddress.regex.hasMatch(address)) {
|
||||
return P2trAddress.fromAddress(address: address, network: network);
|
||||
} else {
|
||||
return P2wpkhAddress.fromAddress(address: address, network: network);
|
||||
}
|
||||
}
|
||||
|
||||
BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
|
||||
if (type is P2pkhAddress) {
|
||||
return P2pkhAddressType.p2pkh;
|
||||
} else if (type is P2shAddress) {
|
||||
return P2shAddressType.p2wpkhInP2sh;
|
||||
} else if (type is P2wshAddress) {
|
||||
return SegwitAddresType.p2wsh;
|
||||
} else if (type is P2trAddress) {
|
||||
return SegwitAddresType.p2tr;
|
||||
} else {
|
||||
return SegwitAddresType.p2wpkh;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
@ -13,24 +11,39 @@ part 'electrum_wallet_addresses.g.dart';
|
|||
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
||||
|
||||
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
ElectrumWalletAddressesBase(WalletInfo walletInfo,
|
||||
{required this.mainHd,
|
||||
required this.sideHd,
|
||||
required this.electrumClient,
|
||||
required this.networkType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
ElectrumWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required this.mainHd,
|
||||
required this.sideHd,
|
||||
required this.network,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
List<BitcoinAddressRecord>? initialSilentAddresses,
|
||||
int initialSilentAddressIndex = 0,
|
||||
SilentPaymentOwner? silentAddress,
|
||||
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
primarySilentAddress = silentAddress,
|
||||
addressesByReceiveType =
|
||||
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
|
||||
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
||||
.toSet()),
|
||||
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
|
||||
.toSet()),
|
||||
currentReceiveAddressIndex = initialRegularAddressIndex,
|
||||
currentChangeAddressIndex = initialChangeAddressIndex,
|
||||
super(walletInfo);
|
||||
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
|
||||
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
|
||||
_addressPageType = walletInfo.addressPageType != null
|
||||
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
|
||||
: SegwitAddresType.p2wpkh,
|
||||
silentAddresses = ObservableList<BitcoinAddressRecord>.of((initialSilentAddresses ?? [])
|
||||
.where((addressRecord) => addressRecord.silentPaymentTweak != null)
|
||||
.toSet()),
|
||||
currentSilentAddressIndex = initialSilentAddressIndex,
|
||||
super(walletInfo) {
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
static const defaultReceiveAddressesCount = 22;
|
||||
static const defaultChangeAddressesCount = 17;
|
||||
|
@ -43,18 +56,52 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
final ObservableList<BitcoinAddressRecord> addresses;
|
||||
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
||||
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
||||
final ElectrumClient electrumClient;
|
||||
final bitcoin.NetworkType networkType;
|
||||
final ObservableList<BitcoinAddressRecord> silentAddresses;
|
||||
final BasedUtxoNetwork network;
|
||||
final bitcoin.HDWallet mainHd;
|
||||
final bitcoin.HDWallet sideHd;
|
||||
|
||||
final SilentPaymentOwner? primarySilentAddress;
|
||||
|
||||
@observable
|
||||
BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh;
|
||||
|
||||
@computed
|
||||
BitcoinAddressType get addressPageType => _addressPageType;
|
||||
|
||||
@observable
|
||||
String? activeSilentAddress;
|
||||
|
||||
@computed
|
||||
List<BitcoinAddressRecord> get allAddresses => _addresses;
|
||||
|
||||
@override
|
||||
@computed
|
||||
String get address {
|
||||
if (isEnabledAutoGenerateSubaddress) {
|
||||
if (receiveAddresses.isEmpty) {
|
||||
final newAddress = generateNewAddress(hd: mainHd).address;
|
||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress;
|
||||
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||
if (activeSilentAddress != null) {
|
||||
return activeSilentAddress!;
|
||||
}
|
||||
|
||||
return primarySilentAddress!.toString();
|
||||
}
|
||||
|
||||
String receiveAddress;
|
||||
|
||||
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
|
||||
|
||||
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
|
||||
typeMatchingReceiveAddresses.isEmpty) {
|
||||
receiveAddress = generateNewAddress().address;
|
||||
} else {
|
||||
final previousAddressMatchesType =
|
||||
previousAddressRecord != null && previousAddressRecord!.type == addressPageType;
|
||||
|
||||
if (previousAddressMatchesType &&
|
||||
typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) {
|
||||
receiveAddress = previousAddressRecord!.address;
|
||||
} else {
|
||||
receiveAddress = typeMatchingReceiveAddresses.first.address;
|
||||
}
|
||||
final receiveAddress = receiveAddresses.first.address;
|
||||
|
||||
|
@ -78,6 +125,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@override
|
||||
set address(String addr) {
|
||||
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||
activeSilentAddress = addr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (addr.startsWith('bitcoincash:')) {
|
||||
addr = toLegacy(addr);
|
||||
}
|
||||
|
@ -94,6 +146,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
int currentReceiveAddressIndex;
|
||||
int currentChangeAddressIndex;
|
||||
|
||||
int currentSilentAddressIndex;
|
||||
|
||||
@observable
|
||||
BitcoinAddressRecord? previousAddressRecord;
|
||||
|
||||
|
@ -157,8 +211,45 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
return address;
|
||||
}
|
||||
|
||||
BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) {
|
||||
final isHidden = hd == sideHd;
|
||||
Map<String, String> get labels {
|
||||
final labels = <String, String>{};
|
||||
for (int i = 0; i < silentAddresses.length; i++) {
|
||||
final silentAddressRecord = silentAddresses[i];
|
||||
final silentAddress =
|
||||
SilentPaymentDestination.fromAddress(silentAddressRecord.address, 0).spendPubkey.toHex();
|
||||
|
||||
if (silentAddressRecord.silentPaymentTweak != null)
|
||||
labels[silentAddress] = silentAddressRecord.silentPaymentTweak!;
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
BitcoinAddressRecord generateNewAddress({String label = ''}) {
|
||||
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||
currentSilentAddressIndex += 1;
|
||||
|
||||
final tweak = BigInt.from(currentSilentAddressIndex);
|
||||
|
||||
final address = BitcoinAddressRecord(
|
||||
SilentPaymentAddress.createLabeledSilentPaymentAddress(
|
||||
primarySilentAddress!.scanPubkey, primarySilentAddress!.spendPubkey, tweak,
|
||||
hrp: primarySilentAddress!.hrp, version: primarySilentAddress!.version)
|
||||
.toString(),
|
||||
index: currentSilentAddressIndex,
|
||||
isHidden: false,
|
||||
name: label,
|
||||
silentPaymentTweak: tweak.toString(),
|
||||
network: network,
|
||||
type: SilentPaymentsAddresType.p2sp,
|
||||
);
|
||||
|
||||
silentAddresses.add(address);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
final newAddressIndex = addressesByReceiveType.fold(
|
||||
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
|
||||
|
||||
final newAddressIndex = addresses.fold(
|
||||
0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
|
||||
|
|
|
@ -12,9 +12,14 @@ class ElectrumWallletSnapshot {
|
|||
required this.password,
|
||||
required this.mnemonic,
|
||||
required this.addresses,
|
||||
required this.silentAddresses,
|
||||
required this.balance,
|
||||
required this.regularAddressIndex,
|
||||
required this.changeAddressIndex});
|
||||
required this.changeAddressIndex,
|
||||
required this.addressPageType,
|
||||
required this.network,
|
||||
required this.silentAddressIndex,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String password;
|
||||
|
@ -22,29 +27,52 @@ class ElectrumWallletSnapshot {
|
|||
|
||||
String mnemonic;
|
||||
List<BitcoinAddressRecord> addresses;
|
||||
List<BitcoinAddressRecord> silentAddresses;
|
||||
ElectrumBalance balance;
|
||||
int regularAddressIndex;
|
||||
int changeAddressIndex;
|
||||
Map<String, int> regularAddressIndex;
|
||||
Map<String, int> changeAddressIndex;
|
||||
int silentAddressIndex;
|
||||
|
||||
static Future<ElectrumWallletSnapshot> load(String name, WalletType type, String password) async {
|
||||
static Future<ElectrumWalletSnapshot> load(
|
||||
String name, WalletType type, String password, BasedUtxoNetwork? network) async {
|
||||
final path = await pathForWallet(name: name, type: type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final addressesTmp = data['addresses'] as List? ?? <Object>[];
|
||||
final mnemonic = data['mnemonic'] as String;
|
||||
|
||||
final addressesTmp = data['addresses'] as List? ?? <Object>[];
|
||||
final addresses = addressesTmp
|
||||
.whereType<String>()
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr))
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
|
||||
.toList();
|
||||
|
||||
final silentAddressesTmp = data['silent_addresses'] as List? ?? <Object>[];
|
||||
final silentAddresses = silentAddressesTmp
|
||||
.whereType<String>()
|
||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
|
||||
.toList();
|
||||
|
||||
final balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
|
||||
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
||||
var regularAddressIndex = 0;
|
||||
var changeAddressIndex = 0;
|
||||
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
var silentAddressIndex = 0;
|
||||
|
||||
try {
|
||||
regularAddressIndex = int.parse(data['account_index'] as String? ?? '0');
|
||||
changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0');
|
||||
} catch (_) {}
|
||||
regularAddressIndexByType = {
|
||||
SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0')
|
||||
};
|
||||
changeAddressIndexByType = {
|
||||
SegwitAddresType.p2wpkh.toString():
|
||||
int.parse(data['change_address_index'] as String? ?? '0')
|
||||
};
|
||||
silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0');
|
||||
} catch (_) {
|
||||
try {
|
||||
regularAddressIndexByType = data["account_index"] as Map<String, int>? ?? {};
|
||||
changeAddressIndexByType = data["change_address_index"] as Map<String, int>? ?? {};
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return ElectrumWallletSnapshot(
|
||||
name: name,
|
||||
|
@ -52,8 +80,13 @@ class ElectrumWallletSnapshot {
|
|||
password: password,
|
||||
mnemonic: mnemonic,
|
||||
addresses: addresses,
|
||||
silentAddresses: silentAddresses,
|
||||
balance: balance,
|
||||
regularAddressIndex: regularAddressIndex,
|
||||
changeAddressIndex: changeAddressIndex);
|
||||
regularAddressIndex: regularAddressIndexByType,
|
||||
changeAddressIndex: changeAddressIndexByType,
|
||||
addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(),
|
||||
network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet,
|
||||
silentAddressIndex: silentAddressIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,15 @@ packages:
|
|||
url: "https://github.com/cake-tech/bitbox-flutter.git"
|
||||
source: git
|
||||
version: "1.0.1"
|
||||
bitcoin_base:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v2
|
||||
resolved-ref: e4686da77cace5400697de69f7885020297cb900
|
||||
url: "https://github.com/cake-tech/bitcoin_base.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
bitcoin_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -84,6 +93,15 @@ packages:
|
|||
url: "https://github.com/cake-tech/bitcoin_flutter.git"
|
||||
source: git
|
||||
version: "2.1.0"
|
||||
blockchain_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v1
|
||||
resolved-ref: b4db705d5409aba30730818ed0d3c09aac5b9992
|
||||
url: "https://github.com/rafael-xmr/blockchain_utils"
|
||||
source: git
|
||||
version: "1.6.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -30,6 +30,10 @@ dependencies:
|
|||
rxdart: ^0.27.5
|
||||
unorm_dart: ^0.2.0
|
||||
cryptography: ^2.0.5
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base.git
|
||||
ref: cake-update-v2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -29,7 +29,10 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||
ref: master
|
||||
bitcoin_base: ^3.0.1
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base.git
|
||||
ref: cake-update-v2
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,16 @@ class SyncingSyncStatus extends SyncStatus {
|
|||
|
||||
@override
|
||||
String toString() => '$blocksLeft';
|
||||
|
||||
factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) {
|
||||
final track = chainTip - initialSyncHeight;
|
||||
final diff = track - (chainTip - syncHeight);
|
||||
final ptc = diff <= 0 ? 0.0 : diff / track;
|
||||
final left = chainTip - syncHeight;
|
||||
|
||||
// sum 1 because if at the chain tip, will say "0 blocks left"
|
||||
return SyncingSyncStatus(left + 1, ptc);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncedSyncStatus extends SyncStatus {
|
||||
|
@ -51,4 +61,6 @@ class ConnectedSyncStatus extends SyncStatus {
|
|||
class LostConnectionSyncStatus extends SyncStatus {
|
||||
@override
|
||||
double progress() => 1.0;
|
||||
}
|
||||
@override
|
||||
String toString() => 'Reconnecting';
|
||||
}
|
||||
|
|
|
@ -16,5 +16,6 @@ class Unspent {
|
|||
bool isFrozen;
|
||||
String note;
|
||||
|
||||
bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc');
|
||||
bool get isP2wpkh =>
|
||||
address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc');
|
||||
}
|
||||
|
|
|
@ -142,27 +142,9 @@ Then we need to generate localization files.
|
|||
|
||||
`$ flutter packages pub run tool/generate_localization.dart`
|
||||
|
||||
Lastly, we will generate mobx models for the project.
|
||||
|
||||
Generate mobx models for `cw_core`:
|
||||
|
||||
`cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
|
||||
|
||||
Generate mobx models for `cw_monero`:
|
||||
|
||||
`cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
|
||||
|
||||
Generate mobx models for `cw_bitcoin`:
|
||||
|
||||
`cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
|
||||
|
||||
Generate mobx models for `cw_haven`:
|
||||
|
||||
`cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
|
||||
|
||||
Finally build mobx models for the app:
|
||||
|
||||
`$ flutter packages pub run build_runner build --delete-conflicting-outputs`
|
||||
`$ ./model_generator.sh`
|
||||
|
||||
### 9. Build!
|
||||
|
||||
|
|
|
@ -176,6 +176,173 @@ class CWBitcoin extends Bitcoin {
|
|||
=> BitcoinTransactionPriority.slow;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPrioritySlow()
|
||||
=> LitecoinTransactionPriority.slow;
|
||||
}
|
||||
List<String> getWordList() => wordlist;
|
||||
|
||||
@override
|
||||
Map<String, String> getWalletKeys(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
|
||||
return <String, String>{
|
||||
'wif': keys.wif,
|
||||
'privateKey': keys.privateKey,
|
||||
'publicKey': keys.publicKey
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all;
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeBitcoinTransactionPriority(int raw) =>
|
||||
BitcoinTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeLitecoinTransactionPriority(int raw) =>
|
||||
LitecoinTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
@override
|
||||
int getFeeRate(Object wallet, TransactionPriority priority) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.feeRate(priority);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> generateNewAddress(Object wallet, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAddress(Object wallet, String address, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
bitcoinWallet.walletAddresses.updateAddress(address, label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentials(List<Output> outputs,
|
||||
{required TransactionPriority priority, int? feeRate}) =>
|
||||
BitcoinTransactionCredentials(
|
||||
outputs
|
||||
.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs,
|
||||
{TransactionPriority? priority, required int feeRate}) =>
|
||||
BitcoinTransactionCredentials(outputs,
|
||||
priority: priority != null ? priority as BitcoinTransactionPriority : null,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
List<String> getAddresses(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.addressesByReceiveType
|
||||
.map((BitcoinAddressRecord addr) => addr.address)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@computed
|
||||
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
return electrumWallet.walletAddresses.addressesByReceiveType
|
||||
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
|
||||
id: addr.index,
|
||||
name: addr.name,
|
||||
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
|
||||
txCount: addr.txCount,
|
||||
balance: addr.balance,
|
||||
isChange: addr.isHidden))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
String getAddress(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.address;
|
||||
}
|
||||
|
||||
@override
|
||||
String formatterBitcoinAmountToString({required int amount}) =>
|
||||
bitcoinAmountToString(amount: amount);
|
||||
|
||||
@override
|
||||
double formatterBitcoinAmountToDouble({required int amount}) =>
|
||||
bitcoinAmountToDouble(amount: amount);
|
||||
|
||||
@override
|
||||
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
|
||||
|
||||
@override
|
||||
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) =>
|
||||
(priority as BitcoinTransactionPriority).labelWithRate(rate);
|
||||
|
||||
@override
|
||||
List<BitcoinUnspent> getUnspents(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.unspentCoins;
|
||||
}
|
||||
|
||||
Future<void> updateUnspents(Object wallet) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.updateUnspent();
|
||||
}
|
||||
|
||||
WalletService createBitcoinWalletService(
|
||||
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
|
||||
return BitcoinWalletService(walletInfoSource, unspentCoinSource);
|
||||
}
|
||||
|
||||
WalletService createLitecoinWalletService(
|
||||
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
|
||||
return LitecoinWalletService(walletInfoSource, unspentCoinSource);
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow;
|
||||
|
||||
@override
|
||||
Future<void> setAddressType(Object wallet, dynamic option) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.walletAddresses.setAddressType(option as BitcoinAddressType);
|
||||
}
|
||||
|
||||
@override
|
||||
BitcoinReceivePageOption getSelectedAddressType(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType);
|
||||
}
|
||||
|
||||
@override
|
||||
List<BitcoinReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
|
||||
|
||||
List<BitcoinAddressRecord> getSilentAddresses(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.walletAddresses.silentAddresses;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class AddressValidator extends TextValidator {
|
|||
return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$'
|
||||
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
|
||||
case CryptoCurrency.btc:
|
||||
return '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$|^bc1[0-9a-zA-Z]{59}\$';
|
||||
return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${bitcoin.SilentPaymentAddress.REGEX.pattern}\$';
|
||||
case CryptoCurrency.nano:
|
||||
return '[0-9a-zA-Z_]';
|
||||
case CryptoCurrency.banano:
|
||||
|
@ -263,12 +263,12 @@ class AddressValidator extends TextValidator {
|
|||
'|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
|
||||
case CryptoCurrency.btc:
|
||||
return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)';
|
||||
return '([^0-9a-zA-Z]|^)${P2pkhAddress.regex.pattern}|\$)'
|
||||
'([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)'
|
||||
'([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)'
|
||||
'([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)'
|
||||
'([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)'
|
||||
'|${bitcoin.SilentPaymentAddress.REGEX.pattern}\$';
|
||||
case CryptoCurrency.ltc:
|
||||
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
|
@ -290,4 +290,4 @@ class AddressValidator extends TextValidator {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart';
|
|||
|
||||
String syncStatusTitle(SyncStatus syncStatus) {
|
||||
if (syncStatus is SyncingSyncStatus) {
|
||||
return S.current.Blocks_remaining('${syncStatus.blocksLeft}');
|
||||
return syncStatus.blocksLeft == 1
|
||||
? S.current.Block_remaining('${syncStatus.blocksLeft}')
|
||||
: S.current.Blocks_remaining('${syncStatus.blocksLeft}');
|
||||
}
|
||||
|
||||
if (syncStatus is SyncedSyncStatus) {
|
||||
|
@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) {
|
|||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,6 +228,7 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
|
|||
import 'package:cake_wallet/core/wallet_loading_service.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cake_wallet/entities/qr_view_data.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc;
|
||||
|
||||
import 'buy/dfx/dfx_buy_provider.dart';
|
||||
import 'core/totp_request_details.dart';
|
||||
|
@ -656,6 +657,10 @@ Future<void> setup({
|
|||
|
||||
getIt.registerFactory<MoneroAccountListViewModel>(() {
|
||||
final wallet = getIt.get<AppStore>().wallet!;
|
||||
// if ((wallet.type == WalletType.bitcoin &&
|
||||
// wallet.walletAddresses.addressPageType == btc.AddressType.p2sp) ||
|
||||
// wallet.type == WalletType.monero ||
|
||||
// wallet.type == WalletType.haven) {
|
||||
if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) {
|
||||
return MoneroAccountListViewModel(wallet);
|
||||
}
|
||||
|
|
|
@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
|
||||
class PresentReceiveOptionPicker extends StatelessWidget {
|
||||
PresentReceiveOptionPicker(
|
||||
{required this.receiveOptionViewModel, required this.color});
|
||||
PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color});
|
||||
|
||||
final ReceiveOptionViewModel receiveOptionViewModel;
|
||||
final Color color;
|
||||
|
@ -43,17 +42,11 @@ class PresentReceiveOptionPicker extends StatelessWidget {
|
|||
Text(
|
||||
S.current.receive,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Lato',
|
||||
color: color),
|
||||
fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 10.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color)))
|
||||
builder: (_) => Text(describeOption(receiveOptionViewModel.selectedReceiveOption),
|
||||
style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color)))
|
||||
],
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
|
@ -73,65 +66,68 @@ class PresentReceiveOptionPicker extends StatelessWidget {
|
|||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children:[ AlertBackground(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Spacer(),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||
child: (ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: receiveOptionViewModel.options.length,
|
||||
itemBuilder: (_, index) {
|
||||
final option = receiveOptionViewModel.options[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(popUpContext);
|
||||
children: [
|
||||
AlertBackground(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Spacer(),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||
child: (ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: receiveOptionViewModel.options.length,
|
||||
itemBuilder: (_, index) {
|
||||
final option = receiveOptionViewModel.options[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(popUpContext);
|
||||
|
||||
receiveOptionViewModel.selectReceiveOption(option);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Observer(builder: (_) {
|
||||
final value = receiveOptionViewModel.selectedReceiveOption;
|
||||
receiveOptionViewModel.selectReceiveOption(option);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Observer(builder: (_) {
|
||||
final value = receiveOptionViewModel.selectedReceiveOption;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(option.toString(),
|
||||
textAlign: TextAlign.left,
|
||||
style: textSmall(
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
||||
).copyWith(
|
||||
fontWeight:
|
||||
value == option ? FontWeight.w800 : FontWeight.w500,
|
||||
)),
|
||||
RoundedCheckbox(
|
||||
value: value == option,
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, index) => SizedBox(height: 30),
|
||||
)),
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(describeOption(option),
|
||||
textAlign: TextAlign.left,
|
||||
style: textSmall(
|
||||
color: Theme.of(context)
|
||||
.extension<CakeTextTheme>()!
|
||||
.titleColor,
|
||||
).copyWith(
|
||||
fontWeight:
|
||||
value == option ? FontWeight.w800 : FontWeight.w500,
|
||||
)),
|
||||
RoundedCheckbox(
|
||||
value: value == option,
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, index) => SizedBox(height: 30),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Spacer()
|
||||
],
|
||||
Spacer()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
|
||||
],
|
||||
),
|
||||
|
|
|
@ -67,8 +67,7 @@ class ReceivePage extends BasePage {
|
|||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(BuildContext context, Widget scaffold) =>
|
||||
GradientBackground(scaffold: scaffold);
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) {
|
||||
|
@ -159,7 +158,8 @@ class ReceivePage extends BasePage {
|
|||
trailingIcon: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
|
||||
color:
|
||||
Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -185,11 +185,19 @@ class ReceivePage extends BasePage {
|
|||
final isCurrent =
|
||||
item.address == addressListViewModel.address.address;
|
||||
final backgroundColor = isCurrent
|
||||
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
|
||||
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
|
||||
? Theme.of(context)
|
||||
.extension<ReceivePageTheme>()!
|
||||
.currentTileBackgroundColor
|
||||
: Theme.of(context)
|
||||
.extension<ReceivePageTheme>()!
|
||||
.tilesBackgroundColor;
|
||||
final textColor = isCurrent
|
||||
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
|
||||
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
|
||||
? Theme.of(context)
|
||||
.extension<ReceivePageTheme>()!
|
||||
.currentTileTextColor
|
||||
: Theme.of(context)
|
||||
.extension<ReceivePageTheme>()!
|
||||
.tilesTextColor;
|
||||
|
||||
return AddressCell.fromItem(item,
|
||||
isCurrent: isCurrent,
|
||||
|
@ -211,6 +219,16 @@ class ReceivePage extends BasePage {
|
|||
child: cell,
|
||||
);
|
||||
})),
|
||||
if (!addressListViewModel.hasSilentAddresses)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
child: Text(S.of(context).electrum_address_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color:
|
||||
Theme.of(context).extension<BalancePageTheme>()!.labelTextColor)),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
|
|||
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
@ -164,7 +165,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
|||
},
|
||||
options: [
|
||||
AddressTextFieldOption.paste,
|
||||
AddressTextFieldOption.qrCode,
|
||||
if (DeviceInfo.instance.isMobile) AddressTextFieldOption.qrCode,
|
||||
AddressTextFieldOption.addressBook
|
||||
],
|
||||
buttonColor:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -133,8 +134,7 @@ class AddressTextField extends StatelessWidget {
|
|||
),
|
||||
)),
|
||||
],
|
||||
if (this.options.contains(AddressTextFieldOption.qrCode) &&
|
||||
DeviceInfo.instance.isMobile) ...[
|
||||
if (this.options.contains(AddressTextFieldOption.qrCode)) ...[
|
||||
Container(
|
||||
width: prefixIconWidth,
|
||||
height: prefixIconHeight,
|
||||
|
@ -194,7 +194,7 @@ class AddressTextField extends StatelessWidget {
|
|||
|
||||
Future<void> _presentQRScanner(BuildContext context) async {
|
||||
bool isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) return;
|
||||
final code = await presentQRScanner();
|
||||
if (code.isEmpty) {
|
||||
|
|
|
@ -274,6 +274,11 @@ abstract class DashboardViewModelBase with Store {
|
|||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
|
||||
|
||||
bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven;
|
||||
// bool get hasRescan =>
|
||||
// (wallet.type == WalletType.bitcoin &&
|
||||
// wallet.walletAddresses.addressPageType == bitcoin.AddressType.p2sp) ||
|
||||
// wallet.type == WalletType.monero ||
|
||||
// wallet.type == WalletType.haven;
|
||||
|
||||
final KeyService keyService;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'rescan_view_model.g.dart';
|
||||
|
@ -9,8 +10,8 @@ enum RescanWalletState { rescaning, none }
|
|||
|
||||
abstract class RescanViewModelBase with Store {
|
||||
RescanViewModelBase(this._wallet)
|
||||
: state = RescanWalletState.none,
|
||||
isButtonEnabled = false;
|
||||
: state = RescanWalletState.none,
|
||||
isButtonEnabled = false;
|
||||
|
||||
final WalletBase _wallet;
|
||||
|
||||
|
@ -23,8 +24,8 @@ abstract class RescanViewModelBase with Store {
|
|||
@action
|
||||
Future<void> rescanCurrentWallet({required int restoreHeight}) async {
|
||||
state = RescanWalletState.rescaning;
|
||||
await _wallet.rescan(height: restoreHeight);
|
||||
_wallet.transactionHistory.clear();
|
||||
_wallet.rescan(height: restoreHeight);
|
||||
if (_wallet.type != WalletType.bitcoin) _wallet.transactionHistory.clear();
|
||||
state = RescanWalletState.none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,8 +167,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
|||
}) : _baseItems = <ListItem>[],
|
||||
selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
|
||||
_cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
|
||||
hasAccounts =
|
||||
appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
|
||||
amount = '',
|
||||
_settingsStore = appStore.settingsStore,
|
||||
super(appStore: appStore) {
|
||||
|
@ -180,7 +178,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
|||
_init();
|
||||
|
||||
selectedCurrency = walletTypeToCryptoCurrency(wallet.type);
|
||||
hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.haven;
|
||||
_hasAccounts =
|
||||
hasSilentAddresses || wallet.type == WalletType.monero || wallet.type == WalletType.haven;
|
||||
}
|
||||
|
||||
static const String _cryptoNumberPattern = '0.00000000';
|
||||
|
@ -339,7 +338,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
|||
}
|
||||
|
||||
@observable
|
||||
bool hasAccounts;
|
||||
bool _hasAccounts = false;
|
||||
|
||||
@computed
|
||||
bool get hasAccounts => _hasAccounts;
|
||||
|
||||
@computed
|
||||
String get accountLabel {
|
||||
|
@ -354,8 +356,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
|||
return '';
|
||||
}
|
||||
|
||||
@observable
|
||||
// ignore: prefer_final_fields
|
||||
bool? _hasSilentAddresses = null;
|
||||
|
||||
@computed
|
||||
bool get hasSilentAddresses => _hasSilentAddresses ?? wallet.type == WalletType.bitcoin;
|
||||
// @computed
|
||||
// bool get hasSilentAddresses =>
|
||||
// _hasSilentAddresses ??
|
||||
// wallet.type == WalletType.bitcoin &&
|
||||
// wallet.walletAddresses.addressPageType == btc.AddressType.p2sp;
|
||||
|
||||
@computed
|
||||
bool get hasAddressList =>
|
||||
hasSilentAddresses ||
|
||||
wallet.type == WalletType.monero ||
|
||||
wallet.type == WalletType.haven ||
|
||||
wallet.type == WalletType.bitcoinCash ||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && cd ..
|
||||
cd cw_polygon && flutter pub get && cd ..
|
||||
cd cw_core; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_evm; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_monero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_bitcoin; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_haven; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_nano; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_bitcoin_cash; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
|
||||
cd cw_polygon; flutter pub get; cd ..
|
||||
cd cw_ethereum; flutter pub get; cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
|
|
|
@ -118,6 +118,7 @@ abstract class Bitcoin {
|
|||
|
||||
List<String> getAddresses(Object wallet);
|
||||
String getAddress(Object wallet);
|
||||
List<BitcoinAddressRecord> getSilentAddresses(Object wallet);
|
||||
|
||||
List<ElectrumSubAddress> getSubAddresses(Object wallet);
|
||||
|
||||
|
|
Loading…
Reference in a new issue