decred: Add send transaction.

This commit is contained in:
JoeGruff 2024-02-06 19:58:19 +09:00
parent 0328f9afb4
commit f0a8e3e879
12 changed files with 257 additions and 37 deletions

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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

View file

@ -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(

View file

@ -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:

View file

@ -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

View file

@ -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');
}

View file

@ -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';

View file

@ -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,

View file

@ -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

View file

@ -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();
}

View file

@ -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);