gui(installer): define bitcoind from general node struct
This commit is contained in:
parent
c5d9d007fb
commit
046b54e6a9
@ -33,7 +33,7 @@ pub enum Message {
|
||||
ImportRemoteWallet(ImportRemoteWallet),
|
||||
SelectBitcoindType(SelectBitcoindTypeMsg),
|
||||
InternalBitcoind(InternalBitcoindMsg),
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
DefineNode(DefineNode),
|
||||
DefineDescriptor(DefineDescriptor),
|
||||
ImportXpub(Fingerprint, Result<DescriptorPublicKey, Error>),
|
||||
HardwareWallets(HardwareWalletMessage),
|
||||
@ -72,8 +72,13 @@ pub enum ImportRemoteWallet {
|
||||
pub enum DefineBitcoind {
|
||||
ConfigFieldEdited(ConfigField, String),
|
||||
RpcAuthTypeSelected(RpcAuthType),
|
||||
PingBitcoindResult(Result<(), Error>),
|
||||
PingBitcoind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DefineNode {
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
PingResult(Result<(), Error>),
|
||||
Ping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -39,7 +39,7 @@ use crate::{
|
||||
|
||||
pub use message::Message;
|
||||
use step::{
|
||||
BackupDescriptor, BackupMnemonic, ChooseBackend, DefineBitcoind, DefineDescriptor, Final,
|
||||
BackupDescriptor, BackupMnemonic, ChooseBackend, DefineDescriptor, DefineNode, Final,
|
||||
ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic,
|
||||
RegisterDescriptor, RemoteBackendLogin, SelectBitcoindTypeStep, ShareXpubs, Step,
|
||||
};
|
||||
@ -126,7 +126,7 @@ impl Installer {
|
||||
RemoteBackendLogin::new(network).into(),
|
||||
SelectBitcoindTypeStep::new().into(),
|
||||
InternalBitcoindStep::new(&context.data_dir).into(),
|
||||
DefineBitcoind::new().into(),
|
||||
DefineNode::default().into(),
|
||||
Final::new().into(),
|
||||
],
|
||||
UserFlow::ShareXpubs => vec![ShareXpubs::new(network, signer.clone()).into()],
|
||||
@ -139,7 +139,7 @@ impl Installer {
|
||||
RegisterDescriptor::new_import_wallet().into(),
|
||||
SelectBitcoindTypeStep::new().into(),
|
||||
InternalBitcoindStep::new(&context.data_dir).into(),
|
||||
DefineBitcoind::new().into(),
|
||||
DefineNode::default().into(),
|
||||
Final::new().into(),
|
||||
],
|
||||
},
|
||||
|
||||
@ -4,8 +4,9 @@ mod mnemonic;
|
||||
mod node;
|
||||
mod share_xpubs;
|
||||
|
||||
pub use node::bitcoind::{
|
||||
DefineBitcoind, DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep,
|
||||
pub use node::{
|
||||
bitcoind::{DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep},
|
||||
DefineNode,
|
||||
};
|
||||
|
||||
pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
|
||||
|
||||
@ -339,11 +339,11 @@ impl Step for SelectBitcoindTypeStep {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DefineBitcoind {
|
||||
rpc_auth_vals: RpcAuthValues,
|
||||
selected_auth_type: RpcAuthType,
|
||||
address: form::Value<String>,
|
||||
is_running: Option<Result<(), Error>>,
|
||||
|
||||
// Internal cache to detect network change.
|
||||
network: Option<Network>,
|
||||
@ -355,42 +355,33 @@ impl DefineBitcoind {
|
||||
rpc_auth_vals: RpcAuthValues::default(),
|
||||
selected_auth_type: RpcAuthType::CookieFile,
|
||||
address: form::Value::default(),
|
||||
is_running: None,
|
||||
network: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ping(&self) -> Command<Message> {
|
||||
let address = self.address.value.to_owned();
|
||||
let selected_auth_type = self.selected_auth_type;
|
||||
pub fn ping(&self) -> Result<(), Error> {
|
||||
let rpc_auth_vals = self.rpc_auth_vals.clone();
|
||||
Command::perform(
|
||||
async move {
|
||||
let builder = match selected_auth_type {
|
||||
RpcAuthType::CookieFile => {
|
||||
let cookie_path = rpc_auth_vals.cookie_path.value;
|
||||
let cookie = std::fs::read_to_string(cookie_path).map_err(|e| {
|
||||
Error::Bitcoind(format!("Failed to read cookie file: {}", e))
|
||||
})?;
|
||||
SimpleHttpTransport::builder().cookie_auth(cookie)
|
||||
}
|
||||
RpcAuthType::UserPass => {
|
||||
let user = rpc_auth_vals.user.value;
|
||||
let password = rpc_auth_vals.password.value;
|
||||
SimpleHttpTransport::builder().auth(user, Some(password))
|
||||
}
|
||||
};
|
||||
let client = Client::with_transport(
|
||||
builder
|
||||
.url(&address)?
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.build(),
|
||||
);
|
||||
client.send_request(client.build_request("echo", &[]))?;
|
||||
Ok(())
|
||||
},
|
||||
|res| Message::DefineBitcoind(message::DefineBitcoind::PingBitcoindResult(res)),
|
||||
)
|
||||
let builder = match self.selected_auth_type {
|
||||
RpcAuthType::CookieFile => {
|
||||
let cookie_path = rpc_auth_vals.cookie_path.value;
|
||||
let cookie = std::fs::read_to_string(cookie_path)
|
||||
.map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?;
|
||||
SimpleHttpTransport::builder().cookie_auth(cookie)
|
||||
}
|
||||
RpcAuthType::UserPass => {
|
||||
let user = rpc_auth_vals.user.value;
|
||||
let password = rpc_auth_vals.password.value;
|
||||
SimpleHttpTransport::builder().auth(user, Some(password))
|
||||
}
|
||||
};
|
||||
let client = Client::with_transport(
|
||||
builder
|
||||
.url(&self.address.value.to_owned())?
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.build(),
|
||||
);
|
||||
client.send_request(client.build_request("echo", &[]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn can_try_ping(&self) -> bool {
|
||||
@ -402,10 +393,8 @@ impl DefineBitcoind {
|
||||
self.address.valid && !self.rpc_auth_vals.cookie_path.value.is_empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for DefineBitcoind {
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
pub fn load_context(&mut self, ctx: &Context) {
|
||||
if self.rpc_auth_vals.cookie_path.value.is_empty()
|
||||
// if network changed then the values must be reset to default.
|
||||
|| self.network != Some(ctx.bitcoin_config.network)
|
||||
@ -422,17 +411,12 @@ impl Step for DefineBitcoind {
|
||||
|
||||
self.network = Some(ctx.bitcoin_config.network);
|
||||
}
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if let Message::DefineBitcoind(msg) = message {
|
||||
|
||||
pub fn update(&mut self, message: message::DefineNode) -> Command<Message> {
|
||||
if let message::DefineNode::DefineBitcoind(msg) = message {
|
||||
match msg {
|
||||
message::DefineBitcoind::PingBitcoind => {
|
||||
self.is_running = None;
|
||||
return self.ping();
|
||||
}
|
||||
message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res),
|
||||
message::DefineBitcoind::ConfigFieldEdited(field, value) => match field {
|
||||
ConfigField::Address => {
|
||||
self.is_running = None;
|
||||
self.address.value.clone_from(&value);
|
||||
self.address.valid = false;
|
||||
if let Some((ip, port)) = value.rsplit_once(':') {
|
||||
@ -444,23 +428,19 @@ impl Step for DefineBitcoind {
|
||||
}
|
||||
}
|
||||
ConfigField::CookieFilePath => {
|
||||
self.is_running = None;
|
||||
self.rpc_auth_vals.cookie_path.value = value;
|
||||
self.rpc_auth_vals.cookie_path.valid = true;
|
||||
}
|
||||
ConfigField::User => {
|
||||
self.is_running = None;
|
||||
self.rpc_auth_vals.user.value = value;
|
||||
self.rpc_auth_vals.user.valid = true;
|
||||
}
|
||||
ConfigField::Password => {
|
||||
self.is_running = None;
|
||||
self.rpc_auth_vals.password.value = value;
|
||||
self.rpc_auth_vals.password.valid = true;
|
||||
}
|
||||
},
|
||||
message::DefineBitcoind::RpcAuthTypeSelected(auth_type) => {
|
||||
self.is_running = None;
|
||||
self.selected_auth_type = auth_type;
|
||||
}
|
||||
};
|
||||
@ -468,7 +448,7 @@ impl Step for DefineBitcoind {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
pub fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
let addr = std::net::SocketAddr::from_str(&self.address.value);
|
||||
let rpc_auth = match self.selected_auth_type {
|
||||
RpcAuthType::CookieFile => {
|
||||
@ -501,28 +481,8 @@ impl Step for DefineBitcoind {
|
||||
}
|
||||
}
|
||||
|
||||
fn view(
|
||||
&self,
|
||||
_hws: &HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&str>,
|
||||
) -> Element<Message> {
|
||||
view::define_bitcoin(
|
||||
progress,
|
||||
&self.address,
|
||||
&self.rpc_auth_vals,
|
||||
&self.selected_auth_type,
|
||||
self.is_running.as_ref(),
|
||||
self.can_try_ping(),
|
||||
)
|
||||
}
|
||||
|
||||
fn load(&self) -> Command<Message> {
|
||||
self.ping()
|
||||
}
|
||||
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
!ctx.bitcoind_is_external || ctx.remote_backend.is_some()
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
view::define_bitcoind(&self.address, &self.rpc_auth_vals, &self.selected_auth_type)
|
||||
}
|
||||
}
|
||||
|
||||
@ -532,12 +492,6 @@ impl Default for DefineBitcoind {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DefineBitcoind> for Box<dyn Step> {
|
||||
fn from(s: DefineBitcoind) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InternalBitcoindStep {
|
||||
liana_datadir: PathBuf,
|
||||
bitcoind_datadir: PathBuf,
|
||||
|
||||
@ -1 +1,110 @@
|
||||
pub mod bitcoind;
|
||||
|
||||
use crate::{
|
||||
hw::HardwareWallets,
|
||||
installer::{
|
||||
context::Context,
|
||||
message::{self, Message},
|
||||
step::{node::bitcoind::DefineBitcoind, Step},
|
||||
view, Error,
|
||||
},
|
||||
};
|
||||
|
||||
use iced::Command;
|
||||
use liana_ui::widget::Element;
|
||||
|
||||
pub struct Node {
|
||||
definition: DefineBitcoind,
|
||||
is_running: Option<Result<(), Error>>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn new() -> Self {
|
||||
Node {
|
||||
definition: DefineBitcoind::new(),
|
||||
is_running: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefineNode {
|
||||
node: Node,
|
||||
}
|
||||
|
||||
impl From<DefineNode> for Box<dyn Step> {
|
||||
fn from(s: DefineNode) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl DefineNode {
|
||||
pub fn new() -> Self {
|
||||
Self { node: Node::new() }
|
||||
}
|
||||
|
||||
fn ping(&self) -> Command<Message> {
|
||||
let def = self.node.definition.clone();
|
||||
Command::perform(async move { def.ping() }, move |res| {
|
||||
Message::DefineNode(message::DefineNode::PingResult(res))
|
||||
})
|
||||
}
|
||||
|
||||
fn update_node(&mut self, message: message::DefineNode) -> Command<Message> {
|
||||
self.node.is_running = None;
|
||||
self.node.definition.update(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefineNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for DefineNode {
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
self.node.definition.load_context(ctx);
|
||||
}
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if let Message::DefineNode(msg) = message {
|
||||
match msg {
|
||||
message::DefineNode::Ping => {
|
||||
return self.ping();
|
||||
}
|
||||
message::DefineNode::PingResult(res) => {
|
||||
self.node.is_running = Some(res);
|
||||
}
|
||||
msg @ message::DefineNode::DefineBitcoind(_) => {
|
||||
return self.update_node(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
self.node.definition.apply(ctx)
|
||||
}
|
||||
|
||||
fn view(
|
||||
&self,
|
||||
_hws: &HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&str>,
|
||||
) -> Element<Message> {
|
||||
view::define_bitcoin_node(
|
||||
progress,
|
||||
self.node.definition.view(),
|
||||
self.node.is_running.as_ref(),
|
||||
self.node.definition.can_try_ping(),
|
||||
)
|
||||
}
|
||||
|
||||
fn load(&self) -> Command<Message> {
|
||||
self.ping()
|
||||
}
|
||||
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
!ctx.bitcoind_is_external || ctx.remote_backend.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ use liana_ui::{
|
||||
use crate::{
|
||||
hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason},
|
||||
installer::{
|
||||
message::{self, Message},
|
||||
message::{self, DefineBitcoind, DefineNode, Message},
|
||||
prompt,
|
||||
step::{DownloadState, InstallState},
|
||||
Error,
|
||||
@ -1159,13 +1159,73 @@ pub fn help_backup<'a>() -> Element<'a, Message> {
|
||||
text(prompt::BACKUP_DESCRIPTOR_HELP).small().into()
|
||||
}
|
||||
|
||||
pub fn define_bitcoin<'a>(
|
||||
pub fn define_bitcoin_node<'a>(
|
||||
progress: (usize, usize),
|
||||
node_view: Element<'a, Message>,
|
||||
is_running: Option<&Result<(), Error>>,
|
||||
can_try_ping: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let col = Column::new()
|
||||
.push(node_view)
|
||||
.push_maybe(if is_running.is_some() {
|
||||
is_running.map(|res| {
|
||||
if res.is_ok() {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_check_icon().style(color::GREEN))
|
||||
.push(text("Connection checked").style(color::GREEN)),
|
||||
)
|
||||
} else {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_cross_icon().style(color::RED))
|
||||
.push(text("Connection failed").style(color::RED)),
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(Container::new(Space::with_height(Length::Fixed(25.0))))
|
||||
})
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(Container::new(
|
||||
button::secondary(None, "Check connection")
|
||||
.on_press_maybe(if can_try_ping {
|
||||
Some(Message::DefineNode(DefineNode::Ping))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.width(Length::Fixed(200.0)),
|
||||
))
|
||||
.push(if is_running.map(|res| res.is_ok()).unwrap_or(false) {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Fixed(200.0))
|
||||
} else {
|
||||
button::primary(None, "Next").width(Length::Fixed(200.0))
|
||||
}),
|
||||
)
|
||||
.spacing(50);
|
||||
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Set up connection to the Bitcoin full node",
|
||||
col,
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn define_bitcoind<'a>(
|
||||
address: &form::Value<String>,
|
||||
rpc_auth_vals: &RpcAuthValues,
|
||||
selected_auth_type: &RpcAuthType,
|
||||
is_running: Option<&Result<(), Error>>,
|
||||
can_try_ping: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let is_loopback = if let Some((ip, _port)) = address.value.clone().rsplit_once(':') {
|
||||
let (ipv4, ipv6) = (Ipv4Addr::from_str(ip), Ipv6Addr::from_str(ip));
|
||||
@ -1182,9 +1242,8 @@ pub fn define_bitcoin<'a>(
|
||||
.push(text("Address:").bold())
|
||||
.push(
|
||||
form::Form::new_trimmed("Address", address, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
|
||||
ConfigField::Address,
|
||||
msg,
|
||||
Message::DefineNode(DefineNode::DefineBitcoind(
|
||||
DefineBitcoind::ConfigFieldEdited(ConfigField::Address, msg),
|
||||
))
|
||||
})
|
||||
.warning("Please enter correct address")
|
||||
@ -1220,9 +1279,9 @@ pub fn define_bitcoin<'a>(
|
||||
*auth_type,
|
||||
Some(*selected_auth_type),
|
||||
|new_selection| {
|
||||
Message::DefineBitcoind(
|
||||
message::DefineBitcoind::RpcAuthTypeSelected(new_selection),
|
||||
)
|
||||
Message::DefineNode(DefineNode::DefineBitcoind(
|
||||
DefineBitcoind::RpcAuthTypeSelected(new_selection),
|
||||
))
|
||||
},
|
||||
))
|
||||
.spacing(30)
|
||||
@ -1233,9 +1292,8 @@ pub fn define_bitcoin<'a>(
|
||||
.push(match selected_auth_type {
|
||||
RpcAuthType::CookieFile => Row::new().push(
|
||||
form::Form::new_trimmed("Cookie path", &rpc_auth_vals.cookie_path, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
|
||||
ConfigField::CookieFilePath,
|
||||
msg,
|
||||
Message::DefineNode(DefineNode::DefineBitcoind(
|
||||
DefineBitcoind::ConfigFieldEdited(ConfigField::CookieFilePath, msg),
|
||||
))
|
||||
})
|
||||
.warning("Please enter correct path")
|
||||
@ -1245,9 +1303,8 @@ pub fn define_bitcoin<'a>(
|
||||
RpcAuthType::UserPass => Row::new()
|
||||
.push(
|
||||
form::Form::new_trimmed("User", &rpc_auth_vals.user, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
|
||||
ConfigField::User,
|
||||
msg,
|
||||
Message::DefineNode(DefineNode::DefineBitcoind(
|
||||
DefineBitcoind::ConfigFieldEdited(ConfigField::User, msg),
|
||||
))
|
||||
})
|
||||
.warning("Please enter correct user")
|
||||
@ -1256,9 +1313,8 @@ pub fn define_bitcoin<'a>(
|
||||
)
|
||||
.push(
|
||||
form::Form::new_trimmed("Password", &rpc_auth_vals.password, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
|
||||
ConfigField::Password,
|
||||
msg,
|
||||
Message::DefineNode(DefineNode::DefineBitcoind(
|
||||
DefineBitcoind::ConfigFieldEdited(ConfigField::Password, msg),
|
||||
))
|
||||
})
|
||||
.warning("Please enter correct password")
|
||||
@ -1269,62 +1325,11 @@ pub fn define_bitcoin<'a>(
|
||||
})
|
||||
.spacing(10);
|
||||
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Set up connection to the Bitcoin full node",
|
||||
Column::new()
|
||||
.push(col_address)
|
||||
.push(col_auth)
|
||||
.push_maybe(if is_running.is_some() {
|
||||
is_running.map(|res| {
|
||||
if res.is_ok() {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_check_icon().style(color::GREEN))
|
||||
.push(text("Connection checked").style(color::GREEN)),
|
||||
)
|
||||
} else {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::circle_cross_icon().style(color::RED))
|
||||
.push(text("Connection failed").style(color::RED)),
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(Container::new(Space::with_height(Length::Fixed(25.0))))
|
||||
})
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(Container::new(
|
||||
button::secondary(None, "Check connection")
|
||||
.on_press_maybe(if can_try_ping {
|
||||
Some(Message::DefineBitcoind(
|
||||
message::DefineBitcoind::PingBitcoind,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.width(Length::Fixed(200.0)),
|
||||
))
|
||||
.push(if is_running.map(|res| res.is_ok()).unwrap_or(false) {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Fixed(200.0))
|
||||
} else {
|
||||
button::primary(None, "Next").width(Length::Fixed(200.0))
|
||||
}),
|
||||
)
|
||||
.spacing(50),
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
Column::new()
|
||||
.push(col_address)
|
||||
.push(col_auth)
|
||||
.spacing(50)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user