defmodule Stellar.TxBuild.Default do
@moduledoc """
Default TxBuild implementation.
"""
alias Stellar.TxBuild
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
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}}
error ->
error
end
end
def new(_source_account, _opts), do: {:error, :invalid_source_account}
@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 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} = tx_build}) do
{:ok, %{tx_build | tx_envelope: TransactionEnvelope.new(tx, signatures)}}
end
def build(error), do: error
@impl true
def envelope({:ok, %TxBuild{tx: tx, signatures: signatures}}) do
tx
|> TransactionEnvelope.new(signatures)
|> TransactionEnvelope.to_xdr()
|> TransactionEnvelope.to_base64()
|> (&{:ok, &1}).()
end
def envelope(error), do: error
@impl true
def sign_envelope(tx_base64, []), do: tx_base64
def sign_envelope({:ok, tx_base64}, signatures), do: sign_envelope(tx_base64, signatures)
def sign_envelope(tx_base64, [%Signature{} = signature | signatures]) do
tx_base64
|> sign_envelope(signature)
|> sign_envelope(signatures)
end
def sign_envelope(tx_base64, %Signature{} = signature) do
tx_base64
|> TransactionEnvelope.add_signature(signature)
|> TransactionEnvelope.to_base64()
|> (&{:ok, &1}).()
end
def sign_envelope(_tx_base64, _signature), do: {:error, :invalid_signature}
@impl true
def hash({:ok, %TxBuild{tx: tx}}) do
tx
|> Transaction.to_xdr()
|> TransactionSignature.base_signature()
|> Base.encode16(case: :lower)
|> (&{:ok, &1}).()
end
def hash(error), do: error
end