From bb11169d8ea1c0c6ba84868561200971cb74d7cb Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Mon, 26 Feb 2024 18:15:22 +0100 Subject: [PATCH 1/2] sim: add simple simulation of the payout Signed-off-by: Vincenzo Palazzo --- cln-integration/Cargo.lock | 136 ++++++++++++++++++++++++++++--- cln-integration/Cargo.toml | 2 + cln-integration/src/main.rs | 80 ++++++++++++++++--- cln-integration/src/utils.rs | 150 +++++++++++++++++++++++++++++++++++ pkg/plugin/fetch_invoice.go | 12 ++- pkg/plugin/pay.go | 60 ++++++++++++++ 6 files changed, 412 insertions(+), 28 deletions(-) create mode 100644 pkg/plugin/pay.go diff --git a/cln-integration/Cargo.lock b/cln-integration/Cargo.lock index 313677d..627e3e6 100644 --- a/cln-integration/Cargo.lock +++ b/cln-integration/Cargo.lock @@ -177,9 +177,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" [[package]] name = "cfg-if" @@ -190,7 +190,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clightning-testing" version = "0.1.0" -source = "git+https://github.com/laanwj/cln4rust.git#46ff0e4dc7426926041410cc7486dc80a764a939" +source = "git+https://github.com/laanwj/cln4rust.git#b79651dfc8606e8ad4cefe652a582abbbb97c0d0" dependencies = [ "anyhow", "bitcoincore-rpc", @@ -204,7 +204,7 @@ dependencies = [ [[package]] name = "clightningrpc" version = "0.3.0-beta.8" -source = "git+https://github.com/laanwj/cln4rust.git#46ff0e4dc7426926041410cc7486dc80a764a939" +source = "git+https://github.com/laanwj/cln4rust.git#b79651dfc8606e8ad4cefe652a582abbbb97c0d0" dependencies = [ "clightningrpc-common", "serde", @@ -229,6 +229,8 @@ dependencies = [ "clightning-testing", "env_logger", "log", + "ntest", + "serde", "serde_json", "tokio", ] @@ -262,6 +264,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -296,10 +304,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] -name = "hermit-abi" -version = "0.3.6" +name = "hashbrown" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hermit-abi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" [[package]] name = "hex_lit" @@ -313,6 +327,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.10" @@ -374,6 +398,39 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ntest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da8ec6d2b73d45307e926f5af46809768581044384637af6b3f3fe7c3c88f512" +dependencies = [ + "ntest_test_cases", + "ntest_timeout", +] + +[[package]] +name = "ntest_test_cases" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be7d33be719c6f4d09e64e27c1ef4e73485dc4cc1f4d22201f89860a7fe22e22" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ntest_timeout" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066b468120587a402f0b47d8f80035c921f6a46f8209efd0632a89a16f5188a4" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -393,6 +450,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -414,6 +477,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -554,7 +627,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.51", ] [[package]] @@ -579,9 +652,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.50" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" dependencies = [ "proc-macro2", "quote", @@ -590,9 +674,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", @@ -625,7 +709,24 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.51", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", ] [[package]] @@ -777,3 +878,12 @@ name = "windows_x86_64_msvc" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/cln-integration/Cargo.toml b/cln-integration/Cargo.toml index 38a08bd..298cab6 100644 --- a/cln-integration/Cargo.toml +++ b/cln-integration/Cargo.toml @@ -9,4 +9,6 @@ env_logger = "0.11.1" anyhow = "1.0.71" log = "0.4" tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } +serde = "1.0" serde_json = "1.0.1" +ntest = "0.9.0" diff --git a/cln-integration/src/main.rs b/cln-integration/src/main.rs index 60fb50d..8f7bab5 100644 --- a/cln-integration/src/main.rs +++ b/cln-integration/src/main.rs @@ -4,6 +4,7 @@ use std::sync::Once; use clightning_testing::cln; +use serde::Deserialize; use serde_json::{json, Value}; mod utils; @@ -23,19 +24,74 @@ fn init() { #[tokio::test(flavor = "multi_thread")] async fn test_init_plugin() -> anyhow::Result<()> { init(); - let pwd = std::env!("PWD"); - let plugin_name = std::env!("PLUGIN_NAME"); - log::debug!("plugin path: {pwd}/../{plugin_name}"); - let cln1 = cln::Node::with_params( - &format!("--developer --experimental-offers --plugin={pwd}/../{plugin_name}"), - "regtest", - ) - .await?; + let cln1 = node!(); let info = cln1.rpc().call::("ocean-info", json!({})); log::info!("{:?}", info); - if info.is_err() { - let _ = cln1.print_logs(); - } - assert!(info.is_ok(), "{:?}", info); + check!(cln1, info, "{:?}", info); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[ntest::timeout(560000)] +async fn test_ocean_simple_payout() -> anyhow::Result<()> { + init(); + + // initial setup of the lightning payout with ocean. + // Long part of the story: + // + // - Ocean pool has a lightning node to do lightning payout; + // - Every miners will open a channel with the pool ln node; + // - The pool lightning node receive in money from the coinbase rewards + // - Wait the maturity of the blocks + // - The pool lightning node identify the channel and splice the amount of money + // inside the channel. + // - The splice transaction get confirmed; + // - The pool pay the bolt12 miners + let ocean_ln = node!(); + let btc = ocean_ln.btc(); + let miner_1 = node!(btc.clone()); + open_channel(&miner_1, &ocean_ln, false)?; + + #[derive(Deserialize, Debug)] + struct Offer { + bolt12: String, + } + + // the miner generate the payout reusable offer + let payout_miner: Offer = miner_1.rpc().call( + "offer", + json!({ + "amount": "any", + "description": "Miner 1 lightning payout for ocean", + + }), + )?; + + log::info!("offer invoice: {:?}", payout_miner); + // FIXME: we are not able at the moment to splice the channel to increase the balance, + // so at the moment, so atm we open a new channel but this is not inside our simulation + open_channel(&ocean_ln, &miner_1, false)?; + + let listchannels = ocean_ln.rpc().listchannels(None, None, None)?.channels; + log::debug!( + "channels before paying: {}", + serde_json::to_string(&listchannels)? + ); + let listchannels = ocean_ln.rpc().listfunds()?.channels; + log::debug!( + "channels in list funds before paying: {}", + serde_json::to_string(&listchannels)? + ); + let payout: Result = ocean_ln.rpc().call( + "ocean-pay", + json!({ + "invstr": payout_miner.bolt12, + "amount_msat": "10sat", + }), + ); + if payout.is_err() { + let _ = ocean_ln.print_logs()?; + payout.unwrap(); + } Ok(()) } diff --git a/cln-integration/src/utils.rs b/cln-integration/src/utils.rs index 6566fa2..a42b795 100644 --- a/cln-integration/src/utils.rs +++ b/cln-integration/src/utils.rs @@ -1,4 +1,8 @@ //! Test Utils +use std::{str::FromStr, sync::Arc}; + +use clightning_testing::{btc, cln, prelude::clightningrpc::requests::AmountOrAll}; + #[macro_export] macro_rules! wait { ($callback:expr, $timeout:expr) => {{ @@ -19,3 +23,149 @@ macro_rules! wait { $crate::wait!($callback, 100); }; } + +#[macro_export] +macro_rules! node { + ($btc:expr) => {{ + let pwd = std::env!("PWD"); + let plugin_name = std::env!("PLUGIN_NAME"); + log::debug!("plugin path: {pwd}/../{plugin_name}"); + cln::Node::with_btc_and_params( + $btc, + &format!("--developer --experimental-offers --plugin={pwd}/../{plugin_name}"), + "regtest", + ) + .await? + }}; + () => {{ + let pwd = std::env!("PWD"); + let plugin_name = std::env!("PLUGIN_NAME"); + log::debug!("plugin path: {pwd}/../{plugin_name}"); + cln::Node::with_params( + &format!("--developer --experimental-offers --plugin={pwd}/../{plugin_name}"), + "regtest", + ) + .await? + }}; +} + +#[macro_export] +macro_rules! check { + ($cln:expr, $value:expr, $($arg:tt)+) => {{ + if $value.is_err() { + let _ = $cln.print_logs(); + } + assert!($value.is_ok()); + }}; +} + +#[macro_export] +macro_rules! wait_sync { + ($cln:expr) => {{ + wait!( + || { + let Ok(cln_info) = $cln.rpc().getinfo() else { + return Err(()); + }; + log::trace!("cln info: {:?}", cln_info); + if cln_info.warning_bitcoind_sync.is_some() { + return Err(()); + } + + if cln_info.warning_lightningd_sync.is_some() { + return Err(()); + } + let mut out = $cln.rpc().listfunds().unwrap().outputs; + log::trace!("{:?}", out); + out.retain(|tx| tx.status == "confirmed"); + if out.is_empty() { + let addr = $cln.rpc().newaddr(None).unwrap().bech32.unwrap(); + let _ = fund_wallet($cln.btc(), &addr, 6); + return Err(()); + } + + Ok(()) + }, + 10000 + ); + }}; +} + +/// Open a channel from node_a -> node_b +pub fn open_channel(node_a: &cln::Node, node_b: &cln::Node, dual_open: bool) -> anyhow::Result<()> { + let addr = node_a.rpc().newaddr(None)?.bech32.unwrap(); + fund_wallet(node_a.btc(), &addr, 8)?; + wait_for_funds(node_a)?; + + wait_sync!(node_a); + + if dual_open { + let addr = node_b.rpc().newaddr(None)?.address.unwrap(); + fund_wallet(node_b.btc(), &addr, 6)?; + } + + let getinfo2 = node_b.rpc().getinfo()?; + node_a + .rpc() + .connect(&getinfo2.id, Some(&format!("127.0.0.1:{}", node_b.port)))?; + let listfunds = node_a.rpc().listfunds()?; + log::debug!("list funds {:?}", listfunds); + node_a + .rpc() + .fundchannel(&getinfo2.id, AmountOrAll::All, None)?; + wait!( + || { + let mut channels = node_a.rpc().listfunds().unwrap().channels; + log::info!("{:?}", channels); + let origin_size = channels.len(); + channels.retain(|chan| chan.state == "CHANNELD_NORMAL"); + if channels.len() == origin_size { + return Ok(()); + } + let addr = node_a.rpc().newaddr(None).unwrap().bech32.unwrap(); + fund_wallet(node_a.btc(), &addr, 6).unwrap(); + wait_sync!(node_a); + Err(()) + }, + 10000 + ); + Ok(()) +} + +pub fn fund_wallet(btc: Arc, addr: &str, blocks: u64) -> anyhow::Result { + use clightning_testing::prelude::bitcoincore_rpc; + use clightning_testing::prelude::bitcoincore_rpc::RpcApi; + // mine some bitcoin inside the lampo address + let address = bitcoincore_rpc::bitcoin::Address::from_str(addr) + .unwrap() + .assume_checked(); + let _ = btc.rpc().generate_to_address(blocks, &address).unwrap(); + + Ok(address.to_string()) +} + +pub fn wait_for_funds(cln: &cln::Node) -> anyhow::Result<()> { + use clightning_testing::prelude::bitcoincore_rpc; + use clightning_testing::prelude::bitcoincore_rpc::RpcApi; + + wait!( + || { + let addr = cln.rpc().newaddr(None).unwrap().bech32.unwrap(); + let address = bitcoincore_rpc::bitcoin::Address::from_str(&addr) + .unwrap() + .assume_checked(); + let _ = cln.btc().rpc().generate_to_address(1, &address).unwrap(); + + let Ok(funds) = cln.rpc().listfunds() else { + return Err(()); + }; + log::trace!("listfunds {:?}", funds); + if funds.outputs.is_empty() { + return Err(()); + } + Ok(()) + }, + 10000 + ); + Ok(()) +} diff --git a/pkg/plugin/fetch_invoice.go b/pkg/plugin/fetch_invoice.go index 5af948a..eb5d719 100644 --- a/pkg/plugin/fetch_invoice.go +++ b/pkg/plugin/fetch_invoice.go @@ -1,6 +1,8 @@ package plugin import ( + "fmt" + json "github.com/mitchellh/mapstructure" "github.com/vincenzopalazzo/cln4go/plugin" @@ -12,10 +14,14 @@ func FetchOffer(cln *plugin.Plugin[*State], fetchInfo map[string]any) (map[strin if err != nil { return nil, err } - - clnFetchInvoice := struct{}{} + cln.Log("debug", fmt.Sprintf("fetch offer result: %s", fetchInvoice)) + clnFetchInvoice := struct { + Invoice string `mapstructure:"invoice"` + }{} if err := json.Decode(fetchInvoice, &clnFetchInvoice); err != nil { return nil, err } - return nil, nil + return map[string]any{ + "bolt11": clnFetchInvoice.Invoice, + }, nil } diff --git a/pkg/plugin/pay.go b/pkg/plugin/pay.go new file mode 100644 index 0000000..0bf37c9 --- /dev/null +++ b/pkg/plugin/pay.go @@ -0,0 +1,60 @@ +package plugin + +import ( + "fmt" + + json "github.com/mitchellh/mapstructure" + + "github.com/vincenzopalazzo/cln4go/plugin" +) + +func OceanPay(cln *plugin.Plugin[*State], request map[string]any) (map[string]any, error) { + cln.Log("debug", fmt.Sprintf("ocean-pay: %s", request)) + clnRequest := struct { + Invstr string `mapstructure:"invstr"` + Amount_msat string `mapstructure:"amount_msat"` + }{} + if err := json.Decode(request, &clnRequest); err != nil { + return nil, err + } + + cln.Log("debug", fmt.Sprintf("decode the string %s", clnRequest.Invstr)) + invstrDecode, err := cln.State.Rpc("decode", map[string]any{ + "string": clnRequest.Invstr, + }) + if err != nil { + return nil, err + } + + clnDecode := struct { + InvType string `mapstructure:"type"` + Valid bool `mapstructure:"valid"` + }{} + + if err := json.Decode(invstrDecode, &clnDecode); err != nil { + return nil, err + } + + cln.Log("debug", fmt.Sprintf("decode offer: %s", invstrDecode)) + var payInvoice map[string]any + switch clnDecode.InvType { + case "bolt12 offer", "bolt12 invoice_request", "bolt12 invoice": + payInvoice, err = FetchOffer(cln, map[string]any{ + "offer": clnRequest.Invstr, + "amount_msat": clnRequest.Amount_msat, + }) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("Invoice String %s not supported: %s", clnDecode.InvType, clnRequest.Invstr) + } + + cln.Log("debug", fmt.Sprintf("paying the offer %s", payInvoice)) + payResponse, err := cln.State.Rpc("pay", payInvoice) + if err != nil { + return nil, err + } + + return payResponse, nil +} From e1749a4c2b29c8f386fc36121cec08bf6fee2962 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Mon, 26 Feb 2024 18:16:21 +0100 Subject: [PATCH 2/2] plugin: refactoring pay implementation Signed-off-by: Vincenzo Palazzo --- pkg/plugin/info.go | 2 +- pkg/plugin/plugin.go | 46 -------------------------------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/pkg/plugin/info.go b/pkg/plugin/info.go index 3f191e4..7d0caa2 100644 --- a/pkg/plugin/info.go +++ b/pkg/plugin/info.go @@ -8,6 +8,6 @@ import ( func GetInfo(cln *plugin.Plugin[*State], _ map[string]any) (map[string]any, error) { cln.Log("debug", "calling getinfo for plugin") return map[string]any{ - "v": "0.0.1", + "version": "0.0.1", }, nil } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 66a8e1c..2764cc4 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -18,52 +18,6 @@ func (self *State) Rpc(method string, args map[string]any) (map[string]any, erro return client.Call[map[string]any, map[string]any](self.rpc, method, args) } -func OceanPay(cln *plugin.Plugin[*State], request map[string]any) (map[string]any, error) { - clnRequest := struct { - invstr string - }{} - if err := json.Decode(request, &clnRequest); err != nil { - return nil, err - } - - invstrDecode, err := cln.State.Rpc("decode", map[string]any{ - "string": clnRequest.invstr, - }) - if err != nil { - return nil, err - } - - clnDecode := struct { - invType string `mapstructure:"type"` - valid bool - }{} - - if err := json.Decode(invstrDecode, &clnDecode); err != nil { - return nil, err - } - - var payInvoice map[string]any - switch clnDecode.invType { - case "bolt12 offer": - case "bolt12 invoice_request", "bolt12 invoice": - payInvoice, err = FetchOffer(cln, map[string]any{ - // TODO: adding the correct request for cln - }) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("Invoice String %s not supported: %s", clnDecode.invType, clnRequest.invstr) - } - - payResponse, err := cln.State.Rpc("pay", payInvoice) - if err != nil { - return nil, err - } - - return payResponse, nil -} - func OnShutdown(plugin *plugin.Plugin[*State], request map[string]any) { os.Exit(0) }