From f217eab4d1b602c5540ca9b1e710f2d6eab39dca Mon Sep 17 00:00:00 2001 From: Awea Date: Thu, 16 May 2024 18:47:21 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20estimate=20a=20transaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Working but need refactoring, split function to test the estimate part and probably replace run_operation by preapply_operation since it should be more reliable --- lib/fee.ex | 87 +++++++++++++++++++ lib/rpc.ex | 207 +++++++++++++++++++++++++++++++--------------- test/rpc_test.exs | 48 +++++++---- 3 files changed, 260 insertions(+), 82 deletions(-) create mode 100644 lib/fee.ex diff --git a/lib/fee.ex b/lib/fee.ex new file mode 100644 index 0000000..447e12e --- /dev/null +++ b/lib/fee.ex @@ -0,0 +1,87 @@ +defmodule Tezex.Fee do + alias Tezex.ForgeOperation + + @default_transaction_gas_limit 1451 + @default_transaction_storage_limit 257 + @hard_gas_limit_per_operation 1_040_000 + @hard_storage_limit_per_operation 60000 + @minimal_fees 100 + @minimal_mutez_per_byte 1 + @minimal_nanotez_per_gas_unit 100 + @reserve 10 + + # size of serialized branch and signature + safe reserve + @extra_size 32 + 64 + + def extra_size, do: @extra_size + + @default_gas_reserve 100 + + def default_gas_reserve, do: @default_gas_reserve + + @spec calculate_fee(map(), pos_integer(), + extra_size: pos_integer(), + minimal_nanotez_per_gas_unit: pos_integer() + ) :: pos_integer() + def calculate_fee(content, consumed_gas, opts \\ []) do + extra_size = Keyword.get(opts, :extra_size, @extra_size) + + minimal_nanotez_per_gas_unit = + Keyword.get(opts, :minimal_nanotez_per_gas_unit, @minimal_nanotez_per_gas_unit) + + size = String.length(ForgeOperation.operation(content)) + extra_size + + fee = + @minimal_fees + @minimal_mutez_per_byte * size + + div(minimal_nanotez_per_gas_unit * consumed_gas, 1000) + + fee + @reserve + end + + # Voir pour utiliser hard_gas_limit_per_content aussi ? + def default_gas_limit(content) do + case content["kind"] do + "reveal" -> + case content["source"] do + "tz1" <> _ -> 176 + "tz2" <> _ -> 162 + "tz3" <> _ -> 1101 + "tz4" <> _ -> 1681 + end + + "delegation" -> + 1000 + + kind + when kind in ~w(origination register_global_constant transfer_ticket smart_rollup_add_messages smart_rollup_execute_outbox_message) -> + @hard_gas_limit_per_operation + + "transaction" -> + if String.starts_with?(content["destination"], "KT"), + do: @hard_gas_limit_per_operation, + else: @default_transaction_gas_limit + end + end + + def default_storage_limit(content) do + case content["kind"] do + "reveal" -> + 0 + + "delegation" -> + 0 + + "origination" -> + @hard_storage_limit_per_operation + + "transaction" -> + if String.starts_with?(content["destination"], "KT"), + do: @hard_storage_limit_per_operation, + else: @default_transaction_storage_limit + + kind + when kind in ~w(register_global_constant transfer_ticket smart_rollup_add_messages smart_rollup_execute_outbox_message) -> + @hard_storage_limit_per_operation + end + end +end diff --git a/lib/rpc.ex b/lib/rpc.ex index 99b8e10..655b4e5 100644 --- a/lib/rpc.ex +++ b/lib/rpc.ex @@ -2,6 +2,7 @@ defmodule Tezex.Rpc do alias Tezex.Crypto alias Tezex.ForgeOperation alias Tezex.Rpc + alias Tezex.Fee @type t() :: %__MODULE__{ endpoint: binary(), @@ -12,95 +13,147 @@ defmodule Tezex.Rpc do defstruct [:endpoint, chain_id: "main", headers: [], opts: []] - # Various constants for the Tezos platforms. Fees are expressed in µtz unless otherwise noted, storage unit is bytes. - # @operation_group_watermark <<3>> - - # @default_simple_transaction_fee 1420 - # @default_transaction_storage_limit 496 - # @default_transaction_gas_limit 10600 - - # @default_delegation_fee 1258 - # @default_delegation_storage_limit 0 - # @default_delegation_gas_limit 1100 - - # @default_key_reveal_fee 1270 - # @default_key_reveal_storage_limit 0 - # @default_key_reveal_gas_limit 1100 - - # @p005_manager_contract_withdrawal_gas_limit 26283 - # @p005_manager_contract_deposit_gas_limit 15285 - # @p005_manager_contract_withdrawal_storage_limit 496 - - # @p001_storage_rate 1_000_000 / 1000 - # @p007_storage_rate 250_000 / 1000 - # @storage_rate @p007_storage_rate - - # @base_operation_fee 100 - - # @p009_block_gas_cap 10_400_000 - # @p010_block_gas_cap 5_200_000 - # @p016_block_gas_cap 2_600_000 - # @p007_operation_gas_cap 1_040_000 - # @operation_gas_cap @p007_operation_gas_cap - # @block_gas_cap @p016_block_gas_cap + def prepare_operation(contents, wallet_address, counter, branch) do + contents = + contents + |> Enum.with_index(counter) + |> Enum.map(fn {content, counter} -> + Map.merge(content, %{ + "counter" => Integer.to_string(counter), + "source" => wallet_address, + "gas_limit" => Integer.to_string(Fee.default_gas_limit(content)), + "storage_limit" => Integer.to_string(Fee.default_storage_limit(content)), + "fee" => "0" + }) + end) - # @p010_operation_storage_cap 60_000 - # @p011_operation_storage_cap 32_768 - # @operation_storage_cap @p011_operation_storage_cap + %{ + "branch" => branch, + "contents" => contents + } + end - # @empty_account_storage_burn 257 + def send_operation(%Rpc{} = rpc, contents, wallet_address, encoded_private_key, opts \\ []) do + offset = Keyword.get(opts, :offset, 0) + storage_limit = Keyword.get(opts, :storage_limit) - # @default_baker_vig 200 + {:ok, block_head} = get_block_at_offset(rpc, offset) + branch = binary_part(block_head["hash"], 0, 51) - # @gas_limit_padding 1000 - # @storage_limit_padding 25 + {:ok, %{"protocol" => protocol}} = get(rpc, "/blocks/#{branch}/protocols") - # @head_branch_offset 54 - # @max_branch_offset 64 + counter = get_next_counter_for_account(rpc, wallet_address) + operation = prepare_operation(contents, wallet_address, counter, branch) - # Outbound operation queue timeout in seconds. After this period, TezosOperationQueue will attempt to submit the transactions currently in queue. - # @default_batch_delay 25 + forged_operation = ForgeOperation.operation_group(operation) - # @p009_block_time 60 - # @p010_block_time 30 - # @default_block_time @p010_block_time + signature = Crypto.sign_operation(encoded_private_key, forged_operation) - # @genesis_block_time ~U[2018-06-30 10:07:32.000Z] + {:ok, [%{"contents" => operation_result}]} = + preapply_operation(rpc, operation, signature, protocol) - def send_operation(%Rpc{} = rpc, contents, wallet_address, encoded_private_key, offset \\ 0) do - {:ok, block_head} = get_block_at_offset(rpc, offset) - branch = binary_part(block_head["hash"], 0, 51) + applied? = + Enum.all?(operation_result, &(&1["metadata"]["operation_result"]["status"] == "applied")) - counter = get_next_counter_for_account(rpc, wallet_address) - payload = prepare_operation(contents, wallet_address, encoded_private_key, counter, branch) + unless applied? do + raise inspect(Enum.map(operation_result, & &1["metadata"]["operation_result"]["errors"])) + end - inject_operation(rpc, payload) - end + number_contents = length(operation_result) - def prepare_operation(contents, wallet_address, encoded_private_key, counter, branch) do contents = - contents - |> Enum.with_index(counter) - |> Enum.map(fn {operation, counter} -> - Map.merge(operation, %{ - "counter" => Integer.to_string(counter), - "source" => wallet_address - }) + Enum.map(operation_result, fn content -> + if validation_passes(content["kind"]) == 3 do + consumed_milligas = + case content["metadata"]["operation_result"]["consumed_milligas"] do + nil -> 0 + v -> String.to_integer(v) + end + + gas_limit_new = ceil(consumed_milligas / 1000) + + gas_limit_new = + if content["kind"] in ~w(origination transaction) do + gas_limit_new + Fee.default_gas_reserve() + else + gas_limit_new + end + + storage_limit_new = + if is_nil(storage_limit) do + paid_storage_size_diff = + case content["metadata"]["operation_result"]["paid_storage_size_diff"] do + nil -> 0 + v -> String.to_integer(v) + end + + burned = + case content["metadata"]["operation_result"]["allocated_destination_contract"] || + content["metadata"]["operation_result"]["originated_contracts"] do + nil -> 0 + _ -> 257 + end + + paid_storage_size_diff + burned + else + div(storage_limit, number_contents) + end + + content = Map.drop(content, ~w(metadata)) + + fee = + Fee.calculate_fee(content, gas_limit_new, + extra_size: 1 + div(Fee.extra_size(), number_contents) + ) + + %{ + content + | "gas_limit" => Integer.to_string(gas_limit_new), + "storage_limit" => Integer.to_string(storage_limit_new), + "fee" => Integer.to_string(fee) + } + else + content + end end) - operation = %{ - "branch" => branch, - "contents" => contents - } + operation = %{operation | "contents" => contents} forged_operation = ForgeOperation.operation_group(operation) + signature = Crypto.sign_operation(encoded_private_key, forged_operation) + payload_signature = - Crypto.sign_operation(encoded_private_key, forged_operation) + signature |> Crypto.decode_signature!() |> Base.encode16(case: :lower) - forged_operation <> payload_signature + payload = forged_operation <> payload_signature + + inject_operation(rpc, payload) + end + + # NOTE: Explaination: https://pytezos.baking-bad.org/tutorials/02.html#operation-group + defp validation_passes(kind) do + case kind do + "failing_noop" -> -1 + "endorsement" -> 0 + "endorsement_with_slot" -> 0 + "proposals" -> 1 + "ballot" -> 1 + "seed_nonce_revelation" -> 2 + "double_endorsement_evidence" -> 2 + "double_baking_evidence" -> 2 + "activate_account" -> 2 + "reveal" -> 3 + "transaction" -> 3 + "origination" -> 3 + "delegation" -> 3 + "register_global_constant" -> 3 + "transfer_ticket" -> 3 + "smart_rollup_add_messages" -> 3 + "smart_rollup_execute_outbox_message" -> 3 + end end def get_counter_for_account(%Rpc{} = rpc, address) do @@ -127,6 +180,15 @@ defmodule Tezex.Rpc do get(rpc, "/blocks/#{head["header"]["level"] - offset}") end + @doc """ + Simulate the application of the operations with the context of the given block and return the result of each operation application. + """ + def preapply_operation(%Rpc{} = rpc, operation, signature, protocol) do + payload = [Map.merge(operation, %{"signature" => signature, "protocol" => protocol})] + + post(rpc, "/chains/#{rpc.chain_id}/blocks/head/helpers/preapply/operations", payload) + end + def inject_operation(%Rpc{} = rpc, payload) do post(rpc, "/injection/operation", payload) end @@ -149,7 +211,16 @@ defmodule Tezex.Rpc do defp post(%Rpc{} = rpc, path, body) do url = URI.parse(rpc.endpoint) - |> URI.append_query(URI.encode_query(%{"chain" => rpc.chain_id})) + + url = + if String.starts_with?(path, "/chains") do + url + else + URI.append_query(url, URI.encode_query(%{"chain" => rpc.chain_id})) + end + + url = + url |> URI.append_path(path) |> URI.to_string() diff --git a/test/rpc_test.exs b/test/rpc_test.exs index 9b69162..5c0fcf6 100644 --- a/test/rpc_test.exs +++ b/test/rpc_test.exs @@ -114,23 +114,24 @@ defmodule Tezex.RpcTest do counter = 1123 branch = "BLWdshvgEYbtUaABnmqkMuyMezpfsu36DEJPDJN63CW3TFuk7bP" - result = - Enum.join( - [ - "6940b2318870c4b84862aef187c1bbcd4138170459e85092fe34be5d179f40ac6c00980d7c", - "ebb50d4b83a4e3307b3ca1b40ebe8f71abdd02e308ba0100640000b75f0c30536bee108b06", - "8d90b151ce846aca11b10054b408a1f2168d72618c3d4c01cd3ea40d7255cae1e52f7bfbbe", - "cfa8a0d0c42a3a958d8f9a457c3d0369fcf9d3c49e6ade9d239548b19ea9c867f694531e7e", - "04" - ], - "" - ) - - assert result == + assert %{ + "branch" => "BLWdshvgEYbtUaABnmqkMuyMezpfsu36DEJPDJN63CW3TFuk7bP", + "contents" => [ + %{ + "amount" => "100", + "counter" => "1123", + "destination" => "tz1cMcDFLgFe2picQbo4DY1i6mZJiVhPCu5B", + "fee" => "0", + "gas_limit" => "1451", + "kind" => "transaction", + "source" => "tz1ZW1ZSN4ruXYc3nCon8EaTXp1t3tKWb9Ew", + "storage_limit" => "257" + } + ] + } == Rpc.prepare_operation( contents, @ghostnet_1_address, - @ghostnet_1_pkey, counter, branch ) @@ -156,4 +157,23 @@ defmodule Tezex.RpcTest do assert is_binary(operation_id) end + + test "raise" do + rpc = %Rpc{endpoint: @endpoint} + + contents = [ + %{ + "amount" => "1000000000", + "destination" => @ghostnet_2_address, + "fee" => "349", + "gas_limit" => "186", + "kind" => "transaction", + "storage_limit" => "0" + } + ] + + assert_raise RuntimeError, fn -> + Rpc.send_operation(rpc, contents, @ghostnet_1_address, @ghostnet_1_pkey) + end + end end