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 25d9038
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 81 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
216 changes: 149 additions & 67 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,149 @@ 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
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)

# @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
%{
"branch" => branch,
"contents" => contents
}
end

# @p010_operation_storage_cap 60_000
# @p011_operation_storage_cap 32_768
# @operation_storage_cap @p011_operation_storage_cap
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)

# @empty_account_storage_burn 257
{:ok, block_head} = get_block_at_offset(rpc, offset)
branch = binary_part(block_head["hash"], 0, 51)

# @default_baker_vig 200
# {:ok, %{"protocol" => protocol}} = get(rpc, "/blocks/#{branch}/protocols")

# @gas_limit_padding 1000
# @storage_limit_padding 25
counter = get_next_counter_for_account(rpc, wallet_address)
operation = prepare_operation(contents, wallet_address, counter, branch)

# @head_branch_offset 54
# @max_branch_offset 64
forged_operation = ForgeOperation.operation_group(operation)

# Outbound operation queue timeout in seconds. After this period, TezosOperationQueue will attempt to submit the transactions currently in queue.
# @default_batch_delay 25
signature = Crypto.sign_operation(encoded_private_key, forged_operation)

# @p009_block_time 60
# @p010_block_time 30
# @default_block_time @p010_block_time
# 🚧 We should go with preapply_operation since it's more reliable
# {:ok, preapply_result} = preapply_operation(rpc, operation, signature)

# @genesis_block_time ~U[2018-06-30 10:07:32.000Z]
{:ok, %{"contents" => operation_result}} = run_operation(rpc, operation, signature)

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 +182,33 @@ defmodule Tezex.Rpc do
get(rpc, "/blocks/#{head["header"]["level"] - offset}")
end

@doc """
Run an operation with the context of the given block and without signature checks. Return the operation application result, including the consumed gas. This RPC does not support consensus operations.
"""
def run_operation(%Rpc{} = rpc, operation, signature) do
{:ok, head} = get_block(rpc)

payload = %{
"operation" => Map.put(operation, "signature", signature),
"chain_id" => head["chain_id"]
}

post(
rpc,
"/chains/#{rpc.chain_id}/blocks/head/helpers/scripts/run_operation",
payload
)
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/main/blocks/head/helpers/preapply/operations", payload)
end

def inject_operation(%Rpc{} = rpc, payload) do
post(rpc, "/injection/operation", payload)
end
Expand All @@ -149,7 +231,7 @@ 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}))
# |> URI.append_query(URI.encode_query(%{"chain" => rpc.chain_id}))
|> URI.append_path(path)
|> URI.to_string()

Expand Down
Loading

0 comments on commit 25d9038

Please sign in to comment.