This repository has been archived by the owner on Nov 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a474dc7
commit 88ff12a
Showing
8 changed files
with
299 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
defmodule Gakimint.Cashu.Token do | ||
@moduledoc """ | ||
Handles the serialization and deserialization of Cashu tokens. | ||
""" | ||
alias CBOR | ||
alias Gakimint.Cashu.Proof | ||
|
||
@type v3_token :: %{ | ||
token: [%{mint: String.t(), proofs: [Proof.t()]}], | ||
unit: String.t() | nil, | ||
memo: String.t() | nil | ||
} | ||
|
||
@doc """ | ||
Serializes a V3 token to a string. | ||
""" | ||
@spec serialize_v3(v3_token()) :: String.t() | ||
def serialize_v3(token) do | ||
json = Jason.encode!(token) | ||
base64 = Base.url_encode64(json, padding: false) | ||
"cashuA#{base64}" | ||
end | ||
|
||
@doc """ | ||
Deserializes a V3 token string to a token struct. | ||
""" | ||
@spec deserialize_v3(String.t()) :: {:ok, v3_token()} | {:error, String.t()} | ||
def deserialize_v3("cashuA" <> base64) do | ||
case Base.url_decode64(base64, padding: false) do | ||
{:ok, json} -> | ||
case Jason.decode(json) do | ||
{:ok, token} -> {:ok, token} | ||
{:error, _} -> {:error, "Invalid JSON in token"} | ||
end | ||
|
||
:error -> | ||
{:error, "Invalid base64 encoding"} | ||
end | ||
end | ||
|
||
def deserialize_v3(_), do: {:error, "Invalid token format"} | ||
end | ||
|
||
defmodule Gakimint.Cashu.TokenV4 do | ||
@moduledoc """ | ||
Handles the serialization and deserialization of Cashu V4 tokens. | ||
""" | ||
|
||
@type v4_token :: %{ | ||
m: String.t(), | ||
u: String.t() | nil, | ||
d: String.t() | nil, | ||
t: [ | ||
%{ | ||
i: binary(), | ||
p: [ | ||
%{ | ||
a: non_neg_integer(), | ||
s: String.t(), | ||
c: binary(), | ||
d: %{e: binary(), s: binary(), r: binary()} | nil, | ||
w: String.t() | nil | ||
} | ||
] | ||
} | ||
] | ||
} | ||
|
||
@doc """ | ||
Serializes a V4 token to a string. | ||
""" | ||
@spec serialize(v4_token()) :: String.t() | ||
def serialize(token) do | ||
cbor = CBOR.encode(token) | ||
base64 = Base.url_encode64(cbor, padding: false) | ||
"cashuB#{base64}" | ||
end | ||
|
||
@doc """ | ||
Deserializes a V4 token string to a token struct. | ||
""" | ||
@spec deserialize(String.t()) :: {:ok, v4_token()} | {:error, String.t()} | ||
def deserialize("cashuB" <> base64) do | ||
case Base.url_decode64(base64, padding: false) do | ||
{:ok, cbor} -> | ||
case CBOR.decode(cbor) do | ||
{:ok, token, ""} -> {:ok, keys_to_atoms(token)} | ||
_ -> {:error, "Invalid CBOR in token"} | ||
end | ||
|
||
:error -> | ||
{:error, "Invalid base64 encoding"} | ||
end | ||
end | ||
|
||
def deserialize(_), do: {:error, "Invalid token format"} | ||
|
||
defp keys_to_atoms(map) when is_map(map) do | ||
Map.new(map, fn | ||
{key, value} when is_binary(key) -> {String.to_existing_atom(key), keys_to_atoms(value)} | ||
{key, value} -> {key, keys_to_atoms(value)} | ||
end) | ||
end | ||
|
||
defp keys_to_atoms(list) when is_list(list) do | ||
Enum.map(list, &keys_to_atoms/1) | ||
end | ||
|
||
defp keys_to_atoms(value), do: value | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
defmodule Gakimint.Cashu.Types do | ||
@moduledoc """ | ||
Defines the core data types used in the Cashu protocol. | ||
""" | ||
|
||
@derive Jason.Encoder | ||
defstruct [:amount, :id, :B_] | ||
|
||
@type t :: %__MODULE__{ | ||
amount: non_neg_integer(), | ||
id: String.t(), | ||
B_: String.t() | ||
} | ||
|
||
@doc """ | ||
Creates a new BlindedMessage struct. | ||
""" | ||
@spec new_blinded_message(non_neg_integer(), String.t(), String.t()) :: t() | ||
def new_blinded_message(amount, id, B_) do | ||
%__MODULE__{amount: amount, id: id, B_: B_} | ||
end | ||
end | ||
|
||
defmodule Gakimint.Cashu.BlindSignature do | ||
@moduledoc """ | ||
Represents a BlindSignature in the Cashu protocol. | ||
""" | ||
|
||
@derive Jason.Encoder | ||
defstruct [:amount, :id, :C_] | ||
|
||
@type t :: %__MODULE__{ | ||
amount: non_neg_integer(), | ||
id: String.t(), | ||
C_: String.t() | ||
} | ||
|
||
@doc """ | ||
Creates a new BlindSignature struct. | ||
""" | ||
@spec new(non_neg_integer(), String.t(), String.t()) :: t() | ||
def new(amount, id, C_) do | ||
%__MODULE__{amount: amount, id: id, C_: C_} | ||
end | ||
end | ||
|
||
defmodule Gakimint.Cashu.Proof do | ||
@moduledoc """ | ||
Represents a Proof in the Cashu protocol. | ||
""" | ||
|
||
@derive Jason.Encoder | ||
defstruct [:amount, :id, :secret, :C] | ||
|
||
@type t :: %__MODULE__{ | ||
amount: non_neg_integer(), | ||
id: String.t(), | ||
secret: String.t(), | ||
C: String.t() | ||
} | ||
|
||
@doc """ | ||
Creates a new Proof struct. | ||
""" | ||
@spec new(non_neg_integer(), String.t(), String.t(), String.t()) :: t() | ||
def new(amount, id, secret, C) do | ||
%__MODULE__{amount: amount, id: id, secret: secret, C: C} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
defmodule Mix.Tasks.Test.Unit.Cashu do | ||
@moduledoc """ | ||
Test for the Cashu token serialization and deserialization. | ||
""" | ||
use Mix.Task | ||
|
||
@shortdoc "Runs the Cashu unit tests" | ||
def run(_) do | ||
Mix.Task.run("app.start", ["--no-start"]) | ||
ExUnit.start() | ||
Code.require_file("test/cashu_token_serde_test.exs") | ||
ExUnit.run() | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
defmodule Gakimint.Cashu.TokenTest do | ||
use ExUnit.Case, async: true | ||
alias Gakimint.Cashu.TokenV4 | ||
|
||
describe "V4 Token" do | ||
@tag :v4_token | ||
test "serialization and deserialization" do | ||
token = %{ | ||
m: "https://example.com", | ||
u: "sat", | ||
d: "Test token", | ||
t: [ | ||
%{ | ||
i: <<1, 2, 3>>, | ||
p: [ | ||
%{ | ||
a: 10, | ||
s: "secret1", | ||
c: <<4, 5, 6>>, | ||
d: %{e: <<7, 8, 9>>, s: <<10, 11, 12>>, r: <<13, 14, 15>>}, | ||
w: "witness1" | ||
}, | ||
%{ | ||
a: 20, | ||
s: "secret2", | ||
c: <<16, 17, 18>>, | ||
w: "witness2" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
|
||
serialized = TokenV4.serialize(token) | ||
assert String.starts_with?(serialized, "cashuB") | ||
|
||
{:ok, deserialized} = TokenV4.deserialize(serialized) | ||
assert normalize_token(deserialized) == normalize_token(token) | ||
end | ||
|
||
test "deserialization of invalid token" do | ||
assert {:error, "Invalid token format"} = TokenV4.deserialize("invalid_token") | ||
assert {:error, "Invalid CBOR in token"} = TokenV4.deserialize("cashuBinvalid_base64") | ||
|
||
assert {:error, "Invalid CBOR in token"} = | ||
TokenV4.deserialize("cashuB" <> Base.url_encode64(<<1, 2, 3>>, padding: false)) | ||
end | ||
end | ||
|
||
# Helper function to normalize binary representations | ||
defp normalize_token(token) do | ||
token | ||
|> normalize_binary_keys([:i, :c, :e, :s, :r]) | ||
end | ||
|
||
defp normalize_binary_keys(map, keys) when is_map(map) do | ||
Enum.reduce(keys, map, fn key, acc -> | ||
if Map.has_key?(acc, key) do | ||
Map.update!(acc, key, &normalize_binary/1) | ||
else | ||
acc | ||
end | ||
end) | ||
|> Map.new(fn {k, v} -> {k, normalize_binary_keys(v, keys)} end) | ||
end | ||
|
||
defp normalize_binary_keys(list, keys) when is_list(list) do | ||
Enum.map(list, &normalize_binary_keys(&1, keys)) | ||
end | ||
|
||
defp normalize_binary_keys(value, _keys), do: value | ||
|
||
defp normalize_binary(value) when is_binary(value), do: Base.encode16(value, case: :lower) | ||
defp normalize_binary(value), do: value | ||
end |