diff --git a/README.md b/README.md index a4fd2ba..393035e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ - # fedimint-clientd: A Fedimint Client for Server Side Applications +# fedimint-clientd: A Fedimint Client for Server Side Applications -fedimint-clientd runs a fedimint client with Ecash, Lightning, and Onchain modules to let a server side application hold and use Bitcoin with Fedimint. It exposes a REST API & provides wrappers in typescript, python, and golang. It uses the `multimint` crate to manages clients connected to multiple Federations from a single `fedimint-clientd` instance. +fedimint-clientd runs a fedimint client with Ecash, Lightning, and Onchain modules to let a server side application hold and use Bitcoin with Fedimint. It exposes a REST API & provides wrappers in typescript, python, goland, and elixir. It uses the `multimint` crate to manage clients connected to multiple Federations from a single `fedimint-clientd` instance. This project is intended to be an easy-to-use starting point for those interested in adding Fedimint client support to their applications. Fedimint-clientd only exposes Fedimint's default modules, and any more complex Fedimint integration will require custom implementation using [Fedimint's rust crates](https://github.com/fedimint/fedimint). @@ -49,7 +49,7 @@ curl http://localhost:3333/fedimint/v2/admin/info -H 'Authorization: Bearer some - `/fedimint/v2/mint/reissue`: Reissue notes received from a third party to avoid double spends. - `/fedimint/v2/mint/spend`: Prepare notes to send to a third party as a payment. -- `/fedimint/v2/mint/validate`: Verifies the signatures of e-cash notes, but *not* if they have been spent already. +- `/fedimint/v2/mint/validate`: Verifies the signatures of e-cash notes, but _not_ if they have been spent already. - `/fedimint/v2/mint/split`: Splits a string containing multiple e-cash notes (e.g. from the `spend` command) into ones that contain exactly one. - `/fedimint/v2/mint/combine`: Combines two or more serialized e-cash notes strings. diff --git a/wrappers/fedimintex/.formatter.exs b/wrappers/fedimintex/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/wrappers/fedimintex/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/wrappers/fedimintex/.gitignore b/wrappers/fedimintex/.gitignore new file mode 100644 index 0000000..5848b88 --- /dev/null +++ b/wrappers/fedimintex/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +fedimintex-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/wrappers/fedimintex/README.md b/wrappers/fedimintex/README.md new file mode 100644 index 0000000..2f72ec9 --- /dev/null +++ b/wrappers/fedimintex/README.md @@ -0,0 +1,63 @@ +# Fedimintex + +# Fedimintex: Fedimint SDK in Elixir + +This is an elixir library that consumes Fedimint HTTP (https://github.com/kodylow/fedimint-http-client)[https://github.com/kodylow/fedimint-http-client], communicating with it via REST endpoints + passowrd. It's a hacky prototype, but it works until we can get a proper elixir client for Fedimint. All of the federation handling code happens in the fedimint-http, this just exposes a simple API for interacting with the client from elixir (mirrored in Go, Python, and TS). + +Start the following in the fedimint-http-client `.env` file: + +```bash +FEDERATION_INVITE_CODE = 'fed1-some-invite-code' +SECRET_KEY = 'some-secret-key' # generate this with `openssl rand -base64 32` +FM_DB_PATH = '/absolute/path/to/fm.db' # just make this a new dir called `fm_db` in the root of the fedimint-http-client and use the absolute path to thatm it'll create the db file for you on startup +PASSWORD = 'password' +DOMAIN = 'localhost' +PORT = 5000 +BASE_URL = 'http://localhost:5000' +``` + +Then start the fedimint-http-client server: + +```bash +cargo run +``` + +Then you're ready to use the elixir client, which will use the same base url and password as the fedimint-http-client, so you'll need to set those in your elixir project's `.env` file: + +```bash +export BASE_URL='http://localhost:5000' +export PASSWORD='password' +``` + +Source the `.env` file and enter the iex shell: + +```bash +source .env +iex -S mix +``` + +Then you can use the client: + +```bash +iex > client = Fedimintex.new() +iex > invoice = Fedimintex.ln.create_invoice(client, 1000) +# pay the invoice +iex > Fedimintex.ln.await_invoice +``` + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `fedimintex` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:fedimintex, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . diff --git a/wrappers/fedimintex/example.env b/wrappers/fedimintex/example.env new file mode 100644 index 0000000..9045e5f --- /dev/null +++ b/wrappers/fedimintex/example.env @@ -0,0 +1,4 @@ +export PASSWORD='password' +export DOMAIN='localhost' +export PORT=5000 +export BASE_URL=http://localhost:5000 diff --git a/wrappers/fedimintex/justfile b/wrappers/fedimintex/justfile new file mode 100644 index 0000000..74a90cc --- /dev/null +++ b/wrappers/fedimintex/justfile @@ -0,0 +1,4 @@ +s: + iex -S mix +env: + source .env diff --git a/wrappers/fedimintex/lib/admin.ex b/wrappers/fedimintex/lib/admin.ex new file mode 100644 index 0000000..9ab3c86 --- /dev/null +++ b/wrappers/fedimintex/lib/admin.ex @@ -0,0 +1,71 @@ +defmodule Fedimintex.Admin do + import Fedimintex.Client, only: [post: 3, get: 2] + + @type tiered :: %{required(integer()) => any()} + @type tiered_summary :: %{required(:tiered) => tiered()} + @type info_response :: %{ + required(:federation_id) => String.t(), + required(:network) => String.t(), + required(:meta) => %{required(String.t()) => String.t()}, + required(:total_amount_msat) => integer(), + required(:total_num_notes) => integer(), + required(:denominations_msat) => tiered_summary() + } + + @doc """ + Fetches wallet (mint and onchain) information including holdings, tiers, and federation metadata. + """ + @spec info(Fedimintex.Client.t()) :: {:ok, info_response()} | {:error, String.t()} + def info(client) do + get(client, "/admin/info") + end + + @type backup_request :: %{required(:metadata) => %{required(String.t()) => String.t()}} + + @doc """ + Uploads the encrypted snapshot of mint notest to the federation + """ + def backup(client, metadata) do + post(client, "/admin/backup", metadata) + end + + @type version_response :: %{required(:version) => String.t()} + + @doc """ + Discovers the highest common version of the mint and api + """ + @spec discover_version(Fedimintex.Client.t()) :: + {:ok, version_response()} | {:error, String.t()} + def discover_version(client) do + get(client, "/admin/discover-version") + end + + @type list_operations_request :: %{required(:limit) => integer()} + @type operation_output :: %{ + required(:id) => String.t(), + required(:creation_time) => String.t(), + required(:operation_kind) => String.t(), + required(:operation_meta) => any(), + optional(:outcome) => any() + } + @type list_operations_response :: [operation_output()] + + @doc """ + Lists all ongoing operations + """ + @spec list_operations(Fedimintex.Client.t(), list_operations_request()) :: + {:ok, list_operations_response()} | {:error, String.t()} + def list_operations(client, request) do + post(client, "/admin/list-operations", request) + end + + @type config_response :: map() + + @doc """ + Get configuration information + """ + @spec config(Fedimintex.Client.t()) :: {:ok, config_response()} | {:error, String.t()} + def config(client) do + get(client, "/admin/config") + end +end diff --git a/wrappers/fedimintex/lib/client.ex b/wrappers/fedimintex/lib/client.ex new file mode 100644 index 0000000..0a5673a --- /dev/null +++ b/wrappers/fedimintex/lib/client.ex @@ -0,0 +1,84 @@ +defmodule Fedimintex.Client do + @moduledoc """ + Handles HTTP requests for the `Fedimintex` client. + """ + + @type t :: %__MODULE__{ + base_url: String.t(), + password: String.t(), + admin: atom(), + mint: atom(), + ln: atom(), + onchain: atom() + } + + @type http_response :: {:ok, map()} | {:error, String.t()} + + defstruct base_url: nil, password: nil, admin: nil, mint: nil, ln: nil, onchain: nil + + @doc """ + Creates a new `Fedimintex.Client` struct. + """ + @spec new() :: t() | {:error, String.t()} + def new() do + base_url = System.get_env("BASE_URL") + password = System.get_env("PASSWORD") + new(base_url, password) + end + + @spec new(nil, nil) :: {:error, String.t()} + def new(nil, nil), do: {:error, "Could not load base_url and password from environment."} + + @spec new(String.t(), String.t()) :: t() + def new(base_url, password) do + %__MODULE__{ + base_url: base_url <> "/v2", + password: password, + admin: Fedimintex.Admin, + mint: Fedimintex.Mint, + ln: Fedimintex.Ln, + onchain: Fedimintex.Wallet + } + end + + @doc """ + Makes a GET request to the `baseURL` at the given `endpoint`. + Receives a JSON response. + """ + @spec get(t(), String.t()) :: http_response() + def get(%__MODULE__{base_url: base_url, password: password}, endpoint) do + headers = [{"Authorization", "Bearer #{password}"}] + + (base_url <> endpoint) + |> Req.get!(headers: headers) + |> handle_response() + end + + @doc """ + Makes a POST request to the `baseURL` at the given `endpoint` + Receives a JSON response. + """ + @spec post(t(), String.t(), map()) :: http_response() + def post(%__MODULE__{password: password, base_url: base_url}, endpoint, body) do + headers = [ + {"Authorization", "Bearer #{password}"}, + {"Content-Type", "application/json"} + ] + + (base_url <> endpoint) + |> Req.post!(json: body, headers: headers) + |> handle_response() + end + + @spec handle_response(Req.Response.t()) :: http_response() + defp handle_response(%{status: 200, body: body}) do + case Jason.decode(body) do + {:ok, body} -> {:ok, body} + {:error, _} -> {:error, "Failed to decode JSON, got #{body}"} + end + end + + defp handle_response(%{status: status}) do + {:error, "Request failed with status #{status}"} + end +end diff --git a/wrappers/fedimintex/lib/fedimintex.ex b/wrappers/fedimintex/lib/fedimintex.ex new file mode 100644 index 0000000..6fed88d --- /dev/null +++ b/wrappers/fedimintex/lib/fedimintex.ex @@ -0,0 +1,5 @@ +defmodule Fedimintex do + @moduledoc """ + Documentation for `Fedimintex`. + """ +end diff --git a/wrappers/fedimintex/lib/ln/ln.ex b/wrappers/fedimintex/lib/ln/ln.ex new file mode 100644 index 0000000..74e1f52 --- /dev/null +++ b/wrappers/fedimintex/lib/ln/ln.ex @@ -0,0 +1,48 @@ +defmodule Fedimintex.Ln do + alias Fedimintex.Client + + alias Fedimint.Ln.{ + AwaitInvoiceRequest, + InvoiceRequest, + InvoiceResponse, + PayRequest, + PayResponse, + AwaitPayRequest, + Gateway, + SwitchGatewayRequest + } + + @spec create_invoice(Client.t(), InvoiceRequest.t()) :: + {:ok, InvoiceResponse.t()} | {:error, String.t()} + def create_invoice(client, request) do + Client.post(client, "/ln/invoice", request) + end + + @spec await_invoice(Client.t(), AwaitInvoiceRequest.t()) :: + {:ok, InvoiceResponse.t()} | {:error, String.t()} + def await_invoice(client, request) do + Client.post(client, "/ln/await-invoice", request) + end + + @spec pay(Client.t(), PayRequest.t()) :: {:ok, PayResponse.t()} | {:error, String.t()} + def pay(client, request) do + Client.post(client, "/ln/pay", request) + end + + @spec await_pay(Client.t(), AwaitPayRequest.t()) :: + {:ok, PayResponse.t()} | {:error, String.t()} + def await_pay(client, request) do + Client.post(client, "/ln/await-pay", request) + end + + @spec list_gateways(Client.t()) :: {:ok, [Gateway.t()]} | {:error, String.t()} + def list_gateways(client) do + Client.get(client, "/ln/list-gateways") + end + + @spec switch_gateway(Client.t(), SwitchGatewayRequest.t()) :: + {:ok, String.t()} | {:error, String.t()} + def switch_gateway(client, request) do + Client.post(client, "/ln/switch-gateway", request) + end +end diff --git a/wrappers/fedimintex/lib/ln/types.ex b/wrappers/fedimintex/lib/ln/types.ex new file mode 100644 index 0000000..fc60314 --- /dev/null +++ b/wrappers/fedimintex/lib/ln/types.ex @@ -0,0 +1,87 @@ +defmodule Fedimintex.Ln.InvoiceRequest do + defstruct [:amount_msat, :description, :expiry_time] + + @type ln_invoice_request :: %__MODULE__{ + amount_msat: non_neg_integer(), + description: String.t(), + expiry_time: non_neg_integer() | nil + } + + @spec new(non_neg_integer(), String.t(), non_neg_integer() | nil) :: ln_invoice_request + def new(amount_msat, description, expiry_time \\ nil) + + def new(amount_msat, description, expiry_time) + when is_integer(amount_msat) and is_binary(description) do + %__MODULE__{ + amount_msat: amount_msat, + description: description, + expiry_time: expiry_time + } + end + + def new(_, _, _), do: {:error, "Invalid arguments passed to InvoiceRequest.new/3."} +end + +defmodule Fedimintex.Ln.AwaitInvoiceRequest do + defstruct [:operation_id] + + @type await_invoice_request :: %__MODULE__{ + operation_id: String.t() + } +end + +defmodule Fedimintex.Ln.InvoiceResponse do + defstruct [:operation_id, :invoice] + + @type ln_invoice_response :: %__MODULE__{ + operation_id: String.t(), + invoice: String.t() + } +end + +defmodule Fedimintex.Ln.PayRequest do + defstruct [:payment_info, :amount_msat, :finish_in_background, :lnurl_comment] + + @type ln_pay_request :: %__MODULE__{ + payment_info: String.t(), + amount_msat: non_neg_integer() | nil, + finish_in_background: boolean(), + lnurl_comment: String.t() | nil + } +end + +defmodule Fedimintex.Ln.AwaitPayRequest do + defstruct [:operation_id] + + @type await_ln_pay_request :: %{ + operation_id: String.t() + } +end + +defmodule Fedimintex.Ln.PayResponse do + defstruct [:operation_id, :payment_type, :contract_id, :fee] + + @type ln_pay_response :: %{ + operation_id: String.t(), + payment_type: String.t(), + contract_id: String.t(), + fee: non_neg_integer() + } +end + +defmodule Fedimintex.Ln.Gateway do + defstruct [:node_pub_key, :active] + + @type gateway :: %{ + node_pub_key: String.t(), + active: boolean() + } +end + +defmodule Fedimintex.Ln.SwitchGatewayRequest do + defstruct [:gateway_id] + + @type gateway :: %{ + gateway_id: String.t() + } +end diff --git a/wrappers/fedimintex/lib/mint.ex b/wrappers/fedimintex/lib/mint.ex new file mode 100644 index 0000000..5411ffe --- /dev/null +++ b/wrappers/fedimintex/lib/mint.ex @@ -0,0 +1,62 @@ +# mint.ex +defmodule Fedimintex.Mint do + alias Fedimintex.Client + + @type federation_id_prefix :: {integer, integer, integer, integer} + @type tiered_multi(t) :: %{integer => [t]} + @type signature :: %{g1_affine: g1_affine} + @type g1_affine :: %{x: fp, y: fp, infinity: choice} + @type fp :: %{integer => [integer]} + @type choice :: %{integer => integer} + @type key_pair :: %{integer => [integer]} + @type oob_notes_data :: %{ + notes: tiered_multi(spendable_note) | nil, + federation_id_prefix: federation_id_prefix | nil, + default: %{variant: integer, bytes: [integer]} | nil + } + @type oob_notes :: %{integer => [oob_notes_data]} + @type spendable_note :: %{signature: signature, spend_key: key_pair} + + @type reissue_request :: %{notes: oob_notes} + @type reissue_response :: %{amount_msat: integer} + + @spec reissue(Client.t(), reissue_request()) :: + {:ok, reissue_response()} | {:error, String.t()} + def reissue(client, request) do + Client.post(client, "/mint/reissue", request) + end + + @type spend_request :: %{amount_msat: integer, allow_overpay: boolean, timeout: integer} + @type spend_response :: %{operation: String.t(), notes: oob_notes} + + @spec spend(Client.t(), spend_request()) :: {:ok, spend_response()} | {:error, String.t()} + def spend(client, request) do + Client.post(client, "/mint/spend", request) + end + + @type validate_request :: %{notes: oob_notes} + @type validate_response :: %{amount_msat: integer} + + @spec validate(Client.t(), validate_request()) :: + {:ok, validate_response()} | {:error, String.t()} + def validate(client, request) do + Client.post(client, "/mint/validate", request) + end + + @type split_request :: %{notes: oob_notes} + @type split_response :: %{notes: %{integer => oob_notes}} + + @spec split(Client.t(), split_request()) :: {:ok, split_response()} | {:error, String.t()} + def split(client, request) do + Client.post(client, "/mint/split", request) + end + + @type combine_request :: %{notes: [oob_notes]} + @type combine_response :: %{notes: oob_notes} + + @spec combine(Client.t(), combine_request()) :: + {:ok, combine_response()} | {:error, String.t()} + def combine(client, request) do + Client.post(client, "/mint/combine", request) + end +end diff --git a/wrappers/fedimintex/lib/onchain.ex b/wrappers/fedimintex/lib/onchain.ex new file mode 100644 index 0000000..d009629 --- /dev/null +++ b/wrappers/fedimintex/lib/onchain.ex @@ -0,0 +1,31 @@ +# onchain.ex +defmodule Fedimintex.Onchain do + import Fedimintex.Client, only: [post: 3] + + @type deposit_address_request :: %{timeout: integer()} + @type deposit_address_response :: %{operation_id: String.t(), address: String.t()} + + @spec create_deposit_address(Fedimintex.Client.t(), deposit_address_request()) :: + {:ok, deposit_address_response()} | {:error, String.t()} + def create_deposit_address(client, request) do + post(client, "/onchain/deposit-address", request) + end + + @type await_deposit_request :: %{operation_id: String.t()} + @type await_deposit_response :: %{status: String.t()} + + @spec await_deposit(Fedimintex.Client.t(), await_deposit_request()) :: + {:ok, await_deposit_response()} | {:error, String.t()} + def await_deposit(client, request) do + post(client, "/onchain/await-deposit", request) + end + + @type withdraw_request :: %{address: String.t(), amount_msat: String.t()} + @type withdraw_response :: %{txid: String.t(), fees_sat: integer()} + + @spec withdraw(Fedimintex.Client.t(), withdraw_request()) :: + {:ok, withdraw_response()} | {:error, String.t()} + def withdraw(client, request) do + post(client, "/onchain/withdraw", request) + end +end diff --git a/wrappers/fedimintex/lib/test.ex b/wrappers/fedimintex/lib/test.ex new file mode 100644 index 0000000..875f2fa --- /dev/null +++ b/wrappers/fedimintex/lib/test.ex @@ -0,0 +1,29 @@ +defmodule Fedimintex.Example do + alias Fedimintex.{Client, Ln} + alias Fedimintex.Ln.{InvoiceRequest, AwaitInvoiceRequest} + + def main() do + client = Fedimintex.Client.new() + + case Client.get(client, "/admin/info") do + {:ok, body} -> IO.puts("Current Total Msats Ecash: " <> body["total_amount_msat"]) + {:error, err} -> IO.inspect(err) + end + + invoice_request = %InvoiceRequest{amount_msat: 10000, description: "test", expiry_time: 3600} + invoice_response = Ln.create_invoice(client, invoice_request) + IO.puts(invoice_response["invoice"]) + + await_invoice_request = %AwaitInvoiceRequest{operation_id: invoice_response["operation_id"]} + payment_response = Ln.await_invoice(client, await_invoice_request) + + case payment_response do + {:ok, resp} -> + IO.puts("Payment received!") + IO.puts("New Total Msats Ecash: " <> resp["total_amount_msat"]) + + {:error, err} -> + IO.inspect(err) + end + end +end diff --git a/wrappers/fedimintex/mix.exs b/wrappers/fedimintex/mix.exs new file mode 100644 index 0000000..4eca99b --- /dev/null +++ b/wrappers/fedimintex/mix.exs @@ -0,0 +1,28 @@ +defmodule Fedimintex.MixProject do + use Mix.Project + + def project do + [ + app: :fedimintex, + version: "0.1.0", + elixir: "~> 1.15", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:req, "~> 0.4.0"}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} + ] + end +end diff --git a/wrappers/fedimintex/mix.lock b/wrappers/fedimintex/mix.lock new file mode 100644 index 0000000..4a44d2a --- /dev/null +++ b/wrappers/fedimintex/mix.lock @@ -0,0 +1,24 @@ +%{ + "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "dotenv": {:hex, :dotenv, "2.1.0", "2a66462dbc3038c43147a67a6c4d6eab365223c4bd5ba6f6205db9da0e3e3068", [:mix], [], "hexpm", "caddac72cac4955ae346306b210608dd6cf380a439b4e18bcdc3d6021f3e4d6b"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "req": {:hex, :req, "0.4.8", "2b754a3925ddbf4ad78c56f30208ced6aefe111a7ea07fb56c23dccc13eb87ae", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7146e51d52593bb7f20d00b5308a5d7d17d663d6e85cd071452b613a8277100c"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, +} diff --git a/wrappers/fedimintex/test/fedimintex_test.exs b/wrappers/fedimintex/test/fedimintex_test.exs new file mode 100644 index 0000000..ee0fbc1 --- /dev/null +++ b/wrappers/fedimintex/test/fedimintex_test.exs @@ -0,0 +1,8 @@ +defmodule FedimintexTest do + use ExUnit.Case + doctest Fedimintex + + test "greets the world" do + assert Fedimintex.hello() == :world + end +end diff --git a/wrappers/fedimintex/test/test_helper.exs b/wrappers/fedimintex/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/wrappers/fedimintex/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()