mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 19:49: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,
|
int balance = 0,
|
||||||
String name = '',
|
String name = '',
|
||||||
bool isUsed = false,
|
bool isUsed = false,
|
||||||
|
required this.type,
|
||||||
|
String? scriptHash,
|
||||||
|
required this.network,
|
||||||
|
this.silentPaymentTweak,
|
||||||
}) : _txCount = txCount,
|
}) : _txCount = txCount,
|
||||||
_balance = balance,
|
_balance = balance,
|
||||||
_name = name,
|
_name = name,
|
||||||
_isUsed = isUsed;
|
_isUsed = isUsed;
|
||||||
|
|
||||||
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
|
||||||
final decoded = json.decode(jsonSource) as Map;
|
final decoded = json.decode(jsonSource) as Map;
|
||||||
|
|
||||||
return BitcoinAddressRecord(decoded['address'] as String,
|
return BitcoinAddressRecord(
|
||||||
index: decoded['index'] as int,
|
decoded['address'] as String,
|
||||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
index: decoded['index'] as int,
|
||||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||||
txCount: decoded['txCount'] as int? ?? 0,
|
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||||
name: decoded['name'] as String? ?? '',
|
txCount: decoded['txCount'] as int? ?? 0,
|
||||||
balance: decoded['balance'] 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;
|
final String address;
|
||||||
|
@ -34,6 +49,9 @@ class BitcoinAddressRecord {
|
||||||
int _balance;
|
int _balance;
|
||||||
String _name;
|
String _name;
|
||||||
bool _isUsed;
|
bool _isUsed;
|
||||||
|
String? scriptHash;
|
||||||
|
BasedUtxoNetwork? network;
|
||||||
|
final String? silentPaymentTweak;
|
||||||
|
|
||||||
int get txCount => _txCount;
|
int get txCount => _txCount;
|
||||||
|
|
||||||
|
@ -66,5 +84,9 @@ class BitcoinAddressRecord {
|
||||||
'name': name,
|
'name': name,
|
||||||
'isUsed': isUsed,
|
'isUsed': isUsed,
|
||||||
'balance': balance,
|
'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_bitcoin/bitcoin_address_record.dart';
|
||||||
import 'package:cw_core/unspent_transaction_output.dart';
|
import 'package:cw_core/unspent_transaction_output.dart';
|
||||||
|
|
||||||
class BitcoinUnspent extends Unspent {
|
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,
|
: bitcoinAddressRecord = addressRecord,
|
||||||
super(addressRecord.address, hash, value, vout, null);
|
super(addressRecord.address, hash, value, vout, null);
|
||||||
|
|
||||||
factory BitcoinUnspent.fromJSON(
|
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) =>
|
||||||
BitcoinAddressRecord address, Map<String, dynamic> json) =>
|
BitcoinUnspent(
|
||||||
BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int,
|
address,
|
||||||
json['tx_pos'] as int);
|
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;
|
final BitcoinAddressRecord bitcoinAddressRecord;
|
||||||
|
String? silentPaymentTweak;
|
||||||
|
BitcoinAddressType? type;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,17 +17,22 @@ part 'bitcoin_wallet.g.dart';
|
||||||
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
|
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
|
||||||
|
|
||||||
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
BitcoinWalletBase(
|
BitcoinWalletBase({
|
||||||
{required String mnemonic,
|
required String mnemonic,
|
||||||
required String password,
|
required String password,
|
||||||
required WalletInfo walletInfo,
|
required WalletInfo walletInfo,
|
||||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||||
required Uint8List seedBytes,
|
required Uint8List seedBytes,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
String? addressPageType,
|
||||||
ElectrumBalance? initialBalance,
|
BasedUtxoNetwork? networkParam,
|
||||||
int initialRegularAddressIndex = 0,
|
List<BitcoinAddressRecord>? initialAddresses,
|
||||||
int initialChangeAddressIndex = 0})
|
ElectrumBalance? initialBalance,
|
||||||
: super(
|
Map<String, int>? initialRegularAddressIndex,
|
||||||
|
Map<String, int>? initialChangeAddressIndex,
|
||||||
|
List<BitcoinAddressRecord>? initialSilentAddresses,
|
||||||
|
int initialSilentAddressIndex = 0,
|
||||||
|
SilentPaymentOwner? silentAddress,
|
||||||
|
}) : super(
|
||||||
mnemonic: mnemonic,
|
mnemonic: mnemonic,
|
||||||
password: password,
|
password: password,
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
|
@ -38,15 +43,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
seedBytes: seedBytes,
|
seedBytes: seedBytes,
|
||||||
currency: CryptoCurrency.btc) {
|
currency: CryptoCurrency.btc) {
|
||||||
walletAddresses = BitcoinWalletAddresses(
|
walletAddresses = BitcoinWalletAddresses(
|
||||||
walletInfo,
|
walletInfo,
|
||||||
electrumClient: electrumClient,
|
initialAddresses: initialAddresses,
|
||||||
initialAddresses: initialAddresses,
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
initialSilentAddresses: initialSilentAddresses,
|
||||||
mainHd: hd,
|
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
|
silentAddress: silentAddress,
|
||||||
.derivePath("m/0'/1"),
|
mainHd: hd,
|
||||||
networkType: networkType);
|
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
|
||||||
|
network: networkParam ?? network,
|
||||||
|
);
|
||||||
autorun((_) {
|
autorun((_) {
|
||||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||||
});
|
});
|
||||||
|
@ -58,20 +65,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
required WalletInfo walletInfo,
|
required WalletInfo walletInfo,
|
||||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
List<BitcoinAddressRecord>? initialAddresses,
|
||||||
|
List<BitcoinAddressRecord>? initialSilentAddresses,
|
||||||
ElectrumBalance? initialBalance,
|
ElectrumBalance? initialBalance,
|
||||||
int initialRegularAddressIndex = 0,
|
Map<String, int>? initialRegularAddressIndex,
|
||||||
int initialChangeAddressIndex = 0
|
Map<String, int>? initialChangeAddressIndex,
|
||||||
|
int initialSilentAddressIndex = 0,
|
||||||
}) async {
|
}) async {
|
||||||
return BitcoinWallet(
|
return BitcoinWallet(
|
||||||
mnemonic: mnemonic,
|
mnemonic: mnemonic,
|
||||||
password: password,
|
password: password,
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
unspentCoinsInfo: unspentCoinsInfo,
|
unspentCoinsInfo: unspentCoinsInfo,
|
||||||
initialAddresses: initialAddresses,
|
initialAddresses: initialAddresses,
|
||||||
initialBalance: initialBalance,
|
initialSilentAddresses: initialSilentAddresses,
|
||||||
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
silentAddress: await SilentPaymentOwner.fromMnemonic(mnemonic,
|
||||||
initialChangeAddressIndex: initialChangeAddressIndex);
|
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
|
||||||
|
initialBalance: initialBalance,
|
||||||
|
seedBytes: await mnemonicToSeedBytes(mnemonic),
|
||||||
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||||
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||||
|
addressPageType: addressPageType,
|
||||||
|
networkParam: network,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<BitcoinWallet> open({
|
static Future<BitcoinWallet> open({
|
||||||
|
@ -82,14 +98,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
}) async {
|
}) async {
|
||||||
final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password);
|
final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password);
|
||||||
return BitcoinWallet(
|
return BitcoinWallet(
|
||||||
mnemonic: snp.mnemonic,
|
mnemonic: snp.mnemonic,
|
||||||
password: password,
|
password: password,
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
unspentCoinsInfo: unspentCoinsInfo,
|
unspentCoinsInfo: unspentCoinsInfo,
|
||||||
initialAddresses: snp.addresses,
|
initialAddresses: snp.addresses,
|
||||||
initialBalance: snp.balance,
|
initialSilentAddresses: snp.silentAddresses,
|
||||||
seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
|
initialSilentAddressIndex: snp.silentAddressIndex,
|
||||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
silentAddress: await SilentPaymentOwner.fromMnemonic(snp.mnemonic,
|
||||||
initialChangeAddressIndex: snp.changeAddressIndex);
|
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;
|
class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses;
|
||||||
|
|
||||||
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||||
BitcoinWalletAddressesBase(WalletInfo walletInfo,
|
BitcoinWalletAddressesBase(
|
||||||
{required bitcoin.HDWallet mainHd,
|
WalletInfo walletInfo, {
|
||||||
required bitcoin.HDWallet sideHd,
|
required super.mainHd,
|
||||||
required bitcoin.NetworkType networkType,
|
required super.sideHd,
|
||||||
required ElectrumClient electrumClient,
|
required super.network,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
super.initialAddresses,
|
||||||
int initialRegularAddressIndex = 0,
|
super.initialRegularAddressIndex,
|
||||||
int initialChangeAddressIndex = 0})
|
super.initialChangeAddressIndex,
|
||||||
: super(walletInfo,
|
super.initialSilentAddresses,
|
||||||
initialAddresses: initialAddresses,
|
super.initialSilentAddressIndex = 0,
|
||||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
super.silentAddress,
|
||||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
}) : super(walletInfo);
|
||||||
mainHd: mainHd,
|
|
||||||
sideHd: sideHd,
|
|
||||||
electrumClient: electrumClient,
|
|
||||||
networkType: networkType);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAddress({required int index, required bitcoin.HDWallet hd}) =>
|
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) {
|
BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
|
||||||
_id += 1;
|
_id += 1;
|
||||||
return subscribe<Object>(
|
return subscribe<Object>(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
|
|
||||||
import 'package:cw_bitcoin/address_from_output.dart';
|
import 'package:cw_bitcoin/address_from_output.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_amount_format.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_direction.dart';
|
||||||
import 'package:cw_core/transaction_info.dart';
|
import 'package:cw_core/transaction_info.dart';
|
||||||
import 'package:cw_core/format_amount.dart';
|
import 'package:cw_core/format_amount.dart';
|
||||||
|
@ -20,6 +20,8 @@ class ElectrumTransactionBundle {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ElectrumTransactionInfo extends TransactionInfo {
|
class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
|
BitcoinUnspent? unspent;
|
||||||
|
|
||||||
ElectrumTransactionInfo(this.type,
|
ElectrumTransactionInfo(this.type,
|
||||||
{required String id,
|
{required String id,
|
||||||
required int height,
|
required int height,
|
||||||
|
@ -28,7 +30,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
required TransactionDirection direction,
|
required TransactionDirection direction,
|
||||||
required bool isPending,
|
required bool isPending,
|
||||||
required DateTime date,
|
required DateTime date,
|
||||||
required int confirmations}) {
|
required int confirmations,
|
||||||
|
String? to,
|
||||||
|
this.unspent}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
|
@ -37,6 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.isPending = isPending;
|
this.isPending = isPending;
|
||||||
this.confirmations = confirmations;
|
this.confirmations = confirmations;
|
||||||
|
this.to = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ElectrumTransactionInfo.fromElectrumVerbose(
|
factory ElectrumTransactionInfo.fromElectrumVerbose(
|
||||||
|
@ -143,52 +148,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
confirmations: bundle.confirmations);
|
confirmations: bundle.confirmations);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex,
|
factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
|
||||||
{List<String>? addresses, required int height, int? timestamp, required int confirmations}) {
|
return ElectrumTransactionInfo(
|
||||||
final tx = bitcoin.Transaction.fromHex(hex);
|
type,
|
||||||
var exist = false;
|
id: data['id'] as String,
|
||||||
var amount = 0;
|
height: data['height'] as int,
|
||||||
|
amount: data['amount'] as int,
|
||||||
if (addresses != null) {
|
fee: data['fee'] as int,
|
||||||
tx.outs.forEach((out) {
|
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||||
try {
|
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||||
final p2pkh = bitcoin.P2PKH(
|
isPending: data['isPending'] as bool,
|
||||||
data: PaymentData(output: out.script), network: bitcoin.bitcoin);
|
confirmations: data['confirmations'] as int,
|
||||||
exist = addresses.contains(p2pkh.data.address);
|
to: data['to'] as String?,
|
||||||
|
unspent: data['unspent'] != null
|
||||||
if (exist) {
|
? BitcoinUnspent.fromJSON(
|
||||||
amount += out.value!;
|
BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String),
|
||||||
}
|
data['unspent'] as Map<String, dynamic>)
|
||||||
} catch (_) {}
|
: null,
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final WalletType type;
|
final WalletType type;
|
||||||
|
@ -232,6 +209,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
m['isPending'] = isPending;
|
m['isPending'] = isPending;
|
||||||
m['confirmations'] = confirmations;
|
m['confirmations'] = confirmations;
|
||||||
m['fee'] = fee;
|
m['fee'] = fee;
|
||||||
|
m['to'] = to;
|
||||||
|
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
|
||||||
return m;
|
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:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||||
|
@ -130,22 +131,93 @@ abstract class ElectrumWalletBase
|
||||||
List<BitcoinUnspent> unspentCoins;
|
List<BitcoinUnspent> unspentCoins;
|
||||||
List<int> _feeRates;
|
List<int> _feeRates;
|
||||||
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
|
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
|
||||||
|
BehaviorSubject<Object>? _chainTipUpdateSubject;
|
||||||
bool _isTransactionUpdating;
|
bool _isTransactionUpdating;
|
||||||
|
// Future<Isolate>? _isolate;
|
||||||
|
|
||||||
void Function(FlutterErrorDetails)? _onError;
|
void Function(FlutterErrorDetails)? _onError;
|
||||||
|
Timer? _autoSaveTimer;
|
||||||
|
static const int _autoSaveInterval = 30;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await walletAddresses.init();
|
await walletAddresses.init();
|
||||||
await transactionHistory.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
|
@action
|
||||||
@override
|
@override
|
||||||
Future<void> startSync() async {
|
Future<void> startSync() async {
|
||||||
try {
|
try {
|
||||||
syncStatus = AttemptingSyncStatus();
|
await _setInitialHeight();
|
||||||
await walletAddresses.discoverAddresses();
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rescan(height: walletInfo.restoreHeight);
|
||||||
|
|
||||||
await updateTransactions();
|
await updateTransactions();
|
||||||
_subscribeForUpdates();
|
_subscribeForUpdates();
|
||||||
await updateUnspent();
|
await updateUnspent();
|
||||||
|
@ -175,6 +247,12 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
syncStatus = ConnectedSyncStatus();
|
syncStatus = ConnectedSyncStatus();
|
||||||
|
|
||||||
|
// final currentChainTip = await electrumClient.getCurrentBlockChainTip();
|
||||||
|
|
||||||
|
// if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
|
||||||
|
// _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
|
||||||
|
// }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
syncStatus = FailedSyncStatus();
|
syncStatus = FailedSyncStatus();
|
||||||
|
@ -197,7 +275,47 @@ abstract class ElectrumWalletBase
|
||||||
for (final utx in unspentCoins) {
|
for (final utx in unspentCoins) {
|
||||||
if (utx.isSending) {
|
if (utx.isSending) {
|
||||||
allInputsAmount += utx.value;
|
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);
|
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PendingBitcoinTransaction(txb.build(), type,
|
if (SilentPaymentAddress.regex.hasMatch(outputAddress)) {
|
||||||
electrumClient: electrumClient, amount: amount, fee: fee)
|
// final outpointsHash = SilentPayment.hashOutpoints(outpoints);
|
||||||
..addListener((transaction) async {
|
// final generatedOutputs = SilentPayment.generateMultipleRecipientPubkeys(inputPrivKeys,
|
||||||
transactionHistory.addOne(transaction);
|
// outpointsHash, SilentPaymentDestination.fromAddress(outputAddress, outputAmount!));
|
||||||
await updateBalance();
|
|
||||||
|
// 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({
|
String toJSON() => json.encode({
|
||||||
'mnemonic': mnemonic,
|
'mnemonic': mnemonic,
|
||||||
'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
|
'account_index': walletAddresses.currentReceiveAddressIndexByType,
|
||||||
'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
|
'change_address_index': walletAddresses.currentChangeAddressIndexByType,
|
||||||
'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
|
'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
|
||||||
'balance': balance[currency]?.toJSON()
|
'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) {
|
int feeRate(TransactionPriority priority) {
|
||||||
|
@ -461,21 +642,38 @@ abstract class ElectrumWalletBase
|
||||||
generateKeyPair(hd: hd, index: index, network: networkType);
|
generateKeyPair(hd: hd, index: index, network: networkType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> rescan({required int height}) async => throw UnimplementedError();
|
Future<void> rescan({required int height, int? chainTip, ScanData? scanData}) async {
|
||||||
|
// _setListeners(height);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
try {
|
try {
|
||||||
await electrumClient.close();
|
await electrumClient.close();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
_autoSaveTimer?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||||
|
|
||||||
Future<void> updateUnspent() async {
|
Future<void> updateUnspent() async {
|
||||||
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
|
// Update unspents stored from scanned silent payment transactions
|
||||||
.getListUnspentWithAddress(address.address, networkType)
|
transactionHistory.transactions.values.forEach((tx) {
|
||||||
.then((unspent) => unspent.map((unspent) {
|
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 {
|
try {
|
||||||
return BitcoinUnspent.fromJSON(address, unspent);
|
return BitcoinUnspent.fromJSON(address, unspent);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -495,8 +693,10 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
if (unspentCoins.isNotEmpty) {
|
if (unspentCoins.isNotEmpty) {
|
||||||
unspentCoins.forEach((coin) {
|
unspentCoins.forEach((coin) {
|
||||||
final coinInfoList = unspentCoinsInfo.values
|
final coinInfoList = unspentCoinsInfo.values.where((element) =>
|
||||||
.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
|
element.walletId.contains(id) &&
|
||||||
|
element.hash.contains(coin.hash) &&
|
||||||
|
element.address.contains(coin.address));
|
||||||
|
|
||||||
if (coinInfoList.isNotEmpty) {
|
if (coinInfoList.isNotEmpty) {
|
||||||
final coinInfo = coinInfoList.first;
|
final coinInfo = coinInfoList.first;
|
||||||
|
@ -513,6 +713,7 @@ abstract class ElectrumWalletBase
|
||||||
await _refreshUnspentCoinsInfo();
|
await _refreshUnspentCoinsInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
|
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
|
||||||
final newInfo = UnspentCoinsInfo(
|
final newInfo = UnspentCoinsInfo(
|
||||||
walletId: id,
|
walletId: id,
|
||||||
|
@ -569,7 +770,22 @@ abstract class ElectrumWalletBase
|
||||||
ins.add(tx);
|
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(
|
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
|
||||||
|
@ -666,7 +882,7 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _subscribeForUpdates() {
|
void _subscribeForUpdates() async {
|
||||||
scriptHashes.forEach((sh) async {
|
scriptHashes.forEach((sh) async {
|
||||||
await _scripthashesUpdateSubject[sh]?.close();
|
await _scripthashesUpdateSubject[sh]?.close();
|
||||||
_scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh);
|
_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 {
|
Future<ElectrumBalance> _fetchBalances() async {
|
||||||
|
@ -698,20 +931,25 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalFrozen = 0;
|
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) {
|
unspentCoinsInfo.values.forEach((info) {
|
||||||
unspentCoins.forEach((element) {
|
unspentCoins.forEach((element) {
|
||||||
if (element.hash == info.hash &&
|
if (element.hash == info.hash &&
|
||||||
info.isFrozen &&
|
|
||||||
element.bitcoinAddressRecord.address == info.address &&
|
element.bitcoinAddressRecord.address == info.address &&
|
||||||
element.value == info.value) {
|
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);
|
final balances = await Future.wait(balanceFutures);
|
||||||
var totalConfirmed = 0;
|
|
||||||
var totalUnconfirmed = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < balances.length; i++) {
|
for (var i = 0; i < balances.length; i++) {
|
||||||
final addressRecord = addresses[i];
|
final addressRecord = addresses[i];
|
||||||
|
@ -758,4 +996,426 @@ abstract class ElectrumWalletBase
|
||||||
final HD = index == null ? hd : hd.derive(index);
|
final HD = index == null ? hd : hd.derive(index);
|
||||||
return base64Encode(HD.signMessage(message));
|
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:bitbox/bitbox.dart' as bitbox;
|
||||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
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_addresses.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
@ -13,24 +11,39 @@ part 'electrum_wallet_addresses.g.dart';
|
||||||
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
||||||
|
|
||||||
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
ElectrumWalletAddressesBase(WalletInfo walletInfo,
|
ElectrumWalletAddressesBase(
|
||||||
{required this.mainHd,
|
WalletInfo walletInfo, {
|
||||||
required this.sideHd,
|
required this.mainHd,
|
||||||
required this.electrumClient,
|
required this.sideHd,
|
||||||
required this.networkType,
|
required this.network,
|
||||||
List<BitcoinAddressRecord>? initialAddresses,
|
List<BitcoinAddressRecord>? initialAddresses,
|
||||||
int initialRegularAddressIndex = 0,
|
Map<String, int>? initialRegularAddressIndex,
|
||||||
int initialChangeAddressIndex = 0})
|
Map<String, int>? initialChangeAddressIndex,
|
||||||
: addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
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 ?? [])
|
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||||
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
||||||
.toSet()),
|
.toSet()),
|
||||||
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||||
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
|
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
|
||||||
.toSet()),
|
.toSet()),
|
||||||
currentReceiveAddressIndex = initialRegularAddressIndex,
|
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
|
||||||
currentChangeAddressIndex = initialChangeAddressIndex,
|
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
|
||||||
super(walletInfo);
|
_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 defaultReceiveAddressesCount = 22;
|
||||||
static const defaultChangeAddressesCount = 17;
|
static const defaultChangeAddressesCount = 17;
|
||||||
|
@ -43,18 +56,52 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
final ObservableList<BitcoinAddressRecord> addresses;
|
final ObservableList<BitcoinAddressRecord> addresses;
|
||||||
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
||||||
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
||||||
final ElectrumClient electrumClient;
|
final ObservableList<BitcoinAddressRecord> silentAddresses;
|
||||||
final bitcoin.NetworkType networkType;
|
final BasedUtxoNetwork network;
|
||||||
final bitcoin.HDWallet mainHd;
|
final bitcoin.HDWallet mainHd;
|
||||||
final bitcoin.HDWallet sideHd;
|
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
|
@override
|
||||||
@computed
|
@computed
|
||||||
String get address {
|
String get address {
|
||||||
if (isEnabledAutoGenerateSubaddress) {
|
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||||
if (receiveAddresses.isEmpty) {
|
if (activeSilentAddress != null) {
|
||||||
final newAddress = generateNewAddress(hd: mainHd).address;
|
return activeSilentAddress!;
|
||||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress;
|
}
|
||||||
|
|
||||||
|
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;
|
final receiveAddress = receiveAddresses.first.address;
|
||||||
|
|
||||||
|
@ -78,6 +125,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
set address(String addr) {
|
set address(String addr) {
|
||||||
|
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||||
|
activeSilentAddress = addr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (addr.startsWith('bitcoincash:')) {
|
if (addr.startsWith('bitcoincash:')) {
|
||||||
addr = toLegacy(addr);
|
addr = toLegacy(addr);
|
||||||
}
|
}
|
||||||
|
@ -94,6 +146,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
int currentReceiveAddressIndex;
|
int currentReceiveAddressIndex;
|
||||||
int currentChangeAddressIndex;
|
int currentChangeAddressIndex;
|
||||||
|
|
||||||
|
int currentSilentAddressIndex;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
BitcoinAddressRecord? previousAddressRecord;
|
BitcoinAddressRecord? previousAddressRecord;
|
||||||
|
|
||||||
|
@ -157,8 +211,45 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) {
|
Map<String, String> get labels {
|
||||||
final isHidden = hd == sideHd;
|
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(
|
final newAddressIndex = addresses.fold(
|
||||||
0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
|
0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
|
||||||
|
|
|
@ -12,9 +12,14 @@ class ElectrumWallletSnapshot {
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.mnemonic,
|
required this.mnemonic,
|
||||||
required this.addresses,
|
required this.addresses,
|
||||||
|
required this.silentAddresses,
|
||||||
required this.balance,
|
required this.balance,
|
||||||
required this.regularAddressIndex,
|
required this.regularAddressIndex,
|
||||||
required this.changeAddressIndex});
|
required this.changeAddressIndex,
|
||||||
|
required this.addressPageType,
|
||||||
|
required this.network,
|
||||||
|
required this.silentAddressIndex,
|
||||||
|
});
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String password;
|
final String password;
|
||||||
|
@ -22,29 +27,52 @@ class ElectrumWallletSnapshot {
|
||||||
|
|
||||||
String mnemonic;
|
String mnemonic;
|
||||||
List<BitcoinAddressRecord> addresses;
|
List<BitcoinAddressRecord> addresses;
|
||||||
|
List<BitcoinAddressRecord> silentAddresses;
|
||||||
ElectrumBalance balance;
|
ElectrumBalance balance;
|
||||||
int regularAddressIndex;
|
Map<String, int> regularAddressIndex;
|
||||||
int changeAddressIndex;
|
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 path = await pathForWallet(name: name, type: type);
|
||||||
final jsonSource = await read(path: path, password: password);
|
final jsonSource = await read(path: path, password: password);
|
||||||
final data = json.decode(jsonSource) as Map;
|
final data = json.decode(jsonSource) as Map;
|
||||||
final addressesTmp = data['addresses'] as List? ?? <Object>[];
|
|
||||||
final mnemonic = data['mnemonic'] as String;
|
final mnemonic = data['mnemonic'] as String;
|
||||||
|
|
||||||
|
final addressesTmp = data['addresses'] as List? ?? <Object>[];
|
||||||
final addresses = addressesTmp
|
final addresses = addressesTmp
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.map((addr) => BitcoinAddressRecord.fromJSON(addr))
|
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
|
||||||
.toList();
|
.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) ??
|
final balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
|
||||||
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
||||||
var regularAddressIndex = 0;
|
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||||
var changeAddressIndex = 0;
|
var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||||
|
var silentAddressIndex = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
regularAddressIndex = int.parse(data['account_index'] as String? ?? '0');
|
regularAddressIndexByType = {
|
||||||
changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0');
|
SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0')
|
||||||
} catch (_) {}
|
};
|
||||||
|
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(
|
return ElectrumWallletSnapshot(
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -52,8 +80,13 @@ class ElectrumWallletSnapshot {
|
||||||
password: password,
|
password: password,
|
||||||
mnemonic: mnemonic,
|
mnemonic: mnemonic,
|
||||||
addresses: addresses,
|
addresses: addresses,
|
||||||
|
silentAddresses: silentAddresses,
|
||||||
balance: balance,
|
balance: balance,
|
||||||
regularAddressIndex: regularAddressIndex,
|
regularAddressIndex: regularAddressIndexByType,
|
||||||
changeAddressIndex: changeAddressIndex);
|
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"
|
url: "https://github.com/cake-tech/bitbox-flutter.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.1"
|
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:
|
bitcoin_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -84,6 +93,15 @@ packages:
|
||||||
url: "https://github.com/cake-tech/bitcoin_flutter.git"
|
url: "https://github.com/cake-tech/bitcoin_flutter.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.1.0"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -30,6 +30,10 @@ dependencies:
|
||||||
rxdart: ^0.27.5
|
rxdart: ^0.27.5
|
||||||
unorm_dart: ^0.2.0
|
unorm_dart: ^0.2.0
|
||||||
cryptography: ^2.0.5
|
cryptography: ^2.0.5
|
||||||
|
bitcoin_base:
|
||||||
|
git:
|
||||||
|
url: https://github.com/cake-tech/bitcoin_base.git
|
||||||
|
ref: cake-update-v2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
@ -29,7 +29,10 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||||
ref: master
|
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
|
@override
|
||||||
String toString() => '$blocksLeft';
|
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 {
|
class SyncedSyncStatus extends SyncStatus {
|
||||||
|
@ -51,4 +61,6 @@ class ConnectedSyncStatus extends SyncStatus {
|
||||||
class LostConnectionSyncStatus extends SyncStatus {
|
class LostConnectionSyncStatus extends SyncStatus {
|
||||||
@override
|
@override
|
||||||
double progress() => 1.0;
|
double progress() => 1.0;
|
||||||
}
|
@override
|
||||||
|
String toString() => 'Reconnecting';
|
||||||
|
}
|
||||||
|
|
|
@ -16,5 +16,6 @@ class Unspent {
|
||||||
bool isFrozen;
|
bool isFrozen;
|
||||||
String note;
|
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`
|
`$ 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:
|
Finally build mobx models for the app:
|
||||||
|
|
||||||
`$ flutter packages pub run build_runner build --delete-conflicting-outputs`
|
`$ ./model_generator.sh`
|
||||||
|
|
||||||
### 9. Build!
|
### 9. Build!
|
||||||
|
|
||||||
|
|
|
@ -176,6 +176,173 @@ class CWBitcoin extends Bitcoin {
|
||||||
=> BitcoinTransactionPriority.slow;
|
=> BitcoinTransactionPriority.slow;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TransactionPriority getLitecoinTransactionPrioritySlow()
|
List<String> getWordList() => wordlist;
|
||||||
=> LitecoinTransactionPriority.slow;
|
|
||||||
}
|
@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}\$'
|
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}\$';
|
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
|
||||||
case CryptoCurrency.btc:
|
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:
|
case CryptoCurrency.nano:
|
||||||
return '[0-9a-zA-Z_]';
|
return '[0-9a-zA-Z_]';
|
||||||
case CryptoCurrency.banano:
|
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]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)'
|
||||||
'|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
|
'|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
|
||||||
case CryptoCurrency.btc:
|
case CryptoCurrency.btc:
|
||||||
return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)'
|
return '([^0-9a-zA-Z]|^)${P2pkhAddress.regex.pattern}|\$)'
|
||||||
'|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)'
|
'([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)'
|
||||||
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)'
|
'([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)'
|
||||||
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)'
|
'([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)'
|
||||||
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)'
|
'([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)'
|
||||||
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)';
|
'|${bitcoin.SilentPaymentAddress.REGEX.pattern}\$';
|
||||||
case CryptoCurrency.ltc:
|
case CryptoCurrency.ltc:
|
||||||
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
|
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]|\$)'
|
'|([^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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart';
|
||||||
|
|
||||||
String syncStatusTitle(SyncStatus syncStatus) {
|
String syncStatusTitle(SyncStatus syncStatus) {
|
||||||
if (syncStatus is SyncingSyncStatus) {
|
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) {
|
if (syncStatus is SyncedSyncStatus) {
|
||||||
|
@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
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:cake_wallet/core/wallet_loading_service.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cake_wallet/entities/qr_view_data.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 'buy/dfx/dfx_buy_provider.dart';
|
||||||
import 'core/totp_request_details.dart';
|
import 'core/totp_request_details.dart';
|
||||||
|
@ -656,6 +657,10 @@ Future<void> setup({
|
||||||
|
|
||||||
getIt.registerFactory<MoneroAccountListViewModel>(() {
|
getIt.registerFactory<MoneroAccountListViewModel>(() {
|
||||||
final wallet = getIt.get<AppStore>().wallet!;
|
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) {
|
if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) {
|
||||||
return MoneroAccountListViewModel(wallet);
|
return MoneroAccountListViewModel(wallet);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
|
||||||
class PresentReceiveOptionPicker extends StatelessWidget {
|
class PresentReceiveOptionPicker extends StatelessWidget {
|
||||||
PresentReceiveOptionPicker(
|
PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color});
|
||||||
{required this.receiveOptionViewModel, required this.color});
|
|
||||||
|
|
||||||
final ReceiveOptionViewModel receiveOptionViewModel;
|
final ReceiveOptionViewModel receiveOptionViewModel;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
@ -43,17 +42,11 @@ class PresentReceiveOptionPicker extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
S.current.receive,
|
S.current.receive,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18.0,
|
fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontFamily: 'Lato',
|
|
||||||
color: color),
|
|
||||||
),
|
),
|
||||||
Observer(
|
Observer(
|
||||||
builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(),
|
builder: (_) => Text(describeOption(receiveOptionViewModel.selectedReceiveOption),
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color)))
|
||||||
fontSize: 10.0,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: color)))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 5),
|
SizedBox(width: 5),
|
||||||
|
@ -73,65 +66,68 @@ class PresentReceiveOptionPicker extends StatelessWidget {
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
children:[ AlertBackground(
|
children: [
|
||||||
child: Column(
|
AlertBackground(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Spacer(),
|
children: [
|
||||||
Container(
|
Spacer(),
|
||||||
margin: EdgeInsets.symmetric(horizontal: 24),
|
Container(
|
||||||
decoration: BoxDecoration(
|
margin: EdgeInsets.symmetric(horizontal: 24),
|
||||||
borderRadius: BorderRadius.circular(30),
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.background,
|
borderRadius: BorderRadius.circular(30),
|
||||||
),
|
color: Theme.of(context).colorScheme.background,
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
child: Padding(
|
||||||
child: (ListView.separated(
|
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||||
padding: EdgeInsets.zero,
|
child: (ListView.separated(
|
||||||
shrinkWrap: true,
|
padding: EdgeInsets.zero,
|
||||||
itemCount: receiveOptionViewModel.options.length,
|
shrinkWrap: true,
|
||||||
itemBuilder: (_, index) {
|
itemCount: receiveOptionViewModel.options.length,
|
||||||
final option = receiveOptionViewModel.options[index];
|
itemBuilder: (_, index) {
|
||||||
return InkWell(
|
final option = receiveOptionViewModel.options[index];
|
||||||
onTap: () {
|
return InkWell(
|
||||||
Navigator.pop(popUpContext);
|
onTap: () {
|
||||||
|
Navigator.pop(popUpContext);
|
||||||
|
|
||||||
receiveOptionViewModel.selectReceiveOption(option);
|
receiveOptionViewModel.selectReceiveOption(option);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||||
child: Observer(builder: (_) {
|
child: Observer(builder: (_) {
|
||||||
final value = receiveOptionViewModel.selectedReceiveOption;
|
final value = receiveOptionViewModel.selectedReceiveOption;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(option.toString(),
|
Text(describeOption(option),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: textSmall(
|
style: textSmall(
|
||||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
color: Theme.of(context)
|
||||||
).copyWith(
|
.extension<CakeTextTheme>()!
|
||||||
fontWeight:
|
.titleColor,
|
||||||
value == option ? FontWeight.w800 : FontWeight.w500,
|
).copyWith(
|
||||||
)),
|
fontWeight:
|
||||||
RoundedCheckbox(
|
value == option ? FontWeight.w800 : FontWeight.w500,
|
||||||
value: value == option,
|
)),
|
||||||
)
|
RoundedCheckbox(
|
||||||
],
|
value: value == option,
|
||||||
);
|
)
|
||||||
}),
|
],
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
},
|
),
|
||||||
separatorBuilder: (_, index) => SizedBox(height: 30),
|
);
|
||||||
)),
|
},
|
||||||
|
separatorBuilder: (_, index) => SizedBox(height: 30),
|
||||||
|
)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Spacer()
|
||||||
Spacer()
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
|
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -67,8 +67,7 @@ class ReceivePage extends BasePage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||||
(BuildContext context, Widget scaffold) =>
|
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||||
GradientBackground(scaffold: scaffold);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget trailing(BuildContext context) {
|
Widget trailing(BuildContext context) {
|
||||||
|
@ -159,7 +158,8 @@ class ReceivePage extends BasePage {
|
||||||
trailingIcon: Icon(
|
trailingIcon: Icon(
|
||||||
Icons.arrow_forward_ios,
|
Icons.arrow_forward_ios,
|
||||||
size: 14,
|
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 =
|
final isCurrent =
|
||||||
item.address == addressListViewModel.address.address;
|
item.address == addressListViewModel.address.address;
|
||||||
final backgroundColor = isCurrent
|
final backgroundColor = isCurrent
|
||||||
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
|
? Theme.of(context)
|
||||||
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
|
.extension<ReceivePageTheme>()!
|
||||||
|
.currentTileBackgroundColor
|
||||||
|
: Theme.of(context)
|
||||||
|
.extension<ReceivePageTheme>()!
|
||||||
|
.tilesBackgroundColor;
|
||||||
final textColor = isCurrent
|
final textColor = isCurrent
|
||||||
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
|
? Theme.of(context)
|
||||||
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
|
.extension<ReceivePageTheme>()!
|
||||||
|
.currentTileTextColor
|
||||||
|
: Theme.of(context)
|
||||||
|
.extension<ReceivePageTheme>()!
|
||||||
|
.tilesTextColor;
|
||||||
|
|
||||||
return AddressCell.fromItem(item,
|
return AddressCell.fromItem(item,
|
||||||
isCurrent: isCurrent,
|
isCurrent: isCurrent,
|
||||||
|
@ -211,6 +219,16 @@ class ReceivePage extends BasePage {
|
||||||
child: cell,
|
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/entities/priority_for_wallet_type.dart';
|
||||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.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/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/payment_request.dart';
|
||||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
@ -164,7 +165,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
AddressTextFieldOption.paste,
|
AddressTextFieldOption.paste,
|
||||||
AddressTextFieldOption.qrCode,
|
if (DeviceInfo.instance.isMobile) AddressTextFieldOption.qrCode,
|
||||||
AddressTextFieldOption.addressBook
|
AddressTextFieldOption.addressBook
|
||||||
],
|
],
|
||||||
buttonColor:
|
buttonColor:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||||
import 'package:cake_wallet/utils/device_info.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:cake_wallet/utils/responsive_layout_util.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -133,8 +134,7 @@ class AddressTextField extends StatelessWidget {
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
if (this.options.contains(AddressTextFieldOption.qrCode) &&
|
if (this.options.contains(AddressTextFieldOption.qrCode)) ...[
|
||||||
DeviceInfo.instance.isMobile) ...[
|
|
||||||
Container(
|
Container(
|
||||||
width: prefixIconWidth,
|
width: prefixIconWidth,
|
||||||
height: prefixIconHeight,
|
height: prefixIconHeight,
|
||||||
|
@ -194,7 +194,7 @@ class AddressTextField extends StatelessWidget {
|
||||||
|
|
||||||
Future<void> _presentQRScanner(BuildContext context) async {
|
Future<void> _presentQRScanner(BuildContext context) async {
|
||||||
bool isCameraPermissionGranted =
|
bool isCameraPermissionGranted =
|
||||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||||
if (!isCameraPermissionGranted) return;
|
if (!isCameraPermissionGranted) return;
|
||||||
final code = await presentQRScanner();
|
final code = await presentQRScanner();
|
||||||
if (code.isEmpty) {
|
if (code.isEmpty) {
|
||||||
|
|
|
@ -274,6 +274,11 @@ abstract class DashboardViewModelBase with Store {
|
||||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
|
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
|
||||||
|
|
||||||
bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven;
|
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;
|
final KeyService keyService;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
part 'rescan_view_model.g.dart';
|
part 'rescan_view_model.g.dart';
|
||||||
|
@ -9,8 +10,8 @@ enum RescanWalletState { rescaning, none }
|
||||||
|
|
||||||
abstract class RescanViewModelBase with Store {
|
abstract class RescanViewModelBase with Store {
|
||||||
RescanViewModelBase(this._wallet)
|
RescanViewModelBase(this._wallet)
|
||||||
: state = RescanWalletState.none,
|
: state = RescanWalletState.none,
|
||||||
isButtonEnabled = false;
|
isButtonEnabled = false;
|
||||||
|
|
||||||
final WalletBase _wallet;
|
final WalletBase _wallet;
|
||||||
|
|
||||||
|
@ -23,8 +24,8 @@ abstract class RescanViewModelBase with Store {
|
||||||
@action
|
@action
|
||||||
Future<void> rescanCurrentWallet({required int restoreHeight}) async {
|
Future<void> rescanCurrentWallet({required int restoreHeight}) async {
|
||||||
state = RescanWalletState.rescaning;
|
state = RescanWalletState.rescaning;
|
||||||
await _wallet.rescan(height: restoreHeight);
|
_wallet.rescan(height: restoreHeight);
|
||||||
_wallet.transactionHistory.clear();
|
if (_wallet.type != WalletType.bitcoin) _wallet.transactionHistory.clear();
|
||||||
state = RescanWalletState.none;
|
state = RescanWalletState.none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,8 +167,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||||
}) : _baseItems = <ListItem>[],
|
}) : _baseItems = <ListItem>[],
|
||||||
selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
|
selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
|
||||||
_cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
|
_cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
|
||||||
hasAccounts =
|
|
||||||
appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
|
|
||||||
amount = '',
|
amount = '',
|
||||||
_settingsStore = appStore.settingsStore,
|
_settingsStore = appStore.settingsStore,
|
||||||
super(appStore: appStore) {
|
super(appStore: appStore) {
|
||||||
|
@ -180,7 +178,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||||
_init();
|
_init();
|
||||||
|
|
||||||
selectedCurrency = walletTypeToCryptoCurrency(wallet.type);
|
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';
|
static const String _cryptoNumberPattern = '0.00000000';
|
||||||
|
@ -339,7 +338,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
bool hasAccounts;
|
bool _hasAccounts = false;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get hasAccounts => _hasAccounts;
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
String get accountLabel {
|
String get accountLabel {
|
||||||
|
@ -354,8 +356,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||||
return '';
|
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
|
@computed
|
||||||
bool get hasAddressList =>
|
bool get hasAddressList =>
|
||||||
|
hasSilentAddresses ||
|
||||||
wallet.type == WalletType.monero ||
|
wallet.type == WalletType.monero ||
|
||||||
wallet.type == WalletType.haven ||
|
wallet.type == WalletType.haven ||
|
||||||
wallet.type == WalletType.bitcoinCash ||
|
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_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_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_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_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_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_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_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_polygon && flutter pub get && cd ..
|
cd cw_ethereum; flutter pub get; cd ..
|
||||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||||
|
|
|
@ -118,6 +118,7 @@ abstract class Bitcoin {
|
||||||
|
|
||||||
List<String> getAddresses(Object wallet);
|
List<String> getAddresses(Object wallet);
|
||||||
String getAddress(Object wallet);
|
String getAddress(Object wallet);
|
||||||
|
List<BitcoinAddressRecord> getSilentAddresses(Object wallet);
|
||||||
|
|
||||||
List<ElectrumSubAddress> getSubAddresses(Object wallet);
|
List<ElectrumSubAddress> getSubAddresses(Object wallet);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue