Merge #898: gui: support user/password for RPC authentication

838550b917d96a6a6d11f5d0103c53706acd9a8c gui: change radio border color (jp1ac4)
38198cc79f29fd13b52f963ce8fe7da2567b3bb4 installer: support user/password RPC authentication (jp1ac4)
3ccdafbda235730ec95d6f2992808beb2eb51aa9 gui: support user/password RPC authentication (jp1ac4)

Pull request description:

  This adds support to the GUI for user and password RPC authentication as part of #356.

  The first commit adds the user/password option to the settings page, and only updates the installer as required for it to compile. It also changes how the managed bitcoind gets the cookie location when starting so that it doesn't rely on the config file.

  The second commit adds the user/password option to the installer when using a self-managed node.

  Updating the managed node to use user/password can be done in a follow-up PR.

ACKs for top commit:
  darosior:
    ACK 838550b917d96a6a6d11f5d0103c53706acd9a8c

Tree-SHA512: 412e167351807bc319d33a0f7ddb34f522f4b146c95f78877936ff476ce2a8d5ef68d359608ad7d3f1ddf4ca9b6a94e035d594ffc464c0fd5940c252d3c4de39
This commit is contained in:
Antoine Poinsot 2024-01-03 14:54:12 +01:00
commit 051957ede7
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
9 changed files with 333 additions and 92 deletions

2
gui/Cargo.lock generated
View File

@ -2431,7 +2431,7 @@ dependencies = [
[[package]]
name = "liana"
version = "4.0.0"
source = "git+https://github.com/wizardsardine/liana?branch=master#dee069e72343a67607f4429125e4c80dc0d73055"
source = "git+https://github.com/wizardsardine/liana?branch=master#b8f8d1b944120879986a71255bab19e5b342ecc8"
dependencies = [
"backtrace",
"bdk_coin_select",

View File

@ -8,12 +8,13 @@ use chrono::prelude::*;
use iced::Command;
use tracing::info;
use liana::config::{BitcoinConfig, BitcoindConfig, Config};
use liana::config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config};
use liana_ui::{component::form, widget::Element};
use crate::{
app::{cache::Cache, error::Error, message::Message, state::settings::Setting, view, State},
bitcoind::{RpcAuthType, RpcAuthValues},
daemon::Daemon,
};
@ -145,7 +146,8 @@ pub struct BitcoindSettings {
bitcoin_config: BitcoinConfig,
edit: bool,
processing: bool,
cookie_path: form::Value<String>,
rpc_auth_vals: RpcAuthValues,
selected_auth_type: RpcAuthType,
addr: form::Value<String>,
daemon_is_external: bool,
bitcoind_is_internal: bool,
@ -164,7 +166,33 @@ impl BitcoindSettings {
daemon_is_external: bool,
bitcoind_is_internal: bool,
) -> BitcoindSettings {
let path = bitcoind_config.cookie_path.to_str().unwrap().to_string();
let (rpc_auth_vals, selected_auth_type) = match &bitcoind_config.rpc_auth {
BitcoindRpcAuth::CookieFile(path) => (
RpcAuthValues {
cookie_path: form::Value {
valid: true,
value: path.to_str().unwrap().to_string(),
},
user: form::Value::default(),
password: form::Value::default(),
},
RpcAuthType::CookieFile,
),
BitcoindRpcAuth::UserPass(user, password) => (
RpcAuthValues {
cookie_path: form::Value::default(),
user: form::Value {
valid: true,
value: user.clone(),
},
password: form::Value {
valid: true,
value: password.clone(),
},
},
RpcAuthType::UserPass,
),
};
let addr = bitcoind_config.addr.to_string();
BitcoindSettings {
daemon_is_external,
@ -173,10 +201,8 @@ impl BitcoindSettings {
bitcoin_config,
edit: false,
processing: false,
cookie_path: form::Value {
valid: true,
value: path,
},
rpc_auth_vals,
selected_auth_type,
addr: form::Value {
valid: true,
value: addr,
@ -214,21 +240,41 @@ impl Setting for BitcoindSettings {
if !self.processing {
match field {
"socket_address" => self.addr.value = value,
"cookie_file_path" => self.cookie_path.value = value,
"cookie_file_path" => self.rpc_auth_vals.cookie_path.value = value,
"user" => self.rpc_auth_vals.user.value = value,
"password" => self.rpc_auth_vals.password.value = value,
_ => {}
}
}
}
view::SettingsEditMessage::BitcoindRpcAuthTypeSelected(auth_type) => {
if !self.processing {
self.selected_auth_type = auth_type;
}
}
view::SettingsEditMessage::Confirm => {
let new_addr = SocketAddr::from_str(&self.addr.value);
self.addr.valid = new_addr.is_ok();
let new_path = PathBuf::from_str(&self.cookie_path.value);
self.cookie_path.valid = new_path.is_ok();
let rpc_auth = match self.selected_auth_type {
RpcAuthType::CookieFile => {
let new_path = PathBuf::from_str(&self.rpc_auth_vals.cookie_path.value);
if let Ok(path) = new_path {
self.rpc_auth_vals.cookie_path.valid = true;
Some(BitcoindRpcAuth::CookieFile(path))
} else {
None
}
}
RpcAuthType::UserPass => Some(BitcoindRpcAuth::UserPass(
self.rpc_auth_vals.user.value.clone(),
self.rpc_auth_vals.password.value.clone(),
)),
};
if self.addr.valid & self.cookie_path.valid {
if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) {
let mut daemon_config = daemon.config().cloned().unwrap();
daemon_config.bitcoind_config = Some(liana::config::BitcoindConfig {
cookie_path: new_path.unwrap(),
rpc_auth,
addr: new_addr.unwrap(),
});
self.processing = true;
@ -247,7 +293,8 @@ impl Setting for BitcoindSettings {
self.bitcoin_config.network,
cache.blockheight,
&self.addr,
&self.cookie_path,
&self.rpc_auth_vals,
&self.selected_auth_type,
self.processing,
)
} else {
@ -347,6 +394,7 @@ impl Setting for RescanSetting {
Message::StartRescan,
);
}
_ => {}
};
Command::none()
}

View File

@ -1,4 +1,4 @@
use crate::app::menu::Menu;
use crate::{app::menu::Menu, bitcoind::RpcAuthType};
use liana::miniscript::bitcoin::bip32::Fingerprint;
#[derive(Debug, Clone)]
@ -75,6 +75,7 @@ pub enum SettingsMessage {
pub enum SettingsEditMessage {
Select,
FieldEdited(&'static str, String),
BitcoindRpcAuthTypeSelected(RpcAuthType),
Cancel,
Confirm,
}

View File

@ -3,11 +3,14 @@ use std::str::FromStr;
use iced::{
alignment,
widget::{scrollable, Space},
widget::{radio, scrollable, Space},
Alignment, Length,
};
use liana::miniscript::bitcoin::{bip32::Fingerprint, Network};
use liana::{
config::BitcoindRpcAuth,
miniscript::bitcoin::{bip32::Fingerprint, Network},
};
use super::{dashboard, message::*};
@ -26,6 +29,7 @@ use crate::{
menu::Menu,
view::{hw, warning::warn},
},
bitcoind::{RpcAuthType, RpcAuthValues},
hw::HardwareWallet,
};
@ -208,7 +212,8 @@ pub fn bitcoind_edit<'a>(
network: Network,
blockheight: i32,
addr: &form::Value<String>,
cookie_path: &form::Value<String>,
rpc_auth_vals: &RpcAuthValues,
selected_auth_type: &RpcAuthType,
processing: bool,
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
@ -244,18 +249,60 @@ pub fn bitcoind_edit<'a>(
col = col
.push(
Column::new()
.push(text("Cookie file path:").bold().small())
[RpcAuthType::CookieFile, RpcAuthType::UserPass]
.iter()
.fold(
Row::new()
.push(text("RPC authentication:").small().bold())
.spacing(10),
|row, auth_type| {
row.push(radio(
format!("{}", auth_type),
*auth_type,
Some(*selected_auth_type),
SettingsEditMessage::BitcoindRpcAuthTypeSelected,
))
.spacing(30)
.align_items(Alignment::Center)
},
),
)
.push(match selected_auth_type {
RpcAuthType::CookieFile => Column::new()
.push(
form::Form::new_trimmed("Cookie file path", cookie_path, |value| {
SettingsEditMessage::FieldEdited("cookie_file_path", value)
})
form::Form::new_trimmed(
"Cookie file path",
&rpc_auth_vals.cookie_path,
|value| SettingsEditMessage::FieldEdited("cookie_file_path", value),
)
.warning("Please enter a valid filesystem path")
.size(20)
.padding(5),
)
.spacing(5),
)
RpcAuthType::UserPass => Column::new()
.push(
Row::new()
.push(
form::Form::new_trimmed("User", &rpc_auth_vals.user, |value| {
SettingsEditMessage::FieldEdited("user", value)
})
.warning("Please enter a valid user")
.size(20)
.padding(5),
)
.push(
form::Form::new_trimmed("Password", &rpc_auth_vals.password, |value| {
SettingsEditMessage::FieldEdited("password", value)
})
.warning("Please enter a valid password")
.size(20)
.padding(5),
)
.spacing(10),
)
.spacing(5),
})
.push(
Column::new()
.push(text("Socket address:").bold().small())
@ -345,13 +392,17 @@ pub fn bitcoind<'a>(
.push(separation().width(Length::Fill));
}
let rows = vec![
(
"Cookie file path:",
config.cookie_path.to_str().unwrap().to_string(),
),
("Socket address:", config.addr.to_string()),
];
let mut rows = vec![];
match &config.rpc_auth {
BitcoindRpcAuth::CookieFile(path) => {
rows.push(("Cookie file path:", path.to_str().unwrap().to_string()));
}
BitcoindRpcAuth::UserPass(user, password) => {
rows.push(("User:", user.clone()));
rows.push(("Password:", password.clone()));
}
}
rows.push(("Socket address:", config.addr.to_string()));
let mut col_fields = Column::new();
for (k, v) in rows {

View File

@ -1,7 +1,9 @@
use liana::{
config::BitcoindConfig,
config::{BitcoindConfig, BitcoindRpcAuth},
miniscript::bitcoin::{self, Network},
};
use liana_ui::component::form;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
@ -222,6 +224,7 @@ impl Bitcoind {
// We've started bitcoind in the background, however it may fail to start for whatever
// reason. And we need its JSONRPC interface to be available to continue. Thus wait for it
// to have created the cookie file, regularly checking it did not fail to start.
let cookie_path = internal_bitcoind_cookie_path(&bitcoind_datadir, network);
loop {
match process.try_wait() {
Ok(None) => {}
@ -229,11 +232,11 @@ impl Bitcoind {
Ok(Some(status)) => {
log::error!("Bitcoind exited with status '{}'", status);
return Err(StartInternalBitcoindError::CookieFileNotFound(
config.cookie_path.to_string_lossy().into_owned(),
cookie_path.to_string_lossy().into_owned(),
));
}
}
if config.cookie_path.exists() {
if cookie_path.exists() {
log::info!("Bitcoind seems to have successfully started.");
break;
}
@ -241,9 +244,10 @@ impl Bitcoind {
thread::sleep(time::Duration::from_millis(500));
}
config.cookie_path = config.cookie_path.canonicalize().map_err(|e| {
config.rpc_auth = BitcoindRpcAuth::CookieFile(cookie_path.canonicalize().map_err(|e| {
StartInternalBitcoindError::CouldNotCanonicalizeCookiePath(e.to_string())
})?;
})?);
liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string())
.map_err(|e| StartInternalBitcoindError::BitcoinDError(e.to_string()))?;
@ -273,3 +277,44 @@ pub fn stop_bitcoind(config: &BitcoindConfig) -> bool {
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum RpcAuthType {
CookieFile,
UserPass,
}
impl fmt::Display for RpcAuthType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RpcAuthType::CookieFile => write!(f, "Cookie file path"),
RpcAuthType::UserPass => write!(f, "User and password"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RpcAuthValues {
pub cookie_path: form::Value<String>,
pub user: form::Value<String>,
pub password: form::Value<String>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ConfigField {
Address,
CookieFilePath,
User,
Password,
}
impl fmt::Display for ConfigField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigField::Address => write!(f, "Socket address"),
ConfigField::CookieFilePath => write!(f, "Cookie file path"),
ConfigField::User => write!(f, "User"),
ConfigField::Password => write!(f, "Password"),
}
}
}

View File

@ -5,7 +5,11 @@ use liana::miniscript::{
use std::path::PathBuf;
use super::Error;
use crate::{bitcoind::Bitcoind, download::Progress, hw::HardwareWalletMessage};
use crate::{
bitcoind::{Bitcoind, ConfigField, RpcAuthType},
download::Progress,
hw::HardwareWalletMessage,
};
use async_hwi::DeviceKind;
#[derive(Debug, Clone)]
@ -39,8 +43,8 @@ pub enum Message {
#[derive(Debug, Clone)]
pub enum DefineBitcoind {
CookiePathEdited(String),
AddressEdited(String),
ConfigFieldEdited(ConfigField, String),
RpcAuthTypeSelected(RpcAuthType),
PingBitcoindResult(Result<(), Error>),
PingBitcoind,
}

View File

@ -9,7 +9,10 @@ use bitcoin_hashes::{sha256, Hash};
#[cfg(any(target_os = "macos", target_os = "linux"))]
use flate2::read::GzDecoder;
use iced::{Command, Subscription};
use liana::{config::BitcoindConfig, miniscript::bitcoin::Network};
use liana::{
config::{BitcoindConfig, BitcoindRpcAuth},
miniscript::bitcoin::Network,
};
#[cfg(any(target_os = "macos", target_os = "linux"))]
use tar::Archive;
use tracing::info;
@ -21,7 +24,7 @@ use liana_ui::{component::form, widget::*};
use crate::{
bitcoind::{
self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory,
Bitcoind, StartInternalBitcoindError, VERSION,
Bitcoind, ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError, VERSION,
},
download,
hw::HardwareWallets,
@ -465,7 +468,8 @@ impl Step for SelectBitcoindTypeStep {
}
pub struct DefineBitcoind {
cookie_path: form::Value<String>,
rpc_auth_vals: RpcAuthValues,
selected_auth_type: RpcAuthType,
address: form::Value<String>,
is_running: Option<Result<(), Error>>,
}
@ -473,7 +477,8 @@ pub struct DefineBitcoind {
impl DefineBitcoind {
pub fn new() -> Self {
Self {
cookie_path: form::Value::default(),
rpc_auth_vals: RpcAuthValues::default(),
selected_auth_type: RpcAuthType::CookieFile,
address: form::Value::default(),
is_running: None,
}
@ -481,16 +486,28 @@ impl DefineBitcoind {
pub fn ping(&self) -> Command<Message> {
let address = self.address.value.to_owned();
let cookie_path = self.cookie_path.value.to_owned();
let selected_auth_type = self.selected_auth_type;
let rpc_auth_vals = self.rpc_auth_vals.clone();
Command::perform(
async move {
let cookie = std::fs::read_to_string(&cookie_path)
.map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?;
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(
SimpleHttpTransport::builder()
builder
.url(&address)?
.timeout(std::time::Duration::from_secs(3))
.cookie_auth(cookie)
.build(),
);
client.send_request(client.build_request("echo", &[]))?;
@ -503,8 +520,8 @@ impl DefineBitcoind {
impl Step for DefineBitcoind {
fn load_context(&mut self, ctx: &Context) {
if self.cookie_path.value.is_empty() {
self.cookie_path.value =
if self.rpc_auth_vals.cookie_path.value.is_empty() {
self.rpc_auth_vals.cookie_path.value =
bitcoind_default_cookie_path(&ctx.bitcoin_config.network).unwrap_or_default()
}
if self.address.value.is_empty() {
@ -519,15 +536,31 @@ impl Step for DefineBitcoind {
return self.ping();
}
message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res),
message::DefineBitcoind::AddressEdited(address) => {
message::DefineBitcoind::ConfigFieldEdited(field, value) => match field {
ConfigField::Address => {
self.is_running = None;
self.address.value = value;
self.address.valid = true;
}
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.address.value = address;
self.address.valid = true;
}
message::DefineBitcoind::CookiePathEdited(path) => {
self.is_running = None;
self.cookie_path.value = path;
self.address.valid = true;
self.selected_auth_type = auth_type;
}
};
};
@ -535,28 +568,29 @@ impl Step for DefineBitcoind {
}
fn apply(&mut self, ctx: &mut Context) -> bool {
match (
PathBuf::from_str(&self.cookie_path.value),
std::net::SocketAddr::from_str(&self.address.value),
) {
(Err(_), Ok(_)) => {
self.cookie_path.valid = false;
false
let addr = std::net::SocketAddr::from_str(&self.address.value);
let rpc_auth = match self.selected_auth_type {
RpcAuthType::CookieFile => {
if let Ok(path) = PathBuf::from_str(&self.rpc_auth_vals.cookie_path.value) {
Some(BitcoindRpcAuth::CookieFile(path))
} else {
self.rpc_auth_vals.cookie_path.valid = false;
None
}
}
(Ok(_), Err(_)) => {
RpcAuthType::UserPass => Some(BitcoindRpcAuth::UserPass(
self.rpc_auth_vals.user.value.clone(),
self.rpc_auth_vals.password.value.clone(),
)),
};
match (rpc_auth, addr) {
(None, Ok(_)) => false,
(_, Err(_)) => {
self.address.valid = false;
false
}
(Err(_), Err(_)) => {
self.cookie_path.valid = false;
self.address.valid = false;
false
}
(Ok(path), Ok(addr)) => {
ctx.bitcoind_config = Some(BitcoindConfig {
cookie_path: path,
addr,
});
(Some(rpc_auth), Ok(addr)) => {
ctx.bitcoind_config = Some(BitcoindConfig { rpc_auth, addr });
true
}
}
@ -566,7 +600,8 @@ impl Step for DefineBitcoind {
view::define_bitcoin(
progress,
&self.address,
&self.cookie_path,
&self.rpc_auth_vals,
&self.selected_auth_type,
self.is_running.as_ref(),
)
}
@ -803,7 +838,7 @@ impl Step for InternalBitcoindStep {
match Bitcoind::start(
&self.network,
BitcoindConfig {
cookie_path,
rpc_auth: BitcoindRpcAuth::CookieFile(cookie_path),
addr: internal_bitcoind_address(rpc_port),
},
&self.liana_datadir,

View File

@ -1,5 +1,6 @@
use iced::widget::{
checkbox, container, pick_list, scrollable, scrollable::Properties, slider, Space, TextInput,
checkbox, container, pick_list, radio, scrollable, scrollable::Properties, slider, Space,
TextInput,
};
use iced::{alignment, widget::progress_bar, Alignment, Length};
@ -21,7 +22,7 @@ use liana_ui::{
};
use crate::{
bitcoind::StartInternalBitcoindError,
bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError},
hw::HardwareWallet,
installer::{
message::{self, Message},
@ -787,14 +788,18 @@ pub fn help_backup<'a>() -> Element<'a, Message> {
pub fn define_bitcoin<'a>(
progress: (usize, usize),
address: &form::Value<String>,
cookie_path: &form::Value<String>,
rpc_auth_vals: &RpcAuthValues,
selected_auth_type: &RpcAuthType,
is_running: Option<&Result<(), Error>>,
) -> Element<'a, Message> {
let col_address = Column::new()
.push(text("Address:").bold())
.push(
form::Form::new_trimmed("Address", address, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::AddressEdited(msg))
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::Address,
msg,
))
})
.warning("Please enter correct address")
.size(20)
@ -802,16 +807,67 @@ pub fn define_bitcoin<'a>(
)
.spacing(10);
let col_cookie = Column::new()
.push(text("Cookie path:").bold())
let col_auth = Column::new()
.push(
form::Form::new_trimmed("Cookie path", cookie_path, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::CookiePathEdited(msg))
})
.warning("Please enter correct path")
.size(20)
.padding(10),
[RpcAuthType::CookieFile, RpcAuthType::UserPass]
.iter()
.fold(
Row::new()
.push(text("RPC authentication:").small().bold())
.spacing(10),
|row, auth_type| {
row.push(radio(
format!("{}", auth_type),
*auth_type,
Some(*selected_auth_type),
|new_selection| {
Message::DefineBitcoind(
message::DefineBitcoind::RpcAuthTypeSelected(new_selection),
)
},
))
.spacing(30)
.align_items(Alignment::Center)
},
),
)
.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,
))
})
.warning("Please enter correct path")
.size(20)
.padding(10),
),
RpcAuthType::UserPass => Row::new()
.push(
form::Form::new_trimmed("User", &rpc_auth_vals.user, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::User,
msg,
))
})
.warning("Please enter correct user")
.size(20)
.padding(10),
)
.push(
form::Form::new_trimmed("Password", &rpc_auth_vals.password, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::Password,
msg,
))
})
.warning("Please enter correct password")
.size(20)
.padding(10),
)
.spacing(10),
})
.spacing(10);
layout(
@ -819,7 +875,7 @@ pub fn define_bitcoin<'a>(
"Set up connection to the Bitcoin full node",
Column::new()
.push(col_address)
.push(col_cookie)
.push(col_auth)
.push_maybe(if is_running.is_some() {
is_running.map(|res| {
if res.is_ok() {

View File

@ -345,7 +345,7 @@ impl radio::StyleSheet for Theme {
background: iced::Color::TRANSPARENT.into(),
dot_color: color::GREEN,
border_width: 1.0,
border_color: color::GREEN,
border_color: color::GREY_7,
text_color: None,
}
}
@ -354,6 +354,7 @@ impl radio::StyleSheet for Theme {
let active = self.active(style, is_selected);
radio::Appearance {
dot_color: color::GREEN,
border_color: color::GREEN,
background: iced::Color::TRANSPARENT.into(),
..active
}