Skip to content

Commit

Permalink
🚧 estimate a transaction
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Awea committed May 16, 2024
1 parent 71b0d0b commit f217eab
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 82 deletions.
87 changes: 87 additions & 0 deletions lib/fee.ex
Original file line number Diff line number Diff line change
@@ -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
207 changes: 139 additions & 68 deletions lib/rpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()

Expand Down
Loading

0 comments on commit f217eab

Please sign in to comment.