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

Commit

Permalink
feat(nut05): post_melt_quote (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdelabro authored Oct 29, 2024
1 parent 5514ff4 commit c53edc8
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 46 deletions.
4 changes: 4 additions & 0 deletions integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ cdk = { git = "https://github.com/cashubtc/cdk", tag = "v0.4.0" }
rand = "0.8.5"
assert_matches = "1.5.0"
tokio = "1.41.0"
uuid = "1.11.0"

[[test]]
name = "nut06"
path = "nut06.rs"

[[test]]
name = "nut05"
path = "nut05.rs"
41 changes: 41 additions & 0 deletions integration-tests/nut05.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use assert_matches::assert_matches;
use cdk::{
amount::Amount,
nuts::{CurrencyUnit, MeltQuoteState},
wallet::MeltQuote,
};
use integration_tests::init_wallet;
use uuid::Uuid;

#[tokio::test]
pub async fn melt_quote_ok() {
const URL: &str = "http://localhost:4000";
let wallet = init_wallet(URL, CurrencyUnit::Sat).unwrap();

let bolt11_invoice = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string();
let melt_quote = wallet.melt_quote(bolt11_invoice, None).await.unwrap();

let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();

assert_matches!(
melt_quote,
MeltQuote {
id,
unit: _,
amount,
request: _,
fee_reserve,
state,
expiry,
payment_preimage
} if Uuid::try_parse(&id).is_ok()
&& amount == Amount::from(10)
&& fee_reserve == Amount::from(1)
&& state == MeltQuoteState::Unpaid
&& expiry >= now
&& payment_preimage.is_none()
)
}
18 changes: 12 additions & 6 deletions integration-tests/nut06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use assert_matches::assert_matches;
use integration_tests::init_wallet;

use cdk::nuts::{
ContactInfo, CurrencyUnit, MintInfo, MintMethodSettings, MintVersion, NUT04Settings, Nuts,
PaymentMethod, PublicKey,
ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion,
NUT04Settings, Nuts, PaymentMethod, PublicKey,
};

#[tokio::test]
Expand All @@ -25,10 +25,16 @@ pub async fn info() {
.to_vec(),
false,
))
.nut05(cdk::nuts::NUT05Settings {
methods: [].to_vec(),
disabled: true,
});
.nut05(cdk::nuts::NUT05Settings::new(
[MeltMethodSettings {
method: PaymentMethod::Bolt11,
unit: CurrencyUnit::Sat,
min_amount: None,
max_amount: None,
}]
.to_vec(),
false,
));
let expected_name = "Cashubrew Cashu Mint";
let expected_pubkey =
PublicKey::from_hex("0381094f72790bb014504dfc9213bd3c8450440f5d220560075dbf2f8113e9fa3e")
Expand Down
4 changes: 2 additions & 2 deletions lib/cashubrew/NUTs/NUT-04/routes.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Cashubrew.Nuts.Nut04.Routes do
@moduledoc """
List the rest routes defined in the NUT-03
List the rest routes defined in the NUT-04
"""

@doc """
Expand All @@ -14,7 +14,7 @@ defmodule Cashubrew.Nuts.Nut04.Routes do
The route to check a quote state
"""
def v1_mint_quote_for_quote_id do
v1_mint_quote() <> "/quote_id"
v1_mint_quote() <> "/:quote_id"
end

@doc """
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Cashubrew.Nuts.Nut04.Info do
defmodule Cashubrew.Nuts.Nut04.MintMethodSetting do
@moduledoc """
The infos retlated to this Nut support
"""
Expand All @@ -10,15 +10,10 @@ defmodule Cashubrew.Nuts.Nut04.Info do
@doc """
Return the map to be used in Nut06 info "nuts" field
"""
def info do
%{
methods: [
%__MODULE__{
method: "bolt11",
unit: "sat"
}
],
disabled: false
def bolt11 do
%__MODULE__{
method: "bolt11",
unit: "sat"
}
end
end
50 changes: 50 additions & 0 deletions lib/cashubrew/NUTs/NUT-05/impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule Cashubrew.Nuts.Nut05.Impl do
@moduledoc """
Implementation and structs of the NUT-05
"""
alias Cashubrew.Schema

defp percent_fee_reserve, do: 1
defp min_fee_reserve, do: 1
# 15min
defp melt_quote_validity_duration_in_sec, do: 900

def create_melt_quote!(request, unit) do
{:ok, ln_invoice} = Bitcoinex.LightningNetwork.decode_invoice(request)

repo = Application.get_env(:cashubrew, :repo)

amount =
case unit do
"sat" -> div(ln_invoice.amount_msat, 1000)
_ -> raise "UnsupportedUnit"
end

# TODO: impl max_amout min_amount config checks

relative_fee_reserve = amount * percent_fee_reserve() / 100
fee = max(relative_fee_reserve, min_fee_reserve())

expiry = System.os_time(:second) + melt_quote_validity_duration_in_sec()
quote_id = Ecto.UUID.bingenerate()
quote_id_as_string = Ecto.UUID.cast!(quote_id)

Schema.MeltQuote.create!(repo, %{
id: quote_id,
request: request,
unit: unit,
amount: amount,
fee_reserve: fee,
expiry: expiry,
request_lookup_id: ln_invoice.payment_hash
})

%{
quote: quote_id_as_string,
amount: amount,
fee_reserve: fee,
state: "UNPAID",
expiry: expiry
}
end
end
26 changes: 26 additions & 0 deletions lib/cashubrew/NUTs/NUT-05/routes.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Cashubrew.Nuts.Nut05.Routes do
@moduledoc """
List the rest routes defined in the NUT-05
"""

@doc """
The route to ask the melt for a quote
"""
def v1_melt_quote do
"/v1/melt/quote/:method"
end

@doc """
The route to check a melt quote state
"""
def v1_melt_quote_for_quote_id do
v1_melt_quote() <> "/:quote_id"
end

@doc """
The route proceed to the melt
"""
def v1_melt do
"/v1/melt/:method"
end
end
35 changes: 35 additions & 0 deletions lib/cashubrew/NUTs/NUT-05/serde.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Cashubrew.Nuts.Nut05.Serde.PostMeltQuoteBolt11Request do
@moduledoc """
The body of the post melt quote request
"""
@enforce_keys [:request, :unit]
@derive [Jason.Encoder]
defstruct [:request, :unit]
end

defmodule Cashubrew.Nuts.Nut05.Serde.PostMeltBolt11Request do
@moduledoc """
The body of the post melt request
"""
alias Cashubrew.Nuts.Nut00.Proof

@enforce_keys [:quote, :inputs]
@derive [Jason.Encoder]
defstruct [:quote, :inputs]

def from_map(map) do
%__MODULE__{
quote: Map.fetch!(map, "quote"),
inputs: Proof.from_list(Map.fetch!(map, "inputs"))
}
end
end

defmodule Cashubrew.Nuts.Nut05.Serde.PostMeltQuoteBolt11Response do
@moduledoc """
The body of the post melt quote response
"""
@enforce_keys [:quote, :amount, :fee_reserve, :state, :expiry]
@derive [Jason.Encoder]
defstruct [:quote, :amount, :fee_reserve, :state, :expiry, :payment_preimage]
end
19 changes: 19 additions & 0 deletions lib/cashubrew/NUTs/NUT-05/settings.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Cashubrew.Nuts.Nut05.MeltMethodSetting do
@moduledoc """
The infos retlated to this Nut support
"""

@enforce_keys [:method, :unit]
@derive [Jason.Encoder]
defstruct [:method, :unit, :min_amount, :max_amount, :description]

@doc """
Return the map to be used in Nut06 info "nuts" field
"""
def bolt11 do
%__MODULE__{
method: "bolt11",
unit: "sat"
}
end
end
5 changes: 3 additions & 2 deletions lib/cashubrew/NUTs/NUT-06/info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Cashubrew.Nuts.Nut06.Info do
Implementation and structs of the NUT-06
"""
alias Cashubrew.Nuts.Nut04
alias Cashubrew.Nuts.Nut05

@derive [Jason.Encoder]
defstruct [
Expand Down Expand Up @@ -46,8 +47,8 @@ defmodule Cashubrew.Nuts.Nut06.Info do
],
time: System.os_time(:second),
nuts: %{
"4": Nut04.Info.info(),
"5": %{methods: [], disabled: true},
"4": %{methods: [Nut04.MintMethodSetting.bolt11()], disabled: false},
"5": %{methods: [Nut05.MeltMethodSetting.bolt11()], disabled: false},
"7": %{supported: false},
"8": %{supported: false},
"9": %{supported: false},
Expand Down
11 changes: 11 additions & 0 deletions lib/cashubrew/schema/melt_quote.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Cashubrew.Schema.MeltQuote do
use Ecto.Schema
import Ecto.Changeset

@primary_key {:id, :binary_id, autogenerate: false}
schema "melt_quote" do
field(:request, :string)
field(:unit, :string)
Expand All @@ -22,4 +23,14 @@ defmodule Cashubrew.Schema.MeltQuote do
|> cast(attrs, [:request, :unit, :amount, :fee_reserve, :expiry, :request_lookup_id])
|> validate_required([:request, :unit, :amount, :fee_reserve, :expiry, :request_lookup_id])
end

def create!(repo, values) do
%__MODULE__{}
|> changeset(values)
|> repo.insert()
|> case do
{:ok, _} -> nil
{:error, changeset} -> raise "Failed to insert key: #{inspect(changeset.errors)}"
end
end
end
43 changes: 25 additions & 18 deletions lib/cashubrew/web/controllers/mint_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ defmodule Cashubrew.Web.MintController do
use Cashubrew.Web, :controller
require :logger
require Logger
alias Cashubrew.Lightning
alias Cashubrew.Mint
alias Cashubrew.Nuts.Nut00
alias Cashubrew.Nuts.Nut01
alias Cashubrew.Nuts.Nut02
alias Cashubrew.Nuts.Nut03
alias Cashubrew.Nuts.Nut04
alias Cashubrew.Nuts.Nut05
alias Cashubrew.Nuts.Nut06

def info(conn, _params) do
Expand Down Expand Up @@ -62,7 +64,7 @@ defmodule Cashubrew.Web.MintController do
} = params["body"]

res = Nut04.Impl.create_mint_quote!(amount, unit)
json(conn, struct(Nut04.Serde.PostMintBolt11Response, %{res | state: "UNPAID"}))
json(conn, struct(Nut04.Serde.PostMintBolt11Response, Map.put(res, :state, "UNPAID")))
rescue
e in RuntimeError -> conn |> put_status(:bad_request) |> json(Nut00.Error.new_error(0, e))
end
Expand Down Expand Up @@ -94,25 +96,30 @@ defmodule Cashubrew.Web.MintController do
e in RuntimeError -> conn |> put_status(:bad_request) |> json(Nut00.Error.new_error(0, e))
end

def melt_quote(conn, %{"request" => request, "unit" => unit}) do
case Mint.create_melt_quote(request, unit) do
{:ok, quote} ->
conn
|> put_status(:created)
|> json(%{
request: quote.request,
quote: quote.id,
amount: quote.amount,
fee_reserve: quote.fee_reserve,
state: "UNPAID",
expiry: quote.expiry
})
def create_melt_quote(conn, params) do
method = params["method"]

{:error, reason} ->
conn
|> put_status(:bad_request)
|> json(%{error: reason})
if method != "bolt11" do
raise "UnsuportedMethod"
end

request = params["request"]

if !request do
raise "NoRequest"
end

unit = params["unit"]

if !unit do
raise "NoUnit"
end

res = Nut05.Impl.create_melt_quote!(request, unit)

json(conn, struct(Nut05.Serde.PostMeltQuoteBolt11Response, res))
rescue
e in RuntimeError -> conn |> put_status(:bad_request) |> json(Nut00.Error.new_error(0, e))
end

def melt_tokens(conn, %{"quote_id" => quote_id, "inputs" => inputs}) do
Expand Down
Loading

0 comments on commit c53edc8

Please sign in to comment.