Skip to content

Commit

Permalink
docs(rpc): spec and document
Browse files Browse the repository at this point in the history
  • Loading branch information
vhf committed May 22, 2024
1 parent 9a64a18 commit fdf860c
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 46 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

- [BREAKING]: `Tezex.Micheline` now only takes care of `PACK`ing and `UNPACK`ing, forging and unforging is now in `Tezex.Forge`
- [BREAKING]: `Tezex.Micheline.Zarith` is replaced with `Tezex.Zarith`
- [crypto] add a pure Elixir [HMAC-DRBG](https://hexdocs.pm/tezex/Tezex.Crypto.HMACDRBG.html) implementation
- [crypto] implement message/operations [signing](https://hexdocs.pm/tezex/Tezex.Crypto.html#sign_message/2)
- implement (un)forging [micheline](https://hexdocs.pm/tezex/Tezex.Forge.html), [operations/operation groups](https://hexdocs.pm/tezex/Tezex.ForgeOperation.html), [calculating gas/storage/fees](https://hexdocs.pm/tezex/Tezex.Fee.html) and [sending them to Tezos RPC nodes](https://hexdocs.pm/tezex/Tezex.Rpc.html).

## v1.2.0

Expand Down
133 changes: 89 additions & 44 deletions lib/rpc.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
defmodule Tezex.Rpc do
@moduledoc """
Send transactions to the Tezos network.
`send_operation/4` is the main function, see tests for usage.
"""

alias Tezex.Crypto
alias Tezex.ForgeOperation
alias Tezex.Rpc
Expand All @@ -10,9 +16,12 @@ defmodule Tezex.Rpc do
headers: Finch.Request.headers(),
opts: Finch.request_opts()
}
@type encoded_private_key() :: <<_::32, _::_*8>>
@type op() :: map()

defstruct [:endpoint, chain_id: "main", headers: [], opts: []]

@spec prepare_operation(op(), nonempty_binary(), integer(), nonempty_binary()) :: op()
def prepare_operation(contents, wallet_address, counter, branch) do
contents =
contents
Expand All @@ -33,20 +42,26 @@ defmodule Tezex.Rpc do
}
end

def fill_operation_fee(operation, operation_result, opts \\ []) do
@spec fill_operation_fee(op(), list(op()), keyword()) :: op()
@spec fill_operation_fee(op(), list(op())) :: op()
def fill_operation_fee(operation, preapplied_operations, opts \\ []) do
storage_limit = Keyword.get(opts, :storage_limit)

applied? =
Enum.all?(operation_result, &(&1["metadata"]["operation_result"]["status"] == "applied"))
Enum.all?(
preapplied_operations,
&(&1["metadata"]["operation_result"]["status"] == "applied")
)

unless applied? do
raise inspect(Enum.map(operation_result, & &1["metadata"]["operation_result"]["errors"]))
errors = Enum.map(preapplied_operations, & &1["metadata"]["operation_result"]["errors"])
raise inspect(errors)
end

number_contents = length(operation_result)
number_contents = length(preapplied_operations)

contents =
Enum.map(operation_result, fn content ->
Enum.map(preapplied_operations, fn content ->
if validation_passes(content["kind"]) == 3 do
consumed_milligas =
case content["metadata"]["operation_result"]["consumed_milligas"] do
Expand Down Expand Up @@ -102,6 +117,11 @@ defmodule Tezex.Rpc do
%{operation | "contents" => contents}
end

@doc """
Send an operation to a Tezos RPC node.
"""
@spec send_operation(t(), op(), nonempty_binary(), encoded_private_key()) ::
{:ok, any()} | {:error, Finch.Error.t()} | {:error, Jason.DecodeError.t()}
def send_operation(%Rpc{} = rpc, contents, wallet_address, encoded_private_key, opts \\ []) do
offset = Keyword.get(opts, :offset, 0)

Expand All @@ -113,17 +133,20 @@ defmodule Tezex.Rpc do
counter = get_next_counter_for_account(rpc, wallet_address)
operation = prepare_operation(contents, wallet_address, counter, branch)

{:ok, [%{"contents" => operation_result}]} =
{:ok, [%{"contents" => preapplied_operations}]} =
preapply_operation(rpc, operation, encoded_private_key, protocol)

operation = fill_operation_fee(operation, operation_result, opts)

payload = injection_payload(operation, encoded_private_key)
operation = fill_operation_fee(operation, preapplied_operations, opts)
payload = forge_and_sign_operation(operation, encoded_private_key)

inject_operation(rpc, payload)
end

def injection_payload(operation, encoded_private_key) do
@doc """
Sign the forged operation and returns the forged operation+signature payload to be injected.
"""
@spec forge_and_sign_operation(op(), encoded_private_key()) :: nonempty_binary()
def forge_and_sign_operation(operation, encoded_private_key) do
forged_operation = ForgeOperation.operation_group(operation)

signature = Crypto.sign_operation(encoded_private_key, forged_operation)
Expand All @@ -136,44 +159,48 @@ defmodule Tezex.Rpc do
forged_operation <> payload_signature
end

# NOTE: Explanation: 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
@spec preapply_operation(t(), map(), encoded_private_key(), any()) ::
{:ok, any()} | {:error, Finch.Error.t()} | {:error, Jason.DecodeError.t()}
@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, encoded_private_key, protocol) do
forged_operation = ForgeOperation.operation_group(operation)
signature = Crypto.sign_operation(encoded_private_key, forged_operation)
payload = [Map.merge(operation, %{"signature" => signature, "protocol" => protocol})]
post(rpc, "/blocks/head/helpers/preapply/operations", payload)
end

@spec get_counter_for_account(t(), nonempty_binary()) ::
integer() | {:error, :not_integer} | {:error, Finch.Error.t()}
def get_counter_for_account(%Rpc{} = rpc, address) do
with {:ok, n} <- get(rpc, "/blocks/head/context/contracts/#{address}/counter"),
{n, ""} <- Integer.parse(n) do
n
else
{:error, _} = err -> err
:error -> {:error, :not_integer}
{_, rest} when is_binary(rest) -> {:error, :not_integer}
end
end

def get_next_counter_for_account(%Rpc{} = rpc, address) do
get_counter_for_account(rpc, address) + 1
end

@spec get_block(t()) ::
{:error, %{:__exception__ => true, :__struct__ => atom(), optional(atom()) => any()}}
| {:ok, any()}
@spec get_block(t(), any()) ::
{:error, %{:__exception__ => true, :__struct__ => atom(), optional(atom()) => any()}}
| {:ok, any()}
def get_block(%Rpc{} = rpc, hash \\ "head") do
get(rpc, "/blocks/#{hash}")
end

@spec get_block_at_offset(t(), number()) ::
{:error, %{:__exception__ => true, :__struct__ => atom(), optional(atom()) => any()}}
| {:ok, any()}
def get_block_at_offset(%Rpc{} = rpc, offset) do
if offset <= 0 do
get_block(rpc)
Expand All @@ -183,23 +210,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, encoded_private_key, protocol) do
forged_operation = ForgeOperation.operation_group(operation)

signature = Crypto.sign_operation(encoded_private_key, forged_operation)

payload = [Map.merge(operation, %{"signature" => signature, "protocol" => protocol})]

post(rpc, "/blocks/head/helpers/preapply/operations", payload)
end

@spec inject_operation(t(), any()) ::
{:error, %{:__exception__ => true, :__struct__ => atom(), optional(atom()) => any()}}
| {:ok, any()}
def inject_operation(%Rpc{} = rpc, payload) do
post(rpc, "/injection/operation", payload)
end

@spec get(Tezex.Rpc.t(), nonempty_binary()) ::
{:ok, any()} | {:error, Finch.Error.t()} | {:error, Jason.DecodeError.t()}
defp get(%Rpc{} = rpc, path) do
url =
URI.parse(rpc.endpoint)
Expand All @@ -215,6 +234,8 @@ defmodule Tezex.Rpc do
end
end

@spec post(Tezex.Rpc.t(), nonempty_binary(), any()) ::
{:ok, any()} | {:error, Finch.Error.t()} | {:error, Jason.DecodeError.t()}
defp post(%Rpc{} = rpc, path, body) do
url =
URI.parse(rpc.endpoint)
Expand All @@ -240,4 +261,28 @@ defmodule Tezex.Rpc do
{:error, _} = err -> err
end
end

# NOTE: Explanation: https://pytezos.baking-bad.org/tutorials/02.html#operation-group
@spec validation_passes(nonempty_binary()) :: -1 | 0 | 1 | 2 | 3
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
end
4 changes: 2 additions & 2 deletions test/rpc_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ defmodule Tezex.RpcTest do
)
end

test "injection_payload/2" do
test "forge_and_sign_operation/2" do
operation = %{
"branch" => "BLWdshvgEYbtUaABnmqkMuyMezpfsu36DEJPDJN63CW3TFuk7bP",
"contents" => [
Expand All @@ -74,7 +74,7 @@ defmodule Tezex.RpcTest do
result =
"6940b2318870c4b84862aef187c1bbcd4138170459e85092fe34be5d179f40ac6c00980d7cebb50d4b83a4e3307b3ca1b40ebe8f71ab00e308ab0b8102640000b75f0c30536bee108b068d90b151ce846aca11b1009bc207291f08c7117ea3a341328a036ce7bd5da996d9148cc491f96c3097748d7adcdb60599504ae5227aea4a6e69d536baaf96ae2bbb6c261a69d000b323f0a"

assert result == Rpc.injection_payload(operation, @ghostnet_1_pkey)
assert result == Rpc.forge_and_sign_operation(operation, @ghostnet_1_pkey)
end

describe "fill_operation_fee/3" do
Expand Down

0 comments on commit fdf860c

Please sign in to comment.