Current section
Files
Jump to
Current section
Files
lib/sutra/cardano/address/parser.ex
defmodule Sutra.Cardano.Address.Parser do
@moduledoc """
Contains function to encode and decode Cardano addresses based on cip-0019
https://cips.cardano.org/cip/CIP-0019
"""
import Bitwise, only: [band: 2, bor: 2, <<<: 2, &&&: 2, >>>: 2]
alias Sutra.Cardano.Address
alias Sutra.Cardano.Address.Credential
alias Sutra.Cardano.Address.Parser
alias Sutra.Cardano.Address.Pointer
defimpl CBOR.Encoder, for: Address do
@impl true
def encode_into(address, _acc) do
Parser.encode(address)
end
end
@doc """
Encode an [Address] to CBOR.
Byron addresses are not supported
Shelley address description from CIP-0019:
Header type (tttt....) Payment Part Delegation Part
(0) 0000.... PaymentKeyHash StakeKeyHash
(1) 0001.... ScriptHash StakeKeyHash
(2) 0010.... PaymentKeyHash ScriptHash
(3) 0011.... ScriptHash ScriptHash
(4) 0100.... PaymentKeyHash Pointer
(5) 0101.... ScriptHash Pointer
(6) 0110.... PaymentKeyHash ø
(7) 0111.... ScriptHash ø
Header type (....tttt)
(0) ....0000 testnet
(1) ....0001 mainnet
For example, `61....(56 chars / 28 bytes)....` is an enterprise address (6, only a payment key) on mainnet (1).
Stake address description from CIP-0019:
Header type (tttt....) Stake Reference
(14) 1110.... StakeKeyHash
(15) 1111.... ScriptHash
"""
@spec encode(Address.t()) :: binary()
def encode(%Address{} = address) do
network = if address.network == :mainnet, do: "1", else: "0"
{header_type, payload} =
do_encode_address(
address.address_type,
address.payment_credential,
address.stake_credential
)
(header_type <> network <> payload) |> Base.decode16!(case: :lower)
end
defp do_encode_address(:shelley, payment_credential, stake_credential) do
case {payment_credential, stake_credential} do
# (0) 0000.... PaymentKeyHash StakeKeyHash
{%Credential{credential_type: :vkey} = p_cred, %Credential{credential_type: :vkey} = s_cred} ->
{"0", p_cred.hash <> s_cred.hash}
# (1) 0001.... ScriptHash StakeKeyHash
{%Credential{credential_type: :script} = p_cred,
%Credential{credential_type: :vkey} = s_cred} ->
{"1", p_cred.hash <> s_cred.hash}
# (2) 0010.... PaymentKeyHash ScriptHash
{%Credential{credential_type: :vkey} = p_cred,
%Credential{credential_type: :script} = s_cred} ->
{"2", p_cred.hash <> s_cred.hash}
# (3) 0011.... ScriptHash ScriptHash
{%Credential{credential_type: :script} = p_cred,
%Credential{credential_type: :script} = s_cred} ->
{"3", p_cred.hash <> s_cred.hash}
# (4) 0100.... PaymentKeyHash Pointer
{%Credential{credential_type: :vkey} = p_cred, %Pointer{} = pointer} ->
pointer_val =
encode_variable_length(pointer.slot) <>
encode_variable_length(pointer.tx_index) <> encode_variable_length(pointer.cert_index)
{"4", p_cred.hash <> Base.encode16(pointer_val, case: :lower)}
# (5) 0101.... ScriptHash Pointer
{%Credential{credential_type: :script} = p_cred, %Pointer{} = pointer} ->
pointer_val =
encode_variable_length(pointer.slot) <>
encode_variable_length(pointer.tx_index) <> encode_variable_length(pointer.cert_index)
{"5", p_cred.hash <> Base.encode16(pointer_val, case: :lower)}
# (6) 0110.... PaymentKeyHash ø
{%Credential{credential_type: :vkey} = p_cred, nil} ->
{"6", p_cred.hash}
# (7) 0111.... ScriptHash ø
{%Credential{credential_type: :script} = p_cred, nil} ->
{"7", p_cred.hash}
end
end
defp do_encode_address(:reward, nil, %Credential{} = stake_cred) do
case stake_cred.credential_type do
# (14) 1110.... StakeKeyHash
:vkey -> {"e", stake_cred.hash}
# (15) 1111.... ScriptHash
:script -> {"f", stake_cred.hash}
end
end
@spec decode(binary()) :: Address.t()
def decode(bytes) do
<<t1::1, t2::1, t3::1, t4::1, n1::1, n2::1, n3::1, n4::1, rest::bitstring>> = bytes
address_header = [t1, t2, t3, t4]
decoded_address = do_decode_bytes(address_header, rest)
%Address{
network: get_network([n1, n2, n3, n4]),
address_type: decoded_address.address_type,
payment_credential: decoded_address.payment_credential,
stake_credential: decoded_address.stake_credential
}
end
# (0) ....0000 testnet
defp get_network([0, 0, 0, 0]), do: :testnet
# (1) ....0001 mainnet
defp get_network([0, 0, 0, 1]), do: :mainnet
# (0) 0000.... PaymentKeyHash StakeKeyHash
defp do_decode_bytes([0, 0, 0, 0], bytes) do
%{
payment_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :vkey
},
stake_credential: %Credential{
hash: binary_slice(bytes, 28, 28) |> Base.encode16(case: :lower),
credential_type: :vkey
},
address_type: :shelley
}
end
# (1) 0001.... ScriptHash StakeKeyHash
defp do_decode_bytes([0, 0, 0, 1], bytes) do
%{
payment_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :script
},
stake_credential: %Credential{
hash: binary_slice(bytes, 28, 28) |> Base.encode16(case: :lower),
credential_type: :vkey
},
address_type: :shelley
}
end
# (2) 0010.... PaymentKeyHash ScriptHash
defp do_decode_bytes([0, 0, 1, 0], bytes) do
%{
payment_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :vkey
},
stake_credential: %Credential{
hash: binary_slice(bytes, 28, 28) |> Base.encode16(case: :lower),
credential_type: :script
},
address_type: :shelley
}
end
# (3) 0011.... ScriptHash ScriptHash
defp do_decode_bytes([0, 0, 1, 1], bytes) do
%{
payment_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :script
},
stake_credential: %Credential{
hash: binary_slice(bytes, 28, 28) |> Base.encode16(case: :lower),
credential_type: :script
},
address_type: :shelley
}
end
# (4) 0100.... PaymentKeyHash Pointer
defp do_decode_bytes([0, 1, 0, 0], bytes) do
bytes_list = :binary.bin_to_list(bytes)
{payment, pointer_bytes} = Enum.split(bytes_list, 28)
{slot, remaining} = decode_variable_length(pointer_bytes)
{index, remaining} = decode_variable_length(remaining)
{cert_index, _} = decode_variable_length(remaining)
%{
payment_credential: %Credential{
hash: :binary.list_to_bin(payment) |> Base.encode16(case: :lower),
credential_type: :vkey
},
stake_credential: %Pointer{slot: slot, tx_index: index, cert_index: cert_index},
address_type: :shelley
}
end
# (5) 0101.... ScriptHash Pointer
defp do_decode_bytes([0, 1, 0, 1], bytes) do
bytes_list = :binary.bin_to_list(bytes)
{payment, pointer_bytes} = Enum.split(bytes_list, 28)
{slot, remaining} = decode_variable_length(pointer_bytes)
{index, remaining} = decode_variable_length(remaining)
{cert_index, _} = decode_variable_length(remaining)
%{
payment_credential: %Credential{
hash: :binary.list_to_bin(payment) |> Base.encode16(case: :lower),
credential_type: :script
},
stake_credential: %Pointer{slot: slot, tx_index: index, cert_index: cert_index},
address_type: :shelley
}
end
# (6) 0110.... PaymentKeyHash ø
defp do_decode_bytes([0, 1, 1, 0], bytes) do
%{
payment_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :vkey
},
stake_credential: nil,
address_type: :shelley
}
end
# (7) 0111.... ScriptHash ø
defp do_decode_bytes([0, 1, 1, 1], bytes) do
%{
payment_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :script
},
stake_credential: nil,
address_type: :shelley
}
end
# (8) 1000.... Byron
defp do_decode_bytes([1, 0, 0, 0], _bytes) do
raise "Not implemented: Byron Address"
end
# (9) 1110.... StakeKeyHash
defp do_decode_bytes([1, 1, 1, 0], bytes) do
%{
payment_credential: nil,
stake_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :vkey
},
address_type: :reward
}
end
# (10) 1111.... ScriptHash
defp do_decode_bytes([1, 1, 1, 1], bytes) do
%{
payment_credential: nil,
stake_credential: %Credential{
hash: binary_slice(bytes, 0, 28) |> Base.encode16(case: :lower),
credential_type: :script
},
address_type: :reward
}
end
defp do_decode_bytes(_, _), do: raise("Invalid address type")
defp decode_variable_length(bytes) do
Enum.reduce_while(bytes, {0, bytes}, fn byte, {val, [_ | t]} ->
new_val = bor(val <<< 7, band(byte, 127))
if band(byte, 128) == 0 do
{:halt, {new_val, t}}
else
{:cont, {new_val, t}}
end
end)
end
defp encode_variable_length(val) when val >= 0 do
bit_length = Integer.to_string(val, 2) |> String.length()
{_, _, buff} =
Enum.reduce_while(0..bit_length, {bit_length, val >>> 7, [val &&& 127]}, fn _,
{l, v, buff} ->
if l > 7 do
new_val = v >>> 7
new_buff_val = (v &&& 127) + 128
{:cont, {l - 7, new_val, [new_buff_val | buff]}}
else
{:halt, {l, v, buff}}
end
end)
:binary.list_to_bin(buff)
end
end