diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index 367b0e013..12308b8ef 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -151,3 +151,54 @@ int calculateEstimatedFeeWithFeeRate(int feeRate, int amount) { // the fee we get back. TODO. return 123000; } + +String createSignedTransaction( + String walletName, String createSignedTransactionReq) { + final cName = walletName.toCString(); + final cCreateSignedTransactionReq = createSignedTransactionReq.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.createSignedTransaction( + cName, cCreateSignedTransactionReq), + ptrsToFree: [cName, cCreateSignedTransactionReq], + ); + return res.payload; +} + +String sendRawTransaction(String walletName, String txHex) { + final cName = walletName.toCString(); + final cTxHex = txHex.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex), + ptrsToFree: [cName, cTxHex], + ); + return res.payload; +} + +String listTransactions(String walletName, String from, String count) { + final cName = walletName.toCString(); + final cFrom = from.toCString(); + final cCount = count.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount), + ptrsToFree: [cName, cFrom, cCount], + ); + return res.payload; +} + +String bestBlock(String walletName) { + final cName = walletName.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.bestBlock(cName), + ptrsToFree: [cName], + ); + return res.payload; +} + +String listUnspents(String walletName) { + final cName = walletName.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.listUnspents(cName), + ptrsToFree: [cName], + ); + return res.payload; +} diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart index a78411ae7..bd16db859 100644 --- a/cw_decred/lib/pending_transaction.dart +++ b/cw_decred/lib/pending_transaction.dart @@ -6,12 +6,14 @@ class DecredPendingTransaction with PendingTransaction { {required this.txid, required this.amount, required this.fee, - required this.rawHex}); + required this.rawHex, + required this.send}); final int amount; final int fee; final String txid; final String rawHex; + final Future<void> Function() send; @override String get id => txid; @@ -27,6 +29,6 @@ class DecredPendingTransaction with PendingTransaction { @override Future<void> commit() async { - // TODO: Submit rawHex using libdcrwallet. + return send(); } } diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 9cc6b6ab3..32e7d8a48 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -1,10 +1,13 @@ +import 'dart:developer'; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_decred/pending_transaction.dart'; +import 'package:cw_decred/transaction_credentials.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; import 'package:cw_decred/api/libdcrwallet.dart' as libdcrwallet; import 'package:cw_decred/transaction_history.dart'; @@ -20,6 +23,7 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; part 'wallet.g.dart'; @@ -28,21 +32,25 @@ class DecredWallet = DecredWalletBase with _$DecredWallet; abstract class DecredWalletBase extends WalletBase<DecredBalance, DecredTransactionHistory, DecredTransactionInfo> with Store { - DecredWalletBase(WalletInfo walletInfo, String password) + DecredWalletBase(WalletInfo walletInfo, String password, + Box<UnspentCoinsInfo> unspentCoinsInfo) : _password = password, - syncStatus = NotConnectedSyncStatus(), - balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), + this.syncStatus = NotConnectedSyncStatus(), + this.unspentCoinsInfo = unspentCoinsInfo, + this.balance = + ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), super(walletInfo) { walletAddresses = DecredWalletAddresses(walletInfo); transactionHistory = DecredTransactionHistory(); } - // password is currently only used for seed display, but would likely also be - // required to sign inputs when creating transactions. + final defaultFeeRate = 10000; final String _password; + final idPrefix = "decred_"; bool connecting = false; String persistantPeer = ""; Timer? syncTimer; + Box<UnspentCoinsInfo> unspentCoinsInfo; @override @observable @@ -204,12 +212,55 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, @override Future<PendingTransaction> createTransaction(Object credentials) async { + final inputs = []; + this.unspentCoinsInfo.values.forEach((unspent) { + if (unspent.isSending) { + final input = {"txid": unspent.hash, "vout": unspent.vout}; + inputs.add(input); + } + }); + final ignoreInputs = []; + this.unspentCoinsInfo.values.forEach((unspent) { + if (unspent.isFrozen) { + final input = {"txid": unspent.hash, "vout": unspent.vout}; + ignoreInputs.add(input); + } + }); + final creds = credentials as DecredTransactionCredentials; + var totalAmt = 0; + final outputs = []; + for (final out in creds.outputs) { + var amt = 0; + if (out.cryptoAmount != null) { + final coins = double.parse(out.cryptoAmount!); + amt = (coins * 1e8).toInt(); + } + totalAmt += amt; + final o = {"address": out.address, "amount": amt}; + outputs.add(o); + } + ; + // TODO: Fix fee rate. + final signReq = { + "inputs": inputs, + "ignoreInputs": ignoreInputs, + "outputs": outputs, + "feerate": creds.feeRate ?? defaultFeeRate, + "password": _password, + }; + final res = libdcrwallet.createSignedTransaction( + walletInfo.name, jsonEncode(signReq)); + final decoded = json.decode(res); + final signedHex = decoded["signedhex"]; + final send = () async { + libdcrwallet.sendRawTransaction(walletInfo.name, signedHex); + }; return DecredPendingTransaction( - txid: - "3cbf3eb9523fd04e96dbaf98cdbd21779222cc8855ece8700494662ae7578e02", - amount: 12345678, - fee: 1234, - rawHex: "baadbeef"); + txid: decoded["txid"] ?? "", + amount: totalAmt, + fee: decoded["fee"] ?? 0, + rawHex: signedHex, + send: send); } int feeRate(TransactionPriority priority) { @@ -302,14 +353,80 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, } List<Unspent> unspents() { - return [ - Unspent( - "DsT4qJPPaYEuQRimfgvSKxKH3paysn1x3Nt", - "3cbf3eb9523fd04e96dbaf98cdbd21779222cc8855ece8700494662ae7578e02", - 1234567, - 0, - null) - ]; + final res = libdcrwallet.listUnspents(walletInfo.name); + final decoded = json.decode(res); + var unspents = <Unspent>[]; + for (final d in decoded) { + final spendable = d["spendable"] ?? false; + if (!spendable) { + continue; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final utxo = Unspent( + d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null); + utxo.isChange = d["ischange"] ?? false; + unspents.add(utxo); + } + this.updateUnspents(unspents); + return unspents; + } + + void updateUnspents(List<Unspent> unspentCoins) { + if (this.unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => this.addCoinInfo(coin)); + return; + } + + if (unspentCoins.isEmpty) { + this.unspentCoinsInfo.clear(); + return; + } + + final walletID = idPrefix + walletInfo.name; + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = this.unspentCoinsInfo.values.where((element) => + element.walletId == walletID && + element.hash == coin.hash && + element.vout == coin.vout); + + if (coinInfoList.isEmpty) { + this.addCoinInfo(coin); + } + }); + } + + final List<dynamic> keys = <dynamic>[]; + this.unspentCoinsInfo.values.forEach((element) { + final existUnspentCoins = + unspentCoins.where((coin) => element.hash.contains(coin.hash)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + + if (keys.isNotEmpty) { + unspentCoinsInfo.deleteAll(keys); + } + } + + void addCoinInfo(Unspent coin) { + final newInfo = UnspentCoinsInfo( + walletId: idPrefix + walletInfo.name, + hash: coin.hash, + isFrozen: false, + isSending: false, + noteRaw: "", + address: coin.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + keyImage: coin.keyImage, + ); + + unspentCoinsInfo.add(newInfo); } @override diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index f6c6ffcf5..9c3bfdf40 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -9,15 +9,17 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; +import 'package:cw_core/unspent_coins_info.dart'; class DecredWalletService extends WalletService< DecredNewWalletCredentials, DecredRestoreWalletFromSeedCredentials, DecredRestoreWalletFromPubkeyCredentials, DecredRestoreWalletFromHardwareCredentials> { - DecredWalletService(this.walletInfoSource); + DecredWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box<WalletInfo> walletInfoSource; + final Box<UnspentCoinsInfo> unspentCoinsInfoSource; static void init() async { // Use the general path for all dcr wallets as the general log directory. @@ -41,7 +43,8 @@ class DecredWalletService extends WalletService< dataDir: credentials.walletInfo!.dirPath, password: credentials.password!, ); - final wallet = DecredWallet(credentials.walletInfo!, credentials.password!); + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -54,7 +57,8 @@ class DecredWalletService extends WalletService< name: walletInfo.name, dataDir: walletInfo.dirPath, ); - final wallet = DecredWallet(walletInfo, password); + final wallet = + DecredWallet(walletInfo, password, this.unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -73,7 +77,8 @@ class DecredWalletService extends WalletService< String currentName, String password, String newName) async { final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( (info) => info.id == WalletBase.idFor(currentName, getType()))!; - final currentWallet = DecredWallet(currentWalletInfo, password); + final currentWallet = + DecredWallet(currentWalletInfo, password, this.unspentCoinsInfoSource); await currentWallet.renameWalletFiles(newName); @@ -95,9 +100,21 @@ class DecredWalletService extends WalletService< @override Future<DecredWallet> restoreFromKeys( - DecredRestoreWalletFromWIFCredentials credentials, - {bool? isTestnet}) async => - throw UnimplementedError(); + DecredRestoreWalletFromPubkeyCredentials credentials, + {bool? isTestnet}) async { + createWatchOnlyWallet( + credentials.walletInfo!.name, + credentials.walletInfo!.dirPath, + credentials.pubkey, + isTestnet == true ? testnet : mainnet, + ); + credentials.walletInfo!.derivationPath = + isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource); + await wallet.init(); + return wallet; + } @override Future<DecredWallet> restoreFromHardwareWallet( diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index dbb6f9541..854198aeb 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -115,7 +115,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zec: pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - pattern = 'D[ksecS]([0-9a-zA-Z])+'; + pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index ccc52ba56..9c8f0dd2a 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -19,8 +19,9 @@ class CWDecred extends Decred { DecredRestoreWalletFromSeedCredentials( name: name, mnemonic: mnemonic, password: password); - WalletService createDecredWalletService(Box<WalletInfo> walletInfoSource) { - return DecredWalletService(walletInfoSource); + WalletService createDecredWalletService(Box<WalletInfo> walletInfoSource, + Box<UnspentCoinsInfo> unspentCoinSource) { + return DecredWalletService(walletInfoSource, unspentCoinSource); } @override diff --git a/lib/di.dart b/lib/di.dart index bb3b3952a..8b3fcc2ca 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1095,7 +1095,7 @@ Future<void> setup({ case WalletType.wownero: return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.decred: - return decred!.createDecredWalletService(_walletInfoSource); + return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index b637db19b..725a0b9b8 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -47,6 +47,7 @@ class PreferencesKey { static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; + static const decredTransactionPriority = 'current_fee_priority_decred'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 94adb4237..1b801a77a 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; @@ -131,6 +132,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialEthereumTransactionPriority, TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority, + TransactionPriority? initialDecredTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap<WalletType, Node>.of(nodes), powNodes = ObservableMap<WalletType, Node>.of(powNodes), @@ -214,6 +216,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; } + if (initialDecredTransactionPriority != null) { + priority[WalletType.decred] = initialDecredTransactionPriority; + } + if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; } @@ -267,6 +273,9 @@ abstract class SettingsStoreBase with Store { case WalletType.polygon: key = PreferencesKey.polygonTransactionPriority; break; + case WalletType.decred: + key = PreferencesKey.decredTransactionPriority; + break; default: key = null; } @@ -870,6 +879,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? polygonTransactionPriority; TransactionPriority? bitcoinCashTransactionPriority; TransactionPriority? wowneroTransactionPriority; + TransactionPriority? decredTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -895,6 +905,10 @@ abstract class SettingsStoreBase with Store { wowneroTransactionPriority = wownero?.deserializeWowneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.wowneroTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + decredTransactionPriority = decred?.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -903,6 +917,7 @@ abstract class SettingsStoreBase with Store { ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); + decredTransactionPriority ??= decred?.getDecredTransactionPriorityMedium(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( @@ -1265,6 +1280,7 @@ abstract class SettingsStoreBase with Store { initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, + initialDecredTransactionPriority: decredTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index b6ce2a656..6caffb067 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -279,6 +279,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type == WalletType.litecoin || wallet.type == WalletType.monero || wallet.type == WalletType.wownero || + wallet.type == WalletType.decred || wallet.type == WalletType.bitcoinCash; @computed diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 52820adcb..de9ced6bc 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -94,6 +95,8 @@ abstract class UnspentCoinsListViewModelBase with Store { return wownero!.formatterWowneroAmountToString(amount: fullBalance); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); + if (wallet.type == WalletType.decred) + return decred!.formatterDecredAmountToString(amount: fullBalance); return ''; } @@ -107,7 +110,9 @@ abstract class UnspentCoinsListViewModelBase with Store { if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) { await bitcoin!.updateUnspents(wallet); } - + if (wallet.type == WalletType.decred) { + decred!.updateUnspents(wallet); + } _updateUnspentCoinsInfo(); } @@ -121,6 +126,8 @@ abstract class UnspentCoinsListViewModelBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); + case WalletType.decred: + return decred!.getUnspents(wallet); default: return List.empty(); } diff --git a/tool/configure.dart b/tool/configure.dart index 2391982f1..07b30f90d 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1416,6 +1416,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:hive/hive.dart'; """; @@ -1431,18 +1432,24 @@ import 'package:cw_decred/transaction_credentials.dart'; const decredContent = """ abstract class Decred { - WalletCredentials createDecredNewWalletCredentials({required String name, WalletInfo? walletInfo}); - WalletCredentials createDecredRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); - WalletService createDecredWalletService(Box<WalletInfo> walletInfoSource); + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}); + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, + required String mnemonic, + required String password}); + WalletService createDecredWalletService(Box<WalletInfo> walletInfoSource, + Box<UnspentCoinsInfo> unspentCoinSource); List<TransactionPriority> getTransactionPriorities(); TransactionPriority getMediumTransactionPriority(); TransactionPriority getDecredTransactionPriorityMedium(); TransactionPriority getDecredTransactionPrioritySlow(); TransactionPriority deserializeDecredTransactionPriority(int raw); - + int getFeeRate(Object wallet, TransactionPriority priority); - Object createDecredTransactionCredentials(List<Output> outputs, TransactionPriority priority); + Object createDecredTransactionCredentials( + List<Output> outputs, TransactionPriority priority); List<String> getAddresses(Object wallet); String getAddress(Object wallet);