Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: feat: migrate fedimintex #31

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).

Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
});
in {
legacyPackages = outputs;
packages = { default = outputs.fedimint-roastr; };
packages = { default = outputs.fedimint-clientd; };
devShells = flakeboxLib.mkShells {
packages = [ ];
buildInputs = commonArgs.buildInputs;
Expand Down
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(),
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
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
Loading