Skip to content

Commit

Permalink
Merge pull request #5 from txbody-org/transaction-encoder
Browse files Browse the repository at this point in the history
Transaction encoder
  • Loading branch information
piyushthapa authored Dec 2, 2024
2 parents 08b0de2 + 82937fc commit 0ebdf03
Show file tree
Hide file tree
Showing 30 changed files with 2,037 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/elixir.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
# and running the workflow steps.
matrix:
otp: ["25.0.4"] # Define the OTP version [required]
elixir: ["1.15.0"] # Define the elixir version [required]
elixir: ["1.17.3"] # Define the elixir version [required]
steps:
# Step: Setup Elixir + Erlang image as the base.
- name: Set up Elixir
Expand Down
49 changes: 26 additions & 23 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,36 @@
flake-utils = { url = "github:numtide/flake-utils"; };
};

outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
inherit (pkgs.lib) optional optionals;
pkgs = import nixpkgs { inherit system; };
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
inherit (pkgs.lib) optional optionals;
pkgs = import nixpkgs { inherit system; };

#elixir = pkgs.beam.packages.erlang_27.elixir.override {
# version = "1.17.2";
# rev = "47abe2d107e654ccede845356773bcf6e11ef7cb";
# sha256 = "sha256-8rb2f4CvJzio3QgoxvCv1iz8HooXze0tWUJ4Sc13dxg=";
#};
elixir = pkgs.beam.packages.erlang_27.elixir.override {
version = "1.17.3";
rev = "78f63d08313677a680868685701ae79a2459dcc1";
sha256 = "sha256-8rb2f4CvJzio3QgoxvCv1iz8HooXze0tWUJ4Sc13dxg=";
};

in
with pkgs;
{
devShell = pkgs.mkShell {
buildInputs = [
elixir_1_16
elixir_ls
glibcLocales

] ++ optional stdenv.isLinux inotify-tools
in
with pkgs;
{
devShell = pkgs.mkShell {
buildInputs = [
elixir
elixir_ls
glibcLocales
cargo
rustc

] ++ optional stdenv.isLinux inotify-tools
++ optional stdenv.isDarwin terminal-notifier
++ optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [
CoreFoundation
CoreServices
]);
};
});
}
};
});
}

24 changes: 24 additions & 0 deletions lib/sutra/blake_2b.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Sutra.Blake2b do
@moduledoc """
Helper function to calculate blake2b hash
"""
alias Blake2.Blake2b

@type blake2b_256() :: String.t()
@type blake2b_224() :: String.t()

@doc """
Returns a 32-byte digest.
"""
@spec blake2b_256(binary()) :: binary()
def blake2b_256(data) do
Blake2b.hash_hex(data, "", 32)
end

@doc """
Returns a 28-byte digest.
"""
def blake2b_224(data) do
Blake2b.hash_hex(data, "", 28)
end
end
4 changes: 4 additions & 0 deletions lib/sutra/cardano/address.ex
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,8 @@ defmodule Sutra.Cardano.Address do
fields: [payment_credential, stake_credential]
}
end

def to_cbor(%Address{} = addr) do
%CBOR.Tag{tag: :bytes, value: Parser.encode(addr)}
end
end
21 changes: 18 additions & 3 deletions lib/sutra/cardano/asset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule Sutra.Cardano.Asset do

alias Sutra.Data

alias Sutra.Data.Cbor

import Sutra.Data.Cbor, only: [extract_value: 1]

def from_plutus(cbor) when is_binary(cbor) do
Expand Down Expand Up @@ -48,7 +50,7 @@ defmodule Sutra.Cardano.Asset do
end

acc
|> Map.put(%CBOR.Tag{tag: :bytes, value: key_val}, from_asset_class(val))
|> Map.put(Cbor.as_byte(key_val), from_asset_class(val))
end)
end

Expand All @@ -59,11 +61,24 @@ defmodule Sutra.Cardano.Asset do

defp from_asset_class(asset_map) when is_map(asset_map) do
Enum.reduce(asset_map, %{}, fn {key, val}, acc ->
key = %CBOR.Tag{tag: :bytes, value: key}
Map.put(acc, key, val)
Map.put(acc, Cbor.as_byte(key), val)
end)
end

def lovelace_of(value) when is_integer(value), do: %{"lovelace" => value}
def lovelace_of(_), do: nil

def from_cbor(lovelace) when is_integer(lovelace), do: lovelace_of(lovelace)

def from_cbor([lovelace, other_assets]) do
with {:ok, assets} <- from_plutus(other_assets) do
Map.put(assets, "lovelace", lovelace)
end
end

def to_cbor(%{"lovelace" => lovelace} = asset) when map_size(asset) == 1, do: lovelace

def to_cbor(assets) do
[Map.get(assets, "lovelace", 0), Map.delete(assets, "lovelace") |> to_plutus()]
end
end
82 changes: 81 additions & 1 deletion lib/sutra/cardano/common/pool_relay.ex
Original file line number Diff line number Diff line change
@@ -1,25 +1,78 @@
defmodule Sutra.Cardano.Common.PoolRelay do
@moduledoc """
Pool Relay Information
This module defines the relay information for a pool.
It can be of three types:
1. single host address,
2. single host name
3. multiple host names.
## CDDL
https://github.com/IntersectMBO/cardano-ledger/blob/master/eras/conway/impl/cddl-files/conway.cddl#L347
```
relay = [single_host_addr // single_host_name // multi_host_name]
```
"""

use TypedStruct

typedstruct(module: SingleHostAddr) do
@moduledoc """
Single Host Address Relay
## CDDL
```
single_host_addr = (0, port / nil, ipv4 / nil, ipv6 / nil)
```
"""
field(:port, :integer)
field(:ipv4, :string)
field(:ipv6, :string)
end

typedstruct(module: SingleHostName) do
@moduledoc """
Single Host Name Relay
## CDDL
```
single_host_name = (1, port / nil, dns_name)
```
"""
field(:port, :integer)
field(:dns_name, :string)
end

typedstruct(module: MultiHostName) do
@moduledoc """
Multiple Host Name Relay
## CDDL
```
multi_host_name = (2, dns_name)
```
"""

field(:dns_name, :string)
end

@doc """
Decode the relay information from the CBOR data.
## Examples
iex> decode([0, "8080", "192.168.1.1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"])
%SingleHostAddr{}
iex> decode([1, "8080", "example.com"])
%SingleHostName{}
iex> decode([2, "example.com"])
%MultiHostName{}
"""
def decode([0, port, ipv4, ipv6]) do
%SingleHostAddr{port: port, ipv4: ipv4, ipv6: ipv6}
end
Expand All @@ -31,4 +84,31 @@ defmodule Sutra.Cardano.Common.PoolRelay do
def decode([2, dns_name]) do
%MultiHostName{dns_name: dns_name}
end

@doc """
Encode the relay information to the CBOR data.
## Examples
iex> encode(%SingleHostAddr{})
[0, port, ipv4, ipv6]
iex> encode(%SingleHostName{})
[1, port, dns_name]
iex> encode(%MultiHostName{})
[2, dns_name]
"""
def encode(%SingleHostAddr{port: port, ipv4: ipv4, ipv6: ipv6}) do
[0, port, ipv4, ipv6]
end

def encode(%SingleHostName{port: port, dns_name: dns_name}) do
[1, port, dns_name]
end

def encode(%MultiHostName{dns_name: dns_name}) do
[2, dns_name]
end
end
24 changes: 24 additions & 0 deletions lib/sutra/cardano/script/native_script.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,28 @@ defmodule Sutra.Cardano.Script.NativeScript do
def from_witness_set([5, slot]) do
%ScriptInvalidHereafter{slot: slot}
end

def to_witness_set(%ScriptPubkey{pubkey_hash: pubkey}) do
[0, pubkey]
end

def to_witness_set(%ScriptAll{scripts: scripts}) do
[1, Enum.map(scripts, &to_witness_set/1)]
end

def to_witness_set(%ScriptAny{scripts: scripts}) do
[2, Enum.map(scripts, &to_witness_set/1)]
end

def to_witness_set(%ScriptNOfK{n: n, scripts: scripts}) do
[3, n, Enum.map(scripts, &to_witness_set/1)]
end

def to_witness_set(%ScriptInvalidBefore{slot: slot}) do
[4, slot]
end

def to_witness_set(%ScriptInvalidHereafter{slot: slot}) do
[5, slot]
end
end
53 changes: 49 additions & 4 deletions lib/sutra/cardano/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Sutra.Cardano.Transaction do
Cardano Transaction
"""

alias Sutra.Blake2b
alias Sutra.Cardano.Transaction.TxBody
alias Sutra.Cardano.Transaction.Witness
alias Sutra.Data.Cbor
Expand All @@ -19,18 +20,34 @@ defmodule Sutra.Cardano.Transaction do
field(:metadata, any())
end

@doc """
Generate transaction from hex encoded cbor
iex> from_hex("valid-hex-transaction")
{:ok, %Sutra.Cardano.Transaction{}}
iex> from_hex("some-invalid-hex-transaction")
{:error, :invalid_cbor}
"""
def from_hex(cbor) when is_binary(cbor) do
case Sutra.Data.decode(cbor) do
{:ok, data} -> from_cbor(data)
{:error, _} -> {:error, :invalid_cbor}
end
end

# Conway era transaction
def from_cbor(%PList{value: [tx_body, witness, is_valid, metadata]})
@doc """
Generate transaction from cbor
iex> from_cbor(%PList{value: [valid_tx_body, valid_witness_cbor, true, metadata]})
%Sutra.Cardano.Transaction{}
"""
@spec from_cbor(CBOR.Tag.t()) :: __MODULE__.t()
def from_cbor(%PList{value: [tx_body, witness_cbor, is_valid, metadata]})
when is_boolean(is_valid) do
witness =
Enum.reduce(Cbor.extract_value!(witness), [], fn w, acc ->
Enum.reduce(Cbor.extract_value!(witness_cbor), [], fn w, acc ->
acc ++ Witness.decode(w)
end)

Expand All @@ -44,7 +61,35 @@ defmodule Sutra.Cardano.Transaction do

def from_cbor(%PList{value: _values}) do
raise """
Only Conway era transaction supported
Only Conway era transaction supported. Todo: support other eras.
"""
end

@doc """
Convert transaction to hex encoded cbor
iex> to_hex(%Sutra.Cardano.Transaction{})
"some-valid-hex-encoded-cbor"
"""
@spec to_cbor(__MODULE__.t()) :: Cbor.t()
def to_cbor(%__MODULE__{} = tx) do
tx_body_cbor = TxBody.to_cbor(tx.tx_body)

%PList{value: [tx_body_cbor, Witness.to_cbor(tx.witnesses), tx.is_valid, tx.metadata]}
end

@doc """
Get transaction id
iex> tx_id(%Sutra.Cardano.Transaction{})
"88350824a9557e16a8f18b9b3cc4ab7cc0c282c178132083babde3cdb33393ee"
"""
@spec tx_id(__MODULE__.t()) :: Blake2b.blake2b_256()
def tx_id(%__MODULE__{} = tx) do
tx.tx_body
|> TxBody.to_cbor()
|> CBOR.encode()
|> Blake2b.blake2b_256()
end
end
Loading

0 comments on commit 0ebdf03

Please sign in to comment.