Current section
Files
Jump to
Current section
Files
lib/sutra/coin_selection/largest_first.ex
defmodule Sutra.CoinSelection.LargestFirst do
@moduledoc """
LargestFirst CoinSelection algorithm.
Strategies:
1. Native Tokens: For each required token, select inputs containing that token,
sorted by the token amount (descending).
2. Lovelace: After satisfying native tokens, if more Lovelace is needed,
select inputs sorted by Lovelace amount (descending).
"""
alias Sutra.Cardano.Asset
alias Sutra.Cardano.Transaction
alias Sutra.Cardano.Transaction.TxBuilder.Error
alias Sutra.CoinSelection
@doc """
Selects Utxos based on LargestFirst CoinSelection Algorithm
"""
@spec select_utxos([Transaction.input()], Asset.t(), Asset.t()) ::
{:ok, CoinSelection.t()} | {:error, String.t()}
def select_utxos(inputs, to_fill, left_over \\ %{})
def select_utxos(_, %{} = to_fill, left_over) when map_size(to_fill) == 0,
do: {:ok, %CoinSelection{selected_inputs: [], change: left_over}}
def select_utxos(inputs, to_fill, left_over) do
# 1. Phase 1: Select for Native Tokens
with {:ok, selected_inputs, remaining_inputs, current_value} <-
select_native_tokens(inputs, to_fill) do
# 2. Phase 2: Select for Lovelace
target_lovelace = Asset.lovelace_of(to_fill)
current_lovelace = Asset.lovelace_of(current_value)
needed_lovelace = target_lovelace - current_lovelace
if needed_lovelace > 0 do
case CoinSelection.select_utxos_for_lovelace(
remaining_inputs,
needed_lovelace,
%{}
) do
{:ok, %CoinSelection{selected_inputs: lovelace_inputs}} ->
final_inputs = selected_inputs ++ lovelace_inputs
final_value = sum_values(final_inputs)
calculate_result(final_inputs, final_value, to_fill, left_over)
{:error, _} ->
# Construct error with missing lovelace
{:error, Error.CannotBalanceTx.new(%{"lovelace" => needed_lovelace})}
end
else
calculate_result(selected_inputs, current_value, to_fill, left_over)
end
end
end
defp calculate_result(selected_inputs, current_value, to_fill, left_over) do
# Change = (Current - Target) + LeftOver
surplus = Asset.merge(current_value, Asset.negate(to_fill))
change = Asset.merge(surplus, left_over)
{:ok,
%CoinSelection{
selected_inputs: selected_inputs,
change: Asset.only_positive(change)
}}
end
defp select_native_tokens(inputs, to_fill) do
requirements = flatten_requirements(to_fill)
# Iterate over requirements and accumulate selection
result =
Enum.reduce_while(
requirements,
{[], inputs, %{}},
&process_token_requirement/2
)
case result do
{:error, missing} -> {:error, Error.CannotBalanceTx.new(missing)}
{selected, available, current_val} -> {:ok, selected, available, current_val}
end
end
defp process_token_requirement(
{policy, name, target_amount},
{selected, available, current_val}
) do
current_amount = Asset.get_quantity(current_val, policy, name)
if current_amount >= target_amount do
{:cont, {selected, available, current_val}}
else
select_more_tokens(
policy,
name,
target_amount,
current_amount,
selected,
available,
current_val
)
end
end
defp select_more_tokens(
policy,
name,
target_amount,
current_amount,
selected,
available,
current_val
) do
needed = target_amount - current_amount
# Filter and sort available inputs for this token
candidates =
available
|> Enum.filter(fn input ->
Asset.get_quantity(input.output.value, policy, name) > 0
end)
|> Enum.sort_by(
fn input -> Asset.get_quantity(input.output.value, policy, name) end,
:desc
)
# Select inputs until satisfied
{newly_selected, _remaining_candidates, new_val, satisfied} =
select_until_satisfied(candidates, policy, name, needed, current_val)
if satisfied do
# Update available inputs: remove newly selected
new_available = available -- newly_selected
{:cont, {selected ++ newly_selected, new_available, new_val}}
else
# Failed to satisfy a token requirement
missing_amount = target_amount - Asset.get_quantity(new_val, policy, name)
{:halt, {:error, %{policy => %{name => missing_amount}}}}
end
end
defp select_until_satisfied(candidates, policy, name, needed, current_val) do
Enum.reduce_while(
candidates,
{[], current_val, needed},
fn input, {sel, val, current_needed} ->
input_qty = Asset.get_quantity(input.output.value, policy, name)
new_val = Asset.merge(val, input.output.value)
new_sel = [input | sel]
new_needed = current_needed - input_qty
if new_needed <= 0 do
{:halt, {new_sel, new_val, true}}
else
{:cont, {new_sel, new_val, new_needed}}
end
end
)
|> case do
{sel, val, true} -> {Enum.reverse(sel), [], val, true}
{sel, val, _} -> {Enum.reverse(sel), [], val, false}
end
end
defp flatten_requirements(assets) do
Enum.reduce(assets, [], &flatten_policy_requirements/2)
end
defp flatten_policy_requirements({"lovelace", _}, acc), do: acc
defp flatten_policy_requirements({policy, tokens}, acc) do
Enum.reduce(tokens, acc, fn {name, amount}, inner_acc ->
if amount > 0, do: [{policy, name, amount} | inner_acc], else: inner_acc
end)
end
defp sum_values(inputs) do
Enum.reduce(inputs, %{}, fn input, acc ->
Asset.merge(acc, input.output.value)
end)
end
end