diff --git a/Cargo.lock b/Cargo.lock index 0ee6d828..a39a0d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,6 +587,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "clightningrpc-plugin-macros" +version = "0.3.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef81a4e59115a4ed470ea218793a76fa67d9974e6fdf9b93cb1c693130eb7755" +dependencies = [ + "clightningrpc-plugin", + "convert_case 0.5.0", + "kproc-parser", + "serde_json", +] + [[package]] name = "coffee" version = "0.0.1-alpha.1" @@ -677,6 +689,7 @@ version = "0.1.0" dependencies = [ "clightningrpc-common", "clightningrpc-plugin", + "clightningrpc-plugin-macros", "coffee_core", "coffee_lib", "log", @@ -736,6 +749,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "cookie" version = "0.16.2" @@ -896,7 +915,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -1411,6 +1430,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kproc-parser" +version = "0.0.1-beta.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a3c5d81e3af35bb269378a933889f7f45c884bdf820a9c268711035430f3f7" + [[package]] name = "language-tags" version = "0.3.2" diff --git a/Makefile b/Makefile index a089eb4d..43af85ea 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ ARGS="" default: fmt $(CC) build - @make example doc-deps: $(CC) install mdbook @@ -13,26 +12,27 @@ doc-deps: fmt: $(CC) fmt --all -check: +check: ## Runs unit testing $(CC) test $(ARGS) -example: - @echo "No example for the moment" - -clean: +clean: ## Clean up everythings $(CC) clean -book: +book: ## Build the release version of documentation cd docs/docs-book; mdbook build -dev-book: +dev-book: ## Build the docs in dev mode cd docs/docs-book; mdbook serve --open -install: +install: ## Install coffee inside the local machine $(CC) install --locked --path ./coffee_cmd -integration: default +integration: default ## Runs integration testing $(CC) test -j 4 -p tests $(ARGS) setup: git config core.hooksPath .githooks + +help: ## Show Help + @grep --no-filename -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/coffee_cmd/src/cmd.rs b/coffee_cmd/src/cmd.rs index 0807d155..6f605021 100644 --- a/coffee_cmd/src/cmd.rs +++ b/coffee_cmd/src/cmd.rs @@ -70,6 +70,9 @@ pub enum CoffeeCommand { #[arg(short, long, action = clap::ArgAction::SetTrue)] verify: bool, }, + /// tipping a plugins developer. + #[clap(arg_required_else_help = false)] + Tip { plugin: String, amount_msat: u64 }, } #[derive(Debug, Subcommand)] @@ -107,6 +110,10 @@ impl From<&CoffeeCommand> for coffee_core::CoffeeOperation { CoffeeCommand::Show { plugin } => Self::Show(plugin.to_owned()), CoffeeCommand::Search { plugin } => Self::Search(plugin.to_owned()), CoffeeCommand::Nurse { verify } => Self::Nurse(*verify), + CoffeeCommand::Tip { + plugin, + amount_msat, + } => Self::Tip(plugin.to_owned(), amount_msat.clone()), } } } diff --git a/coffee_cmd/src/coffee_term/command_show.rs b/coffee_cmd/src/coffee_term/command_show.rs index 64267d29..819635ca 100644 --- a/coffee_cmd/src/coffee_term/command_show.rs +++ b/coffee_cmd/src/coffee_term/command_show.rs @@ -2,12 +2,12 @@ //! the command result on the terminal! use radicle_term as term; -use radicle_term::table::TableOptions; +use term::table::TableOptions; +use term::Element; use coffee_lib::error; use coffee_lib::errors::CoffeeError; -use coffee_lib::types::response::{CoffeeList, CoffeeNurse, CoffeeRemote, NurseStatus}; -use term::Element; +use coffee_lib::types::response::{CoffeeList, CoffeeNurse, CoffeeRemote, CoffeeTip, NurseStatus}; pub fn show_list(coffee_list: Result) -> Result<(), CoffeeError> { let remotes = coffee_list?; @@ -119,6 +119,30 @@ pub fn show_nurse_result( } Err(err) => eprintln!("{}", err), } + Ok(()) +} +pub fn show_tips(coffee_tip: &CoffeeTip) -> Result<(), CoffeeError> { + term::println(term::format::bold("●"), term::format::tertiary("Plugin")); + let mut table = radicle_term::Table::new(TableOptions::bordered()); + table.push([ + term::format::dim(String::from("●")), + term::format::bold(String::from("Plugin")), + term::format::bold(String::from("Receiver")), + term::format::bold(String::from("Amount Sent (msat)")), + ]); + table.divider(); + + table.push([ + if coffee_tip.status == "completed" { + term::format::positive("●").into() + } else { + term::format::negative("●").into() + }, + term::format::highlight(coffee_tip.for_plugin.clone()), + term::format::bold(coffee_tip.destination.clone().unwrap_or_default()), + term::format::highlight(coffee_tip.amount_msat.to_string()), + ]); + table.print(); Ok(()) } diff --git a/coffee_cmd/src/main.rs b/coffee_cmd/src/main.rs index b90f09db..623a6f24 100644 --- a/coffee_cmd/src/main.rs +++ b/coffee_cmd/src/main.rs @@ -169,6 +169,13 @@ async fn run(args: CoffeeArgs, mut coffee: CoffeeManager) -> Result<(), CoffeeEr coffee_term::show_nurse_result(nurse_result)?; } } + CoffeeCommand::Tip { + plugin, + amount_msat, + } => { + let tip_result = coffee.tip(&plugin, amount_msat).await?; + coffee_term::show_tips(&tip_result)?; + } }; Ok(()) } diff --git a/coffee_core/src/coffee.rs b/coffee_core/src/coffee.rs index a548cf9b..a01e21af 100644 --- a/coffee_core/src/coffee.rs +++ b/coffee_core/src/coffee.rs @@ -1,5 +1,4 @@ //! Coffee mod implementation - use std::collections::HashMap; use std::fmt::Debug; use std::vec::Vec; @@ -12,6 +11,7 @@ use clightningrpc_conf::{CLNConf, SyncCLNConf}; use log; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use serde_json::json; use tokio::process::Command; use coffee_github::repository::Github; @@ -547,6 +547,46 @@ impl PluginManager for CoffeeManager { } Ok(nurse_actions) } + + async fn tip(&mut self, plugin: &str, amount_msat: u64) -> Result { + let plugins = self + .config + .plugins + .iter() + .filter(|repo_plugin| plugin == repo_plugin.name()) + .collect::>(); + let plugin = plugins.first().ok_or(error!( + "No plugin with name `{plugin}` found in the plugins installed" + ))?; + + let Some(tipping) = plugin.tipping_info() else { + return Err(error!("Plugin `{plugin}` has no tipping information")); + }; + // FIXME write a tip_plugin method as method + #[derive(Debug, Deserialize)] + struct FetchResult { + invoice: String, + } + let invoice: FetchResult = self + .cln( + "fetchinvoice", + json!({ + "offer": tipping.bolt12, + "amount_msat": amount_msat, + }), + ) + .await?; + let tip: CoffeeTip = self + .cln( + "pay", + json!({ + "bolt11": invoice.invoice, + "amount_msat": amount_msat, + }), + ) + .await?; + Ok(tip) + } } // FIXME: we need to move on but this is not safe and with the coffee diff --git a/coffee_core/src/lib.rs b/coffee_core/src/lib.rs index 4f0c5292..fe695e1e 100644 --- a/coffee_core/src/lib.rs +++ b/coffee_core/src/lib.rs @@ -22,6 +22,10 @@ pub enum CoffeeOperation { /// Search(plugin name) Search(String), Nurse(bool), + /// Tip operation + /// + /// (plugin_name, amount_msat) + Tip(String, u64), } #[derive(Clone, Debug)] diff --git a/coffee_lib/src/plugin.rs b/coffee_lib/src/plugin.rs index 0dba972a..e32b052d 100644 --- a/coffee_lib/src/plugin.rs +++ b/coffee_lib/src/plugin.rs @@ -8,7 +8,7 @@ use tokio::process::Command; use crate::errors::CoffeeError; use crate::macros::error; -use crate::plugin_conf::Conf; +use crate::plugin_conf::{Conf, Tipping}; use crate::sh; /// Plugin language definition @@ -156,6 +156,10 @@ impl Plugin { pub fn name(&self) -> String { self.name.clone() } + + pub fn tipping_info(&self) -> Option { + self.conf.as_ref().and_then(|conf| conf.tipping.clone()) + } } impl fmt::Display for Plugin { diff --git a/coffee_lib/src/plugin_conf.rs b/coffee_lib/src/plugin_conf.rs index 3cb14969..b241c6b8 100644 --- a/coffee_lib/src/plugin_conf.rs +++ b/coffee_lib/src/plugin_conf.rs @@ -2,13 +2,12 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] - pub struct Conf { pub plugin: Plugin, + pub tipping: Option, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] - pub struct Plugin { pub name: String, pub version: String, @@ -24,8 +23,7 @@ pub struct Deprecaterd { pub reason: String, } -#[cfg(test)] -mod tests { - #[test] - fn test_remote() {} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Tipping { + pub bolt12: String, } diff --git a/coffee_lib/src/plugin_manager.rs b/coffee_lib/src/plugin_manager.rs index ba40e475..65bca03a 100644 --- a/coffee_lib/src/plugin_manager.rs +++ b/coffee_lib/src/plugin_manager.rs @@ -61,4 +61,12 @@ pub trait PluginManager { &mut self, repos: Vec, ) -> Result, CoffeeError>; + + /// tip a specific plugins of the following amount + /// + /// The tip command required that the receiver of the + /// donation is runing the coffee core lightning plugin. + /// + /// P.S: only Bitcoin ofc + async fn tip(&mut self, plugin: &str, amount_msat: u64) -> Result; } diff --git a/coffee_lib/src/types/mod.rs b/coffee_lib/src/types/mod.rs index fe77e87b..794dc53a 100644 --- a/coffee_lib/src/types/mod.rs +++ b/coffee_lib/src/types/mod.rs @@ -234,4 +234,16 @@ pub mod response { } } } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct CoffeeTip { + pub for_plugin: String, + pub invoice: String, + pub status: String, + pub destination: Option, + pub amount_msat: u64, + // This includes the fee + pub amount_sent_msat: u64, + pub warning_partial_completion: Option, + } } diff --git a/coffee_plugin/Cargo.toml b/coffee_plugin/Cargo.toml index b9502945..2fcbd5ff 100644 --- a/coffee_plugin/Cargo.toml +++ b/coffee_plugin/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" tokio = { version = "1.22.0", features = ["rt"] } clightningrpc-common = "0.3.0-beta.3" clightningrpc-plugin = { version = "0.3.0-beta.8", features = ["log"] } +clightningrpc-plugin-macros = "0.3.0-beta.4" coffee_core = { path = "../coffee_core" } coffee_lib = { path = "../coffee_lib" } serde_json = "1" diff --git a/coffee_plugin/src/plugin/plugin_mod.rs b/coffee_plugin/src/plugin/plugin_mod.rs index 34383eb3..4f3fa403 100644 --- a/coffee_plugin/src/plugin/plugin_mod.rs +++ b/coffee_plugin/src/plugin/plugin_mod.rs @@ -2,14 +2,16 @@ //! Coffee as a core lightning plugin. use std::fmt::Display; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tokio::runtime::Runtime; use clightningrpc_common::json_utils; use clightningrpc_plugin::commands::RPCCommand; +use clightningrpc_plugin::error; use clightningrpc_plugin::plugin::{debug, info}; -use clightningrpc_plugin::{add_rpc, error}; use clightningrpc_plugin::{errors::PluginError, plugin::Plugin}; +use clightningrpc_plugin_macros::{plugin, rpc_method}; use coffee_core::coffee::CoffeeManager; use coffee_lib::errors::CoffeeError; @@ -21,10 +23,19 @@ use super::state::PluginArgs; use crate::plugin::State; pub fn build_plugin() -> Result, PluginError> { - let mut plugin = Plugin::::new(State::new(), /* dynamic */ true).on_init(on_init); - add_rpc!(plugin, CoffeeInstall); - add_rpc!(plugin, CoffeeList); - add_rpc!(plugin, CoffeeRemote); + let mut plugin = plugin! { + state: State::new(), + dynamic: true, + notification: [], + methods: [ + coffee_install, + coffee_list, + coffee_remote, + coffee_generate_tip, + ], + hooks: [], + }; + plugin.on_init(on_init); Ok(plugin) } @@ -70,104 +81,73 @@ fn from(err: T) -> PluginError { error!("{err}") } -#[derive(Clone)] -struct CoffeeInstall { - name: String, - usage: String, - description: String, +#[rpc_method( + rpc_name = "coffee_install", + description = "install a plugin from one of the repository choosed" +)] +fn coffee_install(plugin: &mut Plugin, request: Value) -> Result { + let coffee = plugin.state.coffee(); + let mut coffee = coffee.lock().unwrap(); + let rt = Runtime::new().unwrap(); + + let request: InstallReq = serde_json::from_value(request)?; + rt.block_on(coffee.install(&request.name, false, true)) + .map_err(from)?; + Ok(json!({})) } -impl CoffeeInstall { - fn new() -> Self { - CoffeeInstall { - name: "coffee_install".to_string(), - usage: String::new(), - description: String::from("install a plugin from one of the repository choosed"), - } - } -} - -impl RPCCommand for CoffeeInstall { - fn call<'c>( - &self, - plugin: &mut Plugin, - request: serde_json::Value, - ) -> Result { - let coffee = plugin.state.coffee(); - let mut coffee = coffee.lock().unwrap(); - let rt = Runtime::new().unwrap(); - - let request: InstallReq = serde_json::from_value(request)?; - rt.block_on(coffee.install(&request.name, false, true)) - .map_err(from)?; - Ok(json!({})) - } -} - -#[derive(Clone)] -struct CoffeeList { - name: String, - usage: String, - description: String, -} - -impl CoffeeList { - fn new() -> Self { - CoffeeList { name: "coffee_list".to_owned(), usage: String::new(), description: "show all the plugin installed and if {remotes} is specified show also the one available".to_owned() } - } +#[rpc_method( + rpc_name = "coffee_list", + description = "show all the plugin installed and if {remotes} is specified show also the one available" +)] +fn coffee_list(plugin: &mut Plugin, _: Value) -> Result { + let runtime = Runtime::new().unwrap(); + let coffee = plugin.state.coffee(); + let mut coffee = coffee.lock().unwrap(); + let result = runtime.block_on(coffee.list()).map_err(from)?; + Ok(serde_json::to_value(result)?) } -impl RPCCommand for CoffeeList { - fn call<'c>( - &self, - plugin: &mut Plugin, - _: serde_json::Value, - ) -> Result { - let runtime = Runtime::new().unwrap(); - let coffee = plugin.state.coffee(); - let mut coffee = coffee.lock().unwrap(); - let result = runtime.block_on(coffee.list()).map_err(from)?; - Ok(serde_json::to_value(result)?) - } +#[rpc_method(rpc_name = "coffee_remote", description = "manage a remote")] +fn coffee_remote(plugin: &mut Plugin, request: Value) -> Result { + let request: RemoteReq = serde_json::from_value(request)?; + let runtime = Runtime::new().unwrap(); + let coffee = plugin.state.coffee(); + + runtime + .block_on(async { + let mut coffee = coffee.lock().unwrap(); + let cmd = request.cmd().unwrap(); + match cmd { + RemoteCmd::Add => coffee.add_remote(&request.name, &request.url()).await, + RemoteCmd::Rm => coffee.rm_remote(&request.name).await, + } + }) + .map_err(from)?; + Ok(json!({})) } -#[derive(Clone)] -struct CoffeeRemote { - name: String, - usage: String, - description: String, -} +#[rpc_method( + rpc_name = "coffee_generate_tip", + description = "Generate the BOLT 12 to add inside a plugin configuration to receive donation" +)] +fn coffee_generate_tip(plugin: &mut Plugin, request: Value) -> Result { + let runtime = Runtime::new().unwrap(); + let coffee = plugin.state.coffee(); -impl CoffeeRemote { - fn new() -> Self { - CoffeeRemote { - name: "coffee_remote".to_owned(), - usage: String::new(), - description: "manage a remote".to_owned(), - } + #[derive(Serialize, Deserialize, Debug)] + struct Offer { + pub bolt12: String, } -} -impl RPCCommand for CoffeeRemote { - fn call<'c>( - &self, - plugin: &mut Plugin, - request: serde_json::Value, - ) -> Result { - let request: RemoteReq = serde_json::from_value(request)?; - let runtime = Runtime::new().unwrap(); - let coffee = plugin.state.coffee(); - - runtime - .block_on(async { - let mut coffee = coffee.lock().unwrap(); - let cmd = request.cmd().unwrap(); - match cmd { - RemoteCmd::Add => coffee.add_remote(&request.name, &request.url()).await, - RemoteCmd::Rm => coffee.rm_remote(&request.name).await, - } - }) - .map_err(from)?; - Ok(json!({})) - } + let offer = runtime + .block_on(async { + let mut coffee = coffee.lock().unwrap(); + coffee.cln::("offer", json!({ + "amount": "any", + "description": "Generating BOLT 12 for coffee tips regarding the plugin ...", + })).await + }) + .map_err(from)?; + Ok(serde_json::to_value(offer)?) } diff --git a/docs/docs-book/src/support-coffee.md b/docs/docs-book/src/support-coffee.md index 59f96a23..27b92f6e 100644 --- a/docs/docs-book/src/support-coffee.md +++ b/docs/docs-book/src/support-coffee.md @@ -41,3 +41,25 @@ itself. With some craziness will be also possible to manage core lightning itsel Please if you feel that additional meta information needs to be specified open an issue https://github.com/coffee-tools/coffee/issues + +## Tipping + +While there are possibility to tipping anything on lightning, there is any solution to tipping a core lightning plugin +developer. So with coffee as developer you can define a BOLT 12 invoice that allow the developer of the plugin +to receive tips. So, the developer of the plugin should define a coffee manifest that specify the `tipping` info. + +An example can be: + +```yaml +--- +plugin: + name: btcli4j + version: 0.0.1 + lang: java + install: | + sh -C ./gradlew createRunnableScript + main: btcli4j-gen.sh +tipping: + bolt12: +``` + diff --git a/docs/docs-book/src/using-coffee.md b/docs/docs-book/src/using-coffee.md index 2e5558a4..a23d367e 100644 --- a/docs/docs-book/src/using-coffee.md +++ b/docs/docs-book/src/using-coffee.md @@ -159,6 +159,15 @@ Additionally, if you wish to perform a verification of coffee without making any coffee nurse --verify ``` _________ +### Tipping a plugin in Bitcoin + +> ✅ Implemented + +``` +coffee tip +``` + +------ ## Running coffee as a server To run Coffee as a server, you can use the `coffee_httpd` binary. diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 00000000..07cde984 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.75