sim: add simple simulation of the payout

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
This commit is contained in:
Vincenzo Palazzo 2024-02-26 18:15:22 +01:00
parent 3a95f266d7
commit bb11169d8e
6 changed files with 412 additions and 28 deletions

View File

@ -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",
]

View File

@ -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"

View File

@ -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::<Value, Value>("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<Value, _> = 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(())
}

View File

@ -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<btc::BtcNode>, addr: &str, blocks: u64) -> anyhow::Result<String> {
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(())
}

View File

@ -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
}

60
pkg/plugin/pay.go Normal file
View File

@ -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
}