Current section
Files
Jump to
Current section
Files
lib/sutra/cardano/address.ex
defmodule Sutra.Cardano.Address do
@moduledoc """
Cardano Address
"""
use TypedStruct
alias Sutra.Cardano.Script
alias __MODULE__, as: Address
alias Sutra.Cardano.Address.Credential
alias Sutra.Cardano.Address.Parser
alias Sutra.Cardano.Address.Pointer
alias Sutra.Data
alias Sutra.Data.Cbor
alias Sutra.Data.Plutus.Constr
import Sutra.Utils, only: [maybe: 3]
@type credential_type :: :vkey | :script
@type address_type :: :shelley | :reward | :byron
@type stake_credential :: Credential.t() | Pointer.t() | nil
@type network :: :mainnet | :testnet
@type bech_32() :: binary()
@valid_network [:mainnet, :preprod, :preview, :custom, :testnet]
typedstruct module: Credential do
@moduledoc """
Address Credential
"""
field(:credential_type, Address.credential_type(), enforce: true)
field(:hash, String.t(), enforce: true)
end
typedstruct module: Pointer do
@moduledoc """
Address Pointer
"""
field(:slot, pos_integer(), enforce: true)
field(:tx_index, pos_integer(), enforce: true)
field(:cert_index, pos_integer(), enforce: true)
end
typedstruct do
field(:network, Address.network(), enforce: true)
field(:address_type, address_type(), enforce: true)
field(:payment_credential, Credential.t())
field(:stake_credential, stake_credential())
end
@doc """
Return an address from a Bech32
## Example
iex> from_bech32("addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8")
{:ok, %Address{network: :mainnet, address_type: :shelley, ..}}
iex> from_bech32("stake178phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gtcccycj5")
{:ok, %Address{network: :mainnet, address_type: :reward, ..}}
"""
def from_bech32(bech32_addr) do
case Bech32.decode(bech32_addr) do
{:ok, _hmr, bytes} -> Parser.decode(bytes)
_ -> {:error, "Invalid Bech32 address"}
end
end
@doc """
Convert an address to a Bech32
## Example
iex> to_bech32(%Address{network: :mainnet, address_type: :shelley})
{:ok, "addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8"}
iex> to_bech32(%Address{network: :mainnet, address_type: :reward})
{:ok, "stake178phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gtcccycj5"}
"""
def to_bech32(%Address{} = address) do
hrp_prefix =
case address.address_type do
:shelley -> "addr"
:reward -> "stake"
_ -> ""
end
is_padded =
case address.stake_credential do
%Pointer{} -> false
_ -> true
end
hrp =
if address.network == :mainnet do
hrp_prefix
else
hrp_prefix <> "_test"
end
data = Parser.encode(address) |> Bech32.convertbits(8, 5, is_padded)
Bech32.encode_from_5bit(hrp, data)
end
@spec from_plutus(binary() | Sutra.Data.Plutus.t()) ::
Address.t() | {:error, String.t()} | {:error, any()}
def from_plutus(data) when is_binary(data) do
case Data.decode(data) do
{:ok, decoded} -> from_plutus(decoded)
{:error, reason} -> {:error, reason}
end
end
def from_plutus(%Constr{index: 0, fields: [pay_cred, stake_cred]}) do
{:ok,
%Address{
network: nil,
address_type: :shelley,
payment_credential: fetch_payment_credential(pay_cred),
stake_credential: fetch_stake_credential(stake_cred)
}}
end
defp fetch_payment_credential(%Constr{index: indx, fields: [vkey_hash]}) do
credential_type = if indx == 0, do: :vkey, else: :script
%Credential{credential_type: credential_type, hash: Cbor.extract_value!(vkey_hash)}
end
defp fetch_stake_credential(%Constr{index: 1}), do: nil
defp fetch_stake_credential(%Constr{
fields: [%Constr{fields: [slot, tx_index, cert_index], index: 1}]
}),
do: %Pointer{slot: slot, tx_index: tx_index, cert_index: cert_index}
defp fetch_stake_credential(%Constr{
fields: [
%Constr{
fields: [%Constr{fields: [stake_cred_hash], index: indx}]
}
]
}) do
credential_type = if indx == 0, do: :vkey, else: :script
%Credential{credential_type: credential_type, hash: Cbor.extract_value!(stake_cred_hash)}
end
@spec to_plutus(Address.t()) :: Sutra.Data.Plutus.t()
def to_plutus(%Address{} = addr) do
payment_credential =
case addr.payment_credential do
%Credential{credential_type: :vkey, hash: hash} ->
%Constr{index: 0, fields: [Cbor.as_byte(hash)]}
%Credential{credential_type: :script, hash: hash} ->
%Constr{index: 1, fields: [Cbor.as_byte(hash)]}
end
stake_credential =
case addr.stake_credential do
%Pointer{slot: slot, tx_index: tx_index, cert_index: cert_index} ->
%Constr{
index: 0,
fields: [
%Constr{
index: 1,
fields: [slot, tx_index, cert_index]
}
]
}
%Credential{credential_type: :vkey, hash: hash} ->
%Constr{
index: 0,
fields: [
%Constr{
index: 0,
fields: [
%Constr{
index: 0,
fields: [Cbor.as_byte(hash)]
}
]
}
]
}
%Credential{credential_type: :script, hash: hash} ->
%Constr{
index: 0,
fields: [
%Constr{
index: 0,
fields: [
%Constr{
index: 1,
fields: [Cbor.as_byte(hash)]
}
]
}
]
}
nil ->
%Constr{index: 1, fields: []}
end
%Constr{
index: 0,
fields: [payment_credential, stake_credential]
}
end
def to_cbor(%Address{} = addr) do
%CBOR.Tag{tag: :bytes, value: Parser.encode(addr)}
end
def from_script(_script, network)
when network not in [:mainnet, :testnet, :preview, :preprod],
do: {:error, "Invalid Network ID"}
def from_script(%Script{} = script, network) do
script_payment_key = Script.hash_script(script)
from_script(script_payment_key, network)
end
def from_script(script_hash, network) when is_binary(script_hash) do
%__MODULE__{
network: network,
address_type: :shelley,
payment_credential: %Credential{
credential_type: :script,
hash: script_hash
},
stake_credential: nil
}
end
def from_verification_key(vkey, stake_key \\ nil, network)
when is_binary(vkey) and network in @valid_network do
stake_key_cred =
maybe(stake_key, nil, fn hash ->
%Credential{credential_type: :vkey, hash: hash}
end)
%__MODULE__{
network: network,
address_type: :shelley,
payment_credential: %Credential{
credential_type: :vkey,
hash: vkey
},
stake_credential: stake_key_cred
}
end
def script_address?(%__MODULE__{
payment_credential: %Credential{credential_type: credential_type}
}),
do: credential_type == :script
def vkey_address?(%__MODULE__{
payment_credential: %Credential{credential_type: credential_type}
}),
do: credential_type == :vkey
end