Merge #565: Update rust-miniscript (and thereby rust-bitcoin) to latest version
a19f2c1536cab6f6a3e85eedcba9be4b907dc33b daemon: drop the base64 dependency (Antoine Poinsot)
96e4cb53537a59f4b2e11011f77e7635ee4a1dd9 Update proc-macro2 to fix a nightly compilation bug (Antoine Poinsot)
b9753b48d03ec7d4398764ec6a91e04388bdb85e descriptors: update the satisfaction size estimation (Antoine Poinsot)
0ac4d80ddbdb61ef3ba9d7e6eb7b76114dfcf961 commands: fix two clippy lints (Antoine Poinsot)
e28010915f4d784ba06a1947ddadaeb5addf6082 lianad: update rust-miniscript (and rust-bitcoin) dependencies (Antoine Poinsot)
Pull request description:
It was a big chunk, especially in making sure we don't introduce any silent bug with all the upstream recent code movements.
Also, we no longer depend on my custom `rust-miniscript` branch! 🎉
ACKs for top commit:
darosior:
Self-ACK a19f2c1536cab6f6a3e85eedcba9be4b907dc33b.
Tree-SHA512: 7b785551b51bd247c233cac8a44148d25832be729772e2987d6e276643d9f781feb5016e81f2914845a2c93542f57ce861d06b31ba6c455f75cf93681fb98805
This commit is contained in:
commit
12a3d50928
51
Cargo.lock
generated
51
Cargo.lock
generated
@ -70,29 +70,45 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.11.0",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.29.2"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3"
|
||||
checksum = "b36f4c848f6bd9ff208128f08751135846cc23ae57d66ab10a22efff1c675f3c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bech32",
|
||||
"bitcoin_hashes",
|
||||
"bitcoin-private",
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-private"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501"
|
||||
dependencies = [
|
||||
"bitcoin-private",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -196,6 +212,12 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.4"
|
||||
@ -219,7 +241,6 @@ name = "liana"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"base64",
|
||||
"bip39",
|
||||
"dirs",
|
||||
"fern",
|
||||
@ -269,10 +290,12 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "miniscript"
|
||||
version = "9.0.0"
|
||||
source = "git+https://github.com/darosior/rust-miniscript?branch=multipath_descriptors_on_9.0#3104519501ce6ad15b36dcec759936f4d3bd3980"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1eb102b66b2127a872dbcc73095b7b47aeb9d92f7b03c2b2298253ffc82c7594"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoin-private",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -308,9 +331,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.47"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
||||
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -391,20 +414,20 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.24.0"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff"
|
||||
checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"secp256k1-sys",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.6.1"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b"
|
||||
checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
@ -25,7 +25,7 @@ daemon = ["libc"]
|
||||
|
||||
[dependencies]
|
||||
# For managing transactions (it re-exports the bitcoin crate)
|
||||
miniscript = { git = "https://github.com/darosior/rust-miniscript", branch = "multipath_descriptors_on_9.0", features = ["serde", "compiler"] }
|
||||
miniscript = { version = "10.0", features = ["serde", "compiler", "base64"] }
|
||||
|
||||
# Don't reinvent the wheel
|
||||
dirs = "5.0"
|
||||
@ -54,9 +54,6 @@ jsonrpc = "0.12"
|
||||
# Used for daemonization
|
||||
libc = { version = "0.2", optional = true }
|
||||
|
||||
# Used for PSBTs
|
||||
base64 = "0.13"
|
||||
|
||||
# Used for generating mnemonics
|
||||
getrandom = "0.2"
|
||||
|
||||
|
||||
4
gui/Cargo.lock
generated
4
gui/Cargo.lock
generated
@ -2788,9 +2788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.56"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
|
||||
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@ -27,7 +27,7 @@ use jsonrpc::{
|
||||
};
|
||||
|
||||
use miniscript::{
|
||||
bitcoin::{self, hashes::hex::FromHex},
|
||||
bitcoin::{self, address, hashes::hex::FromHex},
|
||||
descriptor,
|
||||
};
|
||||
|
||||
@ -651,6 +651,7 @@ impl BitcoinD {
|
||||
bitcoin::Network::Testnet => "test",
|
||||
bitcoin::Network::Regtest => "regtest",
|
||||
bitcoin::Network::Signet => "signet",
|
||||
_ => "Unknown network, undefined at the time of writing",
|
||||
};
|
||||
if bitcoind_net != bip70_net {
|
||||
return Err(BitcoindError::NetworkMismatch(
|
||||
@ -1072,7 +1073,7 @@ pub struct LSBlockEntry {
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub amount: bitcoin::Amount,
|
||||
pub block_height: Option<i32>,
|
||||
pub address: bitcoin::Address,
|
||||
pub address: bitcoin::Address<address::NetworkUnchecked>,
|
||||
pub parent_descs: Vec<descriptor::Descriptor<descriptor::DescriptorPublicKey>>,
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ use crate::{
|
||||
|
||||
use std::{fmt, sync};
|
||||
|
||||
use miniscript::bitcoin;
|
||||
use miniscript::bitcoin::{self, address};
|
||||
|
||||
/// Information about a block
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
||||
@ -408,5 +408,5 @@ pub struct UTxO {
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub amount: bitcoin::Amount,
|
||||
pub block_height: Option<i32>,
|
||||
pub address: bitcoin::Address,
|
||||
pub address: bitcoin::Address<address::NetworkUnchecked>,
|
||||
}
|
||||
|
||||
@ -31,16 +31,28 @@ fn update_coins(
|
||||
descs: &[descriptors::SinglePathLianaDesc],
|
||||
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
||||
) -> UpdatedCoins {
|
||||
let network = db_conn.network();
|
||||
let curr_coins = db_conn.coins(CoinType::All);
|
||||
log::debug!("Current coins: {:?}", curr_coins);
|
||||
|
||||
// Start by fetching newly received coins.
|
||||
let mut received = Vec::new();
|
||||
for utxo in bit.received_coins(previous_tip, descs) {
|
||||
let UTxO {
|
||||
outpoint,
|
||||
amount,
|
||||
address,
|
||||
..
|
||||
} = utxo;
|
||||
// We can only really treat them if we know the derivation index that was used.
|
||||
if let Some((derivation_index, is_change)) =
|
||||
db_conn.derivation_index_by_address(&utxo.address)
|
||||
{
|
||||
let address = match address.require_network(network) {
|
||||
Ok(addr) => addr,
|
||||
Err(e) => {
|
||||
log::error!("Invalid network for address: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some((derivation_index, is_change)) = db_conn.derivation_index_by_address(&address) {
|
||||
// First of if we are receiving coins that are beyond our next derivation index,
|
||||
// adjust it.
|
||||
if derivation_index > db_conn.receive_index() {
|
||||
@ -52,9 +64,6 @@ fn update_coins(
|
||||
|
||||
// Now record this coin as a newly received one.
|
||||
if !curr_coins.contains_key(&utxo.outpoint) {
|
||||
let UTxO {
|
||||
outpoint, amount, ..
|
||||
} = utxo;
|
||||
let coin = Coin {
|
||||
outpoint,
|
||||
amount,
|
||||
@ -71,7 +80,7 @@ fn update_coins(
|
||||
log::error!(
|
||||
"Could not get derivation index for coin '{}' (address: '{}')",
|
||||
&utxo.outpoint,
|
||||
&utxo.address
|
||||
&address
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use utils::{
|
||||
deser_amount_from_sats, deser_base64, deser_hex, ser_amount, ser_base64, ser_hex,
|
||||
to_base64_string,
|
||||
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
|
||||
ser_hex, ser_to_string,
|
||||
};
|
||||
|
||||
use std::{
|
||||
@ -23,8 +23,9 @@ use std::{
|
||||
|
||||
use miniscript::{
|
||||
bitcoin::{
|
||||
self,
|
||||
util::psbt::{Input as PsbtIn, Output as PsbtOut, PartiallySignedTransaction as Psbt},
|
||||
self, address,
|
||||
locktime::absolute,
|
||||
psbt::{Input as PsbtIn, Output as PsbtOut, PartiallySignedTransaction as Psbt},
|
||||
},
|
||||
psbt::PsbtExt,
|
||||
};
|
||||
@ -49,7 +50,7 @@ pub enum CommandError {
|
||||
InvalidFeerate(/* sats/vb */ u64),
|
||||
UnknownOutpoint(bitcoin::OutPoint),
|
||||
AlreadySpent(bitcoin::OutPoint),
|
||||
AddressNetwork(bitcoin::Address, /* Expected */ bitcoin::Network),
|
||||
Address(bitcoin::address::Error),
|
||||
InvalidOutputValue(bitcoin::Amount),
|
||||
InsufficientFunds(
|
||||
/* in value */ bitcoin::Amount,
|
||||
@ -77,10 +78,9 @@ impl fmt::Display for CommandError {
|
||||
Self::InvalidFeerate(sats_vb) => write!(f, "Invalid feerate: {} sats/vb.", sats_vb),
|
||||
Self::AlreadySpent(op) => write!(f, "Coin at '{}' is already spent.", op),
|
||||
Self::UnknownOutpoint(op) => write!(f, "Unknown outpoint '{}'.", op),
|
||||
Self::AddressNetwork(addr, expected) => write!(
|
||||
Self::Address(e) => write!(
|
||||
f,
|
||||
"Invalid network for address '{}'. Our network is '{}' but address is for '{}'.",
|
||||
addr, expected, addr.network
|
||||
"Address error: {}", e
|
||||
),
|
||||
Self::InvalidOutputValue(amount) => write!(f, "Invalid output value '{}'.", amount),
|
||||
Self::InsufficientFunds(in_val, out_val, feerate) => if let Some(out_val) = out_val {
|
||||
@ -115,7 +115,7 @@ impl fmt::Display for CommandError {
|
||||
Self::SanityCheckFailure(psbt) => write!(
|
||||
f,
|
||||
"BUG! Please report this. Failed sanity checks for PSBT '{}'.",
|
||||
to_base64_string(psbt)
|
||||
psbt
|
||||
),
|
||||
Self::UnknownSpend(txid) => write!(f, "Unknown spend transaction '{}'.", txid),
|
||||
Self::SpendFinalization(e) => {
|
||||
@ -141,7 +141,7 @@ impl std::error::Error for CommandError {}
|
||||
// Sanity check the value of a transaction output.
|
||||
fn check_output_value(value: bitcoin::Amount) -> Result<(), CommandError> {
|
||||
// NOTE: the network parameter isn't used upstream
|
||||
if value.to_sat() > bitcoin::blockdata::constants::max_money(bitcoin::Network::Bitcoin)
|
||||
if value.to_sat() > bitcoin::blockdata::constants::MAX_MONEY
|
||||
|| value.to_sat() < DUST_OUTPUT_SATS
|
||||
{
|
||||
Err(CommandError::InvalidOutputValue(value))
|
||||
@ -192,7 +192,7 @@ fn sanity_check_psbt(
|
||||
let value_out: u64 = tx.output.iter().map(|o| o.value).sum();
|
||||
let abs_fee = value_in
|
||||
.checked_sub(value_out)
|
||||
.ok_or_else(|| CommandError::InsaneFees(InsaneFeeInfo::NegativeFee))?;
|
||||
.ok_or(CommandError::InsaneFees(InsaneFeeInfo::NegativeFee))?;
|
||||
if abs_fee > MAX_FEE {
|
||||
return Err(CommandError::InsaneFees(InsaneFeeInfo::TooHighFee(abs_fee)));
|
||||
}
|
||||
@ -201,7 +201,7 @@ fn sanity_check_psbt(
|
||||
let tx_vb = (tx.vsize() + spent_desc.max_sat_vbytes() * tx.input.len()) as u64;
|
||||
let feerate_sats_vb = abs_fee
|
||||
.checked_div(tx_vb)
|
||||
.ok_or_else(|| CommandError::InsaneFees(InsaneFeeInfo::InvalidFeerate))?;
|
||||
.ok_or(CommandError::InsaneFees(InsaneFeeInfo::InvalidFeerate))?;
|
||||
if !(1..=MAX_FEERATE).contains(&feerate_sats_vb) {
|
||||
return Err(CommandError::InsaneFees(InsaneFeeInfo::TooHighFeerate(
|
||||
feerate_sats_vb,
|
||||
@ -235,19 +235,14 @@ impl DaemonControl {
|
||||
}
|
||||
|
||||
// Check whether this address is valid for the network we are operating on.
|
||||
fn validate_address(&self, addr: &bitcoin::Address) -> Result<(), CommandError> {
|
||||
// NOTE: signet uses testnet addresses
|
||||
if addr.network == self.config.bitcoin_config.network
|
||||
|| (addr.network == bitcoin::Network::Testnet
|
||||
&& self.config.bitcoin_config.network == bitcoin::Network::Signet)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(CommandError::AddressNetwork(
|
||||
addr.clone(),
|
||||
self.config.bitcoin_config.network,
|
||||
))
|
||||
fn validate_address(
|
||||
&self,
|
||||
addr: bitcoin::Address<address::NetworkUnchecked>,
|
||||
) -> Result<bitcoin::Address, CommandError> {
|
||||
// NOTE: signet uses testnet addresses, and legacy addresses on regtest use testnet
|
||||
// encoding.
|
||||
addr.require_network(self.config.bitcoin_config.network)
|
||||
.map_err(CommandError::Address)
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +282,7 @@ impl DaemonControl {
|
||||
.receive_descriptor()
|
||||
.derive(index, &self.secp)
|
||||
.address(self.config.bitcoin_config.network);
|
||||
GetAddressResult { address }
|
||||
GetAddressResult::new(address)
|
||||
}
|
||||
|
||||
/// Get a list of all known coins.
|
||||
@ -323,7 +318,7 @@ impl DaemonControl {
|
||||
|
||||
pub fn create_spend(
|
||||
&self,
|
||||
destinations: &HashMap<bitcoin::Address, u64>,
|
||||
destinations: &HashMap<bitcoin::Address<bitcoin::address::NetworkUnchecked>, u64>,
|
||||
coins_outpoints: &[bitcoin::OutPoint],
|
||||
feerate_vb: u64,
|
||||
) -> Result<CreateSpendResult, CommandError> {
|
||||
@ -396,7 +391,7 @@ impl DaemonControl {
|
||||
let mut txouts = Vec::with_capacity(destinations.len());
|
||||
let mut psbt_outs = Vec::with_capacity(destinations.len());
|
||||
for (address, value_sat) in destinations {
|
||||
self.validate_address(address)?;
|
||||
let address = self.validate_address(address.clone())?;
|
||||
|
||||
let amount = bitcoin::Amount::from_sat(*value_sat);
|
||||
check_output_value(amount)?;
|
||||
@ -409,7 +404,7 @@ impl DaemonControl {
|
||||
// If it's an address of ours, signal it as change to signing devices by adding the
|
||||
// BIP32 derivation path to the PSBT output.
|
||||
let bip32_derivation =
|
||||
if let Some((index, is_change)) = db_conn.derivation_index_by_address(address) {
|
||||
if let Some((index, is_change)) = db_conn.derivation_index_by_address(&address) {
|
||||
let desc = if is_change {
|
||||
self.config.main_descriptor.change_descriptor()
|
||||
} else {
|
||||
@ -430,7 +425,7 @@ impl DaemonControl {
|
||||
// isn't much less than what was asked (and obviously that fees aren't negative).
|
||||
let mut tx = bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0), // TODO: randomized anti fee sniping
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO), // TODO: randomized anti fee sniping
|
||||
input: txins,
|
||||
output: txouts,
|
||||
};
|
||||
@ -691,21 +686,21 @@ impl DaemonControl {
|
||||
/// Note that not all coins may be spendable through a single recovery path at the same time.
|
||||
pub fn create_recovery(
|
||||
&self,
|
||||
address: bitcoin::Address,
|
||||
address: bitcoin::Address<address::NetworkUnchecked>,
|
||||
feerate_vb: u64,
|
||||
timelock: Option<u16>,
|
||||
) -> Result<CreateRecoveryResult, CommandError> {
|
||||
if feerate_vb < 1 {
|
||||
return Err(CommandError::InvalidFeerate(feerate_vb));
|
||||
}
|
||||
self.validate_address(&address)?;
|
||||
let address = self.validate_address(address)?;
|
||||
let mut db_conn = self.db.connection();
|
||||
|
||||
// The transaction template. We'll fill-in the inputs afterward.
|
||||
let mut psbt = Psbt {
|
||||
unsigned_tx: bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0), // TODO: anti-fee sniping
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO), // TODO: anti-fee sniping
|
||||
input: Vec::new(),
|
||||
output: vec![bitcoin::TxOut {
|
||||
script_pubkey: address.script_pubkey(),
|
||||
@ -737,7 +732,7 @@ impl DaemonControl {
|
||||
// that is fed to the transaction while doing so, to compute the fees afterward.
|
||||
let mut in_value = bitcoin::Amount::from_sat(0);
|
||||
let txin_sat_vb = self.config.main_descriptor.max_sat_vbytes();
|
||||
let mut sat_vb = 0;
|
||||
let mut sat_vb = 1; // Start at 1 for the segwit marker size, rounded up.
|
||||
let mut spent_txs = HashMap::new();
|
||||
for coin in sweepable_coins {
|
||||
in_value += coin.amount;
|
||||
@ -813,7 +808,18 @@ pub struct GetInfoResult {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetAddressResult {
|
||||
pub address: bitcoin::Address,
|
||||
#[serde(deserialize_with = "deser_addr_assume_checked")]
|
||||
address: bitcoin::Address,
|
||||
}
|
||||
|
||||
impl GetAddressResult {
|
||||
pub fn new(address: bitcoin::Address) -> Self {
|
||||
Self { address }
|
||||
}
|
||||
|
||||
pub fn address(&self) -> &bitcoin::Address {
|
||||
&self.address
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
@ -843,13 +849,13 @@ pub struct ListCoinsResult {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CreateSpendResult {
|
||||
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_base64")]
|
||||
#[serde(serialize_with = "ser_to_string", deserialize_with = "deser_fromstr")]
|
||||
pub psbt: Psbt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListSpendEntry {
|
||||
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_base64")]
|
||||
#[serde(serialize_with = "ser_to_string", deserialize_with = "deser_fromstr")]
|
||||
pub psbt: Psbt,
|
||||
pub updated_at: Option<u32>,
|
||||
}
|
||||
@ -874,7 +880,7 @@ pub struct TransactionInfo {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CreateRecoveryResult {
|
||||
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_base64")]
|
||||
#[serde(serialize_with = "ser_to_string", deserialize_with = "deser_fromstr")]
|
||||
pub psbt: Psbt,
|
||||
}
|
||||
|
||||
@ -884,14 +890,13 @@ mod tests {
|
||||
use crate::{bitcoin::Block, database::BlockInfo, testutils::*};
|
||||
|
||||
use bitcoin::{
|
||||
bip32::{self, ChildNumber},
|
||||
blockdata::transaction::{TxIn, TxOut},
|
||||
util::bip32::ChildNumber,
|
||||
OutPoint, PackedLockTime, Script, Sequence, Transaction, Txid, Witness,
|
||||
locktime::absolute,
|
||||
OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
|
||||
#[test]
|
||||
fn getinfo() {
|
||||
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
|
||||
@ -913,6 +918,7 @@ mod tests {
|
||||
"bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
|
||||
)
|
||||
.unwrap()
|
||||
.assume_checked()
|
||||
);
|
||||
// We won't get the same twice.
|
||||
let addr2 = control.get_new_address().address;
|
||||
@ -933,7 +939,7 @@ mod tests {
|
||||
(
|
||||
bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
},
|
||||
@ -947,10 +953,11 @@ mod tests {
|
||||
let dummy_addr =
|
||||
bitcoin::Address::from_str("bc1qnsexk3gnuyayu92fc3tczvc7k62u22a22ua2kv").unwrap();
|
||||
let dummy_value = 10_000;
|
||||
let mut destinations: HashMap<bitcoin::Address, u64> = [(dummy_addr.clone(), dummy_value)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut destinations: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
||||
[(dummy_addr.clone(), dummy_value)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
control.create_spend(&destinations, &[], 1),
|
||||
Err(CommandError::NoOutpoint)
|
||||
@ -982,15 +989,18 @@ mod tests {
|
||||
assert_eq!(tx.input.len(), 1);
|
||||
assert_eq!(tx.input[0].previous_output, dummy_op);
|
||||
assert_eq!(tx.output.len(), 2);
|
||||
assert_eq!(tx.output[0].script_pubkey, dummy_addr.script_pubkey());
|
||||
assert_eq!(
|
||||
tx.output[0].script_pubkey,
|
||||
dummy_addr.payload.script_pubkey()
|
||||
);
|
||||
assert_eq!(tx.output[0].value, dummy_value);
|
||||
|
||||
// Transaction is 1 in (P2WSH satisfaction), 2 outs. At 1sat/vb, it's 171 sats fees.
|
||||
// Transaction is 1 in (P2WSH satisfaction), 2 outs. At 1sat/vb, it's 170 sats fees.
|
||||
// At 2sats/vb, it's twice that.
|
||||
assert_eq!(tx.output[1].value, 89_829);
|
||||
assert_eq!(tx.output[1].value, 89_830);
|
||||
let res = control.create_spend(&destinations, &[dummy_op], 2).unwrap();
|
||||
let tx = res.psbt.unsigned_tx;
|
||||
assert_eq!(tx.output[1].value, 89_658);
|
||||
assert_eq!(tx.output[1].value, 89_660);
|
||||
|
||||
// A feerate of 555 won't trigger the sanity checks (they were previously not taking the
|
||||
// satisfaction size into account and overestimating the feerate).
|
||||
@ -1025,22 +1035,16 @@ mod tests {
|
||||
);
|
||||
|
||||
// If we ask to create an output for an address from another network, it will fail.
|
||||
let invalid_addr = bitcoin::Address {
|
||||
network: bitcoin::Network::Testnet,
|
||||
payload: dummy_addr.payload.clone(),
|
||||
};
|
||||
let invalid_destinations: HashMap<bitcoin::Address, u64> =
|
||||
[(invalid_addr.clone(), dummy_value)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
let invalid_addr =
|
||||
bitcoin::Address::new(bitcoin::Network::Testnet, dummy_addr.payload.clone());
|
||||
let invalid_destinations: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
||||
[(invalid_addr, dummy_value)].iter().cloned().collect();
|
||||
assert!(matches!(
|
||||
control.create_spend(&invalid_destinations, &[dummy_op], 1),
|
||||
Err(CommandError::AddressNetwork(
|
||||
invalid_addr,
|
||||
bitcoin::Network::Bitcoin
|
||||
Err(CommandError::Address(
|
||||
address::Error::NetworkValidation { .. }
|
||||
))
|
||||
);
|
||||
));
|
||||
|
||||
// If we ask for a large, but valid, output we won't get a change output. 95_000 because we
|
||||
// won't create an output lower than 5k sats.
|
||||
@ -1050,7 +1054,10 @@ mod tests {
|
||||
assert_eq!(tx.input.len(), 1);
|
||||
assert_eq!(tx.input[0].previous_output, dummy_op);
|
||||
assert_eq!(tx.output.len(), 1);
|
||||
assert_eq!(tx.output[0].script_pubkey, dummy_addr.script_pubkey());
|
||||
assert_eq!(
|
||||
tx.output[0].script_pubkey,
|
||||
dummy_addr.payload.script_pubkey()
|
||||
);
|
||||
assert_eq!(tx.output[0].value, 95_000);
|
||||
|
||||
// Now if we mark the coin as spent, we won't create another Spend transaction containing
|
||||
@ -1104,7 +1111,7 @@ mod tests {
|
||||
let mut dummy_bitcoind = DummyBitcoind::new();
|
||||
let dummy_tx = bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
};
|
||||
@ -1145,17 +1152,17 @@ mod tests {
|
||||
bitcoin::Address::from_str("bc1q39srgatmkp6k2ne3l52yhkjprdvunvspqydmkx").unwrap();
|
||||
let dummy_value_a = 50_000;
|
||||
let dummy_value_b = 60_000;
|
||||
let destinations_a: HashMap<bitcoin::Address, u64> =
|
||||
let destinations_a: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
||||
[(dummy_addr_a.clone(), dummy_value_a)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let destinations_b: HashMap<bitcoin::Address, u64> =
|
||||
let destinations_b: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
||||
[(dummy_addr_b.clone(), dummy_value_b)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let destinations_c: HashMap<bitcoin::Address, u64> =
|
||||
let destinations_c: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
||||
[(dummy_addr_a, dummy_value_a), (dummy_addr_b, dummy_value_b)]
|
||||
.iter()
|
||||
.cloned()
|
||||
@ -1185,7 +1192,7 @@ mod tests {
|
||||
assert_eq!(db_conn.spend_tx(&txid_c).unwrap(), psbt_c);
|
||||
|
||||
// As well as update them, with or without new signatures
|
||||
let sig = bitcoin::EcdsaSig::from_str("304402204004fcdbb9c0d0cbf585f58cee34dccb012efbd8fc2b0d5e97760045ae35803802201a0bd7ec2383e0b93748abc9946c8e17a8312e314dab85982aeba650e738cbf401").unwrap();
|
||||
let sig = bitcoin::ecdsa::Signature::from_str("304402204004fcdbb9c0d0cbf585f58cee34dccb012efbd8fc2b0d5e97760045ae35803802201a0bd7ec2383e0b93748abc9946c8e17a8312e314dab85982aeba650e738cbf401").unwrap();
|
||||
psbt_a.inputs[0].partial_sigs.insert(
|
||||
bitcoin::PublicKey::from_str(
|
||||
"023a664c5617412f0b292665b1fd9d766456a7a3b1614c7e7c5f411200ff1958ef",
|
||||
@ -1224,68 +1231,68 @@ mod tests {
|
||||
|
||||
let deposit1: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: outpoint,
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 100_000_000,
|
||||
}],
|
||||
};
|
||||
|
||||
let deposit2: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: outpoint,
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 2000,
|
||||
}],
|
||||
};
|
||||
|
||||
let deposit3: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: outpoint,
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 3000,
|
||||
}],
|
||||
};
|
||||
|
||||
let spend_tx: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: OutPoint {
|
||||
txid: deposit1.txid(),
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 4000,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 100_000_000 - 4000 - 1000,
|
||||
},
|
||||
],
|
||||
@ -1447,45 +1454,45 @@ mod tests {
|
||||
|
||||
let tx1: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: outpoint,
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 100_000_000,
|
||||
}],
|
||||
};
|
||||
|
||||
let tx2: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: outpoint,
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 2000,
|
||||
}],
|
||||
};
|
||||
|
||||
let tx3: Transaction = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(1),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
||||
input: vec![TxIn {
|
||||
witness: Witness::new(),
|
||||
previous_output: outpoint,
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Script::new(),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
value: 3000,
|
||||
}],
|
||||
};
|
||||
|
||||
@ -1,6 +1,36 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use miniscript::bitcoin::{self, consensus, hashes::hex::FromHex};
|
||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn deser_fromstr<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
T::from_str(&string).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
pub fn ser_to_string<T: std::fmt::Display, S: Serializer>(
|
||||
field: T,
|
||||
s: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&field.to_string())
|
||||
}
|
||||
|
||||
/// Deserialize an address from string, assuming the network was checked.
|
||||
pub fn deser_addr_assume_checked<'de, D>(deserializer: D) -> Result<bitcoin::Address, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
bitcoin::Address::from_str(&string)
|
||||
.map(|addr| addr.assume_checked())
|
||||
.map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
/// Serialize an amount as sats
|
||||
pub fn ser_amount<S: Serializer>(amount: &bitcoin::Amount, s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_u64(amount.to_sat())
|
||||
@ -15,28 +45,6 @@ where
|
||||
Ok(bitcoin::Amount::from_sat(a))
|
||||
}
|
||||
|
||||
pub fn to_base64_string<T: consensus::Encodable>(t: T) -> String {
|
||||
base64::encode(consensus::serialize(&t))
|
||||
}
|
||||
|
||||
pub fn ser_base64<S, T>(t: T, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: consensus::Encodable,
|
||||
{
|
||||
s.serialize_str(&to_base64_string(t))
|
||||
}
|
||||
|
||||
pub fn deser_base64<'de, D, T>(d: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: consensus::Decodable,
|
||||
{
|
||||
let s = String::deserialize(d)?;
|
||||
let s = base64::decode(s).map_err(de::Error::custom)?;
|
||||
consensus::deserialize(&s).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
pub fn ser_hex<S, T>(t: T, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
|
||||
@ -14,10 +14,7 @@ use crate::{
|
||||
|
||||
use std::{collections::HashMap, sync};
|
||||
|
||||
use miniscript::bitcoin::{
|
||||
self, secp256k1,
|
||||
util::{bip32, psbt::PartiallySignedTransaction as Psbt},
|
||||
};
|
||||
use miniscript::bitcoin::{self, bip32, psbt::PartiallySignedTransaction as Psbt, secp256k1};
|
||||
|
||||
pub trait DatabaseInterface: Send {
|
||||
fn connection(&self) -> Box<dyn DatabaseConnection>;
|
||||
@ -220,8 +217,12 @@ impl DatabaseConnection for SqliteConn {
|
||||
&mut self,
|
||||
address: &bitcoin::Address,
|
||||
) -> Option<(bip32::ChildNumber, bool)> {
|
||||
self.db_address(address)
|
||||
.map(|db_addr| (db_addr.derivation_index, address == &db_addr.change_address))
|
||||
self.db_address(address).map(|db_addr| {
|
||||
(
|
||||
db_addr.derivation_index,
|
||||
address == &db_addr.change_address.assume_checked(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn coins_by_outpoints(
|
||||
|
||||
@ -28,11 +28,11 @@ use crate::{
|
||||
use std::{cmp, convert::TryInto, fmt, io, path};
|
||||
|
||||
use miniscript::bitcoin::{
|
||||
self,
|
||||
self, bip32,
|
||||
consensus::encode,
|
||||
hashes::hex::ToHex,
|
||||
hashes::{sha256, Hash},
|
||||
psbt::PartiallySignedTransaction as Psbt,
|
||||
secp256k1,
|
||||
util::{bip32, psbt::PartiallySignedTransaction as Psbt},
|
||||
};
|
||||
|
||||
const DB_VERSION: i64 = 1;
|
||||
@ -84,6 +84,24 @@ impl From<rusqlite::Error> for SqliteDbError {
|
||||
}
|
||||
}
|
||||
|
||||
// In Bitcoin land, txids are usually displayed in reverse byte order. This is what rust-bitcoin
|
||||
// implements as `fmt::Display` for `bitcoin::Txid`. However, we store them as raw bytes in the
|
||||
// database and it so happens we sometimes have to look for a txid in hex, in which case we want
|
||||
// the "frontward" hex serialization. This is a hack to implement it.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct FrontwardHexTxid(bitcoin::Txid);
|
||||
|
||||
impl fmt::Display for FrontwardHexTxid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:x}",
|
||||
// sha256 isn't displayed in reverse byte order (contrary to sha256d).
|
||||
sha256::Hash::from_byte_array(self.0.to_byte_array())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FreshDbOptions {
|
||||
pub(self) bitcoind_network: bitcoin::Network,
|
||||
@ -217,7 +235,7 @@ impl SqliteConn {
|
||||
db_tx
|
||||
.execute(
|
||||
"UPDATE tip SET blockheight = (?1), blockhash = (?2)",
|
||||
rusqlite::params![tip.height, tip.hash.to_vec()],
|
||||
rusqlite::params![tip.height, tip.hash[..].to_vec()],
|
||||
)
|
||||
.map(|_| ())
|
||||
})
|
||||
@ -369,7 +387,7 @@ impl SqliteConn {
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
rusqlite::params![
|
||||
WALLET_ID,
|
||||
coin.outpoint.txid.to_vec(),
|
||||
coin.outpoint.txid[..].to_vec(),
|
||||
coin.outpoint.vout,
|
||||
coin.amount.to_sat(),
|
||||
deriv_index,
|
||||
@ -388,7 +406,7 @@ impl SqliteConn {
|
||||
for outpoint in outpoints {
|
||||
db_tx.execute(
|
||||
"DELETE FROM coins WHERE txid = ?1 AND vout = ?2",
|
||||
rusqlite::params![outpoint.txid.to_vec(), outpoint.vout,],
|
||||
rusqlite::params![outpoint.txid[..].to_vec(), outpoint.vout,],
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -406,7 +424,7 @@ impl SqliteConn {
|
||||
for (outpoint, height, time) in outpoints {
|
||||
db_tx.execute(
|
||||
"UPDATE coins SET blockheight = ?1, blocktime = ?2 WHERE txid = ?3 AND vout = ?4",
|
||||
rusqlite::params![height, time, outpoint.txid.to_vec(), outpoint.vout,],
|
||||
rusqlite::params![height, time, outpoint.txid[..].to_vec(), outpoint.vout,],
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -424,7 +442,11 @@ impl SqliteConn {
|
||||
for (outpoint, spend_txid) in outpoints {
|
||||
db_tx.execute(
|
||||
"UPDATE coins SET spend_txid = ?1 WHERE txid = ?2 AND vout = ?3",
|
||||
rusqlite::params![spend_txid.to_vec(), outpoint.txid.to_vec(), outpoint.vout,],
|
||||
rusqlite::params![
|
||||
spend_txid[..].to_vec(),
|
||||
outpoint.txid[..].to_vec(),
|
||||
outpoint.vout,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -444,10 +466,10 @@ impl SqliteConn {
|
||||
db_tx.execute(
|
||||
"UPDATE coins SET spend_txid = ?1, spend_block_height = ?2, spend_block_time = ?3 WHERE txid = ?4 AND vout = ?5",
|
||||
rusqlite::params![
|
||||
spend_txid.to_vec(),
|
||||
spend_txid[..].to_vec(),
|
||||
height,
|
||||
time,
|
||||
outpoint.txid.to_vec(),
|
||||
outpoint.txid[..].to_vec(),
|
||||
outpoint.vout,
|
||||
],
|
||||
)?;
|
||||
@ -473,10 +495,11 @@ impl SqliteConn {
|
||||
// SELECT * FROM coins WHERE (txid, vout) IN ((txidA, voutA), (txidB, voutB));
|
||||
let mut query = "SELECT * FROM coins WHERE (txid, vout) IN (VALUES ".to_string();
|
||||
for (i, outpoint) in outpoints.iter().enumerate() {
|
||||
// NOTE: the txid is not stored as little-endian. Convert it to vec first.
|
||||
// NOTE: SQLite doesn't know Satoshi decided txids would be displayed as little-endian
|
||||
// hex.
|
||||
query += &format!(
|
||||
"(x'{}', {})",
|
||||
&outpoint.txid.to_vec().to_hex(),
|
||||
FrontwardHexTxid(outpoint.txid),
|
||||
outpoint.vout
|
||||
);
|
||||
if i != outpoints.len() - 1 {
|
||||
@ -495,7 +518,7 @@ impl SqliteConn {
|
||||
db_query(
|
||||
&mut self.conn,
|
||||
"SELECT * FROM spend_transactions WHERE txid = ?1",
|
||||
rusqlite::params![txid.to_vec()],
|
||||
rusqlite::params![txid[..].to_vec()],
|
||||
|row| row.try_into(),
|
||||
)
|
||||
.expect("Db must not fail")
|
||||
@ -504,14 +527,13 @@ impl SqliteConn {
|
||||
|
||||
/// Insert a new Spend transaction or replace an existing one.
|
||||
pub fn store_spend(&mut self, psbt: &Psbt) {
|
||||
let txid = psbt.unsigned_tx.txid().to_vec();
|
||||
let psbt = encode::serialize(psbt);
|
||||
let txid = &psbt.unsigned_tx.txid()[..].to_vec();
|
||||
|
||||
db_exec(&mut self.conn, |db_tx| {
|
||||
db_tx.execute(
|
||||
"INSERT into spend_transactions (psbt, txid, updated_at) VALUES (?1, ?2, ?3) \
|
||||
ON CONFLICT DO UPDATE SET psbt=excluded.psbt",
|
||||
rusqlite::params![psbt, txid, curr_timestamp()],
|
||||
rusqlite::params![psbt.serialize(), txid, curr_timestamp()],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@ -564,7 +586,7 @@ impl SqliteConn {
|
||||
db_exec(&mut self.conn, |db_tx| {
|
||||
db_tx.execute(
|
||||
"DELETE FROM spend_transactions WHERE txid = ?1",
|
||||
rusqlite::params![txid.to_vec()],
|
||||
rusqlite::params![txid[..].to_vec()],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@ -593,7 +615,7 @@ impl SqliteConn {
|
||||
)?;
|
||||
db_tx.execute(
|
||||
"UPDATE tip SET blockheight = (?1), blockhash = (?2)",
|
||||
rusqlite::params![new_tip.height, new_tip.hash.to_vec()],
|
||||
rusqlite::params![new_tip.height, new_tip.hash[..].to_vec()],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@ -612,7 +634,7 @@ mod tests {
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitcoin::{hashes::Hash, util::bip32};
|
||||
use bitcoin::{bip32, hashes::Hash};
|
||||
|
||||
// The database schema used by the first versions of Liana (database version 0). Used to test
|
||||
// migrations starting from the first version.
|
||||
@ -687,7 +709,7 @@ CREATE TABLE spend_transactions (
|
||||
";
|
||||
|
||||
fn psbt_from_str(psbt_str: &str) -> Psbt {
|
||||
bitcoin::consensus::deserialize(&base64::decode(psbt_str).unwrap()).unwrap()
|
||||
Psbt::from_str(psbt_str).unwrap()
|
||||
}
|
||||
|
||||
fn dummy_options() -> FreshDbOptions {
|
||||
@ -1449,14 +1471,13 @@ CREATE TABLE spend_transactions (
|
||||
// The helper that was used to store Spend transaction in previous versions of the software
|
||||
// when there was no associated timestamp.
|
||||
fn store_spend_old(conn: &mut rusqlite::Connection, psbt: &Psbt) {
|
||||
let txid = psbt.unsigned_tx.txid().to_vec();
|
||||
let psbt = encode::serialize(psbt);
|
||||
let txid = &psbt.unsigned_tx.txid()[..].to_vec();
|
||||
|
||||
db_exec(conn, |db_tx| {
|
||||
db_tx.execute(
|
||||
"INSERT into spend_transactions (psbt, txid) VALUES (?1, ?2) \
|
||||
ON CONFLICT DO UPDATE SET psbt=excluded.psbt",
|
||||
rusqlite::params![psbt, txid],
|
||||
rusqlite::params![psbt.serialize(), txid],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@ -3,9 +3,7 @@ use crate::descriptors::LianaDescriptor;
|
||||
use std::{convert::TryFrom, str::FromStr};
|
||||
|
||||
use miniscript::bitcoin::{
|
||||
self,
|
||||
consensus::encode,
|
||||
util::{bip32, psbt::PartiallySignedTransaction as Psbt},
|
||||
self, address, bip32, consensus::encode, psbt::PartiallySignedTransaction as Psbt,
|
||||
};
|
||||
|
||||
pub const SCHEMA: &str = "\
|
||||
@ -219,8 +217,8 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DbAddress {
|
||||
pub receive_address: bitcoin::Address,
|
||||
pub change_address: bitcoin::Address,
|
||||
pub receive_address: bitcoin::Address<address::NetworkUnchecked>,
|
||||
pub change_address: bitcoin::Address<address::NetworkUnchecked>,
|
||||
pub derivation_index: bip32::ChildNumber,
|
||||
}
|
||||
|
||||
@ -264,7 +262,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbSpendTransaction {
|
||||
let id: i64 = row.get(0)?;
|
||||
|
||||
let psbt: Vec<u8> = row.get(1)?;
|
||||
let psbt: Psbt = encode::deserialize(&psbt).expect("We only store valid PSBTs");
|
||||
let psbt = Psbt::deserialize(&psbt).expect("We only store valid PSBTs");
|
||||
|
||||
let txid: Vec<u8> = row.get(2)?;
|
||||
let txid: bitcoin::Txid = encode::deserialize(&txid).expect("We only store valid txids");
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use miniscript::{
|
||||
bitcoin::{util::bip32, Sequence},
|
||||
bitcoin::{bip32, Sequence},
|
||||
descriptor,
|
||||
policy::{compiler, Concrete as ConcretePolicy, Liftable, Semantic as SemanticPolicy},
|
||||
ScriptContext,
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
use miniscript::{
|
||||
bitcoin::{
|
||||
self,
|
||||
self, bip32,
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
util::bip32,
|
||||
},
|
||||
hash256, MiniscriptKey, ToPublicKey,
|
||||
};
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
use miniscript::{
|
||||
bitcoin::{
|
||||
self, secp256k1,
|
||||
util::{
|
||||
bip32,
|
||||
psbt::{Input as PsbtIn, Psbt},
|
||||
},
|
||||
self, bip32,
|
||||
psbt::{Input as PsbtIn, Psbt},
|
||||
secp256k1,
|
||||
},
|
||||
descriptor, translate_hash_clone, ForEachKey, TranslatePk, Translator,
|
||||
};
|
||||
@ -21,13 +19,6 @@ pub use analysis::*;
|
||||
|
||||
const WITNESS_FACTOR: usize = 4;
|
||||
|
||||
// Convert a size in weight units to a size in virtual bytes, rounding up.
|
||||
fn wu_to_vb(vb: usize) -> usize {
|
||||
(vb + WITNESS_FACTOR - 1)
|
||||
.checked_div(WITNESS_FACTOR)
|
||||
.expect("Non 0")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LianaDescError {
|
||||
Miniscript(miniscript::Error),
|
||||
@ -189,18 +180,30 @@ impl LianaDescriptor {
|
||||
.0
|
||||
}
|
||||
|
||||
/// Get the maximum size in WU of a satisfaction for this descriptor.
|
||||
/// Get the maximum size difference of a transaction input spending a Script derived from this
|
||||
/// descriptor before and after satisfaction. The returned value is in weight units.
|
||||
/// Callers are expected to account for the Segwit marker (2 WU). This takes into account the
|
||||
/// size of the witness stack length varint.
|
||||
pub fn max_sat_weight(&self) -> usize {
|
||||
// We add one to account for the witness stack size, as the `max_weight_to_satisfy` method
|
||||
// computes the difference in size for a satisfied input that was *already* in a
|
||||
// transaction that spent one or more Segwit coins (and thus already have 1 WU accounted
|
||||
// for the emtpy witness). But this method is used to account between a completely "nude"
|
||||
// transaction (and therefore no Segwit marker nor empty witness in inputs) and a satisfied
|
||||
// transaction.
|
||||
self.multi_desc
|
||||
.max_satisfaction_weight()
|
||||
.expect("Cannot fail for P2WSH")
|
||||
.max_weight_to_satisfy()
|
||||
.expect("Always satisfiable")
|
||||
+ 1
|
||||
}
|
||||
|
||||
/// Get the maximum size in vbytes (rounded up) of a satisfaction for this descriptor.
|
||||
/// Get the maximum size difference of a transaction input spending a Script derived from this
|
||||
/// descriptor before and after satisfaction. The returned value is in (rounded up) virtual
|
||||
/// bytes.
|
||||
/// Callers are expected to account for the Segwit marker (2 WU). This takes into account the
|
||||
/// size of the witness stack length varint.
|
||||
pub fn max_sat_vbytes(&self) -> usize {
|
||||
self.multi_desc
|
||||
.max_satisfaction_weight()
|
||||
.expect("Cannot fail for P2WSH")
|
||||
self.max_sat_weight()
|
||||
.checked_add(WITNESS_FACTOR - 1)
|
||||
.unwrap()
|
||||
.checked_div(WITNESS_FACTOR)
|
||||
@ -211,7 +214,7 @@ impl LianaDescriptor {
|
||||
/// a coin with this Script.
|
||||
pub fn spender_input_size(&self) -> usize {
|
||||
// txid + vout + nSequence + empty scriptSig + witness
|
||||
32 + 4 + 4 + 1 + wu_to_vb(self.max_sat_weight())
|
||||
32 + 4 + 4 + 1 + self.max_sat_vbytes()
|
||||
}
|
||||
|
||||
/// Get some information about a PSBT input spending Liana coins.
|
||||
@ -367,11 +370,11 @@ impl DerivedSinglePathLianaDesc {
|
||||
.expect("A P2WSH always has an address")
|
||||
}
|
||||
|
||||
pub fn script_pubkey(&self) -> bitcoin::Script {
|
||||
pub fn script_pubkey(&self) -> bitcoin::ScriptBuf {
|
||||
self.0.script_pubkey()
|
||||
}
|
||||
|
||||
pub fn witness_script(&self) -> bitcoin::Script {
|
||||
pub fn witness_script(&self) -> bitcoin::ScriptBuf {
|
||||
self.0.explicit_script().expect("Not a Taproot descriptor")
|
||||
}
|
||||
|
||||
@ -415,6 +418,13 @@ mod tests {
|
||||
descriptor::DescriptorPublicKey::from_str(&xpub_str).unwrap()
|
||||
}
|
||||
|
||||
// Convert a size in weight units to a size in virtual bytes, rounding up.
|
||||
fn wu_to_vb(vb: usize) -> usize {
|
||||
(vb + WITNESS_FACTOR - 1)
|
||||
.checked_div(WITNESS_FACTOR)
|
||||
.expect("Non 0")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptor_creation() {
|
||||
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
|
||||
@ -605,7 +615,7 @@ mod tests {
|
||||
#[test]
|
||||
fn inheritance_descriptor_sat_size() {
|
||||
let desc = LianaDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh([abcdef01]tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#ravw7jw5").unwrap();
|
||||
assert_eq!(desc.max_sat_vbytes(), (1 + 69 + 1 + 34 + 73 + 3) / 4); // See the stack details below.
|
||||
assert_eq!(desc.max_sat_vbytes(), (1 + 66 + 1 + 34 + 73 + 3) / 4); // See the stack details below.
|
||||
|
||||
// Maximum input size is (txid + vout + scriptsig + nSequence + max_sat).
|
||||
// Where max_sat is:
|
||||
@ -616,11 +626,11 @@ mod tests {
|
||||
// - Push a signature for the recovery key
|
||||
// NOTE: The specific value is asserted because this was tested against a regtest
|
||||
// transaction.
|
||||
let stack = vec![vec![0; 68], vec![0; 0], vec![0; 33], vec![0; 72]];
|
||||
let stack = vec![vec![0; 65], vec![0; 0], vec![0; 33], vec![0; 72]];
|
||||
let witness_size = bitcoin::VarInt(stack.len() as u64).len()
|
||||
+ stack
|
||||
.iter()
|
||||
.map(|item| bitcoin::VarInt(stack.len() as u64).len() + item.len())
|
||||
.map(|item| bitcoin::VarInt(item.len() as u64).len() + item.len())
|
||||
.sum::<usize>();
|
||||
assert_eq!(
|
||||
desc.spender_input_size(),
|
||||
@ -733,7 +743,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn psbt_from_str(psbt_str: &str) -> Psbt {
|
||||
bitcoin::consensus::deserialize(&base64::decode(psbt_str).unwrap()).unwrap()
|
||||
Psbt::from_str(psbt_str).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -973,7 +983,7 @@ mod tests {
|
||||
"0282574238aea21ec72ffffd8d8a981a30004b5794c8094ff394fec79509a5834f",
|
||||
)
|
||||
.unwrap();
|
||||
let dummy_sig = bitcoin::EcdsaSig::from_str ("30440220264d47ed3fd613e4ac34303c59a0e558d41e487a68af5c5d4bb790f6ccf218ab02203213fe4d51729f9852a28f7d22b2ecb2b096eaf07ad44638af77e4bdbdd4462901").unwrap();
|
||||
let dummy_sig = bitcoin::ecdsa::Signature::from_str("30440220264d47ed3fd613e4ac34303c59a0e558d41e487a68af5c5d4bb790f6ccf218ab02203213fe4d51729f9852a28f7d22b2ecb2b096eaf07ad44638af77e4bdbdd4462901").unwrap();
|
||||
let dummy_der_path = bip32::DerivationPath::from_str("m/0/1").unwrap();
|
||||
let fingerprint = prim_path.thresh_origins().1.into_iter().next().unwrap().0;
|
||||
psbt.inputs[0]
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::{
|
||||
|
||||
use std::{collections::HashMap, convert::TryInto, str::FromStr};
|
||||
|
||||
use miniscript::bitcoin::{self, consensus, util::psbt::PartiallySignedTransaction as Psbt};
|
||||
use miniscript::bitcoin::{self, psbt::PartiallySignedTransaction as Psbt};
|
||||
|
||||
fn create_spend(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
|
||||
let destinations = params
|
||||
@ -19,7 +19,7 @@ fn create_spend(control: &DaemonControl, params: Params) -> Result<serde_json::V
|
||||
let amount: u64 = v.as_i64()?.try_into().ok()?;
|
||||
Some((addr, amount))
|
||||
})
|
||||
.collect::<Option<HashMap<bitcoin::Address, u64>>>()
|
||||
.collect::<Option<HashMap<bitcoin::Address<bitcoin::address::NetworkUnchecked>, u64>>>()
|
||||
})
|
||||
.ok_or_else(|| Error::invalid_params("Invalid 'destinations' parameter."))?;
|
||||
let outpoints = params
|
||||
@ -51,8 +51,7 @@ fn update_spend(control: &DaemonControl, params: Params) -> Result<serde_json::V
|
||||
.get(0, "psbt")
|
||||
.ok_or_else(|| Error::invalid_params("Missing 'psbt' parameter."))?
|
||||
.as_str()
|
||||
.and_then(|s| base64::decode(s).ok())
|
||||
.and_then(|bytes| consensus::deserialize(&bytes).ok())
|
||||
.and_then(|s| Psbt::from_str(s).ok())
|
||||
.ok_or_else(|| Error::invalid_params("Invalid 'psbt' parameter."))?;
|
||||
control.update_spend(psbt)?;
|
||||
|
||||
|
||||
@ -155,7 +155,7 @@ impl From<commands::CommandError> for Error {
|
||||
| commands::CommandError::UnknownOutpoint(..)
|
||||
| commands::CommandError::InvalidFeerate(..)
|
||||
| commands::CommandError::AlreadySpent(..)
|
||||
| commands::CommandError::AddressNetwork(..)
|
||||
| commands::CommandError::Address(..)
|
||||
| commands::CommandError::InvalidOutputValue(..)
|
||||
| commands::CommandError::InsufficientFunds(..)
|
||||
| commands::CommandError::InsaneFees(..)
|
||||
|
||||
@ -222,6 +222,10 @@ fn maybe_delete_watchonly_wallet(
|
||||
miniscript::bitcoin::Network::Testnet
|
||||
| miniscript::bitcoin::Network::Signet
|
||||
| miniscript::bitcoin::Network::Regtest => parent_dir.join("wallets").join(wallet_name),
|
||||
net => panic!(
|
||||
"Unsupported network '{}', unknown at the time of writing.",
|
||||
net
|
||||
),
|
||||
};
|
||||
|
||||
if wallet_path.exists() {
|
||||
|
||||
@ -67,7 +67,7 @@ fn additional_data() -> Result<[u8; 32], RandomnessError> {
|
||||
engine.input(&pid.to_be_bytes());
|
||||
// TODO: get some more contextual information
|
||||
|
||||
Ok(*sha256::Hash::from_engine(engine).as_inner())
|
||||
Ok(sha256::Hash::from_engine(engine).to_byte_array())
|
||||
}
|
||||
|
||||
/// Get 32 random bytes. This is mainly based on OS-provided randomness (`getrandom` or
|
||||
@ -86,7 +86,7 @@ pub fn random_bytes() -> Result<[u8; 32], RandomnessError> {
|
||||
engine.input(&additional_data()?);
|
||||
// TODO: add more sources of randomness
|
||||
|
||||
Ok(*sha256::Hash::from_engine(engine).as_inner())
|
||||
Ok(sha256::Hash::from_engine(engine).to_byte_array())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -15,14 +15,11 @@ use std::{
|
||||
|
||||
use miniscript::bitcoin::{
|
||||
self,
|
||||
bip32::{self, Error as Bip32Error},
|
||||
ecdsa,
|
||||
hashes::Hash,
|
||||
secp256k1,
|
||||
util::{
|
||||
bip32::{self, Error as Bip32Error},
|
||||
ecdsa,
|
||||
psbt::Psbt,
|
||||
sighash,
|
||||
},
|
||||
psbt::Psbt,
|
||||
secp256k1, sighash,
|
||||
};
|
||||
|
||||
/// An error related to using a signer.
|
||||
@ -78,7 +75,7 @@ fn create_dir(path: &path::Path) -> io::Result<()> {
|
||||
|
||||
// TODO: permissions on Windows..
|
||||
#[cfg(not(unix))]
|
||||
return { fs::create_dir_all(path) };
|
||||
fs::create_dir_all(path)
|
||||
}
|
||||
|
||||
// Create a file with no permission for the group and other users, and only read permissions for
|
||||
@ -259,7 +256,7 @@ impl HotSigner {
|
||||
let sighash = sighash_cache
|
||||
.segwit_signature_hash(i, witscript, value, sig_type)
|
||||
.map_err(|_| SignerError::InsanePsbt)?;
|
||||
let sighash = secp256k1::Message::from_slice(sighash.as_hash().as_inner())
|
||||
let sighash = secp256k1::Message::from_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
|
||||
// Then provide a signature for all the keys they asked for.
|
||||
@ -277,7 +274,7 @@ impl HotSigner {
|
||||
let sig = secp.sign_ecdsa_low_r(&sighash, &privkey.inner);
|
||||
psbt.inputs[i].partial_sigs.insert(
|
||||
pubkey,
|
||||
ecdsa::EcdsaSig {
|
||||
ecdsa::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
},
|
||||
@ -301,7 +298,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{descriptors, testutils::*};
|
||||
use miniscript::{
|
||||
bitcoin::util::psbt::Input as PsbtIn,
|
||||
bitcoin::{locktime::absolute, psbt::Input as PsbtIn},
|
||||
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
|
||||
};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
@ -434,7 +431,7 @@ mod tests {
|
||||
let mut dummy_psbt = Psbt {
|
||||
unsigned_tx: bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
||||
input: vec![bitcoin::TxIn {
|
||||
sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
previous_output: bitcoin::OutPoint::from_str(
|
||||
@ -449,6 +446,7 @@ mod tests {
|
||||
"bc1qvklensptw5lk7d470ds60pcpsr0psdpgyvwepv",
|
||||
)
|
||||
.unwrap()
|
||||
.payload
|
||||
.script_pubkey(),
|
||||
}],
|
||||
},
|
||||
|
||||
@ -9,9 +9,7 @@ use std::{collections::HashMap, env, fs, io, path, process, str::FromStr, sync,
|
||||
|
||||
use miniscript::{
|
||||
bitcoin::{
|
||||
self, secp256k1,
|
||||
util::{bip32, psbt::PartiallySignedTransaction as Psbt},
|
||||
Transaction, Txid,
|
||||
self, bip32, psbt::PartiallySignedTransaction as Psbt, secp256k1, Transaction, Txid,
|
||||
},
|
||||
descriptor,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user