diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e0e0b6f --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +LN_BITS_API_ENDPOINT=https://your-lnbits-node +LN_BITS_API_KEY=ADMIN_API_KEY \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6c11904..752981f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ cashubrew-*.tar priv/static/ -benchmarks/output \ No newline at end of file +benchmarks/output + +.env \ No newline at end of file diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 628905b..b1bb0bc 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: postgres: - image: postgres:13 + image: postgres:17 container_name: cashubrew_postgres environment: POSTGRES_DB: cashubrew_dev diff --git a/lib/cashubrew/application.ex b/lib/cashubrew/application.ex index 928a6cf..af1363b 100644 --- a/lib/cashubrew/application.ex +++ b/lib/cashubrew/application.ex @@ -1,6 +1,7 @@ defmodule Cashubrew.Application do @moduledoc false use Application + alias Cashubrew.Lightning.LightningNetworkService alias Cashubrew.Lightning.MockLightningNetworkService alias Cashubrew.Web.Endpoint @@ -10,7 +11,8 @@ defmodule Cashubrew.Application do Cashubrew.Web.Telemetry, {Phoenix.PubSub, name: Cashubrew.PubSub}, Endpoint, - MockLightningNetworkService + MockLightningNetworkService, + LightningNetworkService ] # Conditionally add the appropriate repo to the children list diff --git a/lib/cashubrew/core/mint.ex b/lib/cashubrew/core/mint.ex index ea172f1..7eb8a91 100644 --- a/lib/cashubrew/core/mint.ex +++ b/lib/cashubrew/core/mint.ex @@ -6,8 +6,11 @@ defmodule Cashubrew.Mint do use GenServer alias Cashubrew.Cashu.BlindSignature alias Cashubrew.Crypto.BDHKE + alias Cashubrew.Lightning.LightningNetworkService alias Cashubrew.Lightning.MockLightningNetworkService - alias Cashubrew.Schema.{Key, Keyset, MintConfiguration, MintQuote} + alias Cashubrew.LNBitsApi + alias Cashubrew.Query.MeltTokens + alias Cashubrew.Schema.{Key, Keyset, MeltQuote, MeltTokens, MintConfiguration, MintQuote} import Ecto.Query @@ -123,7 +126,7 @@ defmodule Cashubrew.Mint do def handle_call({:create_mint_quote, amount, description}, _from, state) do repo = Application.get_env(:cashubrew, :repo) - case MockLightningNetworkService.create_invoice(amount, description) do + case LightningNetworkService.create_invoice(amount, description) do {:ok, payment_request, _payment_hash} -> # 1 hour expiry expiry = :os.system_time(:second) + 3600 @@ -133,6 +136,7 @@ defmodule Cashubrew.Mint do payment_request: payment_request, expiry: expiry, description: description + # payment_hash: _payment_hash, } case repo.insert(MintQuote.changeset(%MintQuote{}, attrs)) do @@ -221,6 +225,80 @@ defmodule Cashubrew.Mint do {:ok, signatures} end + def handle_call({:create_melt_quote, request, unit}, _from, state) do + repo = Application.get_env(:cashubrew, :repo) + # # Check LN invoice and info + {:ok, invoice} = Bitcoinex.LightningNetwork.decode_invoice(request) + # To call the function and print the hash: + {:ok, request} = RandomHash.generate_hash() + # Used amount + # If :amount exists, returns its value; otherwise returns 1000 + amount = Map.get(invoice, :amount_msat, 1000) + + fee_reserve = 0 + # Create and Saved melt quote + expiry = :os.system_time(:second) + 3600 + + attrs = %{ + # quote_id + request: request, + unit: unit, + amount: amount, + fee_reserve: fee_reserve, + expiry: expiry, + request_lookup_id: request + } + + case repo.insert(MeltQuote.changeset(%MeltQuote{}, attrs)) do + {:ok, melt_quote} -> + {:reply, {:ok, melt_quote}, state} + + {:error, changeset} -> + {:reply, {:error, changeset}, state} + end + end + + def handle_call({:create_melt_tokens, quote_id, inputs}, _from, state) do + repo = Application.get_env(:cashubrew, :repo) + + # TODO + # Verify quote_id + + {:ok, melt_find} = Cashubrew.Query.MeltTokens.get_melt_by_quote_id!(quote_id) + IO.puts("melt_find: #{melt_find}") + + # Check if quote is already paid or not + + # Check total amount + + # Check proofs + + # Verify proof spent + + fee_reserve = 0 + # Create and Saved melt quote + + attrs = %{ + # quote_id + request: quote_id, + unit: quote_id, + amount: 0, + fee_reserve: 0, + expiry: 0, + request_lookup_id: quote_id + } + + expiry = :os.system_time(:second) + 3600 + + case repo.insert(MeltTokens.changeset(%MeltTokens{}, attrs)) do + {:ok, melt_quote} -> + {:reply, {:ok, melt_quote}, state} + + {:error, changeset} -> + {:reply, {:error, changeset}, state} + end + end + # Public API def get_keysets do @@ -267,4 +345,12 @@ defmodule Cashubrew.Mint do def mint_tokens(quote, blinded_messages) do GenServer.call(__MODULE__, {:mint_tokens, quote, blinded_messages}) end + + def create_melt_quote(request, unit) do + GenServer.call(__MODULE__, {:create_melt_quote, request, unit}) + end + + def create_melt_tokens(quote_id, inputs) do + GenServer.call(__MODULE__, {:create_melt_tokens, quote_id, inputs}) + end end diff --git a/lib/cashubrew/crypto/random.ex b/lib/cashubrew/crypto/random.ex new file mode 100644 index 0000000..9f5772c --- /dev/null +++ b/lib/cashubrew/crypto/random.ex @@ -0,0 +1,24 @@ +defmodule RandomHash do + @moduledoc """ + Random Hash module + """ + # Import the necessary modules + require Logger + alias :crypto, as: Crypto + + def generate_hash do + # Step 1: Generate a random 32-byte binary + random_bytes = :crypto.strong_rand_bytes(32) + + # Step 2: Hash the random bytes using SHA-256 + hash = Crypto.hash(:sha256, random_bytes) + + # Step 3: Encode the hash in hexadecimal + hash_hex = Base.encode16(hash, case: :lower) + + # Return the result + {:ok, hash_hex} + rescue + error -> {:error, error} + end +end diff --git a/lib/cashubrew/db/query/melt_tokens.ex b/lib/cashubrew/db/query/melt_tokens.ex new file mode 100644 index 0000000..deec018 --- /dev/null +++ b/lib/cashubrew/db/query/melt_tokens.ex @@ -0,0 +1,25 @@ +defmodule Cashubrew.Query.MeltTokens do + @moduledoc """ + Query Melt Tokens + """ + import Ecto.Query, warn: false + alias Cashubrew.Repo + alias Cashubrew.Schema.MeltTokens + + # Fetch all users + def list_melt_tokens do + Repo.all(MeltTokens) + end + + # Fetch a quote by id + def get_melt_by_quote_id!(quote_id) do + query = + from(u in MeltTokens, + where: u.request == ^quote_id, + select: u + ) + + # Return a single user (or nil if no match) + Repo.one(query) + end +end diff --git a/lib/cashubrew/lightning/lightning_network_service.ex b/lib/cashubrew/lightning/lightning_network_service.ex new file mode 100644 index 0000000..b844aa4 --- /dev/null +++ b/lib/cashubrew/lightning/lightning_network_service.ex @@ -0,0 +1,54 @@ +defmodule Cashubrew.Lightning.LightningNetworkService do + @moduledoc """ + Lightning Network Services. + """ + use GenServer + alias Cashubrew.LNBitsApi + + def start_link(_) do + GenServer.start_link(__MODULE__, %{}, name: __MODULE__) + end + + def init(_) do + {:ok, %{invoices: %{}}} + end + + def create_invoice(amount, description) do + GenServer.call(__MODULE__, {:create_invoice, amount, description}) + end + + def check_payment(payment_hash) do + GenServer.call(__MODULE__, {:check_payment, payment_hash}) + end + + def handle_call({:create_invoice, amount, _description}, _from, state) do + unit_input = "sat" + + attributes = %{ + out: "false", + amount: amount, + unit_input: unit_input + } + + case LNBitsApi.post_data("api/v1/payments", attributes) do + {:ok, response_body} -> + IO.puts("Success create in: #{response_body}") + + response_map = Jason.decode!(response_body) + payment_hash = response_map["payment_hash"] + payment_request = response_map["payment_request"] + {:reply, {:ok, payment_request, payment_hash}, response_map} + + {:error, reason} -> + IO.puts("Error: #{reason}") + end + end + + def handle_call({:check_payment, payment_hash}, _from, state) do + case get_in(state, [:invoices, payment_hash]) do + nil -> {:reply, {:error, :not_found}, state} + %{paid: true} -> {:reply, {:ok, :paid}, state} + %{paid: false} -> {:reply, {:ok, :unpaid}, state} + end + end +end diff --git a/lib/cashubrew/lightning/ln_bits.ex b/lib/cashubrew/lightning/ln_bits.ex new file mode 100644 index 0000000..f5a4b0c --- /dev/null +++ b/lib/cashubrew/lightning/ln_bits.ex @@ -0,0 +1,53 @@ +defmodule Cashubrew.LNBitsApi do + @moduledoc """ + LN BITS Api Network Services. + """ + Dotenv.load() + + @api_endpoint System.get_env("LN_BITS_API_ENDPOINT") + @api_key System.get_env("LN_BITS_API_KEY") + + def fetch_data(path, attributes) do + api_base_url = System.get_env("LN_BITS_API_ENDPOINT") + api_key = System.get_env("LN_BITS_API_KEY") + headers = [{"X-Api-Key", "#{api_key}"}] + full_url = "#{api_base_url}#{path}" + + case HTTPoison.get(full_url, headers) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + {:ok, body} + + {:ok, %HTTPoison.Response{status_code: status_code}} -> + {:error, "Received #{status_code} status code"} + + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} + end + end + + # Function to send a POST request with a JSON body + def post_data(path, attributes) do + api_base_url = System.get_env("LN_BITS_API_ENDPOINT") + api_key = System.get_env("LN_BITS_API_KEY") + + headers = [ + {"X-Api-Key", "#{api_key}"}, + {"Content-Type", "application/json"} + ] + + # Convert Elixir map to JSON string + body = Jason.encode!(attributes) + full_url = "#{api_base_url}#{path}" + + case HTTPoison.post(full_url, body, headers) do + {:ok, %HTTPoison.Response{status_code: 201, body: response_body}} -> + {:ok, response_body} + + {:ok, %HTTPoison.Response{status_code: status_code, body: error_body}} -> + {:error, "Received #{status_code}: #{error_body}"} + + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} + end + end +end diff --git a/lib/cashubrew/lightning/ln_lib.ex b/lib/cashubrew/lightning/ln_lib.ex new file mode 100644 index 0000000..848fbb1 --- /dev/null +++ b/lib/cashubrew/lightning/ln_lib.ex @@ -0,0 +1,21 @@ +defmodule Cashubrew.LightningLib do + @moduledoc """ + Mock Lightning Network Service for testing purposes. + """ + use GenServer + + def start_link(_) do + GenServer.start_link(__MODULE__, %{}, name: __MODULE__) + end + + def init(_) do + {:ok, %{invoices: %{}}} + end + + def decode_invoice(payment_hash) do + GenServer.call(__MODULE__, {:decode_invoice, payment_hash}) + end + + def handle_call({:decode_invoice, invoice}, _from, state) do + end +end diff --git a/lib/cashubrew/mix/tasks/test.e2e.mint.ex b/lib/cashubrew/mix/tasks/test.e2e.mint.ex new file mode 100644 index 0000000..ed4b8c7 --- /dev/null +++ b/lib/cashubrew/mix/tasks/test.e2e.mint.ex @@ -0,0 +1,14 @@ +defmodule Mix.Tasks.Test.E2e.Mint do + @moduledoc """ + Test for the BMint + """ + use Mix.Task + + @shortdoc "Runs the Mint end-to-end test" + def run(_) do + Mix.Task.run("app.start", ["--no-start"]) + ExUnit.start() + Code.require_file("test/integration_mint.exs") + ExUnit.run() + end +end diff --git a/lib/cashubrew/schema/melt_quote.ex b/lib/cashubrew/schema/melt_quote.ex new file mode 100644 index 0000000..3eefe21 --- /dev/null +++ b/lib/cashubrew/schema/melt_quote.ex @@ -0,0 +1,25 @@ +defmodule Cashubrew.Schema.MeltQuote do + @moduledoc """ + Schema for a mint quote. + """ + use Ecto.Schema + import Ecto.Changeset + + schema "melt_quote" do + field(:request, :string) + field(:unit, :string) + # Make sure to use the correct field name + field(:amount, :integer) + field(:fee_reserve, :integer) + field(:expiry, :integer) + field(:request_lookup_id, :string) + + timestamps() + end + + def changeset(quote, attrs) do + quote + |> cast(attrs, [:request, :unit, :amount, :fee_reserve, :expiry, :request_lookup_id]) + |> validate_required([:request, :unit, :amount, :fee_reserve, :expiry, :request_lookup_id]) + end +end diff --git a/lib/cashubrew/schema/melt_tokens.ex b/lib/cashubrew/schema/melt_tokens.ex new file mode 100644 index 0000000..d19ee34 --- /dev/null +++ b/lib/cashubrew/schema/melt_tokens.ex @@ -0,0 +1,24 @@ +defmodule Cashubrew.Schema.MeltTokens do + @moduledoc """ + Schema for a mint quote. + """ + use Ecto.Schema + import Ecto.Changeset + + schema "melt_tokens" do + field(:request, :string) + field(:unit, :string) + field(:amount, :integer) + field(:fee_reserve, :integer) + field(:expiry, :integer) + field(:request_lookup_id, :string) + + timestamps() + end + + def changeset(melt_tokens, attrs) do + melt_tokens + |> cast(attrs, [:request, :unit, :amount, :fee_reserve, :expiry, :request_lookup_id]) + |> validate_required([:request, :unit, :amount, :fee_reserve, :expiry, :request_lookup_id]) + end +end diff --git a/lib/cashubrew/schema/mint_quote.ex b/lib/cashubrew/schema/mint_quote.ex index 6eab68c..3f61eae 100644 --- a/lib/cashubrew/schema/mint_quote.ex +++ b/lib/cashubrew/schema/mint_quote.ex @@ -11,13 +11,14 @@ defmodule Cashubrew.Schema.MintQuote do field(:state, :string, default: "UNPAID") field(:expiry, :integer) field(:description, :string) + field(:payment_hash, :string) timestamps() end def changeset(quote, attrs) do quote - |> cast(attrs, [:amount, :payment_request, :state, :expiry, :description]) + |> cast(attrs, [:amount, :payment_request, :state, :expiry, :description, :payment_hash]) |> validate_required([:amount, :payment_request, :state, :expiry]) |> validate_inclusion(:state, ["UNPAID", "PAID", "ISSUED"]) end diff --git a/lib/cashubrew/schema/proof.ex b/lib/cashubrew/schema/proof.ex new file mode 100644 index 0000000..3552932 --- /dev/null +++ b/lib/cashubrew/schema/proof.ex @@ -0,0 +1,23 @@ +defmodule Cashubrew.Schema.Proof do + @moduledoc """ + Schema for a mint quote. + """ + use Ecto.Schema + import Ecto.Changeset + + schema "proof" do + field(:quote_id, :string) + field(:secret, :string) + field(:amount, :integer) + field(:y, :string) + field(:c, :string) + + timestamps() + end + + def changeset(quote, attrs) do + quote + |> cast(attrs, [:quote_id, :secret, :amount, :y, :c]) + |> validate_required([:quote_id, :secret, :amount, :y, :c]) + end +end diff --git a/lib/cashubrew/web/controllers/mint_controller.ex b/lib/cashubrew/web/controllers/mint_controller.ex index c4c1c7d..e7e0446 100644 --- a/lib/cashubrew/web/controllers/mint_controller.ex +++ b/lib/cashubrew/web/controllers/mint_controller.ex @@ -216,4 +216,44 @@ defmodule Cashubrew.Web.MintController do end end 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 + }) + + {:error, reason} -> + conn + |> put_status(:bad_request) + |> json(%{error: reason}) + end + end + + def melt_tokens(conn, %{"quote_id" => quote_id, "inputs" => inputs}) do + case Mint.create_melt_tokens(quote_id, inputs) do + {:ok, quote} -> + conn + |> put_status(:created) + |> json(%{ + quote: quote.request, + request: quote.request, + state: "UNPAID", + expiry: quote.expiry + }) + + {:error, reason} -> + conn + |> put_status(:bad_request) + |> json(%{error: reason}) + end + end end diff --git a/lib/cashubrew/web/router.ex b/lib/cashubrew/web/router.ex index c63af5b..bd1c7d2 100644 --- a/lib/cashubrew/web/router.ex +++ b/lib/cashubrew/web/router.ex @@ -31,6 +31,10 @@ defmodule Cashubrew.Web.Router do get("/v1/mint/quote/bolt11/:quote_id", MintController, :get_mint_quote) post("/v1/mint/bolt11", MintController, :mint_tokens) + # NUT-05 + post("/v1/melt/quote/bolt11", MintController, :melt_quote) + post("/v1/melt/bolt11", MintController, :melt_tokens) + # NUT-06 get("/v1/info", MintController, :info) end diff --git a/mix.exs b/mix.exs index eb807db..740c42f 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,9 @@ defmodule Cashubrew.MixProject do {:postgrex, ">= 0.0.0"}, {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, {:telemetry_metrics, "~> 0.6"}, - {:telemetry_poller, "~> 1.0"} + {:telemetry_poller, "~> 1.0"}, + {:bitcoinex, "~> 0.1.7"}, + {:dotenv, "~> 3.0.0"} ] end diff --git a/mix.lock b/mix.lock index 989a813..54e172b 100644 --- a/mix.lock +++ b/mix.lock @@ -2,11 +2,13 @@ "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, "benchee_html": {:hex, :benchee_html, "1.0.1", "1e247c0886c3fdb0d3f4b184b653a8d6fb96e4ad0d0389267fe4f36968772e24", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "b00a181af7152431901e08f3fc9f7197ed43ff50421a8347b0c80bf45d5b3fef"}, "benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"}, + "bitcoinex": {:hex, :bitcoinex, "0.1.8", "f1d12df7e9f7235ce699f3ecbfbf833361c8b5961cb6e0bca6d34548a0830716", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "9953101cf4dad21c5a07a7044044d67c2799a85be9323b08e4dd190ae2ae7872"}, "block_keys": {:hex, :block_keys, "1.0.2", "8ec4808256af826e407f1011571682d941e14c38b7a9241a1ce4af724d5d3f43", [:mix], [{:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:ex_keccak, "~> 0.7.3", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:ex_secp256k1, "~> 0.7.2", [hex: :ex_secp256k1, repo: "hexpm", optional: false]}], "hexpm", "eda5508f7d2c65cad58baebc79fa27a88d1ec6f781cb34a76597794d7598dcd1"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, @@ -15,6 +17,7 @@ "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "dotenv": {:hex, :dotenv, "3.0.0", "52a28976955070d8312a81d59105b57ecf5d6a755c728b49c70a7e2120e6cb40", [:mix], [], "hexpm", "f8a7d800b6b419a8d8a8bc5b5cd820a181c2b713aab7621794febe934f7bd84e"}, "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, "ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"}, @@ -55,6 +58,8 @@ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, + "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, + "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, diff --git a/priv/repo/migrations/20240918113122_create_mint_quote.exs b/priv/repo/migrations/20240918113122_create_mint_quote.exs index 10449cf..3988309 100644 --- a/priv/repo/migrations/20240918113122_create_mint_quote.exs +++ b/priv/repo/migrations/20240918113122_create_mint_quote.exs @@ -4,12 +4,19 @@ defmodule Cashubrew.Repo.Migrations.CreateMintQuote do def change do create table(:mint_quotes) do add :amount, :integer, null: false - add :payment_request, :string, null: false + add :payment_request, :text, null: false add :state, :string, default: "UNPAID", null: false add :expiry, :integer, null: false add :description, :string + add :payment_hash, :string timestamps() end end + + # def change do + # alter table(:mint_quotes) do + # modify :payment_request, :text, null: false + # end + # end end diff --git a/priv/repo/migrations/20240918113124_create_melt_quote.exs b/priv/repo/migrations/20240918113124_create_melt_quote.exs new file mode 100644 index 0000000..2af4a99 --- /dev/null +++ b/priv/repo/migrations/20240918113124_create_melt_quote.exs @@ -0,0 +1,16 @@ +defmodule Cashubrew.Repo.Migrations.CreateMeltQuote do + use Ecto.Migration + + def change do + create table(:melt_quote) do + add :request, :string, null: false + add :unit, :string, null: false + add :amount, :integer, null: false + add :fee_reserve, :integer, null: false + add :expiry, :integer, null: false + add :request_lookup_id, :string, null: false + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20240918113125_create_melt_tokens.exs b/priv/repo/migrations/20240918113125_create_melt_tokens.exs new file mode 100644 index 0000000..4ed75c8 --- /dev/null +++ b/priv/repo/migrations/20240918113125_create_melt_tokens.exs @@ -0,0 +1,16 @@ +defmodule Cashubrew.Repo.Migrations.CreateMeltQuoteResponse do + use Ecto.Migration + + def change do + create table(:melt_tokens) do + add :request, :string, null: false + add :unit, :string, null: false + add :amount, :integer, null: false + add :fee_reserve, :integer, null: false + add :expiry, :integer, null: false + add :request_lookup_id, :string, null: false + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20240918113126_create_proof.exs b/priv/repo/migrations/20240918113126_create_proof.exs new file mode 100644 index 0000000..b76712c --- /dev/null +++ b/priv/repo/migrations/20240918113126_create_proof.exs @@ -0,0 +1,15 @@ +defmodule Cashubrew.Repo.Migrations.CreateProof do + use Ecto.Migration + + def change do + create table(:proof) do + add :quote_id, :string, null: false + add :secret, :string, null: false + add :amount, :integer, null: false + add :y, :string, null: false + add :c, :string, null: false + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20240918113127.migrate_text.exs b/priv/repo/migrations/20240918113127.migrate_text.exs new file mode 100644 index 0000000..2d04635 --- /dev/null +++ b/priv/repo/migrations/20240918113127.migrate_text.exs @@ -0,0 +1,9 @@ +defmodule Cashubrew.Repo.Migrations.ChangePaymentRequestToText do + use Ecto.Migration + + def change do + alter table(:mint_quotes) do + modify :payment_request, :text, null: false + end + end +end diff --git a/test/integration_mint.exs b/test/integration_mint.exs new file mode 100644 index 0000000..3839bca --- /dev/null +++ b/test/integration_mint.exs @@ -0,0 +1,59 @@ +defmodule Cashubrew.Test.Mint do + use ExUnit.Case, async: true + alias Cashubrew.Mint + alias Cashubrew.Query.MeltTokens + alias Cashubrew.Schema + + describe "Mint" do + @tag :mint_quote + + test "Mint quote" do + description_input = "lfg" + unit_input = "sat" + amount_input = 1 + {:ok, mint} = Mint.create_mint_quote(amount_input, description_input) + + attributes = %{ + out: "false", + amount: amount_input, + unit_input: unit_input + } + + request = Map.get(mint, :payment_request) + assert String.starts_with?(request, "ln") + + amount = Map.get(mint, :amount) + assert amount == amount_input + + description = Map.get(mint, :description) + end + + test "Melt quote" do + invoice = + "lnbc10n1pn0dr0gdr8vs6xyd3c8qekxdfsvcunywtpxdjk2vmpx4snwdejv3nrye34xycrqvehvdnrqdtzxyukzdfsvycr2e3hvcunzvmzvgukzvrpv9nrzvqnp4qvhnmyfaj2k6gckmxsdcx0wrxdghx9eks0ck536sghwfjd6nv2pawpp5f3jjx7ag5fwk5jwa5g7gycl99e6y2ja93r5hu5m4gtllvlkausqqsp57545s7xnlmj7xkt08a9ze7cmmtvmsjjgqvqjcum2h2ge4fg4yp3s9qyysgqcqpcxqyz5vqgersy5pln9yxz6cksh8nmu4rs8mml6f0wzddaw0pr2l2t9rxkhrqfu89xnjcdukvn3v22t6w4lvp8g3ynymzn02952njk0ennzrunusqgd3w9d" + + unit_input = "sat" + # msat + amount_input_invoice = 1000 + {:ok, melt_quote} = Mint.create_melt_quote(invoice, unit_input) + request = Map.get(melt_quote, :request) + assert String.length(request) == 64 + + amount = Map.get(melt_quote, :amount) + assert amount == amount_input_invoice + + unit = Map.get(melt_quote, :unit) + assert unit == "sat" + + melt = MeltTokens.get_melt_by_quote_id!(request) + + if melt != nil do + request_id_db = Map.get(melt, :request) + assert request == request_id_db + end + end + end + + defp normalize_binary(value) when is_binary(value), do: Base.encode16(value, case: :lower) + defp normalize_binary(value), do: value +end