defmodule Soroban.Contract.InvokeHostFunction do
@moduledoc """
`InvokeHostFunction` implementation to invoke authorized and not authorized contract functions.
"""
alias Soroban.RPC
alias Soroban.RPC.{SendTransactionResponse, SimulateTransactionResponse}
alias Stellar.{Horizon.Accounts, TxBuild}
alias Stellar.TxBuild.{
Account,
ContractAuth,
HostFunction,
InvokeHostFunction,
SCVal,
SequenceNumber,
Signature
}
@type account :: Account.t()
@type function_args :: list(struct())
@type auth :: String.t() | nil
@type auth_account :: String.t() | nil
@type auth_accounts :: list(binary())
@type invoke_host_function :: InvokeHostFunction.t()
@type function_name :: String.t()
@type contract_id :: binary()
@type source_secret_key :: binary()
@type simulate_response :: {:ok, SimulateTransactionResponse.t()}
@type send_response :: {:ok, SendTransactionResponse.t()}
@type signature :: Signature.t()
@type sequence_number :: SequenceNumber.t()
@type sc_val_list :: list(SCVal.t())
@spec invoke(
contract_id :: contract_id(),
source_secret_key :: source_secret_key(),
function_name :: function_name(),
function_args :: function_args(),
auth_accounts :: auth_accounts()
) :: send_response()
def invoke(
contract_id,
source_secret_key,
function_name,
function_args,
auth_accounts \\ []
) do
with {public_key, _secret} = keypair <- Stellar.KeyPair.from_secret_seed(source_secret_key),
{:ok, seq_num} <- Accounts.fetch_next_sequence_number(public_key),
{:ok, function_args} <- convert_to_sc_val(function_args),
signature <- Signature.new(keypair),
source_account <- Account.new(public_key),
sequence_number <- SequenceNumber.new(seq_num),
auth_account <- Enum.at(auth_accounts, 0) do
invoke_host_function_op = create_host_function_op(contract_id, function_name, function_args)
invoke_host_function_op
|> simulate(source_account, sequence_number)
|> send_transaction(
source_account,
sequence_number,
signature,
auth_account,
invoke_host_function_op
)
end
end
@spec simulate(
invoke_host_function_op :: invoke_host_function(),
source_account :: account(),
sequence_number :: sequence_number()
) :: simulate_response()
defp simulate(
invoke_host_function_op,
source_account,
sequence_number
) do
{:ok, envelop_xdr} =
source_account
|> TxBuild.new(sequence_number: sequence_number)
|> TxBuild.add_operation(invoke_host_function_op)
|> TxBuild.envelope()
RPC.simulate_transaction(envelop_xdr)
end
@spec send_transaction(
simulate_response :: simulate_response(),
source_account :: account(),
sequence_number :: sequence_number(),
signature :: signature(),
auth_account :: auth_account(),
invoke_host_function_op :: invoke_host_function()
) :: send_response() | simulate_response()
defp send_transaction(
{:ok, %SimulateTransactionResponse{results: [%{footprint: footprint, auth: auth}]}},
source_account,
sequence_number,
signature,
auth_account,
invoke_host_function_op
) do
invoke_host_function_op =
set_invoke_host_function_params(invoke_host_function_op, footprint, auth, auth_account)
{:ok, envelope_xdr} =
source_account
|> TxBuild.new(sequence_number: sequence_number)
|> TxBuild.add_operation(invoke_host_function_op)
|> TxBuild.sign(signature)
|> TxBuild.envelope()
RPC.send_transaction(envelope_xdr)
end
defp send_transaction(
{:ok, %SimulateTransactionResponse{}} = response,
_source_account,
_sequence_number,
_signature,
_auth_account,
_invoke_host_function_op
),
do: response
@spec create_host_function_op(
contract_id :: contract_id(),
function_name :: function_name(),
function_args :: function_args()
) :: invoke_host_function()
defp create_host_function_op(contract_id, function_name, function_args) do
function =
HostFunction.new(
type: :invoke,
contract_id: contract_id,
function_name: function_name,
args: function_args
)
InvokeHostFunction.new(function: function)
end
@spec set_invoke_host_function_params(
invoke_host_function :: invoke_host_function(),
footprint :: String.t(),
auth :: auth(),
auth_account :: auth_account()
) :: invoke_host_function() | {:error, :required_auth}
defp set_invoke_host_function_params(invoke_host_function_op, footprint, [auth], nil) do
invoke_host_function_op
|> InvokeHostFunction.set_footprint(footprint)
|> InvokeHostFunction.set_contract_auth(auth)
end
defp set_invoke_host_function_params(invoke_host_function_op, footprint, [auth], auth_account) do
authorization = ContractAuth.sign_xdr(auth, auth_account)
invoke_host_function_op
|> InvokeHostFunction.set_footprint(footprint)
|> InvokeHostFunction.set_contract_auth(authorization)
end
defp set_invoke_host_function_params(invoke_host_function_op, footprint, nil, _auth_account),
do: InvokeHostFunction.set_footprint(invoke_host_function_op, footprint)
@spec convert_to_sc_val(function_args :: function_args()) :: {:ok, sc_val_list()}
defp convert_to_sc_val(function_args) do
sc_vals = Enum.map(function_args, fn %{__struct__: struct} = arg -> struct.to_sc_val(arg) end)
{:ok, sc_vals}
end
end