Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

Commit

Permalink
feat: lnd client + rust binary to run btcd and lnd (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdelabro authored Nov 6, 2024
1 parent 196ecc8 commit 24996b5
Show file tree
Hide file tree
Showing 21 changed files with 3,725 additions and 195 deletions.
19 changes: 15 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,21 @@ jobs:

- name: Integration tests
run: |-
mix phx.server &
CASHUBREW_SERVER_PID=$!
cd integration-tests && cargo test
kill $CASHUBREW_SERVER_PID
cd integration-tests
touch .env
cargo run &
BTCD_AND_LND_SERVERS_PID=$!
# Wait until the nodes are running by checking if the the env var are exported
while [[ -z "${LND_URL}" ]]; do sleep 1 && source .env; done
cd ..
# mix doesn't behave when run in background, so we use `erlang -detached` instead
# but the `$!` thing won't work coz the app is run in another thread,
# so we write the actual pid in a file and later read it to kill it
elixir --erl "-detached" -e "File.write! 'pid', :os.getpid" -S mix phx.server
cd integration-tests
cargo test
cat ../pid | xargs kill
kill $BTCD_AND_LND_SERVERS_PID
Expand Down
Empty file added .gitmodules
Empty file.
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ config :cashubrew, Cashubrew.Repo,
config :cashubrew, :repo, Cashubrew.Repo

config :cashubrew, ecto_repos: [Cashubrew.Repo]

config :cashubrew, :lnd_client, Cashubrew.LightingNetwork.Lnd
2 changes: 2 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ config :cashubrew, Cashubrew.Web.Endpoint, secret_key_base: System.get_env("SECR

# Do not print debug messages in production
config :logger, level: :info

config :cashubrew, :lnd_client, Cashubrew.LightingNetwork.Lnd
2 changes: 2 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Config

config :cashubrew, :lnd_client, Cashubrew.LightingNetwork.MockLnd

if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
Expand Down
2 changes: 2 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ else
pool_size: 10

config :cashubrew, :repo, Cashubrew.Repo

config :cashubrew, :lnd_client, Cashubrew.LightingNetwork.MockLnd
end
5 changes: 5 additions & 0 deletions integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ rand = "0.8.5"
assert_matches = "1.5.0"
tokio = "1.41.0"
uuid = "1.11.0"
anyhow = "1.0.91"
bitcoind = { version = "0.34.2", features = ["26_0"] }
lnd = { version = "0.1.6", features = ["lnd_0_17_5"] }
tonic_lnd = "0.5.1"
ctrlc = "3.4"

[[test]]
name = "nut06"
Expand Down
60 changes: 60 additions & 0 deletions integration-tests/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use bitcoind::bitcoincore_rpc::RpcApi;
use lnd::Lnd;
use lnd::LndConf;
use std::sync::mpsc::channel;

#[tokio::main]
async fn main() {
println!("Downloading bitcoind...");
let bitcoind = bitcoind::BitcoinD::from_downloaded().unwrap();
assert_eq!(0, bitcoind.client.get_blockchain_info().unwrap().blocks);
println!("Done");
println!(
"bitcoind is running. Listening to {:?}",
bitcoind.params.rpc_socket
);

println!("Downloading lnd...");
let lnd_conf = LndConf::default();
let mut lnd = Lnd::with_conf(
lnd::exe_path().unwrap(),
&lnd_conf,
bitcoind
.params
.cookie_file
.clone()
.into_os_string()
.into_string()
.unwrap(),
bitcoind.params.rpc_socket.to_string(),
&bitcoind,
)
.await
.unwrap();
assert!(lnd
.client
.lightning()
.get_info(tonic_lnd::lnrpc::GetInfoRequest {})
.await
.is_ok());
println!("Done");
std::fs::write(
".env",
format!(
"export LND_URL={:?}\nexport LND_CERT={:?}\nexport LND_MACAROON={:?}",
lnd.grpc_url,
lnd.tls_cert_path(),
lnd.admin_macaroon_path()
),
)
.unwrap();

let (tx, rx) = channel();

ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
.expect("Error setting Ctrl-C handler");

println!("Press ctrl+c to exit");

rx.recv().expect("Could not receive from channel.");
}
6 changes: 3 additions & 3 deletions lib/cashubrew/NUTs/NUT-04/impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ defmodule Cashubrew.Nuts.Nut04.Impl do
@moduledoc """
Implementation and structs of the NUT-04
"""
alias Cashubrew.Lightning.LightningNetworkService
alias Cashubrew.Mint
alias Cashubrew.Nuts.Nut00
alias Cashubrew.Nuts.Nut04.Impl.MintQuoteMutex
alias Cashubrew.Schema

def create_mint_quote!(amount, unit) do
def create_mint_quote!(amount, unit, description) do
repo = Application.get_env(:cashubrew, :repo)

{payment_request, _payment_hash} = LightningNetworkService.create_invoice!(amount, unit)
{payment_request, _payment_hash} =
Cashubrew.LightingNetwork.Lnd.create_invoice!(amount, unit, description)

# Note: quote is a unique and random id generated by the mint to internally look up the payment state.
# quote MUST remain a secret between user and mint and MUST NOT be derivable from the payment request.
Expand Down
16 changes: 4 additions & 12 deletions lib/cashubrew/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,13 @@ defmodule Cashubrew.Application do
def start(_type, _args) do
children = [
Cashubrew.Web.Telemetry,
Application.get_env(:cashubrew, :lnd_client),
{Phoenix.PubSub, name: Cashubrew.PubSub},
Endpoint
Endpoint,
Application.get_env(:cashubrew, :repo),
{Task, fn -> Cashubrew.Mint.init() end}
]

# Conditionally add the appropriate repo to the children list
children =
case Application.get_env(:cashubrew, :repo) do
Cashubrew.MockRepo -> [Cashubrew.MockRepo | children]
Cashubrew.Repo -> [Cashubrew.Repo | children]
_ -> children
end

# Always add Cashubrew.Mint after the repo
children = children ++ [{Task, fn -> Cashubrew.Mint.init() end}]

opts = [strategy: :one_for_one, name: Cashubrew.Supervisor]
Supervisor.start_link(children, opts)
end
Expand Down
37 changes: 0 additions & 37 deletions lib/cashubrew/lightning/lightning_network_service.ex

This file was deleted.

51 changes: 0 additions & 51 deletions lib/cashubrew/lightning/ln_bits.ex

This file was deleted.

21 changes: 0 additions & 21 deletions lib/cashubrew/lightning/ln_lib.ex

This file was deleted.

72 changes: 72 additions & 0 deletions lib/cashubrew/lightning/lnd_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule Cashubrew.LightingNetwork.Lnd do
@moduledoc """
Client to interact with lnd
"""
use GenServer
require Logger

def start_link(arg) do
GenServer.start_link(__MODULE__, arg, name: __MODULE__)
end

def init(args) do
node_url = URI.parse(System.get_env("LND_URL"))
creds = get_creds(System.get_env("LND_CERT"))
macaroon = get_macaroon(System.get_env("LND_MACAROON"))

Logger.debug("gRPC client connecting to gateway at #{node_url}")

case GRPC.Stub.connect("#{node_url.host}:#{node_url.port}",
cred: creds
) do
{:error, error} ->
Logger.critical("Could not connect. Retrying... #{error}")
init(args)

{:ok, channel} ->
Logger.info("Connected to the gateway at #{node_url}")
{:ok, %{channel: channel, macaroon: macaroon}}
end
end

# one day
def validity, do: 86_400

def create_invoice!(amount, unit, description) do
GenServer.call(__MODULE__, {:create_invoice, amount, unit, description}, __MODULE__)
end

def handle_call({:create_invoice, amount, unit, description}, _from, state) do
if unit != "sat" do
raise "UnsuportedUnit"
end

amount_ms = amount * 1000

expiry = validity() + System.os_time(:second)

request = %Cashubrew.Lnrpc.Invoice{
memo: description,
value_msat: amount_ms,
expiry: expiry
}

{:ok, response} = Cashubrew.Lnrpc.Lightning.Stub.add_invoice(state["channel"], request)
{:reply, response, state}
end

defp get_creds(cert_path) do
filename = Path.expand(cert_path)

# ++ [verify: true/:verify_none]
ssl_opts = [cacertfile: filename]

GRPC.Credential.new(ssl: ssl_opts)
end

defp get_macaroon(macaroon_path) do
filename = Path.expand(macaroon_path)

File.read!(filename) |> Base.encode16()
end
end
Loading

0 comments on commit 24996b5

Please sign in to comment.