defmodule Stellar.TxBuild.Default do @moduledoc """ Default TxBuild implementation. """ alias StellarBase.XDR.{TransactionExt, SorobanTransactionData} alias Stellar.{TxBuild, Network} alias Stellar.TxBuild.{ Account, Memo, BaseFee, Operation, Operations, SequenceNumber, Signature, Preconditions, TimeBounds, Transaction, TransactionEnvelope, TransactionSignature } @behaviour Stellar.TxBuild.Spec @preconditions_keys [ :time_bounds, :ledger_bounds, :min_seq_num, :min_seq_age, :min_seq_ledger_gap, :extra_signers ] @impl true def new(%Account{} = source_account, opts) do network_passphrase = Keyword.get(opts, :network_passphrase, Network.testnet_passphrase()) sequence_number = Keyword.get(opts, :sequence_number, SequenceNumber.new()) base_fee = Keyword.get(opts, :base_fee, BaseFee.new()) memo = Keyword.get(opts, :memo, Memo.new()) operations = Keyword.get(opts, :operations, Operations.new()) preconditions = opts |> Keyword.take(@preconditions_keys) |> Preconditions.new() case Transaction.new( source_account: source_account, sequence_number: sequence_number, base_fee: base_fee, preconditions: preconditions, memo: memo, operations: operations ) do %Transaction{} = transaction -> {:ok, %TxBuild{ tx: transaction, signatures: [], tx_envelope: nil, network_passphrase: network_passphrase }} error -> error end end def new(_source_account, _opts), do: {:error, :invalid_source_account} @impl true def set_network_passphrase({:ok, %TxBuild{} = tx_build}, network_passphrase), do: {:ok, %{tx_build | network_passphrase: network_passphrase}} @impl true def add_memo({:ok, %TxBuild{tx: tx} = tx_build}, %Memo{} = memo) do transaction = %{tx | memo: memo} {:ok, %{tx_build | tx: transaction}} end def add_memo({:ok, %TxBuild{}}, _memo), do: {:error, :invalid_memo} def add_memo(error, _memo), do: error @impl true def set_time_bounds( {:ok, %TxBuild{tx: %{preconditions: %{type: :none} = preconditions} = tx} = tx_build}, %TimeBounds{} = time_bounds ) do preconditions = %{preconditions | type: :precond_time, preconditions: time_bounds} transaction = %{tx | preconditions: preconditions} {:ok, %{tx_build | tx: transaction}} end def set_time_bounds( {:ok, %TxBuild{tx: %{preconditions: %{type: :precond_time} = preconditions} = tx} = tx_build}, %TimeBounds{} = time_bounds ) do preconditions = %{preconditions | preconditions: time_bounds} transaction = %{tx | preconditions: preconditions} {:ok, %{tx_build | tx: transaction}} end def set_time_bounds( {:ok, %TxBuild{ tx: %{ preconditions: %{type: :precond_v2, preconditions: inner_preconditions} = preconditions } = tx } = tx_build}, %TimeBounds{} = time_bounds ) do inner_preconditions = Keyword.put(inner_preconditions, :time_bounds, time_bounds) preconditions = %{preconditions | preconditions: inner_preconditions} transaction = %{tx | preconditions: preconditions} {:ok, %{tx_build | tx: transaction}} end def set_time_bounds({:ok, %TxBuild{}}, _time_bounds), do: {:error, :invalid_time_bounds} def set_time_bounds(error, _time_bounds), do: error @impl true def set_preconditions({:ok, %TxBuild{tx: tx} = tx_build}, %Preconditions{} = preconditions) do transaction = %{tx | preconditions: preconditions} {:ok, %{tx_build | tx: transaction}} end def set_preconditions({:ok, %TxBuild{}}, _preconditions), do: {:error, :invalid_preconditions} def set_preconditions(error, _preconditions), do: error @impl true def set_base_fee({:ok, %TxBuild{tx: tx} = tx_build}, %BaseFee{} = base_fee) do %Transaction{operations: %Operations{count: ops_count}} = tx transaction = %{tx | base_fee: BaseFee.increment(base_fee, ops_count)} {:ok, %{tx_build | tx: transaction}} end def set_base_fee({:ok, %TxBuild{}}, _base_fee), do: {:error, :invalid_base_fee} def set_base_fee(error, _base_fee), do: error @impl true def set_sequence_number({:ok, %TxBuild{tx: tx} = tx_build}, %SequenceNumber{} = seq_num) do transaction = %{tx | sequence_number: seq_num} {:ok, %{tx_build | tx: transaction}} end def set_sequence_number({:ok, %TxBuild{}}, _seq_num), do: {:error, :invalid_sequence_number} def set_sequence_number(error, _seq_num), do: error @impl true def add_operations({:ok, %TxBuild{}} = tx_build, []), do: tx_build def add_operations({:ok, %TxBuild{}} = tx_build, [operation | operations]) do tx_build |> add_operation(operation) |> add_operations(operations) end def add_operations({:ok, %TxBuild{}}, _operations), do: {:error, :invalid_operation} def add_operations(error, _operations), do: error @impl true def add_operation({:ok, %TxBuild{tx: tx} = tx_build}, operation_body) do with %Operation{} = operation <- Operation.new(operation_body), %Operations{} = operations <- Operations.add(tx.operations, operation) do transaction = %{tx | operations: operations, base_fee: BaseFee.increment(tx.base_fee)} {:ok, %{tx_build | tx: transaction}} end end def add_operation(error, _operation), do: error @impl true def set_soroban_data({:ok, %TxBuild{}} = tx_build, soroban_data) when is_binary(soroban_data) do case check_soroban_data(soroban_data) do {:ok, soroban_tx_data} -> set_soroban_data(tx_build, soroban_tx_data) error -> error end end def set_soroban_data( {:ok, %TxBuild{tx: tx} = tx_build}, %SorobanTransactionData{} = soroban_tx_data ) do ext = TransactionExt.new(soroban_tx_data, 1) transaction = %{tx | ext: ext} {:ok, %{tx_build | tx: transaction}} end def set_soroban_data({:ok, %TxBuild{}}, _soroban_tx_data), do: {:error, :invalid_soroban_data} def set_soroban_data(error, _soroban_data), do: error @impl true def sign({:ok, %TxBuild{}} = tx_build, []), do: tx_build def sign({:ok, %TxBuild{}} = tx_build, [%Signature{} = signature | signatures]) do tx_build |> sign(signature) |> sign(signatures) end def sign({:ok, %TxBuild{signatures: signatures} = tx_build}, %Signature{} = signature) do {:ok, %{tx_build | signatures: signatures ++ [signature]}} end def sign({:ok, %TxBuild{}}, _signature), do: {:error, :invalid_signature} def sign(error, _signature), do: error @impl true def build( {:ok, %TxBuild{tx: tx, signatures: signatures, network_passphrase: network_passphrase} = tx_build} ) do tx_envelope = TransactionEnvelope.new( tx: tx, signatures: signatures, network_passphrase: network_passphrase ) {:ok, %{tx_build | tx_envelope: tx_envelope}} end def build(error), do: error @impl true def envelope( {:ok, %TxBuild{tx: tx, signatures: signatures, network_passphrase: network_passphrase}} ) do [tx: tx, signatures: signatures, network_passphrase: network_passphrase] |> TransactionEnvelope.new() |> TransactionEnvelope.to_xdr() |> TransactionEnvelope.to_base64() |> (&{:ok, &1}).() end def envelope(error), do: error @impl true def sign_envelope(tx_base64, [], _network_passphrase), do: tx_base64 def sign_envelope({:ok, tx_base64}, signatures, network_passphrase), do: sign_envelope(tx_base64, signatures, network_passphrase) def sign_envelope(tx_base64, [%Signature{} = signature | signatures], network_passphrase) do tx_base64 |> sign_envelope(signature, network_passphrase) |> sign_envelope(signatures, network_passphrase) end def sign_envelope(tx_base64, %Signature{} = signature, network_passphrase) do tx_base64 |> TransactionEnvelope.add_signature(signature, network_passphrase) |> TransactionEnvelope.to_base64() |> (&{:ok, &1}).() end def sign_envelope(_tx_base64, _signature, _network_passphrase), do: {:error, :invalid_signature} @impl true def hash({:ok, %TxBuild{tx: tx, network_passphrase: network_passphrase}}) do tx |> Transaction.to_xdr() |> TransactionSignature.base_signature(network_passphrase) |> Base.encode16(case: :lower) |> (&{:ok, &1}).() end def hash(error), do: error @spec check_soroban_data(soroban_data :: binary()) :: {:ok, SorobanTransactionData.t()} | {:error, atom()} defp check_soroban_data(soroban_data) do with {:ok, raw_soroban_data} <- Base.decode64(soroban_data), {:ok, {%SorobanTransactionData{} = soroban_tx_data, ""}} <- SorobanTransactionData.decode_xdr(raw_soroban_data) do {:ok, soroban_tx_data} else _ -> {:error, :invalid_soroban_data} end end end