Packages

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

Current section

Files

Jump to
sutra_cardano lib sutra cardano transaction tx_builder.ex
Raw

lib/sutra/cardano/transaction/tx_builder.ex

defmodule Sutra.Cardano.Transaction.TxBuilder do
@moduledoc """
A composable builder for constructing Cardano Transactions.
`TxBuilder` provides a pipeline-based API to build transactions step-by-step. It handles:
- Adding inputs (spending UTxOs)
- Adding outputs (sending payments)
- Minting/burning assets
- Managing witnesses (signatures, scripts)
- Handling collateral and fees
- Balancing the transaction (calculating change)
## Example Usage
```elixir
alias Sutra.Cardano.Transaction.TxBuilder
import Sutra.Cardano.Transaction.TxBuilder
new_tx()
|> add_input(utxos_to_spend)
|> add_output(receiver_address, 10_000_000)
|> build_tx!(wallet_address: change_address)
|> sign_tx(signing_key)
|> submit_tx()
```
"""
require Sutra.Cardano.Script
require Sutra.Data.Plutus
alias Sutra.Cardano.Address
alias Sutra.Cardano.Address.Credential
alias Sutra.Cardano.Asset
alias Sutra.Cardano.Script
alias Sutra.Cardano.Transaction
alias Sutra.Cardano.Transaction.Datum
alias Sutra.Cardano.Transaction.Input
alias Sutra.Cardano.Transaction.Output
alias Sutra.Cardano.Transaction.TxBuilder.CertificateHelper
alias Sutra.Cardano.Transaction.TxBuilder.Internal
alias Sutra.Cardano.Transaction.TxBuilder.TxConfig
alias Sutra.Cardano.Transaction.Witness
alias Sutra.Cardano.Transaction.Witness.VkeyWitness
alias Sutra.Crypto.Key
alias Sutra.Data
alias Sutra.Data.Plutus
alias Sutra.ProtocolParams
alias Sutra.Provider
import Sutra.Utils, only: [maybe: 2]
defstruct config: %TxConfig{},
inputs: [],
outputs: [],
ref_inputs: [],
errors: [],
mints: %{},
script_lookup: %{},
required_signers: MapSet.new(),
plutus_data: %{},
valid_to: nil,
valid_from: nil,
redeemer_lookup: %{},
metadata: nil,
used_scripts: MapSet.new(),
collateral_inputs: [],
certificates: [],
total_deposit: Asset.zero(),
withdrawals: %{}
@doc """
Initialize a new empty `TxBuilder`.
## Examples
iex> new_tx()
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def new_tx do
%__MODULE__{
config: %TxConfig{evaluate_provider_uplc: false}
}
end
@doc """
Overrides the default provider for the transaction builder.
## Examples
iex> new_tx() |> use_provider(Sutra.Provider.Koios)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def use_provider(%__MODULE__{config: cfg} = builder, provider) when not is_nil(provider) do
%__MODULE__{builder | config: TxConfig.__set_cfg(cfg, :provider, provider)}
end
@doc """
Sets custom Protocol Parameters for transaction building.
Useful when you want to override the provider's fetched protocol parameters or use specific values for fee calculation.
## Examples
iex> params = %Sutra.ProtocolParams{min_fee_a: 44, min_fee_b: 155381}
iex> new_tx() |> set_protocol_params(params)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def set_protocol_params(%__MODULE__{config: cfg} = builder, %ProtocolParams{} = protocol_params) do
%__MODULE__{builder | config: TxConfig.__set_cfg(cfg, :protocol_params, protocol_params)}
end
@doc """
Toggles runtime UPLC script evaluation by the provider during transaction building.
When set to `true`, the builder will send the transaction to the configured provider (e.g., Blockfrost, Maestro)
to evaluate script execution units (ExUnits) and validate script logic before finalizing the transaction.
This is highly recommended for transactions involving Plutus scripts to ensure they will pass on-chain validation
and to accurately calculate execution fees.
Defaults to `false`.
## Examples
iex> new_tx() |> evaluate_provider_uplc(true)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def evaluate_provider_uplc(%__MODULE__{config: cfg} = builder, evaluate \\ true) do
%__MODULE__{builder | config: TxConfig.__set_cfg(cfg, :evaluate_provider_uplc, evaluate)}
end
@doc """
Sets the wallet address(es) to be used for the transaction.
This is primarily used to:
1. Fetch UTxOs for balancing (if `wallet_utxos` is not provided in `build_tx!`).
2. Determine the change address (if `set_change_address` is not explicitly called).
## Examples
iex> address = Sutra.Cardano.Address.from_bech32("addr_test1...")
iex> new_tx() |> set_wallet_address(address)
%Sutra.Cardano.Transaction.TxBuilder{}
iex> addresses = [address1, address2]
iex> new_tx() |> set_wallet_address(addresses)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def set_wallet_address(%__MODULE__{config: cfg} = builder, %Address{} = address) do
%__MODULE__{builder | config: TxConfig.__set_cfg(cfg, :wallet_address, [address])}
end
def set_wallet_address(%__MODULE__{config: cfg} = builder, addresses) when is_list(addresses) do
%__MODULE__{builder | config: TxConfig.__set_cfg(cfg, :wallet_address, addresses)}
end
@doc """
Sets a custom change address for the transaction.
If not set, the first address from `set_wallet_address` is used as the change address.
You can also specify a datum to be attached to the change output.
## Examples
iex> address = Sutra.Cardano.Address.from_bech32("addr_test1...")
iex> new_tx() |> set_change_address(address)
%Sutra.Cardano.Transaction.TxBuilder{}
# Change Address with datum
iex> datum = %Sutra.Cardano.Transaction.Datum{kind: :inline_datum, value: "data"}
iex> new_tx() |> set_change_address(address, {:inline_datum, datum})
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def set_change_address(
%__MODULE__{config: %TxConfig{} = cfg, plutus_data: prev_plutus_data} = builder,
%Address{} = address,
datum \\ nil
) do
{new_plutus_data, datum_info} = extract_datum(datum)
new_plutus_data =
if datum_info.kind == :datum_hash,
do: [new_plutus_data | prev_plutus_data],
else: prev_plutus_data
change_datum_info = if datum_info.kind == :no_datum, do: cfg.change_datum, else: datum_info
%__MODULE__{
builder
| plutus_data: new_plutus_data,
config: %TxConfig{cfg | change_address: address, change_datum: change_datum_info}
}
end
@doc """
Adds Inputs (UTxOs) to the transaction.
## Parameters
- `builder`: The `TxBuilder` instance.
- `inputs`: A list of `%Sutra.Cardano.Transaction.Input{}` to spend.
- `opts`: options for spending inputs.
## Options
- `witness`: The witness for the input.
- `:vkey_witness` (default) - For standard wallet inputs (P2PKH).
- `%Script{}` or `%NativeScript{}` - The spending script itself.
- `%Input{}` - The reference input containing the spending script (CIP-33).
- `:ref_inputs` - Indicates the script is provided via `add_reference_inputs/2`.
- `redeemer`: The redeemer data (required for script spending).
- `datum`: Explicit datum info (e.g., `{:datum_hash, data}`) if the UTxO from provider doesn't contain it.
## Examples
# Simple wallet spend
iex> new_tx() |> add_input(wallet_inputs)
%Sutra.Cardano.Transaction.TxBuilder{}
# Spending from a Native Script
iex> new_tx() |> add_input(native_script_inputs, witness: native_script)
%Sutra.Cardano.Transaction.TxBuilder{}
# Spending from a Plutus Script
iex> new_tx() |> add_input(script_inputs, witness: script, redeemer: redeemer_data)
%Sutra.Cardano.Transaction.TxBuilder{}
# Spending using a Reference Script on-chain
iex> new_tx() |> add_input(script_inputs, witness: ref_script_utxo, redeemer: redeemer_data)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def add_input(%__MODULE__{} = cfg, [%Input{} | _] = inputs, opts \\ []) do
witness = opts[:witness] || :vkey_witness
redeemer = opts[:redeemer]
passed_datum = opts[:datum]
Enum.reduce(inputs, cfg, fn %Input{output: output} = input, %__MODULE__{} = acc_cfg ->
exact_datum = output.datum_raw || passed_datum
# Add datum in witness if input has datum with DatumHash kind
new_plutus_data =
if Datum.datum_kind(output.datum) == :datum_hash and not is_nil(exact_datum),
do: Map.put_new(cfg.plutus_data, output.datum.value, exact_datum),
else: cfg.plutus_data
case validate_script_witness(cfg.script_lookup, input, redeemer, witness) do
# Spending for Varification Key as payment Credential
{:ok, :vkey_witness, _} ->
%__MODULE__{
acc_cfg
| inputs: [input | acc_cfg.inputs],
plutus_data: new_plutus_data
}
# Spending from Script Address
{:ok, used_script_type, witness_key} ->
%__MODULE__{
acc_cfg
| inputs: [input | acc_cfg.inputs],
script_lookup: Map.put_new(acc_cfg.script_lookup, witness_key, witness),
used_scripts: MapSet.put(cfg.used_scripts, used_script_type),
plutus_data: new_plutus_data,
redeemer_lookup:
Map.put_new(cfg.redeemer_lookup, {:spend, Input.extract_ref(input)}, redeemer)
}
{:error, err_key} ->
%__MODULE__{
acc_cfg
| errors: [%{key: err_key, value: input} | acc_cfg.errors]
}
end
end)
end
@doc """
Adds Reference Inputs to the transaction (CIP-31).
Reference incoming inputs allow reading from UTxOs without spending them.
Commonly used for:
- Reference scripts (CIP-33) to use scripts without including them in witness set.
- Using Inline Datums (CIP-32) from reference inputs.
## Examples
iex> add_reference_inputs(new_tx(), [script_ref_utxo])
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def add_reference_inputs(%__MODULE__{} = builder, [%Input{} | _] = inputs) do
new_script_lookup =
Enum.reduce(inputs, builder.script_lookup, fn %Input{} = input, acc ->
if Script.script?(input.output.reference_script),
do: Map.put(acc, Script.hash_script(input.output.reference_script), input),
else: acc
end)
%__MODULE__{
builder
| ref_inputs: builder.ref_inputs ++ inputs,
script_lookup: new_script_lookup
}
end
# Checks if inputs is valid with correct redeemer and witness
defp validate_script_witness(
_script_lookup,
%Input{
output: %Output{
address: %Address{
payment_credential: %Credential{credential_type: :vkey, hash: vkey_hash}
}
}
},
_redeemer,
_witness
),
do: {:ok, :vkey_witness, vkey_hash}
defp validate_script_witness(
script_lookup,
%Input{
output: %Output{
address: %Address{payment_credential: %Credential{hash: script_hash}}
}
},
redeemer,
witness
),
do: validate_script_witness(script_lookup, script_hash, redeemer, witness)
defp validate_script_witness(script_lookup, script_hash, redeemer, witness) do
exact_witness =
if witness == :ref_inputs,
do: extract_from_script_lookup(script_lookup[script_hash]),
else: witness
used_script_type =
if Script.is_plutus_script(exact_witness), do: exact_witness.script_type, else: :native
cond do
not Script.script?(exact_witness) ->
{:error, :missing_script_witness}
Script.hash_script(exact_witness) != script_hash ->
{:error, :invalid_script_witness}
Script.is_plutus_script(exact_witness) and not Plutus.is_plutus_data(redeemer) ->
{:error, :invalid_redeemer}
true ->
{:ok, used_script_type, script_hash}
end
end
defp extract_from_script_lookup(%Input{output: %Output{reference_script: script}}), do: script
defp extract_from_script_lookup(_), do: nil
@doc """
Adds an Output to the transaction.
You can pass a constructed `%Output{}` struct or use the helper with address, value, and optional datum.
## Examples
iex> output = %Sutra.Cardano.Transaction.Output{address: address, value: %{"lovelace" => 100}}
iex> add_output(%TxBuilder{}, output)
%Sutra.Cardano.Transaction.TxBuilder{}
# Simple payment (Lovelace only)
iex> add_output(%TxBuilder{}, address, 5_000_000)
%Sutra.Cardano.Transaction.TxBuilder{}
# Payment with Native Assets
iex> add_output(%TxBuilder{}, address, %{"lovelace" => 2_000_000, "policy_id" => %{"token" => 10}})
%Sutra.Cardano.Transaction.TxBuilder{}
# Output with inline datum
iex> add_output(%TxBuilder{}, address, 2_000_000, {:inline_datum, datum_data})
%Sutra.Cardano.Transaction.TxBuilder{}
# Output with datum hash
iex> add_output(%TxBuilder{}, address, 2_000_000, {:datum_hash, datum_data})
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def add_output(%__MODULE__{} = cfg, %Output{} = output) do
plutus_data =
if Datum.datum_kind(output.datum) == :datum_hash and not is_nil(output.datum_raw),
do: Map.put_new(cfg.plutus_data, output.datum.value, output.datum_raw),
else: cfg.plutus_data
%__MODULE__{cfg | outputs: [output | cfg.outputs], plutus_data: plutus_data}
end
def add_output(%__MODULE__{} = cfg, %Address{} = out_addr, assets, datum \\ nil) do
{plutus_data, datum_info} = extract_datum(datum)
output = %Output{address: out_addr, value: assets, datum: datum_info, datum_raw: plutus_data}
add_output(cfg, output)
end
defp extract_datum({:inline_datum, val}) do
raw_data = Data.encode(val)
{Data.decode!(raw_data), Datum.inline(raw_data)}
end
defp extract_datum({:datum_hash, val}) do
raw_data = Data.encode(val)
hashed_datum = Datum.calculate_datum_hash(raw_data)
{Data.decode!(raw_data), Datum.datum_hash(hashed_datum)}
end
defp extract_datum(_), do: {nil, Datum.no_datum()}
@doc """
Mints or Burns assets.
## Parameters
- `builder`: The `TxBuilder` instance.
- `policy_id`: Hex string of the minting policy ID.
- `assets`: A map of asset names to quantities (e.g. `%{"tkn" => 100}`). Negative quantities mean burning.
- `minting_policy`: The witness for minting.
- `%Script{}` or `%NativeScript{}`
- `:ref_inputs` (if policy script is in reference inputs)
- `redeemer`: Redeemer data (required for Plutus minting).
## Examples
# Minting with Native Script
iex> mint_asset(%TxBuilder{}, policy_id, %{"tkn" => 100}, native_script)
%Sutra.Cardano.Transaction.TxBuilder{}
# Burning with Plutus Script
iex> mint_asset(%TxBuilder{}, policy_id, %{"tkn" => -100}, plutus_script, redeemer)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def mint_asset(builder, policy_id, assets, policy, redeemer \\ nil)
# Already minted same token
def mint_asset(%__MODULE__{mints: mints} = cfg, policy_id, _, _, _)
when is_map_key(mints, policy_id),
do: %__MODULE__{
cfg
| errors: [%{key: :multiple_mints, value: policy_id} | cfg.errors]
}
def mint_asset(%__MODULE__{} = cfg, policy_id, assets, minting_policy, redeemer)
when Script.is_script(minting_policy) or minting_policy == :ref_inputs do
case validate_script_witness(cfg.script_lookup, policy_id, redeemer, minting_policy) do
{:ok, used_script_type, _} ->
new_redeemer_lookup =
if is_nil(redeemer),
do: cfg.redeemer_lookup,
else: Map.put_new(cfg.redeemer_lookup, {:mint, policy_id}, redeemer)
%__MODULE__{
cfg
| mints: Map.put_new(cfg.mints, policy_id, assets),
script_lookup: Map.put_new(cfg.script_lookup, policy_id, minting_policy),
used_scripts: MapSet.put(cfg.used_scripts, used_script_type),
redeemer_lookup: new_redeemer_lookup
}
{:error, :invalid_script_witness} ->
%__MODULE__{
cfg
| errors: [%{key: :invalid_minting_policy, value: policy_id} | cfg.errors]
}
{:error, :missing_script_witness} ->
%__MODULE__{
cfg
| errors: [%{key: :missing_minting_policy, value: policy_id} | cfg.errors]
}
{:error, :invalid_redeemer} ->
%__MODULE__{
cfg
| errors: [%{key: :invalid_redeemer_for_policy, value: policy_id} | cfg.errors]
}
end
end
@doc """
Deploys a script to the blockchain (Create reference script output).
Creates an output at the specified address containing the script as a reference script.
The output will contain the minimum required Ada.
## Parameters
- `builder`: The `TxBuilder` instance.
- `out_addr`: The Address where the utility output will be sent.
- `script`: The `%Script{}` or `%NativeScript{}` to be attached.
## Examples
iex> deploy_script(new_tx(), address, plutus_script)
%Sutra.Cardano.Transaction.TxBuilder{}
iex> deploy_script(new_tx(), address, native_script)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def deploy_script(%__MODULE__{} = cfg, %Address{} = out_addr, script)
when Script.is_script(script) do
output = %Output{
address: out_addr,
reference_script: script,
datum: Datum.no_datum(),
value: Asset.zero()
}
add_output(cfg, output)
end
@doc """
Explicitly adds a required signer to the transaction body.
This is useful when the transaction needs to be signed by a key that isn't necessarily
spending a UTxO (e.g., for governance actions or special script requirements).
## Examples
iex> add_signer(new_tx(), address)
%Sutra.Cardano.Transaction.TxBuilder{}
iex> add_signer(new_tx(), "pubkey_hash_hex")
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def add_signer(
%__MODULE__{} = cfg,
%Address{payment_credential: %Credential{} = payment_credential} = addr
) do
if Address.vkey_address?(addr),
do: %__MODULE__{
cfg
| required_signers: MapSet.put(cfg.required_signers, payment_credential.hash)
},
else: %__MODULE__{cfg | errors: [%{key: :invalid_payment_signer, value: addr}]}
end
def add_signer(
%__MODULE__{} = cfg,
pubkey_hash
)
when is_binary(pubkey_hash) do
%__MODULE__{
cfg
| required_signers: MapSet.put(cfg.required_signers, pubkey_hash)
}
end
@doc """
Explicitly adds datum to the witness set (lookup map).
This is useful when you want to include datum in the transaction body without embedding it in an output,
usually for script validation purposes.
## Examples
iex> attach_datum(new_tx(), %Sutra.Data.Plutus.Constr{tag: 121, fields: []})
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def attach_datum(%__MODULE__{} = cfg, datum) do
encoded_datum = Data.encode(datum)
datum_hash = Datum.calculate_datum_hash(encoded_datum)
%__MODULE__{
cfg
| plutus_data:
Map.put_new(
cfg.plutus_data,
datum_hash,
Data.decode!(encoded_datum)
)
}
end
@doc """
Attaches JSON metadata to the transaction.
## Parameters
- `builder`: The `TxBuilder` instance.
- `label`: Integer label for the metadata (e.g., 721 for NFTs).
- `metadata`: The metadata content (Map, List, String, Integer, etc.).
## Examples
iex> attach_metadata(new_tx(), 721, %{"policy" => ...})
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def attach_metadata(%__MODULE__{} = builder, label, metadata)
when not is_nil(metadata) and is_integer(label) do
%__MODULE__{builder | metadata: Map.put(%{}, label, metadata)}
end
@doc """
Sets the transaction validity start interval.
## Examples
iex> valid_from(new_tx(), DateTime.utc_now())
%Sutra.Cardano.Transaction.TxBuilder{}
iex> valid_from(new_tx(), 1678900000000)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def valid_from(%__MODULE__{} = cfg, %DateTime{} = dt) do
%__MODULE__{cfg | valid_from: DateTime.to_unix(dt, :millisecond)}
end
def valid_from(%__MODULE__{} = cfg, timestamp) when is_integer(timestamp),
do: %__MODULE__{cfg | valid_from: timestamp}
@doc """
Sets the transaction validity end interval (TTL).
## Examples
iex> valid_to(new_tx(), DateTime.utc_now() |> DateTime.add(300, :second))
%Sutra.Cardano.Transaction.TxBuilder{}
iex> valid_to(new_tx(), 1678900000000)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def valid_to(%__MODULE__{} = cfg, %DateTime{} = dt) do
%__MODULE__{cfg | valid_to: DateTime.to_unix(dt, :millisecond)}
end
def valid_to(%__MODULE__{} = cfg, timestamp) when is_integer(timestamp),
do: %__MODULE__{cfg | valid_to: timestamp}
@doc """
Sets the datum to be used for the change output.
If not set, the change output will not have a datum.
## Examples
iex> set_change_datum(new_tx(), datum_data)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def set_change_datum(%__MODULE__{} = cfg, datum) when Plutus.is_plutus_data(datum) do
%__MODULE__{cfg | config: TxConfig.__set_cfg(cfg.config, :change_datum, datum)}
end
@doc """
Withdraws rewards from a stake address.
## Parameters
- `builder`: The `TxBuilder` instance.
- `stake_credential`: The stake address or credential to withdraw from.
- `lovelace`: The amount to withdraw.
- `redeemer`: (Optional) Redeemer if withdrawing from a script credential.
## Examples
# Withdraw from key credential
iex> withdraw_stake(new_tx(), stake_address, 500_000)
%Sutra.Cardano.Transaction.TxBuilder{}
# Withdraw from script credential
iex> withdraw_stake(new_tx(), script, redeemer, 500_000)
%Sutra.Cardano.Transaction.TxBuilder{}
"""
def withdraw_stake(
%__MODULE__{} = cfg,
%Address{
stake_credential: %Credential{credential_type: :vkey, hash: stake_hash}
},
lovelace
)
when is_integer(lovelace) do
%__MODULE__{
cfg
| withdrawals: Map.put_new(cfg.withdrawals, stake_hash, Asset.from_lovelace(lovelace))
}
end
def withdraw_stake(%__MODULE__{} = cfg, native_script, lovelace)
when Script.is_native_script(native_script) and is_integer(lovelace) do
script_hash = Script.hash_script(native_script)
%__MODULE__{
cfg
| withdrawals: Map.put_new(cfg.withdrawals, script_hash, Asset.from_lovelace(lovelace)),
script_lookup: Map.put_new(cfg.script_lookup, script_hash, native_script),
used_scripts: MapSet.put(cfg.used_scripts, :native)
}
end
def withdraw_stake(%__MODULE__{} = cfg, plutus_script, redeemer, lovelace)
when Script.is_plutus_script(plutus_script) and is_integer(lovelace) and
Plutus.is_plutus_data(redeemer) do
script_hash = Script.hash_script(plutus_script)
%__MODULE__{
cfg
| withdrawals: Map.put_new(cfg.withdrawals, script_hash, Asset.from_lovelace(lovelace)),
script_lookup: Map.put_new(cfg.script_lookup, script_hash, plutus_script),
used_scripts: MapSet.put(cfg.used_scripts, plutus_script.script_type),
redeemer_lookup: Map.put_new(cfg.redeemer_lookup, {:reward, script_hash}, redeemer)
}
end
@doc delegate_to: {CertificateHelper, :register_stake_credential, 3}
defdelegate register_stake_credential(builder, credential, redeemer \\ nil),
to: CertificateHelper
@doc delegate_to: {CertificateHelper, :delegate_vote, 3}
defdelegate delegate_vote(builder, credential, drep, redeemer \\ nil), to: CertificateHelper
@doc delegate_to: {CertificateHelper, :delegate_stake_and_vote, 5}
defdelegate delegate_stake_and_vote(
builder,
credential,
drep,
stake_pool_key_hash,
redeemer \\ nil
),
to: CertificateHelper
@doc """
Builds the transaction body.
This function balances the transaction by:
1. Fetching UTxOs from the wallet address (if not provided).
2. Selecting necessary inputs to cover outputs and fees.
3. Calculating fees and change.
4. Evaluating script execution units (if `evaluate_provider_uplc` is enabled).
## Options
- `wallet_utxos`: A list of `%Input{}` to be used for balancing. If not provided, they are fetched from the wallet address.
- `collateral_inputs`: A list of `%Input{}` to be used as collateral (required for Plutus scripts).
## Examples
iex> build_tx(builder)
{:ok, %Sutra.Cardano.Transaction{}}
iex> build_tx(builder, wallet_utxos: my_utxos)
{:ok, %Sutra.Cardano.Transaction{}}
"""
def build_tx(cfg, opts \\ [])
def build_tx(%__MODULE__{errors: [_ | _]} = cfg, _opts), do: {:error, cfg.errors}
def build_tx(%__MODULE__{} = cfg, opts) do
final_cfg = TxConfig.__setup(cfg.config, opts) |> TxConfig.__init()
collateral_inputs = opts[:collateral_inputs] || []
wallet_inputs = maybe(opts[:wallet_utxos], fn -> load_wallet_utxos(final_cfg) end)
ref_inputs = Enum.uniq_by(cfg.ref_inputs, & &1.output_reference)
inputs = Enum.uniq_by(cfg.inputs, & &1.output_reference)
with :ok <- check_mint_balanced(cfg) do
%__MODULE__{
cfg
| config: final_cfg,
inputs: inputs,
ref_inputs: ref_inputs,
used_scripts: MapSet.to_list(cfg.used_scripts),
certificates: Enum.reverse(cfg.certificates),
outputs: Enum.reverse(cfg.outputs)
}
|> Internal.process_build_tx(wallet_inputs, collateral_inputs)
end
end
@doc """
Same as `build_tx/2` but raises an exception on error.
"""
def build_tx!(%__MODULE__{} = cfg, opts \\ []) do
case build_tx(cfg, opts) do
{:ok, tx} ->
tx
{:error, errors} ->
raise inspect(errors)
end
end
defp check_mint_balanced(%__MODULE__{mints: m}) when m == %{}, do: :ok
defp check_mint_balanced(%__MODULE__{mints: mints, outputs: outputs}) do
left_over_mint =
Enum.reduce_while(outputs, mints, fn o, acc ->
if Asset.only_positive(acc) == %{},
do: {:halt, acc},
else: {:cont, Asset.diff(o.value, acc)}
end)
if Asset.only_positive(left_over_mint) == %{},
do: :ok,
else:
{:error,
"No Output Found with Minting Policies: #{Enum.join(Map.keys(left_over_mint), ", ")}"}
end
defp load_wallet_utxos(%TxConfig{} = cfg) do
addresses =
if is_list(cfg.wallet_address),
do: cfg.wallet_address,
else: [cfg.wallet_address]
cfg.provider.utxos_at_addresses(addresses)
end
@doc """
Signs the transaction with the provided key(s).
## Parameters
- `tx`: The `%Transaction{}` struct (usually result of `build_tx!`).
- `signers`: A signing key (Bech32 string or key tuple) or a list of signing keys.
## Examples
iex> sign_tx(tx, signing_key)
%Sutra.Cardano.Transaction{}
"""
def sign_tx(%Transaction{witnesses: %Witness{} = witness} = tx, signers)
when is_list(signers) do
tx_hash = Transaction.tx_id(tx) |> Base.decode16!(case: :mixed)
new_vkey_witness =
Enum.reduce(signers, MapSet.new(witness.vkey_witness), fn sk, acc ->
signing_key = if is_binary(sk), do: Key.from_bech32(sk), else: {:ok, sk}
case signing_key do
{:ok, key} ->
MapSet.put(acc, %VkeyWitness{
vkey: Key.public_key(key),
signature: Key.sign(key, tx_hash)
})
_ ->
acc
end
end)
%Transaction{
tx
| witnesses: %Witness{witness | vkey_witness: MapSet.to_list(new_vkey_witness)}
}
end
def sign_tx(%Transaction{} = tx, signer), do: sign_tx(tx, [signer])
@doc """
Signs the transaction using a raw extended private key.
This is useful when you have the raw private key bytes (e.g. from a derived child key or a generated keypair)
and need to sign the transaction. The key can be a payment key or a stake key.
## Examples
# Basic Usage with Hex String
iex> sign_tx_with_raw_extended_key(tx, "5820...")
%Sutra.Cardano.Transaction{}
# Signing with derived keys from Sutra.Crypto.Key
# `derive_child/3` returns an `%ExtendedKey{}` containing both payment and stake keys.
iex> root_key = Sutra.Crypto.Key.generate_root_key("seed phrase ...")
iex> {:ok, %ExtendedKey{} = key} = Sutra.Crypto.Key.derive_child(root_key, 0, 0)
# Interact with the keys directly
iex> tx = sign_tx_with_raw_extended_key(tx, key.payment_key)
iex> tx = sign_tx_with_raw_extended_key(tx, key.stake_key)
"""
def sign_tx_with_raw_extended_key(
%Transaction{witnesses: %Witness{vkey_witness: vkey_witness} = witness} = tx,
raw_extended_key
)
when is_binary(raw_extended_key) do
tx_hash = Transaction.tx_id(tx) |> Base.decode16!(case: :mixed)
new_vkey_witness =
MapSet.new(vkey_witness)
|> MapSet.put(%VkeyWitness{
vkey: Key.public_key(raw_extended_key),
signature: Key.sign(raw_extended_key, tx_hash)
})
%Transaction{
tx
| witnesses: %Witness{witness | vkey_witness: MapSet.to_list(new_vkey_witness)}
}
end
@doc """
Submits the signed transaction to the blockchain.
If no provider is specified, it uses the globally configured submitter.
## Examples
iex> submit_tx(signed_tx)
{:ok, "tx_hash_hex"}
iex> submit_tx(signed_tx, provider)
{:ok, "tx_hash_hex"}
"""
def submit_tx(%Transaction{} = signed_tx) do
with {:ok, provider} <- Provider.get_submitter() do
submit_tx(signed_tx, provider)
end
end
def submit_tx(%Transaction{} = signed_tx, provider) do
provider.submit_tx(signed_tx)
end
end