inital commit: testnet, silent payments

This commit is contained in:
Rafael Saes 2023-10-06 12:20:45 -03:00
parent 021e88e667
commit 4a3140035f
61 changed files with 826 additions and 457 deletions

View file

@ -1,15 +1,17 @@
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
class BitcoinUnspent { class BitcoinUnspent {
BitcoinUnspent(this.address, this.hash, this.value, this.vout) BitcoinUnspent(this.address, this.hash, this.value, this.vout, {bool? isSilent})
: isSending = true, : isSending = true,
isFrozen = false, isFrozen = false,
note = ''; note = '',
isSilent = isSilent ?? false;
factory BitcoinUnspent.fromJSON( factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json,
BitcoinAddressRecord address, Map<String, dynamic> json) => {bool? isSilent}) =>
BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, BitcoinUnspent(
json['tx_pos'] as int); address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int,
isSilent: isSilent);
final BitcoinAddressRecord address; final BitcoinAddressRecord address;
final String hash; final String hash;
@ -17,8 +19,12 @@ class BitcoinUnspent {
final int vout; final int vout;
bool get isP2wpkh => bool get isP2wpkh =>
address.address.startsWith('bc') || address.address.startsWith('ltc'); address.address.startsWith('bc') ||
// testnet
address.address.startsWith('tb') ||
address.address.startsWith('ltc');
bool isSending; bool isSending;
bool isFrozen; bool isFrozen;
bool isSilent;
String note; String note;
} }

View file

@ -23,77 +23,84 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
bitcoin.NetworkType? networkType,
required Uint8List seedBytes, required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils, required EncryptionFileUtils encryptionFileUtils,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
int initialRegularAddressIndex = 0, int initialRegularAddressIndex = 0,
int initialChangeAddressIndex = 0}) int initialChangeAddressIndex = 0,
bitcoin.SilentPaymentAddress? silentAddress})
: super( : super(
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
networkType: bitcoin.bitcoin, networkType: networkType ?? bitcoin.bitcoin,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: seedBytes, seedBytes: seedBytes,
currency: CryptoCurrency.btc, currency: CryptoCurrency.btc,
encryptionFileUtils: encryptionFileUtils) { encryptionFileUtils: encryptionFileUtils) {
walletAddresses = BitcoinWalletAddresses( walletAddresses = BitcoinWalletAddresses(walletInfo,
walletInfo,
electrumClient: electrumClient, electrumClient: electrumClient,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd, mainHd: hd,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
.derivePath("m/0'/1"), networkType: networkType ?? bitcoin.bitcoin,
networkType: networkType); silentAddress: silentAddress);
} }
static Future<BitcoinWallet> create({ static Future<BitcoinWallet> create(
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 EncryptionFileUtils encryptionFileUtils, bitcoin.NetworkType? networkType,
List<BitcoinAddressRecord>? initialAddresses, required EncryptionFileUtils encryptionFileUtils,
ElectrumBalance? initialBalance, List<BitcoinAddressRecord>? initialAddresses,
int initialRegularAddressIndex = 0, ElectrumBalance? initialBalance,
int initialChangeAddressIndex = 0 int initialRegularAddressIndex = 0,
}) async { int initialChangeAddressIndex = 0}) async {
return BitcoinWallet( return BitcoinWallet(
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
networkType: networkType,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils, encryptionFileUtils: encryptionFileUtils,
seedBytes: await mnemonicToSeedBytes(mnemonic), seedBytes: await mnemonicToSeedBytes(mnemonic),
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex); initialChangeAddressIndex: initialChangeAddressIndex,
silentAddress: await bitcoin.SilentPaymentAddress.fromMnemonic(mnemonic,
hrp: networkType == bitcoin.bitcoin ? 'sp' : 'tsp'));
} }
static Future<BitcoinWallet> open({ static Future<BitcoinWallet> open(
required String name, {required String name,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password, required String password,
required EncryptionFileUtils encryptionFileUtils required EncryptionFileUtils encryptionFileUtils}) async {
}) async { final snp =
final snp = await ElectrumWallletSnapshot.load(encryptionFileUtils, name, walletInfo.type, password); await ElectrumWallletSnapshot.load(encryptionFileUtils, 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,
networkType: snp.networkType,
initialAddresses: snp.addresses, initialAddresses: snp.addresses,
initialBalance: snp.balance, initialBalance: snp.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic), seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
encryptionFileUtils: encryptionFileUtils, encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex); initialChangeAddressIndex: snp.changeAddressIndex,
silentAddress: await bitcoin.SilentPaymentAddress.fromMnemonic(snp.mnemonic,
hrp: snp.networkType == bitcoin.bitcoin ? 'sp' : 'tsp'));
} }
} }

View file

@ -4,36 +4,34 @@ import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'bitcoin_wallet_addresses.g.dart'; part 'bitcoin_wallet_addresses.g.dart';
class BitcoinWalletAddresses = BitcoinWalletAddressesBase class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses;
with _$BitcoinWalletAddresses;
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
with Store { BitcoinWalletAddressesBase(WalletInfo walletInfo,
BitcoinWalletAddressesBase(
WalletInfo walletInfo,
{required bitcoin.HDWallet mainHd, {required bitcoin.HDWallet mainHd,
required bitcoin.HDWallet sideHd, required bitcoin.HDWallet sideHd,
required bitcoin.NetworkType networkType, required bitcoin.NetworkType networkType,
required ElectrumClient electrumClient, required ElectrumClient electrumClient,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
int initialRegularAddressIndex = 0, int initialRegularAddressIndex = 0,
int initialChangeAddressIndex = 0}) int initialChangeAddressIndex = 0,
: super( bitcoin.SilentPaymentAddress? silentAddress})
walletInfo, : super(walletInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: mainHd, mainHd: mainHd,
sideHd: sideHd, sideHd: sideHd,
electrumClient: electrumClient, electrumClient: electrumClient,
networkType: networkType); networkType: networkType,
silentAddress: silentAddress);
@override @override
String getAddress({required int index, required bitcoin.HDWallet hd}) => String getAddress({required int index, required bitcoin.HDWallet hd}) =>
generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
} }

View file

@ -12,11 +12,10 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
class BitcoinWalletService extends WalletService< class BitcoinWalletService extends WalletService<BitcoinNewWalletCredentials,
BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> {
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -27,12 +26,13 @@ class BitcoinWalletService extends WalletService<
WalletType getType() => WalletType.bitcoin; WalletType getType() => WalletType.bitcoin;
@override @override
Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials) async { Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
final wallet = await BitcoinWalletBase.create( final wallet = await BitcoinWalletBase.create(
mnemonic: await generateMnemonic(), mnemonic: await generateMnemonic(),
password: credentials.password!, password: credentials.password!,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
networkType: isTestnet == true ? bitcoin.testnet : bitcoin.bitcoin,
encryptionFileUtils: encryptionFileUtilsFor(isDirect)); encryptionFileUtils: encryptionFileUtilsFor(isDirect));
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
@ -45,8 +45,8 @@ class BitcoinWalletService extends WalletService<
@override @override
Future<BitcoinWallet> openWallet(String name, String password) async { Future<BitcoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values.firstWhereOrNull( final walletInfo = walletInfoSource.values
(info) => info.id == WalletBase.idFor(name, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
final wallet = await BitcoinWalletBase.open( final wallet = await BitcoinWalletBase.open(
password: password, password: password,
name: name, name: name,
@ -59,17 +59,16 @@ class BitcoinWalletService extends WalletService<
@override @override
Future<void> remove(String wallet) async { Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())) File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
.delete(recursive: true); final walletInfo = walletInfoSource.values
final walletInfo = walletInfoSource.values.firstWhereOrNull( .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
(info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key); await walletInfoSource.delete(walletInfo.key);
} }
@override @override
Future<void> rename(String currentName, String password, String newName) async { Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( final currentWalletInfo = walletInfoSource.values
(info) => info.id == WalletBase.idFor(currentName, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!;
final currentWallet = await BitcoinWalletBase.open( final currentWallet = await BitcoinWalletBase.open(
password: password, password: password,
name: currentName, name: currentName,
@ -87,13 +86,11 @@ class BitcoinWalletService extends WalletService<
} }
@override @override
Future<BitcoinWallet> restoreFromKeys( Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials) async =>
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
throw UnimplementedError(); throw UnimplementedError();
@override @override
Future<BitcoinWallet> restoreFromSeed( Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials) async {
BitcoinRestoreWalletFromSeedCredentials credentials) async {
if (!validateMnemonic(credentials.mnemonic)) { if (!validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException(); throw BitcoinMnemonicIsIncorrectException();
} }

View file

@ -22,10 +22,7 @@ String jsonrpc(
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
class SocketTask { class SocketTask {
SocketTask({ SocketTask({required this.isSubscription, this.completer, this.subject});
required this.isSubscription,
this.completer,
this.subject});
final Completer<dynamic>? completer; final Completer<dynamic>? completer;
final BehaviorSubject<dynamic>? subject; final BehaviorSubject<dynamic>? subject;
@ -51,8 +48,7 @@ class ElectrumClient {
Timer? _aliveTimer; Timer? _aliveTimer;
String unterminatedString; String unterminatedString;
Future<void> connectToUri(Uri uri) async => Future<void> connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port);
await connect(host: uri.host, port: uri.port);
Future<void> connect({required String host, required int port}) async { Future<void> connect({required String host, required int port}) async {
try { try {
@ -104,21 +100,20 @@ class ElectrumClient {
} }
if (isJSONStringCorrect(unterminatedString)) { if (isJSONStringCorrect(unterminatedString)) {
final response = final response = json.decode(unterminatedString) as Map<String, dynamic>;
json.decode(unterminatedString) as Map<String, dynamic>;
_handleResponse(response); _handleResponse(response);
unterminatedString = ''; unterminatedString = '';
} }
} on TypeError catch (e) { } on TypeError catch (e) {
if (!e.toString().contains('Map<String, Object>') && !e.toString().contains('Map<String, dynamic>')) { if (!e.toString().contains('Map<String, Object>') &&
!e.toString().contains('Map<String, dynamic>')) {
return; return;
} }
unterminatedString += message; unterminatedString += message;
if (isJSONStringCorrect(unterminatedString)) { if (isJSONStringCorrect(unterminatedString)) {
final response = final response = json.decode(unterminatedString) as Map<String, dynamic>;
json.decode(unterminatedString) as Map<String, dynamic>;
_handleResponse(response); _handleResponse(response);
// unterminatedString = null; // unterminatedString = null;
unterminatedString = ''; unterminatedString = '';
@ -142,8 +137,7 @@ class ElectrumClient {
} }
} }
Future<List<String>> version() => Future<List<String>> version() => call(method: 'server.version').then((dynamic result) {
call(method: 'server.version').then((dynamic result) {
if (result is List) { if (result is List) {
return result.map((dynamic val) => val.toString()).toList(); return result.map((dynamic val) => val.toString()).toList();
} }
@ -180,9 +174,8 @@ class ElectrumClient {
Future<List<Map<String, dynamic>>> getListUnspentWithAddress( Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
String address, NetworkType networkType) => String address, NetworkType networkType) =>
call( call(
method: 'blockchain.scripthash.listunspent', method: 'blockchain.scripthash.listunspent',
params: [scriptHash(address, networkType: networkType)]) params: [scriptHash(address, networkType: networkType)]).then((dynamic result) {
.then((dynamic result) {
if (result is List) { if (result is List) {
return result.map((dynamic val) { return result.map((dynamic val) {
if (val is Map<String, dynamic>) { if (val is Map<String, dynamic>) {
@ -229,19 +222,25 @@ class ElectrumClient {
return []; return [];
}); });
Future<Map<String, dynamic>> getTransactionRaw( Future<dynamic> getTransactionRaw(
{required String hash}) async => {required String hash, required NetworkType networkType}) async =>
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) callWithTimeout(
method: 'blockchain.transaction.get',
params: networkType == bitcoin ? [hash, true] : [hash],
timeout: 10000)
.then((dynamic result) { .then((dynamic result) {
if (result is Map<String, dynamic>) { if (result is Map<String, dynamic>) {
return result; return result;
} }
if (networkType == testnet && result is String) {
return result;
}
return <String, dynamic>{}; return <String, dynamic>{};
}); });
Future<String> getTransactionHex( Future<String> getTransactionHex({required String hash}) async =>
{required String hash}) async =>
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000)
.then((dynamic result) { .then((dynamic result) {
if (result is String) { if (result is String) {
@ -251,8 +250,7 @@ class ElectrumClient {
return ''; return '';
}); });
Future<String> broadcastTransaction( Future<String> broadcastTransaction({required String transactionRaw}) async =>
{required String transactionRaw}) async =>
call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
.then((dynamic result) { .then((dynamic result) {
if (result is String) { if (result is String) {
@ -262,19 +260,15 @@ class ElectrumClient {
return ''; return '';
}); });
Future<Map<String, dynamic>> getMerkle( Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
{required String hash, required int height}) async => await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
await call(
method: 'blockchain.transaction.get_merkle',
params: [hash, height]) as Map<String, dynamic>;
Future<Map<String, dynamic>> getHeader({required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height])
as Map<String, dynamic>; as Map<String, dynamic>;
Future<Map<String, dynamic>> getHeader({required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
Future<double> estimatefee({required int p}) => Future<double> estimatefee({required int p}) =>
call(method: 'blockchain.estimatefee', params: [p]) call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) {
.then((dynamic result) {
if (result is double) { if (result is double) {
return result; return result;
} }
@ -319,15 +313,9 @@ class ElectrumClient {
final topDoubleString = await estimatefee(p: 1); final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 5); final middleDoubleString = await estimatefee(p: 5);
final bottomDoubleString = await estimatefee(p: 100); final bottomDoubleString = await estimatefee(p: 100);
final top = final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
(stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
.round(); final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
final middle =
(stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000)
.round();
final bottom =
(stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000)
.round();
return [bottom, middle, top]; return [bottom, middle, top];
} catch (_) { } catch (_) {
@ -344,16 +332,14 @@ class ElectrumClient {
} }
BehaviorSubject<T>? subscribe<T>( BehaviorSubject<T>? subscribe<T>(
{required String id, {required String id, required String method, List<Object> params = const []}) {
required String method,
List<Object> params = const []}) {
try { try {
final subscription = BehaviorSubject<T>(); final subscription = BehaviorSubject<T>();
_regisrySubscription(id, subscription); _regisrySubscription(id, subscription);
socket!.write(jsonrpc(method: method, id: _id, params: params)); socket!.write(jsonrpc(method: method, id: _id, params: params));
return subscription; return subscription;
} catch(e) { } catch (e) {
print(e.toString()); print(e.toString());
return null; return null;
} }
@ -370,9 +356,7 @@ class ElectrumClient {
} }
Future<dynamic> callWithTimeout( Future<dynamic> callWithTimeout(
{required String method, {required String method, List<Object> params = const [], int timeout = 4000}) async {
List<Object> params = const [],
int timeout = 4000}) async {
try { try {
final completer = Completer<dynamic>(); final completer = Completer<dynamic>();
_id += 1; _id += 1;
@ -386,7 +370,7 @@ class ElectrumClient {
}); });
return completer.future; return completer.future;
} catch(e) { } catch (e) {
print(e.toString()); print(e.toString());
} }
} }
@ -397,8 +381,8 @@ class ElectrumClient {
onConnectionStatusChange = null; onConnectionStatusChange = null;
} }
void _registryTask(int id, Completer<dynamic> completer) => _tasks[id.toString()] = void _registryTask(int id, Completer<dynamic> completer) =>
SocketTask(completer: completer, isSubscription: false); _tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false);
void _regisrySubscription(String id, BehaviorSubject<dynamic> subject) => void _regisrySubscription(String id, BehaviorSubject<dynamic> subject) =>
_tasks[id] = SocketTask(subject: subject, isSubscription: true); _tasks[id] = SocketTask(subject: subject, isSubscription: true);
@ -419,8 +403,7 @@ class ElectrumClient {
} }
} }
void _methodHandler( void _methodHandler({required String method, required Map<String, dynamic> request}) {
{required String method, required Map<String, dynamic> request}) {
switch (method) { switch (method) {
case 'blockchain.scripthash.subscribe': case 'blockchain.scripthash.subscribe':
final params = request['params'] as List<dynamic>; final params = request['params'] as List<dynamic>;
@ -451,8 +434,8 @@ class ElectrumClient {
_methodHandler(method: method, request: response); _methodHandler(method: method, request: response);
return; return;
} }
if (id != null){ if (id != null) {
_finish(id, result); _finish(id, result);
} }
} }

View file

@ -35,13 +35,15 @@ import 'package:cw_bitcoin/electrum.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
part 'electrum_wallet.g.dart'; part 'electrum_wallet.g.dart';
class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, abstract class ElectrumWalletBase
ElectrumTransactionHistory, ElectrumTransactionInfo> with Store { extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
with Store {
ElectrumWalletBase( ElectrumWalletBase(
{required String password, {required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
@ -54,28 +56,25 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
ElectrumClient? electrumClient, ElectrumClient? electrumClient,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
CryptoCurrency? currency}) CryptoCurrency? currency})
: hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"),
.derivePath("m/0'/0"),
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
_password = password, _password = password,
_feeRates = <int>[], _feeRates = <int>[],
_isTransactionUpdating = false, _isTransactionUpdating = false,
unspentCoins = [], unspentCoins = [],
_scripthashesUpdateSubject = {}, _scripthashesUpdateSubject = {},
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of( balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
currency != null ? {
? {currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, currency:
frozen: 0)} initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0)
: {}), }
: {}),
this.unspentCoinsInfo = unspentCoinsInfo, this.unspentCoinsInfo = unspentCoinsInfo,
super(walletInfo) { super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient(); this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = transactionHistory = ElectrumTransactionHistory(
ElectrumTransactionHistory( walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils);
walletInfo: walletInfo,
password: password,
encryptionFileUtils: encryptionFileUtils);
} }
static int estimatedTransactionSize(int inputsCount, int outputsCounts) => static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
@ -104,9 +103,9 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
.toList(); .toList();
List<String> get publicScriptHashes => walletAddresses.addresses List<String> get publicScriptHashes => walletAddresses.addresses
.where((addr) => !addr.isHidden) .where((addr) => !addr.isHidden)
.map((addr) => scriptHash(addr.address, networkType: networkType)) .map((addr) => scriptHash(addr.address, networkType: networkType))
.toList(); .toList();
String get xpub => hd.base58!; String get xpub => hd.base58!;
@ -119,8 +118,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
bitcoin.NetworkType networkType; bitcoin.NetworkType networkType;
@override @override
BitcoinWalletKeys get keys => BitcoinWalletKeys( BitcoinWalletKeys get keys =>
wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!);
String _password; String _password;
List<BitcoinUnspent> unspentCoins; List<BitcoinUnspent> unspentCoins;
@ -148,8 +147,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
await updateBalance(); await updateBalance();
_feeRates = await electrumClient.feeRates(); _feeRates = await electrumClient.feeRates();
Timer.periodic(const Duration(minutes: 1), Timer.periodic(
(timer) async => _feeRates = await electrumClient.feeRates()); const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} catch (e, stacktrace) { } catch (e, stacktrace) {
@ -178,8 +177,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
@override @override
Future<PendingBitcoinTransaction> createTransaction( Future<PendingBitcoinTransaction> createTransaction(Object credentials) async {
Object credentials) async {
const minAmount = 546; const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials; final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[]; final inputs = <BitcoinUnspent>[];
@ -202,9 +200,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
throw BitcoinTransactionNoInputsException(); throw BitcoinTransactionNoInputsException();
} }
final allAmountFee = transactionCredentials.feeRate != null final allAmountFee = 188;
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
final allAmount = allInputsAmount - allAmountFee; final allAmount = allInputsAmount - allAmountFee;
@ -213,13 +209,11 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
var fee = 0; var fee = 0;
if (hasMultiDestination) { if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
|| item.formattedCryptoAmount! <= 0)) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
} }
credentialsAmount = outputs.fold(0, (acc, value) => credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
acc + value.formattedCryptoAmount!);
if (allAmount - credentialsAmount < minAmount) { if (allAmount - credentialsAmount < minAmount) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
@ -236,9 +230,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
} else { } else {
final output = outputs.first; final output = outputs.first;
credentialsAmount = !output.sendAll credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
? output.formattedCryptoAmount!
: 0;
if (credentialsAmount > allAmount) { if (credentialsAmount > allAmount) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
@ -257,14 +249,14 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
} }
if (fee == 0) { if (fee == 0 && networkType == bitcoin.bitcoin) {
throw BitcoinTransactionWrongBalanceException(currency); // throw BitcoinTransactionWrongBalanceException(currency);
} }
final totalAmount = amount + fee; final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
throw BitcoinTransactionWrongBalanceException(currency); // throw BitcoinTransactionWrongBalanceException(currency);
} }
final txb = bitcoin.TransactionBuilder(network: networkType); final txb = bitcoin.TransactionBuilder(network: networkType);
@ -291,18 +283,26 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
if (amount <= 0 || totalInputAmount < totalAmount) { if (amount <= 0 || totalInputAmount < totalAmount) {
throw BitcoinTransactionWrongBalanceException(currency); // throw BitcoinTransactionWrongBalanceException(currency);
} }
txb.setVersion(1); txb.setVersion(1);
List<bitcoin.PrivateKeyInfo> privateKeys = [];
List<bitcoin.Outpoint> outpoints = [];
inputs.forEach((input) { inputs.forEach((input) {
privateKeys.add(bitcoin.PrivateKeyInfo(
bitcoin.ECPrivateKey(Uint8List.fromList(
HEX.decode(walletAddresses.sideHd.derive(input.address.index).privKey!))),
false));
outpoints.add(bitcoin.Outpoint(Uint8List.fromList(HEX.decode(input.hash)), input.vout));
if (input.isP2wpkh) { if (input.isP2wpkh) {
final p2wpkh = bitcoin final p2wpkh = bitcoin
.P2WPKH( .P2WPKH(
data: generatePaymentData( data: generatePaymentData(
hd: input.address.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, hd: input.address.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: input.address.index), index: input.address.index),
network: networkType) network: networkType)
.data; .data;
txb.addInput(input.hash, input.vout, null, p2wpkh.output); txb.addInput(input.hash, input.vout, null, p2wpkh.output);
@ -311,28 +311,43 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
}); });
List<bitcoin.SilentPaymentDestination> silentDestinations = [];
outputs.forEach((item) { outputs.forEach((item) {
final outputAmount = hasMultiDestination final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
? item.formattedCryptoAmount final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
: amount; if (outputAddress.startsWith('tsp1')) {
final outputAddress = item.isParsedAddress silentDestinations
? item.extractedAddress! .add(bitcoin.SilentPaymentDestination.fromAddress(outputAddress, outputAmount!));
: item.address; } else {
txb.addOutput( txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!);
addressToOutputScript(outputAddress, networkType), }
outputAmount!);
}); });
final estimatedSize = if (silentDestinations.isNotEmpty) {
estimatedTransactionSize(inputs.length, outputs.length + 1); final outpointsHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
var feeAmount = 0;
if (transactionCredentials.feeRate != null) { final sumOfInputPrivKeys = bitcoin.getSumInputPrivKeys(privateKeys);
feeAmount = transactionCredentials.feeRate! * estimatedSize;
} else { final generatedPubkeys = bitcoin.SilentPayment.generateMultipleRecipientPubkeys(
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; sumOfInputPrivKeys, outpointsHash, silentDestinations);
generatedPubkeys.forEach((recipientSilentAddress, generatedOutputs) {
generatedOutputs.forEach((output) {
final generatedPubkey = HEX.encode(output.$1.data);
txb.addOutput(bitcoin.getTaproot(generatedPubkey).toScriptPubKey().toBytes(), output.$2);
});
});
} }
final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1);
var feeAmount = 188;
// if (transactionCredentials.feeRate != null) {
// feeAmount = transactionCredentials.feeRate! * estimatedSize;
// } else {
// feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
// }
final changeValue = totalInputAmount - amount - feeAmount; final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) { if (changeValue > minAmount) {
@ -363,7 +378,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
'account_index': walletAddresses.currentReceiveAddressIndex.toString(), 'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
'balance': balance[currency]?.toJSON() 'balance': balance[currency]?.toJSON(),
'network_type': networkType.toString()
}); });
int feeRate(TransactionPriority priority) { int feeRate(TransactionPriority priority) {
@ -373,34 +389,29 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
return 0; return 0;
} catch(_) { } catch (_) {
return 0; return 0;
} }
} }
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int feeAmountForPriority(
int outputsCount) => BitcoinTransactionPriority priority, int inputsCount, int outputsCount) =>
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
int feeAmountWithFeeRate(int feeRate, int inputsCount, int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) =>
int outputsCount) =>
feeRate * estimatedTransactionSize(inputsCount, outputsCount); feeRate * estimatedTransactionSize(inputsCount, outputsCount);
@override @override
int calculateEstimatedFee(TransactionPriority? priority, int? amount, int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount}) {
{int? outputsCount}) {
if (priority is BitcoinTransactionPriority) { if (priority is BitcoinTransactionPriority) {
return calculateEstimatedFeeWithFeeRate( return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount,
feeRate(priority), outputsCount: outputsCount);
amount,
outputsCount: outputsCount);
} }
return 0; return 0;
} }
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) {
{int? outputsCount}) {
int inputsCount = 0; int inputsCount = 0;
if (amount != null) { if (amount != null) {
@ -429,8 +440,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
// If send all, then we have no change value // If send all, then we have no change value
final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); final _outputsCount = outputsCount ?? (amount != null ? 2 : 1);
return feeAmountWithFeeRate( return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount);
feeRate, inputsCount, _outputsCount);
} }
@override @override
@ -445,8 +455,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
final currentWalletFile = File(currentWalletPath); final currentWalletFile = File(currentWalletPath);
final currentDirPath = final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
// Copies current wallet files into new wallet name's dir and files // Copies current wallet files into new wallet name's dir and files
@ -483,21 +492,63 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} catch (_) {} } catch (_) {}
} }
Future<String> makePath() async => Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> updateUnspent() async { Future<void> updateUnspent() async {
final unspent = await Future.wait(walletAddresses final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
.addresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, networkType) .getListUnspentWithAddress(address.address, networkType)
.then((unspent) => unspent .then((unspent) => unspent.map((unspent) {
.map((unspent) {
try { try {
return BitcoinUnspent.fromJSON(address, unspent); return BitcoinUnspent.fromJSON(address, unspent);
} catch(_) { } catch (_) {
return null; return null;
} }
}).whereNotNull()))); }).whereNotNull())));
final uri = Uri(
scheme: 'https',
host: 'blockstream.info',
path: '/testnet/api/tx/986547a4daec37b21d2252e39c740d77ff92d927343b0b6e017d45e857955efa');
await http.get(uri).then((response) {
final obj = json.decode(response.body);
final scanPrivateKey = walletAddresses.silentAddress!.scanPrivkey;
final spendPublicKey = walletAddresses.silentAddress!.spendPubkey;
Uint8List? sumOfInputPublicKeys;
List<bitcoin.Outpoint> outpoints = [];
obj["vin"].forEach((input) {
sumOfInputPublicKeys = Uint8List.fromList(HEX.decode(input["witness"][1] as String));
outpoints.add(bitcoin.Outpoint(
Uint8List.fromList(HEX.decode(input['txid'] as String)), input['vout'] as int));
});
final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
List<Uint8List> outputs = [];
obj['vout'].forEach((out) {
outputs.add(Uint8List.fromList(
HEX.decode(bitcoin.getScript(out["scriptpubkey"] as String)[1] as String)));
});
final result = bitcoin.scanOutputs(
scanPrivateKey.data, spendPublicKey.data, sumOfInputPublicKeys!, outpointHash, outputs);
result.forEach((key, value) {
final tweak = value;
// TODO: store tweak for BitcoinUnspent
final spendPrivateKey = walletAddresses.silentAddress!.spendPrivkey;
final privKey = spendPrivateKey.tweak(tweak);
final pubKey = bitcoin.ECPrivateKey(privKey!.data).pubkey;
int i = 0;
final vout = obj['vout'].firstWhere((out) {
final script = bitcoin.getScript(out["scriptpubkey"] as String);
final scriptHash = script[1] as String;
i++;
return scriptHash == key;
});
unspent.add([
BitcoinUnspent.fromJSON(
BitcoinAddressRecord(vout["scriptpubkey_address"] as String, index: 0, isUsed: true),
{"tx_hash": obj["txid"], "value": vout["value"], "tx_pos": i},
isSilent: true)
]);
});
});
unspentCoins = unspent.expand((e) => e).toList(); unspentCoins = unspent.expand((e) => e).toList();
if (unspentCoinsInfo.isEmpty) { if (unspentCoinsInfo.isEmpty) {
@ -508,7 +559,9 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
if (unspentCoins.isNotEmpty) { if (unspentCoins.isNotEmpty) {
unspentCoins.forEach((coin) { unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where((element) => final coinInfoList = unspentCoinsInfo.values.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.address));
if (coinInfoList.isNotEmpty) { if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first; final coinInfo = coinInfoList.first;
@ -527,14 +580,14 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
Future<void> _addCoinInfo(BitcoinUnspent coin) async { Future<void> _addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo( final newInfo = UnspentCoinsInfo(
walletId: id, walletId: id,
hash: coin.hash, hash: coin.hash,
isFrozen: coin.isFrozen, isFrozen: coin.isFrozen,
isSending: coin.isSending, isSending: coin.isSending,
noteRaw: coin.note, noteRaw: coin.note,
address: coin.address.address, address: coin.address.address,
value: coin.value, value: coin.value,
vout: coin.vout, vout: coin.vout,
); );
await unspentCoinsInfo.add(newInfo); await unspentCoinsInfo.add(newInfo);
@ -543,8 +596,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
Future<void> _refreshUnspentCoinsInfo() async { Future<void> _refreshUnspentCoinsInfo() async {
try { try {
final List<dynamic> keys = <dynamic>[]; final List<dynamic> keys = <dynamic>[];
final currentWalletUnspentCoins = unspentCoinsInfo.values final currentWalletUnspentCoins =
.where((element) => element.walletId.contains(id)); unspentCoinsInfo.values.where((element) => element.walletId.contains(id));
if (currentWalletUnspentCoins.isNotEmpty) { if (currentWalletUnspentCoins.isNotEmpty) {
currentWalletUnspentCoins.forEach((element) { currentWalletUnspentCoins.forEach((element) {
@ -566,12 +619,23 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
Future<ElectrumTransactionBundle> getTransactionExpanded( Future<ElectrumTransactionBundle> getTransactionExpanded(
{required String hash, required int height}) async { {required String hash, required int height}) async {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); final verboseTransaction =
final transactionHex = verboseTransaction['hex'] as String; await electrumClient.getTransactionRaw(hash: hash, networkType: networkType);
String transactionHex;
int? time;
int confirmations = 0;
if (networkType == bitcoin.testnet) {
transactionHex = verboseTransaction as String;
confirmations = 1;
} else {
transactionHex = verboseTransaction['hex'] as String;
time = verboseTransaction['time'] as int?;
confirmations = verboseTransaction['confirmations'] as int? ?? 0;
}
final original = bitcoin.Transaction.fromHex(transactionHex); final original = bitcoin.Transaction.fromHex(transactionHex);
final ins = <bitcoin.Transaction>[]; final ins = <bitcoin.Transaction>[];
final time = verboseTransaction['time'] as int?;
final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
for (final vin in original.ins) { for (final vin in original.ins) {
final id = HEX.encode(vin.hash!.reversed.toList()); final id = HEX.encode(vin.hash!.reversed.toList());
@ -580,27 +644,19 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
ins.add(tx); ins.add(tx);
} }
return ElectrumTransactionBundle( return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations);
original,
ins: ins,
time: time,
confirmations: confirmations);
} }
Future<ElectrumTransactionInfo?> fetchTransactionInfo( Future<ElectrumTransactionInfo?> fetchTransactionInfo(
{required String hash, required int height}) async { {required String hash, required int height}) async {
try { try {
final tx = await getTransactionExpanded(hash: hash, height: height); final tx = await getTransactionExpanded(hash: hash, height: height);
final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet();
return ElectrumTransactionInfo.fromElectrumBundle( return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType,
tx, addresses: addresses, height: height);
walletInfo.type, } catch (_) {
networkType, return null;
addresses: addresses, }
height: height);
} catch(_) {
return null;
}
} }
@override @override
@ -611,10 +667,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
final sh = scriptHash(addressRecord.address, networkType: networkType); final sh = scriptHash(addressRecord.address, networkType: networkType);
addressHashes[sh] = addressRecord; addressHashes[sh] = addressRecord;
}); });
final histories = final histories = addressHashes.keys.map((scriptHash) =>
addressHashes.keys.map((scriptHash) => electrumClient electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
.getHistory(scriptHash)
.then((history) => {scriptHash: history}));
final historyResults = await Future.wait(histories); final historyResults = await Future.wait(histories);
historyResults.forEach((history) { historyResults.forEach((history) {
history.entries.forEach((historyItem) { history.entries.forEach((historyItem) {
@ -625,19 +679,16 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
}); });
}); });
final historiesWithDetails = await Future.wait( final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
normalizedHistories try {
.map((transaction) { return fetchTransactionInfo(
try { hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
return fetchTransactionInfo( } catch (_) {
hash: transaction['tx_hash'] as String, return Future.value(null);
height: transaction['height'] as int); }
} catch(_) { }));
return Future.value(null); return historiesWithDetails
} .fold<Map<String, ElectrumTransactionInfo>>(<String, ElectrumTransactionInfo>{}, (acc, tx) {
}));
return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
<String, ElectrumTransactionInfo>{}, (acc, tx) {
if (tx == null) { if (tx == null) {
return acc; return acc;
} }
@ -690,6 +741,9 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
final addresses = walletAddresses.addresses.toList(); final addresses = walletAddresses.addresses.toList();
final balanceFutures = <Future<Map<String, dynamic>>>[]; final balanceFutures = <Future<Map<String, dynamic>>>[];
var totalConfirmed = 0;
var totalUnconfirmed = 0;
for (var i = 0; i < addresses.length; i++) { for (var i = 0; i < addresses.length; i++) {
final addressRecord = addresses[i]; final addressRecord = addresses[i];
final sh = scriptHash(addressRecord.address, networkType: networkType); final sh = scriptHash(addressRecord.address, networkType: networkType);
@ -700,16 +754,21 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
var totalFrozen = 0; var totalFrozen = 0;
unspentCoinsInfo.values.forEach((info) { unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) { unspentCoins.forEach((element) {
if (element.hash == info.hash && info.isFrozen && element.address.address == info.address if (element.hash == info.hash &&
&& element.value == info.value) { info.isFrozen &&
element.address.address == info.address &&
element.value == info.value) {
totalFrozen += element.value; totalFrozen += element.value;
} else if (element.hash == info.hash &&
element.isSilent &&
element.address.address == info.address &&
element.value == info.value) {
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];
@ -724,8 +783,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
} }
return ElectrumBalance(confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, return ElectrumBalance(
frozen: totalFrozen); confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen);
} }
Future<void> updateBalance() async { Future<void> updateBalance() async {
@ -736,9 +795,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
String getChangeAddress() { String getChangeAddress() {
const minCountOfHiddenAddresses = 5; const minCountOfHiddenAddresses = 5;
final random = Random(); final random = Random();
var addresses = walletAddresses.addresses var addresses = walletAddresses.addresses.where((addr) => addr.isHidden).toList();
.where((addr) => addr.isHidden)
.toList();
if (addresses.length < minCountOfHiddenAddresses) { if (addresses.length < minCountOfHiddenAddresses) {
addresses = walletAddresses.addresses.toList(); addresses = walletAddresses.addresses.toList();

View file

@ -8,8 +8,7 @@ import 'package:mobx/mobx.dart';
part 'electrum_wallet_addresses.g.dart'; part 'electrum_wallet_addresses.g.dart';
class ElectrumWalletAddresses = ElectrumWalletAddressesBase class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
with _$ElectrumWalletAddresses;
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(WalletInfo walletInfo, ElectrumWalletAddressesBase(WalletInfo walletInfo,
@ -19,20 +18,19 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
required this.networkType, required this.networkType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
int initialRegularAddressIndex = 0, int initialRegularAddressIndex = 0,
int initialChangeAddressIndex = 0}) int initialChangeAddressIndex = 0,
: addresses = ObservableList<BitcoinAddressRecord>.of( bitcoin.SilentPaymentAddress? silentAddress})
(initialAddresses ?? []).toSet()), : addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of( silentAddress = silentAddress,
(initialAddresses ?? []) receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
changeAddresses = ObservableList<BitcoinAddressRecord>.of( changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
(initialAddresses ?? [])
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
currentReceiveAddressIndex = initialRegularAddressIndex, currentReceiveAddressIndex = initialRegularAddressIndex,
currentChangeAddressIndex = initialChangeAddressIndex, currentChangeAddressIndex = initialChangeAddressIndex,
super(walletInfo); super(walletInfo);
static const defaultReceiveAddressesCount = 22; static const defaultReceiveAddressesCount = 22;
static const defaultChangeAddressesCount = 17; static const defaultChangeAddressesCount = 17;
@ -46,9 +44,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final bitcoin.HDWallet mainHd; final bitcoin.HDWallet mainHd;
final bitcoin.HDWallet sideHd; final bitcoin.HDWallet sideHd;
@override // TODO: labels -> disable edit on receive page
final bitcoin.SilentPaymentAddress? silentAddress;
@observable
String? activeAddress;
@computed @computed
String get address { String get receiveAddress {
if (receiveAddresses.isEmpty) { if (receiveAddresses.isEmpty) {
return generateNewAddress().address; return generateNewAddress().address;
} }
@ -57,28 +60,40 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
@override @override
set address(String addr) => null; @computed
String get address {
if (activeAddress != null) {
return activeAddress!;
}
if (receiveAddresses.isEmpty) {
return generateNewAddress().address;
}
return receiveAddresses.first.address;
}
@override
set address(String addr) => activeAddress = addr;
int currentReceiveAddressIndex; int currentReceiveAddressIndex;
int currentChangeAddressIndex; int currentChangeAddressIndex;
@computed @computed
int get totalCountOfReceiveAddresses => int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) {
addresses.fold(0, (acc, addressRecord) { if (!addressRecord.isHidden) {
if (!addressRecord.isHidden) { return acc + 1;
return acc + 1; }
} return acc;
return acc; });
});
@computed @computed
int get totalCountOfChangeAddresses => int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) {
addresses.fold(0, (acc, addressRecord) { if (addressRecord.isHidden) {
if (addressRecord.isHidden) { return acc + 1;
return acc + 1; }
} return acc;
return acc; });
});
Future<void> discoverAddresses() async { Future<void> discoverAddresses() async {
await _discoverAddresses(mainHd, false); await _discoverAddresses(mainHd, false);
@ -105,15 +120,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
Future<String> getChangeAddress() async { Future<String> getChangeAddress() async {
updateChangeAddresses(); updateChangeAddresses();
if (changeAddresses.isEmpty) { if (changeAddresses.isEmpty) {
final newAddresses = await _createNewAddresses( final newAddresses = await _createNewAddresses(gap,
gap, hd: sideHd,
hd: sideHd, startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
startIndex: totalCountOfChangeAddresses > 0 isHidden: true);
? totalCountOfChangeAddresses - 1
: 0,
isHidden: true);
_addAddresses(newAddresses); _addAddresses(newAddresses);
} }
@ -127,8 +139,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address; return address;
} }
BitcoinAddressRecord generateNewAddress( BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, bool isHidden = false}) {
{bitcoin.HDWallet? hd, bool isHidden = false}) {
currentReceiveAddressIndex += 1; currentReceiveAddressIndex += 1;
// FIX-ME: Check logic for whichi HD should be used here ??? // FIX-ME: Check logic for whichi HD should be used here ???
final address = BitcoinAddressRecord( final address = BitcoinAddressRecord(
@ -155,16 +166,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
void updateReceiveAddresses() { void updateReceiveAddresses() {
receiveAddresses.removeRange(0, receiveAddresses.length); receiveAddresses.removeRange(0, receiveAddresses.length);
final newAdresses = addresses final newAdresses =
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
receiveAddresses.addAll(newAdresses); receiveAddresses.addAll(newAdresses);
} }
@action @action
void updateChangeAddresses() { void updateChangeAddresses() {
changeAddresses.removeRange(0, changeAddresses.length); changeAddresses.removeRange(0, changeAddresses.length);
final newAdresses = addresses final newAdresses =
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
changeAddresses.addAll(newAdresses); changeAddresses.addAll(newAdresses);
} }
@ -173,20 +184,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinAddressRecord> addrs; List<BitcoinAddressRecord> addrs;
if (addresses.isNotEmpty) { if (addresses.isNotEmpty) {
addrs = addresses addrs = addresses.where((addr) => addr.isHidden == isHidden).toList();
.where((addr) => addr.isHidden == isHidden)
.toList();
} else { } else {
addrs = await _createNewAddresses( addrs = await _createNewAddresses(
isHidden isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount,
? defaultChangeAddressesCount
: defaultReceiveAddressesCount,
startIndex: 0, startIndex: 0,
hd: hd, hd: hd,
isHidden: isHidden); isHidden: isHidden);
} }
while(hasAddrUse) { while (hasAddrUse) {
final addr = addrs.last.address; final addr = addrs.last.address;
hasAddrUse = await _hasAddressUsed(addr); hasAddrUse = await _hasAddressUsed(addr);
@ -196,11 +203,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final start = addrs.length; final start = addrs.length;
final count = start + gap; final count = start + gap;
final batch = await _createNewAddresses( final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden);
count,
startIndex: start,
hd: hd,
isHidden: isHidden);
addrs.addAll(batch); addrs.addAll(batch);
} }
@ -224,21 +227,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
if (countOfReceiveAddresses < defaultReceiveAddressesCount) { if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
final newAddresses = await _createNewAddresses( final newAddresses = await _createNewAddresses(addressesCount,
addressesCount, startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false);
startIndex: countOfReceiveAddresses,
hd: mainHd,
isHidden: false);
addresses.addAll(newAddresses); addresses.addAll(newAddresses);
} }
if (countOfHiddenAddresses < defaultChangeAddressesCount) { if (countOfHiddenAddresses < defaultChangeAddressesCount) {
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
final newAddresses = await _createNewAddresses( final newAddresses = await _createNewAddresses(addressesCount,
addressesCount, startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true);
startIndex: countOfHiddenAddresses,
hd: sideHd,
isHidden: true);
addresses.addAll(newAddresses); addresses.addAll(newAddresses);
} }
} }
@ -248,10 +245,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final list = <BitcoinAddressRecord>[]; final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) { for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord( final address =
getAddress(index: i, hd: hd), BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden);
index: i,
isHidden: isHidden);
list.add(address); list.add(address);
} }
@ -270,4 +265,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final transactionHistory = await electrumClient.getHistory(sh); final transactionHistory = await electrumClient.getHistory(sh);
return transactionHistory.isNotEmpty; return transactionHistory.isNotEmpty;
} }
} }

View file

@ -4,6 +4,7 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/encryption_file_utils.dart'; import 'package:cw_bitcoin/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
class ElectrumWallletSnapshot { class ElectrumWallletSnapshot {
ElectrumWallletSnapshot({ ElectrumWallletSnapshot({
@ -13,6 +14,7 @@ class ElectrumWallletSnapshot {
required this.mnemonic, required this.mnemonic,
required this.addresses, required this.addresses,
required this.balance, required this.balance,
required this.networkType,
required this.regularAddressIndex, required this.regularAddressIndex,
required this.changeAddressIndex}); required this.changeAddressIndex});
@ -23,6 +25,7 @@ class ElectrumWallletSnapshot {
String mnemonic; String mnemonic;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
ElectrumBalance balance; ElectrumBalance balance;
bitcoin.NetworkType networkType;
int regularAddressIndex; int regularAddressIndex;
int changeAddressIndex; int changeAddressIndex;
@ -38,6 +41,7 @@ class ElectrumWallletSnapshot {
.toList(); .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);
final networkType = bitcoin.testnet;
var regularAddressIndex = 0; var regularAddressIndex = 0;
var changeAddressIndex = 0; var changeAddressIndex = 0;
@ -53,6 +57,7 @@ class ElectrumWallletSnapshot {
mnemonic: mnemonic, mnemonic: mnemonic,
addresses: addresses, addresses: addresses,
balance: balance, balance: balance,
networkType: networkType,
regularAddressIndex: regularAddressIndex, regularAddressIndex: regularAddressIndex,
changeAddressIndex: changeAddressIndex); changeAddressIndex: changeAddressIndex);
} }

View file

@ -27,7 +27,7 @@ class LitecoinWalletService extends WalletService<
WalletType getType() => WalletType.litecoin; WalletType getType() => WalletType.litecoin;
@override @override
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials) async { Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
final wallet = await LitecoinWalletBase.create( final wallet = await LitecoinWalletBase.create(
mnemonic: await generateMnemonic(), mnemonic: await generateMnemonic(),
password: credentials.password!, password: credentials.password!,

View file

@ -66,15 +66,27 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "1.0.6"
bitcoin_base:
dependency: "direct main"
description:
path: "/home/rafael/Storage/Repositories/btc-silent-payments/bitcoin_base"
relative: false
source: path
version: "1.1.0"
bitcoin_flutter: bitcoin_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "/home/rafael/Storage/Repositories/btc-silent-payments/bitcoin_flutter"
ref: cake-update-v3 relative: false
resolved-ref: df9204144011ed9419eff7d9ef3143102a40252d source: path
url: "https://github.com/cake-tech/bitcoin_flutter.git"
source: git
version: "2.0.2" version: "2.0.2"
blockchain_utils:
dependency: transitive
description:
path: "/home/rafael/Storage/Repositories/btc-silent-payments/blockchain_utils"
relative: false
source: path
version: "0.4.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -196,6 +208,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.4.0" version: "4.4.0"
coinlib:
dependency: transitive
description:
path: "/home/rafael/Storage/Repositories/btc-silent-payments/coinlib/coinlib"
relative: false
source: path
version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -243,6 +262,22 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.1"
dart_base_x:
dependency: transitive
description:
name: dart_base_x
sha256: c8af4f6a6518daab4aa85bb27ee148221644e80446bb44117052b6f4674cdb23
url: "https://pub.dev"
source: hosted
version: "1.0.0"
dart_bech32:
dependency: transitive
description:
name: dart_bech32
sha256: "0e1dc1ff39c9669c9ffeafd5d675104918f7b50799692491badfea7e1fb40888"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -251,6 +286,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.4" version: "2.2.4"
elliptic:
dependency: transitive
description:
name: elliptic
sha256: "98e2fa89a714c649174553c823db2612dc9581814477fe1264a499d448237b6b"
url: "https://pub.dev"
source: hosted
version: "0.3.10"
encrypt: encrypt:
dependency: "direct main" dependency: "direct main"
description: description:
@ -271,10 +314,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.1.0"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -553,10 +596,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pointycastle name: pointycastle
sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.6.2" version: "3.7.3"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -710,10 +753,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.2"
unorm_dart: unorm_dart:
dependency: "direct main" dependency: "direct main"
description: description:
@ -730,6 +773,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
wasm_interop:
dependency: transitive
description:
name: wasm_interop
sha256: b1b378f07a4cf0103c25faf34d9a64d2c3312135b9efb47e0ec116ec3b14e48f
url: "https://pub.dev"
source: hosted
version: "2.0.1"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View file

@ -20,9 +20,9 @@ dependencies:
cw_core: cw_core:
path: ../cw_core path: ../cw_core
bitcoin_flutter: bitcoin_flutter:
git: path: /home/rafael/Storage/Repositories/btc-silent-payments/bitcoin_flutter
url: https://github.com/cake-tech/bitcoin_flutter.git bitcoin_base:
ref: cake-update-v3 path: /home/rafael/Storage/Repositories/btc-silent-payments/bitcoin_base
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

View file

@ -6,7 +6,7 @@ abstract class WalletService<N extends WalletCredentials,
RFS extends WalletCredentials, RFK extends WalletCredentials> { RFS extends WalletCredentials, RFK extends WalletCredentials> {
WalletType getType(); WalletType getType();
Future<WalletBase> create(N credentials); Future<WalletBase> create(N credentials, {bool? isTestnet});
Future<WalletBase> restoreFromSeed(RFS credentials); Future<WalletBase> restoreFromSeed(RFS credentials);

View file

@ -21,7 +21,7 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
final bool isDirect; final bool isDirect;
@override @override
Future<EthereumWallet> create(EthereumNewWalletCredentials credentials) async { Future<EthereumWallet> create(EthereumNewWalletCredentials credentials, {bool? isTestnet}) async {
final mnemonic = bip39.generateMnemonic(); final mnemonic = bip39.generateMnemonic();
final wallet = EthereumWallet( final wallet = EthereumWallet(
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,

View file

@ -68,7 +68,7 @@ class HavenWalletService extends WalletService<
WalletType getType() => WalletType.haven; WalletType getType() => WalletType.haven;
@override @override
Future<HavenWallet> create(HavenNewWalletCredentials credentials) async { Future<HavenWallet> create(HavenNewWalletCredentials credentials, {bool? isTestnet}) async {
try { try {
final path = await pathForWallet(name: credentials.name, type: getType()); final path = await pathForWallet(name: credentials.name, type: getType());
await haven_wallet_manager.createWallet( await haven_wallet_manager.createWallet(

View file

@ -65,7 +65,7 @@ class MoneroWalletService extends WalletService<
WalletType getType() => WalletType.monero; WalletType getType() => WalletType.monero;
@override @override
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials) async { Future<MoneroWallet> create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async {
try { try {
final path = await pathForWallet(name: credentials.name, type: getType()); final path = await pathForWallet(name: credentials.name, type: getType());
await monero_wallet_manager.createWallet( await monero_wallet_manager.createWallet(

View file

@ -105,6 +105,16 @@ class CWBitcoin extends Bitcoin {
return bitcoinWallet.walletAddresses.address; return bitcoinWallet.walletAddresses.address;
} }
String getReceiveAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.receiveAddress;
}
btc.SilentPaymentAddress? getSilentAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.silentAddress;
}
@override @override
String formatterBitcoinAmountToString({required int amount}) String formatterBitcoinAmountToString({required int amount})
=> bitcoinAmountToString(amount: amount); => bitcoinAmountToString(amount: amount);

View file

@ -25,7 +25,10 @@ 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}\$'; final p2sh = '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$';
final testnet = '^tb1[0-9a-zA-Z]{59}\$';
final silentpayments = '^tsp1[0-9a-zA-Z]{113}\$';
return '^bc1[0-9a-zA-Z]{59}\$|$p2sh|$testnet|$silentpayments';
case CryptoCurrency.nano: case CryptoCurrency.nano:
return '[0-9a-zA-Z_]'; return '[0-9a-zA-Z_]';
case CryptoCurrency.usdc: case CryptoCurrency.usdc:

View file

@ -53,7 +53,7 @@ class WalletCreationService {
} }
} }
Future<WalletBase> create(WalletCredentials credentials) async { Future<WalletBase> create(WalletCredentials credentials, {bool? isTestnet}) async {
checkIfExists(credentials.name); checkIfExists(credentials.name);
if (credentials.password == null) { if (credentials.password == null) {
@ -62,7 +62,7 @@ class WalletCreationService {
password: credentials.password!, walletName: credentials.name); password: credentials.password!, walletName: credentials.name);
} }
final wallet = await _service!.create(credentials); final wallet = await _service!.create(credentials, isTestnet: isTestnet);
if (wallet.type == WalletType.monero) { if (wallet.type == WalletType.monero) {
await sharedPreferences await sharedPreferences

View file

@ -1054,7 +1054,7 @@ Future<void> setup({
IoniaPaymentStatusPage( IoniaPaymentStatusPage(
getIt.get<IoniaPaymentStatusViewModel>(param1: paymentInfo, param2: committedInfo))); getIt.get<IoniaPaymentStatusViewModel>(param1: paymentInfo, param2: committedInfo)));
getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>( getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, void Function(), void>(
(type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>())); (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<WalletUnlockLoadableViewModel, WalletUnlockArguments, void>((args, _) { getIt.registerFactoryParam<WalletUnlockLoadableViewModel, WalletUnlockArguments, void>((args, _) {

123
lib/nano/nano.dart Normal file
View file

@ -0,0 +1,123 @@
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_core/account.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/nano_account_info_response.dart';
import 'package:mobx/mobx.dart';
import 'package:hive/hive.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_mnemonic.dart';
import 'package:cw_nano/nano_wallet.dart';
import 'package:cw_nano/nano_wallet_service.dart';
import 'package:cw_nano/nano_transaction_info.dart';
import 'package:cw_nano/nano_transaction_credentials.dart';
import 'package:cw_nano/nano_wallet_creation_credentials.dart';
// needed for nano_util:
import 'dart:convert';
import 'dart:typed_data';
import 'package:convert/convert.dart';
import "package:ed25519_hd_key/ed25519_hd_key.dart";
import 'package:libcrypto/libcrypto.dart';
import 'package:nanodart/nanodart.dart' as ND;
import 'package:decimal/decimal.dart';
part 'cw_nano.dart';
Nano? nano = CWNano();
NanoUtil? nanoUtil = CWNanoUtil();
abstract class Nano {
NanoAccountList getAccountList(Object wallet);
Account getCurrentAccount(Object wallet);
void setCurrentAccount(Object wallet, int id, String label, String? balance);
WalletService createNanoWalletService(Box<WalletInfo> walletInfoSource, bool isDirect);
WalletCredentials createNanoNewWalletCredentials({
required String name,
String? password,
});
WalletCredentials createNanoRestoreWalletFromSeedCredentials({
required String name,
required String password,
required String mnemonic,
DerivationType? derivationType,
});
WalletCredentials createNanoRestoreWalletFromKeysCredentials({
required String name,
required String password,
required String seedKey,
DerivationType? derivationType,
});
List<String> getNanoWordList(String language);
Map<String, String> getKeys(Object wallet);
Object createNanoTransactionCredentials(List<Output> outputs);
Future<void> changeRep(Object wallet, String address);
Future<void> updateTransactions(Object wallet);
BigInt getTransactionAmountRaw(TransactionInfo transactionInfo);
}
abstract class NanoAccountList {
ObservableList<NanoAccount> get accounts;
void update(Object wallet);
void refresh(Object wallet);
Future<List<NanoAccount>> getAll(Object wallet);
Future<void> addAccount(Object wallet, {required String label});
Future<void> setLabelAccount(Object wallet, {required int accountIndex, required String label});
}
abstract class NanoUtil {
String seedToPrivate(String seed, int index);
String seedToAddress(String seed, int index);
String seedToMnemonic(String seed);
Future<String> mnemonicToSeed(String mnemonic);
String privateKeyToPublic(String privateKey);
String addressToPublicKey(String publicAddress);
String privateKeyToAddress(String privateKey);
String publicKeyToAddress(String publicKey);
bool isValidSeed(String seed);
Future<String> hdMnemonicListToSeed(List<String> words);
Future<String> hdSeedToPrivate(String seed, int index);
Future<String> hdSeedToAddress(String seed, int index);
Future<String> uniSeedToAddress(String seed, int index, String type);
Future<String> uniSeedToPrivate(String seed, int index, String type);
bool isValidBip39Seed(String seed);
static const int maxDecimalDigits = 6; // Max digits after decimal
BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000");
BigInt rawPerNyano = BigInt.parse("1000000000000000000000000");
BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000");
BigInt rawPerXMR = BigInt.parse("1000000000000");
BigInt convertXMRtoNano = BigInt.parse("1000000000000000000");
Decimal getRawAsDecimal(String? raw, BigInt? rawPerCur);
String truncateDecimal(Decimal input, {int digits = maxDecimalDigits});
String getRawAsUsableString(String? raw, BigInt rawPerCur);
String getRawAccuracy(String? raw, BigInt rawPerCur);
String getAmountAsRaw(String amount, BigInt rawPerCur);
// derivationInfo:
Future<AccountInfoResponse?> getInfoFromSeedOrMnemonic(
DerivationType derivationType, {
String? seedKey,
String? mnemonic,
required Node node,
});
Future<List<DerivationType>> compareDerivationMethods({
String? mnemonic,
String? privateKey,
required Node node,
});
}

View file

@ -571,11 +571,13 @@ Route<dynamic> createRoute(RouteSettings settings) {
param2: url)); param2: url));
case Routes.advancedPrivacySettings: case Routes.advancedPrivacySettings:
final type = settings.arguments as WalletType; final args = settings.arguments as List;
final type = args.first as WalletType;
final func = args[1] as void Function();
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: (_) => AdvancedPrivacySettingsPage( builder: (_) => AdvancedPrivacySettingsPage(
getIt.get<AdvancedPrivacySettingsViewModel>(param1: type), getIt.get<AdvancedPrivacySettingsViewModel>(param1: func),
getIt.get<NodeCreateOrEditViewModel>(param1: type), getIt.get<NodeCreateOrEditViewModel>(param1: type),
)); ));

View file

@ -177,8 +177,7 @@ class AddressPage extends BasePage {
return GestureDetector( return GestureDetector(
onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled
? await showPopUp<void>( ? await showPopUp<void>(
context: context, context: context, builder: (_) => getIt.get<MoneroAccountListPage>())
builder: (_) => getIt.get<MoneroAccountListPage>())
: Navigator.of(context).pushNamed(Routes.receive), : Navigator.of(context).pushNamed(Routes.receive),
child: Container( child: Container(
height: 50, height: 50,
@ -198,26 +197,29 @@ class AddressPage extends BasePage {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Observer( Observer(
builder: (_) { builder: (_) {
String label = addressListViewModel.hasAccounts String label = addressListViewModel.hasSilentAddresses
? S.of(context).accounts_subaddresses ? S.of(context).address_and_silent_addresses
: S.of(context).addresses; : addressListViewModel.hasAccounts
? S.of(context).accounts_subaddresses
: S.of(context).addresses;
if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) { if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) {
label = addressListViewModel.hasAccounts label = addressListViewModel.hasAccounts
? S.of(context).accounts ? S.of(context).accounts
: S.of(context).account; : S.of(context).account;
} }
return Text( return Text(
label, label,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context) color: Theme.of(context)
.extension<SyncIndicatorTheme>()! .extension<SyncIndicatorTheme>()!
.textColor), .textColor),
); );
},), },
),
Icon( Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 14, size: 14,
@ -227,7 +229,8 @@ class AddressPage extends BasePage {
), ),
), ),
); );
} else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || addressListViewModel.showElectrumAddressDisclaimer) { } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled ||
addressListViewModel.showElectrumAddressDisclaimer) {
return Text(S.of(context).electrum_address_disclaimer, return Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(

View file

@ -75,6 +75,18 @@ class _AdvancedPrivacySettingsBodyState extends State<AdvancedPrivacySettingsBod
), ),
); );
}), }),
if (widget.nodeViewModel.hasTestnetSupport)
Observer(builder: (_) {
return Column(
children: [
SettingsSwitcherCell(
title: S.current.use_testnet,
value: widget.privacySettingsViewModel.useTestnet,
onValueChange: (_, __) => widget.privacySettingsViewModel.toggleUseTestnet(),
),
],
);
}),
Observer(builder: (_) { Observer(builder: (_) {
return Column( return Column(
children: [ children: [

View file

@ -297,8 +297,8 @@ class _WalletNameFormState extends State<WalletNameForm> {
const SizedBox(height: 25), const SizedBox(height: 25),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context) Navigator.of(context).pushNamed(Routes.advancedPrivacySettings,
.pushNamed(Routes.advancedPrivacySettings, arguments: _walletNewVM.type); arguments: [_walletNewVM.type, _walletNewVM.toggleUseTestnet]);
}, },
child: Text(S.of(context).advanced_privacy_settings), child: Text(S.of(context).advanced_privacy_settings),
), ),

View file

@ -66,8 +66,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) {
@ -98,7 +97,8 @@ class ReceivePage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return (addressListViewModel.type == WalletType.monero || return (addressListViewModel.type == WalletType.bitcoin ||
addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) addressListViewModel.type == WalletType.haven)
? KeyboardActions( ? KeyboardActions(
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
@ -144,7 +144,8 @@ class ReceivePage extends BasePage {
icon: Icon( icon: 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,
)); ));
} }
@ -152,11 +153,12 @@ class ReceivePage extends BasePage {
cell = HeaderTile( cell = HeaderTile(
onTap: () => onTap: () =>
Navigator.of(context).pushNamed(Routes.newSubaddress), Navigator.of(context).pushNamed(Routes.newSubaddress),
title: S.of(context).addresses, title: S.of(context).silent_addresses,
icon: Icon( icon: Icon(
Icons.add, Icons.add,
size: 20, size: 20,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor, color:
Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
)); ));
} }
@ -165,11 +167,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,
@ -190,6 +200,15 @@ class ReceivePage extends BasePage {
child: cell, child: cell,
); );
})), })),
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)),
),
], ],
), ),
)) ))

View file

@ -2,7 +2,7 @@ import 'package:cake_wallet/core/seed_validator.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Annotation extends Comparable<Annotation> { class Annotation implements Comparable<Annotation> {
Annotation({required this.range, required this.style}); Annotation({required this.range, required this.style});
final TextRange range; final TextRange range;

View file

@ -1,7 +1,6 @@
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'advanced_privacy_settings_view_model.g.dart'; part 'advanced_privacy_settings_view_model.g.dart';
@ -10,7 +9,8 @@ class AdvancedPrivacySettingsViewModel = AdvancedPrivacySettingsViewModelBase
with _$AdvancedPrivacySettingsViewModel; with _$AdvancedPrivacySettingsViewModel;
abstract class AdvancedPrivacySettingsViewModelBase with Store { abstract class AdvancedPrivacySettingsViewModelBase with Store {
AdvancedPrivacySettingsViewModelBase(this.type, this._settingsStore) : _addCustomNode = false; AdvancedPrivacySettingsViewModelBase(this.changeUseTestnet, this._settingsStore)
: _addCustomNode = false;
@computed @computed
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
@ -21,13 +21,20 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
@observable @observable
bool _addCustomNode = false; bool _addCustomNode = false;
final WalletType type; @observable
bool _useTestnet = false;
// TODO: electrum's node as default for testnet
final void Function() changeUseTestnet;
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
@computed @computed
bool get addCustomNode => _addCustomNode; bool get addCustomNode => _addCustomNode;
@computed
bool get useTestnet => _useTestnet;
@action @action
void setFiatApiMode(FiatApiMode fiatApiMode) => _settingsStore.fiatApiMode = fiatApiMode; void setFiatApiMode(FiatApiMode fiatApiMode) => _settingsStore.fiatApiMode = fiatApiMode;
@ -36,4 +43,10 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
@action @action
void toggleAddCustomNode() => _addCustomNode = !_addCustomNode; void toggleAddCustomNode() => _addCustomNode = !_addCustomNode;
@action
void toggleUseTestnet() {
_useTestnet = !_useTestnet;
changeUseTestnet();
}
} }

View file

@ -62,6 +62,8 @@ abstract class NodeCreateOrEditViewModelBase with Store {
bool get hasAuthCredentials => bool get hasAuthCredentials =>
_walletType == WalletType.monero || _walletType == WalletType.haven; _walletType == WalletType.monero || _walletType == WalletType.haven;
bool get hasTestnetSupport => _walletType == WalletType.bitcoin;
String get uri { String get uri {
var uri = address; var uri = address;

View file

@ -115,8 +115,9 @@ 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 = hasAccounts = appStore.wallet!.type == WalletType.bitcoin ||
appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, appStore.wallet!.type == WalletType.monero ||
appStore.wallet!.type == WalletType.haven,
amount = '', amount = '',
super(appStore: appStore) { super(appStore: appStore) {
_init(); _init();
@ -127,7 +128,9 @@ 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 = wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.monero ||
wallet.type == WalletType.haven;
} }
static const String _cryptoNumberPattern = '0.00000000'; static const String _cryptoNumberPattern = '0.00000000';
@ -217,9 +220,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
} }
if (wallet.type == WalletType.bitcoin) { if (wallet.type == WalletType.bitcoin) {
final primaryAddress = bitcoin!.getAddress(wallet); final receiveAddress = bitcoin!.getReceiveAddress(wallet);
final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { final silentAddress = bitcoin!.getSilentAddress(wallet).toString();
final isPrimary = addr == primaryAddress; final bitcoinAddresses = [receiveAddress, silentAddress].map((addr) {
final isPrimary = addr == receiveAddress;
return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr);
}); });
@ -252,7 +256,13 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
} }
@computed @computed
bool get hasAddressList => wallet.type == WalletType.monero || wallet.type == WalletType.haven; bool get hasSilentAddresses => wallet.type == WalletType.bitcoin;
@computed
bool get hasAddressList =>
wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.monero ||
wallet.type == WalletType.haven;
@computed @computed
bool get showElectrumAddressDisclaimer => bool get showElectrumAddressDisclaimer =>

View file

@ -51,7 +51,7 @@ abstract class WalletCreationVMBase with Store {
bool typeExists(WalletType type) bool typeExists(WalletType type)
=> walletCreationService.typeExists(type); => walletCreationService.typeExists(type);
Future<void> create({dynamic options, RestoredWallet? restoreWallet}) async { Future<void> create({dynamic options, RestoredWallet? restoreWallet, bool? isTestnet}) async {
final type = restoreWallet?.type ?? this.type; final type = restoreWallet?.type ?? this.type;
try { try {
state = IsExecutingState(); state = IsExecutingState();

View file

@ -1,6 +1,4 @@
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
@ -25,6 +23,12 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
: selectedMnemonicLanguage = '', : selectedMnemonicLanguage = '',
super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: false); super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: false);
@observable
bool _useTestnet = false;
@computed
bool get useTestnet => _useTestnet;
@observable @observable
String selectedMnemonicLanguage; String selectedMnemonicLanguage;
@ -46,13 +50,16 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
case WalletType.ethereum: case WalletType.ethereum:
return ethereum!.createEthereumNewWalletCredentials(name: name, password: walletPassword); return ethereum!.createEthereumNewWalletCredentials(name: name, password: walletPassword);
default: default:
throw Exception('Unexpected type: ${type.toString()}');; throw Exception('Unexpected type: ${type.toString()}');
} }
} }
@override @override
Future<WalletBase> process(WalletCredentials credentials) async { Future<WalletBase> process(WalletCredentials credentials) async {
walletCreationService.changeWalletType(type: type); walletCreationService.changeWalletType(type: type);
return walletCreationService.create(credentials); return walletCreationService.create(credentials, isTestnet: useTestnet);
} }
@action
void toggleUseTestnet() => _useTestnet = !_useTestnet;
} }

View file

@ -8,7 +8,6 @@
#include <cw_monero/cw_monero_plugin.h> #include <cw_monero/cw_monero_plugin.h>
#include <devicelocale/devicelocale_plugin.h> #include <devicelocale/devicelocale_plugin.h>
#include <platform_device_id_linux/platform_device_id_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
@ -18,9 +17,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) devicelocale_registrar = g_autoptr(FlPluginRegistrar) devicelocale_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DevicelocalePlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "DevicelocalePlugin");
devicelocale_plugin_register_with_registrar(devicelocale_registrar); devicelocale_plugin_register_with_registrar(devicelocale_registrar);
g_autoptr(FlPluginRegistrar) platform_device_id_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PlatformDeviceIdLinuxPlugin");
platform_device_id_linux_plugin_register_with_registrar(platform_device_id_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View file

@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
cw_monero cw_monero
devicelocale devicelocale
platform_device_id_linux
url_launcher_linux url_launcher_linux
) )

View file

@ -12,8 +12,6 @@ import devicelocale
import in_app_review import in_app_review
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
import platform_device_id
import platform_device_id_macos
import share_plus_macos import share_plus_macos
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
@ -27,8 +25,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin"))
PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View file

@ -79,9 +79,7 @@ dependencies:
url_launcher_android: 6.0.24 url_launcher_android: 6.0.24
sensitive_clipboard: ^1.0.0 sensitive_clipboard: ^1.0.0
bitcoin_flutter: bitcoin_flutter:
git: path: /home/rafael/Storage/Repositories/btc-silent-payments/bitcoin_flutter
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -697,6 +697,8 @@
"buy_provider_unavailable": "مزود حاليا غير متوفر.", "buy_provider_unavailable": "مزود حاليا غير متوفر.",
"do_not_have_enough_gas_asset": "ليس لديك ما يكفي من ${currency} لإجراء معاملة وفقًا لشروط شبكة blockchain الحالية. أنت بحاجة إلى المزيد من ${currency} لدفع رسوم شبكة blockchain، حتى لو كنت ترسل أصلًا مختلفًا.", "do_not_have_enough_gas_asset": "ليس لديك ما يكفي من ${currency} لإجراء معاملة وفقًا لشروط شبكة blockchain الحالية. أنت بحاجة إلى المزيد من ${currency} لدفع رسوم شبكة blockchain، حتى لو كنت ترسل أصلًا مختلفًا.",
"totp_auth_url": " TOTP ﺔﻗﺩﺎﺼﻤﻟ URL ﻥﺍﻮﻨﻋ" "totp_auth_url": " TOTP ﺔﻗﺩﺎﺼﻤﻟ URL ﻥﺍﻮﻨﻋ",
"use_testnet": "استخدم testnet",
"address_and_silent_addresses": "العنوان والعناوين الصامتة",
"silent_addresses": "عناوين صامتة"
} }

View file

@ -692,5 +692,8 @@
"ask_each_time": "Питайте всеки път", "ask_each_time": "Питайте всеки път",
"buy_provider_unavailable": "Понастоящем доставчик не е наличен.", "buy_provider_unavailable": "Понастоящем доставчик не е наличен.",
"do_not_have_enough_gas_asset": "Нямате достатъчно ${currency}, за да извършите транзакция с текущите условия на блокчейн мрежата. Имате нужда от повече ${currency}, за да платите таксите за блокчейн мрежа, дори ако изпращате различен актив.", "do_not_have_enough_gas_asset": "Нямате достатъчно ${currency}, за да извършите транзакция с текущите условия на блокчейн мрежата. Имате нужда от повече ${currency}, за да платите таксите за блокчейн мрежа, дори ако изпращате различен актив.",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "Използвайте TestNet",
"address_and_silent_addresses": "Адрес и мълчаливи адреси",
"silent_addresses": "Безшумни адреси"
} }

View file

@ -692,5 +692,8 @@
"ask_each_time": "Zeptejte se pokaždé", "ask_each_time": "Zeptejte se pokaždé",
"buy_provider_unavailable": "Poskytovatel aktuálně nedostupný.", "buy_provider_unavailable": "Poskytovatel aktuálně nedostupný.",
"do_not_have_enough_gas_asset": "Nemáte dostatek ${currency} k provedení transakce s aktuálními podmínkami blockchainové sítě. K placení poplatků za blockchainovou síť potřebujete více ${currency}, i když posíláte jiné aktivum.", "do_not_have_enough_gas_asset": "Nemáte dostatek ${currency} k provedení transakce s aktuálními podmínkami blockchainové sítě. K placení poplatků za blockchainovou síť potřebujete více ${currency}, i když posíláte jiné aktivum.",
"totp_auth_url": "URL AUTH TOTP" "totp_auth_url": "URL AUTH TOTP",
"use_testnet": "Použijte testNet",
"address_and_silent_addresses": "Adresa a tiché adresy",
"silent_addresses": "Tiché adresy"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Jedes Mal fragen", "ask_each_time": "Jedes Mal fragen",
"buy_provider_unavailable": "Anbieter derzeit nicht verfügbar.", "buy_provider_unavailable": "Anbieter derzeit nicht verfügbar.",
"do_not_have_enough_gas_asset": "Sie verfügen nicht über genügend ${currency}, um eine Transaktion unter den aktuellen Bedingungen des Blockchain-Netzwerks durchzuführen. Sie benötigen mehr ${currency}, um die Gebühren für das Blockchain-Netzwerk zu bezahlen, auch wenn Sie einen anderen Vermögenswert senden.", "do_not_have_enough_gas_asset": "Sie verfügen nicht über genügend ${currency}, um eine Transaktion unter den aktuellen Bedingungen des Blockchain-Netzwerks durchzuführen. Sie benötigen mehr ${currency}, um die Gebühren für das Blockchain-Netzwerk zu bezahlen, auch wenn Sie einen anderen Vermögenswert senden.",
"totp_auth_url": "TOTP-Auth-URL" "totp_auth_url": "TOTP-Auth-URL",
"use_testnet": "TESTNET verwenden",
"address_and_silent_addresses": "Adresse und stille Adressen",
"silent_addresses": "Stille Adressen"
} }

View file

@ -701,5 +701,8 @@
"robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.", "robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.",
"buy_provider_unavailable": "Provider currently unavailable.", "buy_provider_unavailable": "Provider currently unavailable.",
"do_not_have_enough_gas_asset": "You do not have enough ${currency} to make a transaction with the current blockchain network conditions. You need more ${currency} to pay blockchain network fees, even if you are sending a different asset.", "do_not_have_enough_gas_asset": "You do not have enough ${currency} to make a transaction with the current blockchain network conditions. You need more ${currency} to pay blockchain network fees, even if you are sending a different asset.",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "Use testnet",
"address_and_silent_addresses": "Address and Silent Addresses",
"silent_addresses": "Silent Addresses"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Pregunta cada vez", "ask_each_time": "Pregunta cada vez",
"buy_provider_unavailable": "Proveedor actualmente no disponible.", "buy_provider_unavailable": "Proveedor actualmente no disponible.",
"do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.", "do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.",
"totp_auth_url": "URL de autenticación TOTP" "totp_auth_url": "URL de autenticación TOTP",
"use_testnet": "Use TestNet",
"address_and_silent_addresses": "Dirección y direcciones silenciosas",
"silent_addresses": "Direcciones silenciosas"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Demandez à chaque fois", "ask_each_time": "Demandez à chaque fois",
"buy_provider_unavailable": "Fournisseur actuellement indisponible.", "buy_provider_unavailable": "Fournisseur actuellement indisponible.",
"do_not_have_enough_gas_asset": "Vous n'avez pas assez de ${currency} pour effectuer une transaction avec les conditions actuelles du réseau blockchain. Vous avez besoin de plus de ${currency} pour payer les frais du réseau blockchain, même si vous envoyez un actif différent.", "do_not_have_enough_gas_asset": "Vous n'avez pas assez de ${currency} pour effectuer une transaction avec les conditions actuelles du réseau blockchain. Vous avez besoin de plus de ${currency} pour payer les frais du réseau blockchain, même si vous envoyez un actif différent.",
"totp_auth_url": "URL D'AUTORISATION TOTP" "totp_auth_url": "URL D'AUTORISATION TOTP",
"use_testnet": "Utiliser TestNet",
"address_and_silent_addresses": "Adresse et adresses silencieuses",
"silent_addresses": "Adresses silencieuses"
} }

View file

@ -678,5 +678,8 @@
"ask_each_time": "Tambaya kowane lokaci", "ask_each_time": "Tambaya kowane lokaci",
"buy_provider_unavailable": "Mai ba da kyauta a halin yanzu babu.", "buy_provider_unavailable": "Mai ba da kyauta a halin yanzu babu.",
"do_not_have_enough_gas_asset": "Ba ku da isassun ${currency} don yin ma'amala tare da yanayin cibiyar sadarwar blockchain na yanzu. Kuna buƙatar ƙarin ${currency} don biyan kuɗaɗen cibiyar sadarwar blockchain, koda kuwa kuna aika wata kadara daban.", "do_not_have_enough_gas_asset": "Ba ku da isassun ${currency} don yin ma'amala tare da yanayin cibiyar sadarwar blockchain na yanzu. Kuna buƙatar ƙarin ${currency} don biyan kuɗaɗen cibiyar sadarwar blockchain, koda kuwa kuna aika wata kadara daban.",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "Amfani da gwaji",
"address_and_silent_addresses": "Adireshin da adreshin shiru",
"silent_addresses": "Adireshin Shiru"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "हर बार पूछें", "ask_each_time": "हर बार पूछें",
"buy_provider_unavailable": "वर्तमान में प्रदाता अनुपलब्ध है।", "buy_provider_unavailable": "वर्तमान में प्रदाता अनुपलब्ध है।",
"do_not_have_enough_gas_asset": "वर्तमान ब्लॉकचेन नेटवर्क स्थितियों में लेनदेन करने के लिए आपके पास पर्याप्त ${currency} नहीं है। ब्लॉकचेन नेटवर्क शुल्क का भुगतान करने के लिए आपको अधिक ${currency} की आवश्यकता है, भले ही आप एक अलग संपत्ति भेज रहे हों।", "do_not_have_enough_gas_asset": "वर्तमान ब्लॉकचेन नेटवर्क स्थितियों में लेनदेन करने के लिए आपके पास पर्याप्त ${currency} नहीं है। ब्लॉकचेन नेटवर्क शुल्क का भुगतान करने के लिए आपको अधिक ${currency} की आवश्यकता है, भले ही आप एक अलग संपत्ति भेज रहे हों।",
"totp_auth_url": "TOTP प्रामाणिक यूआरएल" "totp_auth_url": "TOTP प्रामाणिक यूआरएल",
"use_testnet": "टेस्टनेट का उपयोग करें",
"address_and_silent_addresses": "पता और मूक पते",
"silent_addresses": "मूक पते"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Pitajte svaki put", "ask_each_time": "Pitajte svaki put",
"buy_provider_unavailable": "Davatelj trenutno nije dostupan.", "buy_provider_unavailable": "Davatelj trenutno nije dostupan.",
"do_not_have_enough_gas_asset": "Nemate dovoljno ${currency} da izvršite transakciju s trenutačnim uvjetima blockchain mreže. Trebate više ${currency} da platite naknade za blockchain mrežu, čak i ako šaljete drugu imovinu.", "do_not_have_enough_gas_asset": "Nemate dovoljno ${currency} da izvršite transakciju s trenutačnim uvjetima blockchain mreže. Trebate više ${currency} da platite naknade za blockchain mrežu, čak i ako šaljete drugu imovinu.",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
} "use_testnet": "Koristite TestNet",
"address_and_silent_addresses": "Adresa i tihe adrese",
"silent_addresses": "Tihe adrese"
}

View file

@ -688,5 +688,8 @@
"ask_each_time": "Tanyakan setiap kali", "ask_each_time": "Tanyakan setiap kali",
"buy_provider_unavailable": "Penyedia saat ini tidak tersedia.", "buy_provider_unavailable": "Penyedia saat ini tidak tersedia.",
"do_not_have_enough_gas_asset": "Anda tidak memiliki cukup ${currency} untuk melakukan transaksi dengan kondisi jaringan blockchain saat ini. Anda memerlukan lebih banyak ${currency} untuk membayar biaya jaringan blockchain, meskipun Anda mengirimkan aset yang berbeda.", "do_not_have_enough_gas_asset": "Anda tidak memiliki cukup ${currency} untuk melakukan transaksi dengan kondisi jaringan blockchain saat ini. Anda memerlukan lebih banyak ${currency} untuk membayar biaya jaringan blockchain, meskipun Anda mengirimkan aset yang berbeda.",
"totp_auth_url": "URL Otentikasi TOTP" "totp_auth_url": "URL Otentikasi TOTP",
} "use_testnet": "Gunakan TestNet",
"address_and_silent_addresses": "Alamat dan alamat diam",
"silent_addresses": "Alamat diam"
}

View file

@ -700,5 +700,8 @@
"ask_each_time": "Chiedi ogni volta", "ask_each_time": "Chiedi ogni volta",
"buy_provider_unavailable": "Provider attualmente non disponibile.", "buy_provider_unavailable": "Provider attualmente non disponibile.",
"do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.", "do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.",
"totp_auth_url": "URL DI AUT. TOTP" "totp_auth_url": "URL DI AUT. TOTP",
"use_testnet": "Usa TestNet",
"address_and_silent_addresses": "Indirizzo e indirizzi silenziosi",
"silent_addresses": "Indirizzi silenziosi"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "毎回尋ねてください", "ask_each_time": "毎回尋ねてください",
"buy_provider_unavailable": "現在、プロバイダーは利用できません。", "buy_provider_unavailable": "現在、プロバイダーは利用できません。",
"do_not_have_enough_gas_asset": "現在のブロックチェーン ネットワークの状況では、トランザクションを行うのに十分な ${currency} がありません。別のアセットを送信する場合でも、ブロックチェーン ネットワーク料金を支払うにはさらに ${currency} が必要です。", "do_not_have_enough_gas_asset": "現在のブロックチェーン ネットワークの状況では、トランザクションを行うのに十分な ${currency} がありません。別のアセットを送信する場合でも、ブロックチェーン ネットワーク料金を支払うにはさらに ${currency} が必要です。",
"totp_auth_url": "TOTP認証URL" "totp_auth_url": "TOTP認証URL",
"use_testnet": "TestNetを使用します",
"address_and_silent_addresses": "住所とサイレントアドレス",
"silent_addresses": "サイレントアドレス"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "매번 물어보십시오", "ask_each_time": "매번 물어보십시오",
"buy_provider_unavailable": "제공자는 현재 사용할 수 없습니다.", "buy_provider_unavailable": "제공자는 현재 사용할 수 없습니다.",
"do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.", "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.",
"totp_auth_url": "TOTP 인증 URL" "totp_auth_url": "TOTP 인증 URL",
"use_testnet": "TestNet을 사용하십시오",
"address_and_silent_addresses": "주소 및 조용한 주소",
"silent_addresses": "조용한 주소"
} }

View file

@ -698,5 +698,8 @@
"ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ", "ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ",
"buy_provider_unavailable": "လက်ရှိတွင်လက်ရှိမရနိုင်ပါ။", "buy_provider_unavailable": "လက်ရှိတွင်လက်ရှိမရနိုင်ပါ။",
"do_not_have_enough_gas_asset": "လက်ရှိ blockchain ကွန်ရက်အခြေအနေများနှင့် အရောင်းအဝယ်ပြုလုပ်ရန် သင့်တွင် ${currency} လုံလောက်မှုမရှိပါ။ သင်သည် မတူညီသော ပိုင်ဆိုင်မှုတစ်ခုကို ပေးပို့နေသော်လည်း blockchain ကွန်ရက်အခကြေးငွေကို ပေးဆောင်ရန် သင်သည် နောက်ထပ် ${currency} လိုအပ်ပါသည်။", "do_not_have_enough_gas_asset": "လက်ရှိ blockchain ကွန်ရက်အခြေအနေများနှင့် အရောင်းအဝယ်ပြုလုပ်ရန် သင့်တွင် ${currency} လုံလောက်မှုမရှိပါ။ သင်သည် မတူညီသော ပိုင်ဆိုင်မှုတစ်ခုကို ပေးပို့နေသော်လည်း blockchain ကွန်ရက်အခကြေးငွေကို ပေးဆောင်ရန် သင်သည် နောက်ထပ် ${currency} လိုအပ်ပါသည်။",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "testnet ကိုသုံးပါ",
"address_and_silent_addresses": "လိပ်စာနှင့်အသံတိတ်လိပ်စာများ",
"silent_addresses": "အသံတိတ်လိပ်စာများ"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Vraag het elke keer", "ask_each_time": "Vraag het elke keer",
"buy_provider_unavailable": "Provider momenteel niet beschikbaar.", "buy_provider_unavailable": "Provider momenteel niet beschikbaar.",
"do_not_have_enough_gas_asset": "U heeft niet genoeg ${currency} om een transactie uit te voeren met de huidige blockchain-netwerkomstandigheden. U heeft meer ${currency} nodig om blockchain-netwerkkosten te betalen, zelfs als u een ander item verzendt.", "do_not_have_enough_gas_asset": "U heeft niet genoeg ${currency} om een transactie uit te voeren met de huidige blockchain-netwerkomstandigheden. U heeft meer ${currency} nodig om blockchain-netwerkkosten te betalen, zelfs als u een ander item verzendt.",
"totp_auth_url": "TOTP AUTH-URL" "totp_auth_url": "TOTP AUTH-URL",
"use_testnet": "Gebruik testnet",
"address_and_silent_addresses": "Adres en stille adressen",
"silent_addresses": "Stille adressen"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Zapytaj za każdym razem", "ask_each_time": "Zapytaj za każdym razem",
"buy_provider_unavailable": "Dostawca obecnie niedostępny.", "buy_provider_unavailable": "Dostawca obecnie niedostępny.",
"do_not_have_enough_gas_asset": "Nie masz wystarczającej ilości ${currency}, aby dokonać transakcji przy bieżących warunkach sieci blockchain. Potrzebujesz więcej ${currency}, aby uiścić opłaty za sieć blockchain, nawet jeśli wysyłasz inny zasób.", "do_not_have_enough_gas_asset": "Nie masz wystarczającej ilości ${currency}, aby dokonać transakcji przy bieżących warunkach sieci blockchain. Potrzebujesz więcej ${currency}, aby uiścić opłaty za sieć blockchain, nawet jeśli wysyłasz inny zasób.",
"totp_auth_url": "Adres URL TOTP AUTH" "totp_auth_url": "Adres URL TOTP AUTH",
"use_testnet": "Użyj testne",
"address_and_silent_addresses": "Adres i ciche adresy",
"silent_addresses": "Ciche adresy"
} }

View file

@ -699,5 +699,8 @@
"ask_each_time": "Pergunte cada vez", "ask_each_time": "Pergunte cada vez",
"buy_provider_unavailable": "Provedor atualmente indisponível.", "buy_provider_unavailable": "Provedor atualmente indisponível.",
"do_not_have_enough_gas_asset": "Você não tem ${currency} suficiente para fazer uma transação com as condições atuais da rede blockchain. Você precisa de mais ${currency} para pagar as taxas da rede blockchain, mesmo se estiver enviando um ativo diferente.", "do_not_have_enough_gas_asset": "Você não tem ${currency} suficiente para fazer uma transação com as condições atuais da rede blockchain. Você precisa de mais ${currency} para pagar as taxas da rede blockchain, mesmo se estiver enviando um ativo diferente.",
"totp_auth_url": "URL de autenticação TOTP" "totp_auth_url": "URL de autenticação TOTP",
"use_testnet": "Use testNet",
"address_and_silent_addresses": "Endereço e endereços silenciosos",
"silent_addresses": "Endereços silenciosos"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Спросите каждый раз", "ask_each_time": "Спросите каждый раз",
"buy_provider_unavailable": "Поставщик в настоящее время недоступен.", "buy_provider_unavailable": "Поставщик в настоящее время недоступен.",
"do_not_have_enough_gas_asset": "У вас недостаточно ${currency} для совершения транзакции при текущих условиях сети блокчейн. Вам нужно больше ${currency} для оплаты комиссий за сеть блокчейна, даже если вы отправляете другой актив.", "do_not_have_enough_gas_asset": "У вас недостаточно ${currency} для совершения транзакции при текущих условиях сети блокчейн. Вам нужно больше ${currency} для оплаты комиссий за сеть блокчейна, даже если вы отправляете другой актив.",
"totp_auth_url": "URL-адрес TOTP-АВТОРИЗАЦИИ" "totp_auth_url": "URL-адрес TOTP-АВТОРИЗАЦИИ",
"use_testnet": "Используйте Testnet",
"address_and_silent_addresses": "Адрес и молчаливые адреса",
"silent_addresses": "Молчаливые адреса"
} }

View file

@ -698,5 +698,8 @@
"ask_each_time": "ถามทุกครั้ง", "ask_each_time": "ถามทุกครั้ง",
"buy_provider_unavailable": "ผู้ให้บริการไม่สามารถใช้งานได้ในปัจจุบัน", "buy_provider_unavailable": "ผู้ให้บริการไม่สามารถใช้งานได้ในปัจจุบัน",
"do_not_have_enough_gas_asset": "คุณมี ${currency} ไม่เพียงพอที่จะทำธุรกรรมกับเงื่อนไขเครือข่ายบล็อคเชนในปัจจุบัน คุณต้องมี ${currency} เพิ่มขึ้นเพื่อชำระค่าธรรมเนียมเครือข่ายบล็อคเชน แม้ว่าคุณจะส่งสินทรัพย์อื่นก็ตาม", "do_not_have_enough_gas_asset": "คุณมี ${currency} ไม่เพียงพอที่จะทำธุรกรรมกับเงื่อนไขเครือข่ายบล็อคเชนในปัจจุบัน คุณต้องมี ${currency} เพิ่มขึ้นเพื่อชำระค่าธรรมเนียมเครือข่ายบล็อคเชน แม้ว่าคุณจะส่งสินทรัพย์อื่นก็ตาม",
"totp_auth_url": "URL การตรวจสอบสิทธิ์ TOTP" "totp_auth_url": "URL การตรวจสอบสิทธิ์ TOTP",
"use_testnet": "ใช้ testnet",
"address_and_silent_addresses": "ที่อยู่และที่อยู่เงียบ",
"silent_addresses": "ที่อยู่เงียบ"
} }

View file

@ -698,5 +698,8 @@
"ask_each_time": "Her seferinde sor", "ask_each_time": "Her seferinde sor",
"buy_provider_unavailable": "Sağlayıcı şu anda kullanılamıyor.", "buy_provider_unavailable": "Sağlayıcı şu anda kullanılamıyor.",
"do_not_have_enough_gas_asset": "Mevcut blockchain ağ koşullarıyla işlem yapmak için yeterli ${currency} paranız yok. Farklı bir varlık gönderiyor olsanız bile blockchain ağ ücretlerini ödemek için daha fazla ${currency} miktarına ihtiyacınız var.", "do_not_have_enough_gas_asset": "Mevcut blockchain ağ koşullarıyla işlem yapmak için yeterli ${currency} paranız yok. Farklı bir varlık gönderiyor olsanız bile blockchain ağ ücretlerini ödemek için daha fazla ${currency} miktarına ihtiyacınız var.",
"totp_auth_url": "TOTP YETKİ URL'si" "totp_auth_url": "TOTP YETKİ URL'si",
"use_testnet": "TestNet kullanın",
"address_and_silent_addresses": "Adres ve sessiz adresler",
"silent_addresses": "Sessiz adresler"
} }

View file

@ -700,5 +700,8 @@
"ask_each_time": "Запитайте кожен раз", "ask_each_time": "Запитайте кожен раз",
"buy_provider_unavailable": "В даний час постачальник недоступний.", "buy_provider_unavailable": "В даний час постачальник недоступний.",
"do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.", "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "Використовуйте тестову мережу",
"address_and_silent_addresses": "Адреса та мовчазні адреси",
"silent_addresses": "Мовчазні адреси"
} }

View file

@ -692,5 +692,8 @@
"ask_each_time": "ہر بار پوچھیں", "ask_each_time": "ہر بار پوچھیں",
"buy_provider_unavailable": "فراہم کنندہ فی الحال دستیاب نہیں ہے۔", "buy_provider_unavailable": "فراہم کنندہ فی الحال دستیاب نہیں ہے۔",
"do_not_have_enough_gas_asset": "آپ کے پاس موجودہ بلاکچین نیٹ ورک کی شرائط کے ساتھ لین دین کرنے کے لیے کافی ${currency} نہیں ہے۔ آپ کو بلاکچین نیٹ ورک کی فیس ادا کرنے کے لیے مزید ${currency} کی ضرورت ہے، چاہے آپ کوئی مختلف اثاثہ بھیج رہے ہوں۔", "do_not_have_enough_gas_asset": "آپ کے پاس موجودہ بلاکچین نیٹ ورک کی شرائط کے ساتھ لین دین کرنے کے لیے کافی ${currency} نہیں ہے۔ آپ کو بلاکچین نیٹ ورک کی فیس ادا کرنے کے لیے مزید ${currency} کی ضرورت ہے، چاہے آپ کوئی مختلف اثاثہ بھیج رہے ہوں۔",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "ٹیسٹ نیٹ استعمال کریں",
"address_and_silent_addresses": "پتہ اور خاموش پتے",
"silent_addresses": "خاموش پتے"
} }

View file

@ -694,5 +694,8 @@
"ask_each_time": "Beere lọwọ kọọkan", "ask_each_time": "Beere lọwọ kọọkan",
"buy_provider_unavailable": "Olupese lọwọlọwọ ko si.", "buy_provider_unavailable": "Olupese lọwọlọwọ ko si.",
"do_not_have_enough_gas_asset": "O ko ni to ${currency} lati ṣe idunadura kan pẹlu awọn ipo nẹtiwọki blockchain lọwọlọwọ. O nilo diẹ sii ${currency} lati san awọn owo nẹtiwọọki blockchain, paapaa ti o ba nfi dukia miiran ranṣẹ.", "do_not_have_enough_gas_asset": "O ko ni to ${currency} lati ṣe idunadura kan pẹlu awọn ipo nẹtiwọki blockchain lọwọlọwọ. O nilo diẹ sii ${currency} lati san awọn owo nẹtiwọọki blockchain, paapaa ti o ba nfi dukia miiran ranṣẹ.",
"totp_auth_url": "TOTP AUTH URL" "totp_auth_url": "TOTP AUTH URL",
"use_testnet": "Lo tele",
"address_and_silent_addresses": "Adirẹsi ati awọn adirẹsi ipalọlọ",
"silent_addresses": "Awọn adirẹsi ipalọlọ"
} }

View file

@ -699,5 +699,8 @@
"ask_each_time": "每次问", "ask_each_time": "每次问",
"buy_provider_unavailable": "提供者目前不可用。", "buy_provider_unavailable": "提供者目前不可用。",
"do_not_have_enough_gas_asset": "您没有足够的 ${currency} 来在当前的区块链网络条件下进行交易。即使您发送的是不同的资产,您也需要更多的 ${currency} 来支付区块链网络费用。", "do_not_have_enough_gas_asset": "您没有足够的 ${currency} 来在当前的区块链网络条件下进行交易。即使您发送的是不同的资产,您也需要更多的 ${currency} 来支付区块链网络费用。",
"totp_auth_url": "TOTP 授权 URL" "totp_auth_url": "TOTP 授权 URL",
"use_testnet": "使用TestNet",
"address_and_silent_addresses": "地址和无声地址",
"silent_addresses": "无声地址"
} }

View file

@ -49,6 +49,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc;
"""; """;
const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinCwPart = "part 'cw_bitcoin.dart';";
const bitcoinContent = """ const bitcoinContent = """
@ -71,6 +72,8 @@ abstract class Bitcoin {
List<String> getAddresses(Object wallet); List<String> getAddresses(Object wallet);
String getAddress(Object wallet); String getAddress(Object wallet);
String getReceiveAddress(Object wallet);
btc.SilentPaymentAddress? getSilentAddress(Object wallet);
String formatterBitcoinAmountToString({required int amount}); String formatterBitcoinAmountToString({required int amount});
double formatterBitcoinAmountToDouble({required int amount}); double formatterBitcoinAmountToDouble({required int amount});
@ -729,4 +732,4 @@ class FakeSecureStorage extends SecureStorage {
} }
await outputFile.writeAsString(output); await outputFile.writeAsString(output);
} }