Skip to content

Commit

Permalink
feat: migrate fedimintex
Browse files Browse the repository at this point in the history
  • Loading branch information
Kodylow committed Apr 23, 2024
1 parent 7a1270c commit d616d95
Show file tree
Hide file tree
Showing 17 changed files with 579 additions and 0 deletions.
4 changes: 4 additions & 0 deletions wrappers/fedimintex/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions wrappers/fedimintex/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
63 changes: 63 additions & 0 deletions wrappers/fedimintex/README.md
Original file line number Diff line number Diff line change
@@ -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 <https://hexdocs.pm/fedimintex>.
4 changes: 4 additions & 0 deletions wrappers/fedimintex/example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export PASSWORD='password'
export DOMAIN='localhost'
export PORT=5000
export BASE_URL=http://localhost:5000
4 changes: 4 additions & 0 deletions wrappers/fedimintex/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
s:
iex -S mix
env:
source .env
71 changes: 71 additions & 0 deletions wrappers/fedimintex/lib/admin.ex
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions wrappers/fedimintex/lib/client.ex
Original file line number Diff line number Diff line change
@@ -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(),
wallet: atom()
}

@type http_response :: {:ok, map()} | {:error, String.t()}

defstruct base_url: nil, password: nil, admin: nil, mint: nil, ln: nil, wallet: 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 <> "/fedimint/v2",
password: password,
admin: Fedimintex.Admin,
mint: Fedimintex.Mint,
ln: Fedimintex.Ln,
wallet: 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
29 changes: 29 additions & 0 deletions wrappers/fedimintex/lib/example.ex
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions wrappers/fedimintex/lib/fedimintex.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Fedimintex do
@moduledoc """
Documentation for `Fedimintex`.
"""
end
48 changes: 48 additions & 0 deletions wrappers/fedimintex/lib/ln/ln.ex
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit d616d95

Please sign in to comment.