Packages

A User-Friendly Component-Based Cardano SDK for Elixir.

Current section

Files

Jump to
sutra_cardano lib sutra cardano asset.ex
Raw

lib/sutra/cardano/asset.ex

defmodule Sutra.Cardano.Asset do
@moduledoc """
A module for handling Cardano Assets (Values).
Cardano Assets are represented as a nested map structure:
`%{policy_id => %{asset_name => quantity}}`
Lovelace (ADA) is a special case where the policy ID is `"lovelace"` and the structure is simplified in some contexts, but when fully normalized in this module, it is treated as a top-level key `"lovelace"` with an integer value, or implicitly as policy ID `""` and asset name `""` in Plutus encoded forms.
This module provides utility functions for creating, merging, diffing, and inspecting these asset structures, as well as encoding/decoding them to/from Plutus Data (CBOR).
## Asset Structure
%{
"lovelace" => 1000000,
"policy_id_hex" => %{
"asset_name_hex" => 100
}
}
"""
@type policy_id() :: String.t()
@type asset_name() :: String.t()
@type quantity() :: integer()
@typedoc """
A map representing a MultiAsset value.
Keys are policy IDs (hex string) or "lovelace".
Values are either the integer amount (for lovelace) or a map of asset names to quantities.
"""
@type t() :: %{
optional(policy_id()) => quantity() | %{asset_name() => quantity()}
}
alias Sutra.Data
alias Sutra.Data.Cbor
import Sutra.Data.Cbor, only: [extract_value: 1]
@doc """
Creates a new Asset structure.
## Examples
# Create Lovelace asset
iex> new("lovelace", 100)
%{"lovelace" => 100}
# Create Token
iex> new("policy_id", "asset_name", 100)
%{"policy_id" => %{"asset_name" => 100}}
"""
def new(policy_id, asset_name, amount) do
add(%{}, policy_id, asset_name, amount)
end
def new("lovelace", amount) do
add(%{}, "lovelace", amount)
end
def new(amount) when is_integer(amount), do: from_lovelace(amount)
@doc """
Decodes Asset from Plutus Data (CBOR).
Handles both the flat integer (Lovelace only) and the nested map structure for MultiAssets.
## Examples
iex> from_plutus(%{"" => %{"" => 200}, "policy-1" => %{"asset-1" => 100}})
{:ok, %{"lovelace" => 200, "policy-1" => %{"asset-1" => 100}}}
iex> from_plutus("A140A1401A000F4240")
{:ok, %{"lovelace" => 1_000_000}}
# Token CBOR example
iex> token_cbor = "A340A1401A000F42404B706F6C6963792D69642D31A244746B6E31186444746B6E3218C84B706F6C6963792D69642D32A144746B6E3319012C"
iex> {:ok, asset} = from_plutus(token_cbor)
iex> asset["lovelace"]
1_000_000
iex> asset["706f6c6963792d69642d31"]
%{"746b6e31" => 100, "746b6e32" => 200}
"""
def from_plutus(cbor) when is_binary(cbor) do
case Data.decode(cbor) do
{:ok, data} -> from_plutus(data)
{:error, _} -> {:error, :invalid_cbor}
end
end
def from_plutus(plutus_data) when is_map(plutus_data) do
result =
Enum.reduce(plutus_data, %{}, fn {key, val}, acc ->
key =
case extract_value(key) do
{:ok, ""} -> "lovelace"
{:ok, value} -> value
end
Map.put(acc, key, to_asset_class(key, val))
end)
{:ok, result}
end
defp to_asset_class("lovelace", %{%CBOR.Tag{tag: :bytes, value: ""} => lovelace}),
do: lovelace
defp to_asset_class("lovelace", %{"" => lovelace}),
do: lovelace
defp to_asset_class(_, value) when is_map(value) do
Enum.reduce(value, %{}, fn {key, val}, acc ->
{:ok, key} = extract_value(key)
Map.put(acc, key, val)
end)
end
@doc """
Encodes an Asset structure to Plutus Data format (as a Map).
## Examples
iex> asset = %{"lovelace" => 300}
iex> to_plutus(asset)
%{%CBOR.Tag{tag: :bytes, value: ""} => %{%CBOR.Tag{tag: :bytes, value: ""} => 300}}
iex> asset = %{"lovelace" => 200, "policy-1" => %{"asset-1" => 300}}
iex> plutus_data = to_plutus(asset)
iex> Map.keys(plutus_data) |> length()
2
iex> asset = %{"lovelace" => 1_000_000}
iex> to_plutus(asset) |> Cbor.encode_hex()
"A140A1401A000F4240"
"""
def to_plutus(data) when is_map(data) do
Enum.reduce(data, %{}, fn {key, val}, acc ->
key_val =
case key do
"lovelace" -> ""
_ -> key
end
acc
|> Map.put(Cbor.as_byte(key_val), from_asset_class(val))
end)
end
def to_plutus(_val), do: {:error, :invalid_data}
defp from_asset_class(lovelace_value) when is_integer(lovelace_value),
do: %{%CBOR.Tag{tag: :bytes, value: ""} => lovelace_value}
defp from_asset_class(asset_map) when is_map(asset_map) do
Enum.reduce(asset_map, %{}, fn {key, val}, acc ->
Map.put(acc, Cbor.as_byte(key), val)
end)
end
@doc """
Creates an Asset structure purely containing Lovelace.
## Examples
iex> from_lovelace(100)
%{"lovelace" => 100}
"""
def from_lovelace(value) when is_number(value), do: %{"lovelace" => value}
def from_lovelace(_), do: nil
@doc """
Returns lovelace amount from asset. returns 0 if no lovelace is found in asset.
## Examples
iex> lovelace_of(%{"policy" => %{"asset" => 1}})
0
iex> lovelace_of(%{"lovelace" => 134_000})
134_000
"""
def lovelace_of(asset) when is_map(asset), do: Map.get(asset, "lovelace", 0)
def lovelace_of(_), do: 0
@doc """
Returns quantity of specific token from asset. returns 0 if no such token is found.
## Examples
iex> asset = %{"policy-1" => %{"asset-1" => 500, "asset-2" => 300}}
iex> get_quantity(asset, "policy-1", "asset-1")
500
iex> get_quantity(%{"policy-1" => %{"asset-1" => 100}}, "policy-1", "asset-3")
0
iex> get_quantity(%{"policy-1" => %{"asset-1" => 100}}, "policy-2", "asset-1")
0
iex> get_quantity(%{"lovelace" => 1000}, "lovelace")
1000
iex> get_quantity(%{}, "lovelace")
0
"""
def get_quantity(asset, policy_id, asset_name)
when is_map(asset) and is_binary(policy_id) and is_binary(asset_name) do
asset
|> Map.get(policy_id, %{})
|> Map.get(asset_name, 0)
end
def get_quantity(asset, "lovelace"), do: lovelace_of(asset)
@doc """
Merge two asset values into one.
## Examples
iex> asset1 = %{"lovelace" => 123}
iex> asset2 = %{"policy-1" => %{"asset1" => 600}}
iex> merge(asset1, asset2)
%{"lovelace" => 123, "policy-1" => %{"asset1" => 600}}
Adds Qty of token for similar policies:
iex> asset1 = %{"lovelace" => 100, "policy-1" => %{"asset1" => 600}}
iex> asset2 = %{"policy-1" => %{"asset1" => 400}}
iex> merge(asset1, asset2)
%{"lovelace" => 100, "policy-1" => %{"asset1" => 1000}}
"""
def merge(asset1, asset2) when is_map(asset1) and is_map(asset2) do
merge(asset1, asset2, fn _k, v1, v2 -> v1 + v2 end)
end
def merge(asset1, asset2, with_func) when is_function(with_func, 3) do
Map.merge(asset1, asset2, fn k, v1, v2 ->
if is_number(v1), do: with_func.(k, v1, v2), else: merge(v1, v2, with_func)
end)
end
@doc """
Subtract value of asset (asset1 - asset2).
## Examples
iex> asset1 = from_lovelace(500)
iex> asset2 = from_lovelace(600)
iex> diff(asset1, asset2)
%{"lovelace" => 100}
iex> asset1 = %{"policy-1" => %{"asset1" => 200}}
iex> asset2 = %{"policy-1" => %{"asset1" => 50, "asset2" => 200 }}
iex> diff(asset1, asset2)
%{"policy-1" => %{"asset1" => -150, "asset2" => 200 }}
"""
def diff(asset1, asset2) when is_map(asset1) and is_map(asset2) do
Enum.map(asset2, fn {k, v} ->
if is_map(v),
do: {k, Map.get(asset1, k, %{}) |> diff(v)},
else: {k, v - Map.get(asset1, k, 0)}
end)
|> Enum.into(%{})
end
def zero?(asset) when is_map(asset), do: asset == %{}
@doc """
Checks if asset has positive amount.
## Examples
iex> is_positive_asset(%{"lovelace" => 100})
true
iex> is_positive_asset(%{"lovelace" => 50, "policy" => %{"asset" => -100}})
true
iex> is_positive_asset(%{"lovelace" => 0, "policy" => %{"asset" => -100}})
false
iex> is_positive_asset(%{"lovelace" => -10, "policy" => %{"asset" => -100}})
false
"""
def is_positive_asset(asset) when is_map(asset) do
Enum.any?(asset, fn {_k, v} ->
if is_number(v), do: v > 0, else: is_positive_asset(v)
end)
end
@doc """
Returns only Positive Assets.
## Examples
iex> only_positive(%{"lovelace" => 200, "policy" => %{"asset" => -10}})
%{"lovelace" => 200}
iex> only_positive(%{"lovelace" => 0, "policy" => %{"asset" => -10}})
%{}
iex> only_positive(%{"lovelace" => 0, "policy" => %{"asset" => 10}})
%{"policy" => %{"asset" => 10}}
"""
def only_positive(asset) when is_map(asset) do
filter_by_value(asset, &(&1 > 0))
end
@doc """
Filter Asset by applying functions against Asset value.
## Examples
iex> asset = %{"lovelace" => 100, "policy" => %{"asset1" => -10, "asset2" => 10}}
iex> filter_by_value(asset, fn v -> v > 0 end)
%{"lovelace" => 100, "policy" => %{"asset2" => 10}}
iex> asset = %{"lovelace" => 10, "policy" => %{"asset1" => -10, "asset2" => 10}}
iex> filter_by_value(asset, &(&1 == 10))
%{"lovelace" => 10, "policy" => %{"asset2" => 10}}
iex> asset = %{"lovelace" => 10, "policy" => %{"asset1" => -10, "asset2" => 10}}
iex> filter_by_value(asset, &(&1 < 0))
%{"policy" => %{"asset1" => -10}}
"""
def filter_by_value(asset, func) when is_function(func, 1) and is_map(asset) do
Enum.reduce(asset, %{}, fn {k, v}, acc ->
if is_map(v) do
result = filter_by_value(v, func)
if map_size(result) > 0, do: Map.put(acc, k, result), else: acc
else
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
if func.(v), do: Map.put(acc, k, v), else: acc
end
end)
end
@doc """
Returns Asset with absolute values (all positive).
## Examples
iex> abs_value(%{"lovelace" => -100})
%{"lovelace" => 100 }
iex> abs_value(%{"lovelace" => 100})
%{"lovelace" => 100 }
"""
def abs_value(asset) when is_map(asset) do
Enum.map(asset, fn {k, v} ->
if is_map(v), do: {k, abs_value(v)}, else: {k, abs(v)}
end)
|> Enum.into(%{})
end
@doc """
Add quantity of single token to Assets.
## Examples
iex> add(from_lovelace(100), "lovelace", 200)
%{"lovelace" => 300}
iex> add(from_lovelace(100), "policy", "asset", 10)
%{"lovelace" => 100, "policy" => %{"asset" => 10}}
"""
def add(asset, "lovelace", amount) when is_map(asset) and is_number(amount),
do: Map.put(asset, "lovelace", lovelace_of(asset) + amount)
def add(asset, policy_id, asset_name, qty)
when is_map(asset) and is_binary(policy_id) and is_binary(asset_name) and is_number(qty) do
merge(asset, Map.put(%{}, policy_id, Map.new([{asset_name, qty}])))
end
@doc """
Returns an empty asset.
"""
def zero, do: %{}
@doc """
Negates quantities of all Assets.
## Examples
iex> negate(from_lovelace(100))
%{"lovelace" => -100}
iex> negate(%{"lovelace" => 10, "policy" => %{"asset" => 70}})
%{"lovelace" => -10, "policy" => %{"asset" => -70}}
"""
def negate(assets) do
Enum.map(assets, fn {k, v} ->
if is_map(v), do: {k, negate(v)}, else: {k, -v}
end)
|> Enum.into(%{})
end
@doc """
Returns Asset without Ada.
## Examples
iex> without_lovelace(%{"lovelace" => 10}) == zero()
true
iex> without_lovelace(%{"lovelace" => 10, "policy" => %{"asset" => 1}})
%{"policy" => %{"asset" => 1}}
"""
def without_lovelace(assets), do: Map.delete(assets, "lovelace")
@doc """
Returns all policies of asset.
## Examples
iex> policies(%{"lovelace" => 100})
[]
iex> policies(%{"policy1" => %{"asset1" => 1}, "policy2" => %{"asset1" => 2}})
["policy1", "policy2"] |> Enum.sort()
"""
def policies(assets) when is_map(assets) do
assets
|> without_lovelace()
|> Map.keys()
end
@doc """
Get a subset of the assets restricted to the given policies.
## Examples
iex> restricted_to(%{"lovelace" => 100, "policy1" => %{"asset1" => 10}}, ["lovelace"])
%{"lovelace" => 100}
iex> restricted_to(%{
...> "lovelace" => 100,
...> "policy1" => %{"asset" => 10},
...> "policy2" => %{"asset" => 50},
...> "policy3" => %{"asset" => 30},
...> }, ["policy1", "policy2"])
%{"policy1" => %{"asset" => 10}, "policy2" => %{"asset" => 50}}
"""
def restricted_to(assets, policy_ids) when is_map(assets) and is_list(policy_ids) do
Enum.reduce(policy_ids, %{}, fn policy_id, acc ->
policy_value = Map.get(assets, policy_id)
if policy_value, do: Map.put(acc, policy_id, policy_value), else: acc
end)
end
@doc """
Checks if both asset has similar tokens (share at least one policy ID).
## Examples
iex> asset1 = %{"lovelace" => 1000, "policy1" => %{"asset" => 1}}
iex> contains_token?(from_lovelace(100), asset1)
false
iex> asset1 = %{"policy1" => %{"asset" => 1}, "policy2" => %{"asset" => 1}}
iex> asset2 = %{"policy3" => %{"asset" => 1}}
iex> contains_token?(asset2, asset1)
false
"""
def contains_token?(asset1, asset2) do
asset2
|> policies()
|> Enum.any?(&Map.has_key?(asset1, &1))
end
@doc """
Encodes Asset to CBOR list format `[lovelace_qty, multi_asset_map]`.
Note: If the asset only contains Lovelace, it returns the integer amount directly, which corresponds to the compact encoding in some contexts, or the way output values are represented when no multi-assets are present.
## Examples
iex> to_cbor(%{"lovelace" => 100})
100
iex> to_cbor(%{"lovelace" => 100, "policy" => %{"asset" => 100}})
[ 100,
%{%CBOR.Tag{tag: :bytes, value: "policy"} =>
%{%CBOR.Tag{tag: :bytes, value: "asset"} => 100}
}
]
"""
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
@doc """
Converts CBOR decoded data back to Asset structure.
## Examples
iex> cbor_list = [
...> 100,
...> %{
...> %CBOR.Tag{tag: :bytes, value: "policy"} =>
...> %{
...> %CBOR.Tag{tag: :bytes, value: "asset"} => 1
...> }
...> }
...> ]
iex> from_cbor(cbor_list)
%{"lovelace" => 100, "706f6c696379" => %{"6173736574" => 1}}
iex> from_cbor(10)
%{"lovelace" => 10}
"""
def from_cbor(lovelace) when is_integer(lovelace), do: from_lovelace(lovelace)
def from_cbor([lovelace, other_assets]) do
with {:ok, assets} <- from_plutus(other_assets) do
Map.put(assets, "lovelace", lovelace)
end
end
def from_seperator(assets, seperator \\ ".") when is_map(assets) do
Enum.reduce(assets, %{}, fn {k, v}, acc ->
[policy_id, asset_name] = String.split(k, seperator)
add(acc, policy_id, asset_name, v)
end)
end
end