From 0175b677b9133096642fcd7058c70ea8fc7411bb Mon Sep 17 00:00:00 2001 From: Edwin Steven Guayacan <80716239+EdwinGuayacan@users.noreply.github.com> Date: Wed, 21 Dec 2022 17:38:19 -0500 Subject: [PATCH] Local endpoint implementation (#177) * Add local endpoint implementation Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> --- README.md | 78 +++++++++++ lib/chainweb/pact.ex | 3 +- lib/chainweb/pact/local.ex | 38 +++++ lib/chainweb/pact/local_request_body.ex | 16 +-- lib/chainweb/pact/spec.ex | 2 +- mix.exs | 10 +- .../chainweb/pact/local_request_body_test.exs | 79 ++++++----- test/chainweb/pact/local_test.exs | 131 ++++++++++++++++++ test/support/fixtures/chainweb/local.json | 30 ++-- 9 files changed, 312 insertions(+), 75 deletions(-) create mode 100644 lib/chainweb/pact/local.ex create mode 100644 test/chainweb/pact/local_test.exs diff --git a/README.md b/README.md index c594f8ab..076e6608 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,84 @@ Chainweb.Pact.send(cmds, network_id: :testnet04, chain_id: 1) }} ``` +### Local endpoint + +Executes a single command on the local server and retrieves the transaction result. Useful with code that queries from blockchain. It does not impact the blockchain when returning transaction results. + +```elixir +Kadena.Chainweb.Pact.local(cmd, network_opts \\ [network_id: :testnet04, chain_id: 0]) +``` + +**Parameters** + +- `cmd`: [PACT command](#pact-commands). +- `network_opts`: Network options. Keyword list with: + + - `network_id` (required): Allowed values: `:testnet04` `mainnet01`. + - `chain_id` (required): Allowed values: integer or string-encoded integer from 0 to 19. + + Defaults to `[network_id: :testnet04, chain_id: 0]` if not specified. + +**Example** + +```elixir +alias Kadena.Chainweb +alias Kadena.Cryptography +alias Kadena.Pact + +{:ok, keypair} = + Cryptography.KeyPair.from_secret_key( + "28834b7a0d6d1f84ae2c2efcb5b1de28122e07e2e4caad04a32988a3c79c547c" + ) + +network_id = :testnet04 + +metadata = + Kadena.Types.MetaData.new( + creation_time: 1_671_462_208, + ttl: 28_800, + gas_limit: 1000, + gas_price: 0.000001, + sender: "k:#{keypair.pub_key}", + chain_id: "1" + ) + +code = "(+ 1 2)" + +cmd = + Pact.ExecCommand.new() + |> Pact.ExecCommand.set_code(code) + |> Pact.ExecCommand.set_metadata(metadata) + |> Pact.ExecCommand.add_keypair(keypair) + |> Pact.ExecCommand.build() + +Chainweb.Pact.local(cmd, network_id: :testnet04, chain_id: 1) + +{:ok, + %Kadena.Chainweb.Pact.LocalResponse{ + continuation: nil, + events: nil, + gas: 5, + logs: "wsATyGqckuIvlm89hhd2j4t6RMkCrcwJe_oeCYr7Th8", + meta_data: %{ + block_height: 2833149, + block_time: 1671577178603103, + prev_block_hash: "7aURwajZ0pBMGEKmOUJ9oLq9MK7QiZeiDPGPb0cXs5c", + public_meta: %{ + chain_id: "1", + creation_time: 1671462208, + gas_limit: 1000, + gas_price: 1.0e-6, + sender: "k:d1a361d721cf81dbc21f676e6897f7e7a336671c0d5d25f87c10933cac6d8cf7", + ttl: 28800 + } + }, + req_key: "8qnotzzhbfe_SSmZcDVQGDpALjQjYqzYYrHc6D-2D_g", + result: %{data: 3, status: "success"}, + tx_id: nil + }} +``` + --- ## Roadmap diff --git a/lib/chainweb/pact.ex b/lib/chainweb/pact.ex index c11b81c0..129d79dc 100644 --- a/lib/chainweb/pact.ex +++ b/lib/chainweb/pact.ex @@ -3,9 +3,10 @@ defmodule Kadena.Chainweb.Pact do Exposes functions to interact with the Pact API endpoints. """ - alias Kadena.Chainweb.Pact.Send + alias Kadena.Chainweb.Pact.{Local, Send} @default_network_opts [network_id: :testnet04, chain_id: 0] defdelegate send(cmds, network_opts \\ @default_network_opts), to: Send, as: :process + defdelegate local(cmds, network_opts \\ @default_network_opts), to: Local, as: :process end diff --git a/lib/chainweb/pact/local.ex b/lib/chainweb/pact/local.ex new file mode 100644 index 00000000..cecb673e --- /dev/null +++ b/lib/chainweb/pact/local.ex @@ -0,0 +1,38 @@ +defmodule Kadena.Chainweb.Pact.Local do + @moduledoc """ + Local endpoint implementation + """ + + alias Kadena.Chainweb.Request + alias Kadena.Chainweb.Pact.{LocalRequestBody, LocalResponse, Spec} + alias Kadena.Types.Command + + @behaviour Spec + + @endpoint "local" + + @type cmd :: Command.t() + @type json :: String.t() + + @impl true + def process(%Command{} = cmd, network_id: network_id, chain_id: chain_id) do + headers = [{"Content-Type", "application/json"}] + body = json_request_body(cmd) + + :post + |> Request.new(pact: [endpoint: @endpoint]) + |> Request.set_chain_id(chain_id) + |> Request.set_network(network_id) + |> Request.add_body(body) + |> Request.add_headers(headers) + |> Request.perform() + |> Request.results(as: LocalResponse) + end + + @spec json_request_body(cmd :: cmd()) :: json() + defp json_request_body(cmd) do + cmd + |> LocalRequestBody.new() + |> LocalRequestBody.to_json!() + end +end diff --git a/lib/chainweb/pact/local_request_body.ex b/lib/chainweb/pact/local_request_body.ex index d2efea97..a98eacf4 100644 --- a/lib/chainweb/pact/local_request_body.ex +++ b/lib/chainweb/pact/local_request_body.ex @@ -7,7 +7,7 @@ defmodule Kadena.Chainweb.Pact.LocalRequestBody do @behaviour Kadena.Chainweb.Pact.Type - @type command :: String.t() + @type command :: Command.t() @type hash :: PactTransactionHash.t() @type sigs :: SignaturesList.t() @type cmd :: String.t() @@ -19,11 +19,8 @@ defmodule Kadena.Chainweb.Pact.LocalRequestBody do defstruct [:hash, :sigs, :cmd] @impl true - def new(args) do - args - |> Command.new() - |> build_local_request_body() - end + def new(%Command{} = cmd), do: build_local_request_body(cmd) + def new(_cmd), do: {:error, [arg: :not_a_command]} @impl true def to_json!(%__MODULE__{hash: hash, sigs: sigs, cmd: cmd}) do @@ -33,17 +30,12 @@ defmodule Kadena.Chainweb.Pact.LocalRequestBody do end end - @spec build_local_request_body(command :: command() | error()) :: t() | error() + @spec build_local_request_body(command :: command()) :: t() defp build_local_request_body(%Command{} = command) do attrs = Map.from_struct(command) struct(%__MODULE__{}, attrs) end - defp build_local_request_body({:error, [command: :not_a_list]}), - do: {:error, [local_request_body: :not_a_list]} - - defp build_local_request_body({:error, reason}), do: {:error, reason} - @spec to_signature_list(signatures :: sigs()) :: {:ok, raw_sigs()} defp to_signature_list(%SignaturesList{signatures: list}) do sigs = Enum.map(list, fn sig -> Map.from_struct(sig) end) diff --git a/lib/chainweb/pact/spec.ex b/lib/chainweb/pact/spec.ex index 4ffa776a..47b38d19 100644 --- a/lib/chainweb/pact/spec.ex +++ b/lib/chainweb/pact/spec.ex @@ -14,7 +14,7 @@ defmodule Kadena.Chainweb.Pact.Spec do alias Kadena.Chainweb.Error alias Kadena.Types.Command - @type data :: list(Command.t()) + @type data :: list(Command.t()) | Command.t() @type error :: {:error, Error.t()} @type chain_id :: 0..19 | String.t() @type network_opts :: [network_id: atom(), chain_id: chain_id()] diff --git a/mix.exs b/mix.exs index e0f5abf6..fecd5118 100644 --- a/mix.exs +++ b/mix.exs @@ -96,6 +96,7 @@ defmodule Kadena.MixProject do Kadena.Chainweb.Client.Spec, Kadena.Chainweb.Network, Kadena.Chainweb.Pact, + Kadena.Chainweb.Pact.Local, Kadena.Chainweb.Pact.Request, Kadena.Chainweb.Pact.Send, Kadena.Chainweb.Pact.Spec, @@ -119,20 +120,15 @@ defmodule Kadena.MixProject do Kadena.Types.Cap, Kadena.Types.CapsList, Kadena.Types.ChainID, - Kadena.Types.ChainwebResponseMetaData, Kadena.Types.Command, Kadena.Types.CommandsList, Kadena.Types.ContPayload, Kadena.Types.EnvData, Kadena.Types.ExecPayload, Kadena.Types.KeyPair, - Kadena.Types.ListenRequestBody, - Kadena.Types.LocalRequestBody, Kadena.Types.MetaData, Kadena.Types.NetworkID, Kadena.Types.OptionalCapsList, - Kadena.Types.OptionalMetaData, - Kadena.Types.OptionalPactEventsList, Kadena.Types.PactCode, Kadena.Types.PactDecimal, Kadena.Types.PactInt, @@ -140,10 +136,8 @@ defmodule Kadena.MixProject do Kadena.Types.PactTransactionHash, Kadena.Types.PactValue, Kadena.Types.PactValuesList, - Kadena.Types.PollRequestBody, Kadena.Types.Proof, Kadena.Types.Rollback, - Kadena.Types.SendRequestBody, Kadena.Types.SignCommand, Kadena.Types.SignatureWithHash, Kadena.Types.Signature, @@ -153,8 +147,6 @@ defmodule Kadena.MixProject do Kadena.Types.SignersList, Kadena.Types.SigningCap, Kadena.Types.Spec, - Kadena.Types.SPVProof, - Kadena.Types.SPVRequestBody, Kadena.Types.Step ], "Chainweb Pact Types": [ diff --git a/test/chainweb/pact/local_request_body_test.exs b/test/chainweb/pact/local_request_body_test.exs index 1088bcc9..2aceee26 100644 --- a/test/chainweb/pact/local_request_body_test.exs +++ b/test/chainweb/pact/local_request_body_test.exs @@ -7,62 +7,67 @@ defmodule Kadena.Chainweb.Pact.LocalRequestBodyTest do alias Kadena.Chainweb.Pact.LocalRequestBody - alias Kadena.Types.{PactTransactionHash, SignaturesList} + alias Kadena.Types.{ + Command, + PactTransactionHash, + Signature, + SignaturesList + } setup do + cmd = + "{\"meta\":{\"chainId\":\"0\",\"creationTime\":1667249173,\"gasLimit\":1000,\"gasPrice\":1.0e-6,\"sender\":\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\",\"ttl\":28800},\"networkId\":\"testnet04\",\"nonce\":\"2023-06-13 17:45:18.211131 UTC\",\"payload\":{\"exec\":{\"code\":\"(+ 5 6)\",\"data\":{}}},\"signers\":[{\"addr\":\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\",\"clist\":[{\"args\":[\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\"],\"name\":\"coin.GAS\"}],\"pubKey\":\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\",\"scheme\":\"ED25519\"}]}" + + hash = %PactTransactionHash{ + hash: "-1npoTU2Mi71pKE_oteJiJuHuXTXxoObJm8zzVRK2pk" + } + + sigs = %SignaturesList{ + signatures: [ + %Signature{ + sig: + "8b234b83570359e52188cceb301036a2a7b255171e856fd550cac687a946f18fbfc0e769fd8393dda44d6d04c31b531eaf35efb3b78b5e40fd857a743133030d" + } + ] + } + + command = %Command{ + cmd: cmd, + hash: hash, + sigs: sigs + } + %{ - hash: "-1npoTU2Mi71pKE_oteJiJuHuXTXxoObJm8zzVRK2pk", - sigs: [ - "8b234b83570359e52188cceb301036a2a7b255171e856fd550cac687a946f18fbfc0e769fd8393dda44d6d04c31b531eaf35efb3b78b5e40fd857a743133030d" - ], - cmd: - "{\"meta\":{\"chainId\":\"0\",\"creationTime\":1667249173,\"gasLimit\":1000,\"gasPrice\":1.0e-6,\"sender\":\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\",\"ttl\":28800},\"networkId\":\"testnet04\",\"nonce\":\"2023-06-13 17:45:18.211131 UTC\",\"payload\":{\"exec\":{\"code\":\"(+ 5 6)\",\"data\":{}}},\"signers\":[{\"addr\":\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\",\"clist\":[{\"args\":[\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\"],\"name\":\"coin.GAS\"}],\"pubKey\":\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\",\"scheme\":\"ED25519\"}]}" + command: command, + cmd: cmd, + hash: hash, + sigs: sigs, + json_result: + "{\"cmd\":\"{\\\"meta\\\":{\\\"chainId\\\":\\\"0\\\",\\\"creationTime\\\":1667249173,\\\"gasLimit\\\":1000,\\\"gasPrice\\\":1.0e-6,\\\"sender\\\":\\\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\\\",\\\"ttl\\\":28800},\\\"networkId\\\":\\\"testnet04\\\",\\\"nonce\\\":\\\"2023-06-13 17:45:18.211131 UTC\\\",\\\"payload\\\":{\\\"exec\\\":{\\\"code\\\":\\\"(+ 5 6)\\\",\\\"data\\\":{}}},\\\"signers\\\":[{\\\"addr\\\":\\\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\\\",\\\"clist\\\":[{\\\"args\\\":[\\\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\\\"],\\\"name\\\":\\\"coin.GAS\\\"}],\\\"pubKey\\\":\\\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\\\",\\\"scheme\\\":\\\"ED25519\\\"}]}\",\"hash\":\"-1npoTU2Mi71pKE_oteJiJuHuXTXxoObJm8zzVRK2pk\",\"sigs\":[{\"sig\":\"8b234b83570359e52188cceb301036a2a7b255171e856fd550cac687a946f18fbfc0e769fd8393dda44d6d04c31b531eaf35efb3b78b5e40fd857a743133030d\"}]}" } end describe "new/1" do - test "with a valid params list", %{hash: hash, sigs: sigs, cmd: cmd} do + test "with a valid params list", %{command: command, hash: hash, sigs: sigs, cmd: cmd} do %LocalRequestBody{ cmd: ^cmd, - hash: %PactTransactionHash{hash: ^hash}, - sigs: %SignaturesList{} - } = LocalRequestBody.new(hash: hash, sigs: sigs, cmd: cmd) + hash: ^hash, + sigs: ^sigs + } = LocalRequestBody.new(command) end - test "with an invalid no list params" do - {:error, [local_request_body: :not_a_list]} = LocalRequestBody.new("No list") - end - - test "with an invalid cmd", %{hash: hash, sigs: sigs} do - {:error, [cmd: :not_a_string]} = LocalRequestBody.new(hash: hash, sigs: sigs, cmd: 123) - end - - test "with an invalid hash", %{sigs: sigs, cmd: cmd} do - {:error, [hash: :invalid]} = LocalRequestBody.new(hash: 123, sigs: sigs, cmd: cmd) - end - - test "with an invalid sigs list", %{hash: hash, cmd: cmd} do - {:error, [sigs: :invalid, signatures: :invalid, sig: :invalid]} = - LocalRequestBody.new(hash: hash, sigs: [invalid_signature: :invalid_value], cmd: cmd) + test "with an invalid command", %{hash: hash, sigs: sigs} do + {:error, [arg: :not_a_command]} = LocalRequestBody.new(hash: hash, sigs: sigs, cmd: 123) end end describe "JSONPayload.parse/1" do - setup do - %{ - json_result: - "{\"cmd\":\"{\\\"meta\\\":{\\\"chainId\\\":\\\"0\\\",\\\"creationTime\\\":1667249173,\\\"gasLimit\\\":1000,\\\"gasPrice\\\":1.0e-6,\\\"sender\\\":\\\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\\\",\\\"ttl\\\":28800},\\\"networkId\\\":\\\"testnet04\\\",\\\"nonce\\\":\\\"2023-06-13 17:45:18.211131 UTC\\\",\\\"payload\\\":{\\\"exec\\\":{\\\"code\\\":\\\"(+ 5 6)\\\",\\\"data\\\":{}}},\\\"signers\\\":[{\\\"addr\\\":\\\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\\\",\\\"clist\\\":[{\\\"args\\\":[\\\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\\\"],\\\"name\\\":\\\"coin.GAS\\\"}],\\\"pubKey\\\":\\\"85bef77ea3570387cac57da34938f246c7460dc533a67823f065823e327b2afd\\\",\\\"scheme\\\":\\\"ED25519\\\"}]}\",\"hash\":\"-1npoTU2Mi71pKE_oteJiJuHuXTXxoObJm8zzVRK2pk\",\"sigs\":[{\"sig\":\"8b234b83570359e52188cceb301036a2a7b255171e856fd550cac687a946f18fbfc0e769fd8393dda44d6d04c31b531eaf35efb3b78b5e40fd857a743133030d\"}]}" - } - end - test "with a valid LocalRequestBody", %{ - hash: hash, - sigs: sigs, - cmd: cmd, + command: command, json_result: json_result } do ^json_result = - [hash: hash, sigs: sigs, cmd: cmd] + command |> LocalRequestBody.new() |> LocalRequestBody.to_json!() end diff --git a/test/chainweb/pact/local_test.exs b/test/chainweb/pact/local_test.exs new file mode 100644 index 00000000..c1b09cc8 --- /dev/null +++ b/test/chainweb/pact/local_test.exs @@ -0,0 +1,131 @@ +defmodule Kadena.Chainweb.Client.CannedLocalRequests do + @moduledoc false + + alias Kadena.Chainweb.Error + alias Kadena.Test.Fixtures.Chainweb + + def request( + :post, + "https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/1/pact/api/v1/local", + _headers, + body, + _options + ) do + case String.contains?(body, "(coin.get-balance 'bad')") do + true -> + response = + Error.new( + {:chainweb, + %{ + status: 400, + title: "Validation failed: Invalid command: Failed reading: empty" + }} + ) + + {:error, response} + + false -> + response = Chainweb.fixture("local") + {:ok, response} + end + end +end + +defmodule Kadena.Chainweb.Pact.LocalTest do + @moduledoc """ + `Local` endpoint implementation tests. + """ + + use ExUnit.Case + + alias Kadena.Chainweb.Client.CannedLocalRequests + alias Kadena.Chainweb.{Error, Pact} + alias Kadena.Chainweb.Pact.LocalResponse + alias Kadena.Cryptography + alias Kadena.Pact.ExecCommand + alias Kadena.Types.MetaData + + setup do + Application.put_env(:kadena, :http_client_impl, CannedLocalRequests) + + on_exit(fn -> + Application.delete_env(:kadena, :http_client_impl) + end) + + success_cmd = create_command("(+ 5 6)") + error_cmd = create_command("(coin.get-balance 'bad')") + + success_response = + {:ok, + %LocalResponse{ + continuation: nil, + events: nil, + gas: 6, + logs: "wsATyGqckuIvlm89hhd2j4t6RMkCrcwJe_oeCYr7Th8", + meta_data: %{ + block_height: 3_303_861, + block_time: 1_671_546_034_831_940, + prev_block_hash: "Y6Wj0sxJpdV8M-3ihAbzUka57Wv5ZV2Uez6H_6_WeeY", + public_meta: %{ + chain_id: "1", + creation_time: 1_671_462_208, + gas_limit: 1000, + gas_price: 1.0e-6, + sender: "k:d1a361d721cf81dbc21f676e6897f7e7a336671c0d5d25f87c10933cac6d8cf7", + ttl: 28_800 + } + }, + req_key: "bTrbGGOdhzA_lwmMoXkqozNe_YKsww7uTNW913B79bs", + result: %{data: 11, status: "success"}, + tx_id: nil + }} + + error_response = + {:error, + %Error{ + status: 400, + title: "Validation failed: Invalid command: Failed reading: empty" + }} + + %{ + success: %{cmd: success_cmd, response: success_response}, + error: %{cmd: error_cmd, response: error_response} + } + end + + test "process/2", %{success: %{cmd: cmd, response: response}} do + ^response = Pact.local(cmd, network_id: :testnet04, chain_id: 1) + end + + test "process/2 error", %{error: %{cmd: cmd, response: response}} do + ^response = Pact.local(cmd, network_id: :testnet04, chain_id: 1) + end + + defp create_command(code) do + network_id = :testnet04 + nonce = "2023-01-01 00:00:00.000000 UTC" + + {:ok, keypair} = + Cryptography.KeyPair.from_secret_key( + "28834b7a0d6d1f84ae2c2efcb5b1de28122e07e2e4caad04a32988a3c79c547c" + ) + + meta_data = + MetaData.new( + creation_time: 1_671_462_208, + ttl: 28_800, + gas_limit: 1000, + gas_price: 1.0e-6, + sender: "k:d1a361d721cf81dbc21f676e6897f7e7a336671c0d5d25f87c10933cac6d8cf7", + chain_id: "1" + ) + + ExecCommand.new() + |> ExecCommand.set_network(network_id) + |> ExecCommand.set_code(code) + |> ExecCommand.set_nonce(nonce) + |> ExecCommand.set_metadata(meta_data) + |> ExecCommand.add_keypair(keypair) + |> ExecCommand.build() + end +end diff --git a/test/support/fixtures/chainweb/local.json b/test/support/fixtures/chainweb/local.json index e32ac0db..d752a14c 100644 --- a/test/support/fixtures/chainweb/local.json +++ b/test/support/fixtures/chainweb/local.json @@ -1,24 +1,24 @@ { - "continuation": null, - "gas": 7, + "gas": 6, + "result": { + "status": "success", + "data": 11 + }, + "reqKey": "bTrbGGOdhzA_lwmMoXkqozNe_YKsww7uTNW913B79bs", "logs": "wsATyGqckuIvlm89hhd2j4t6RMkCrcwJe_oeCYr7Th8", "metaData": { - "blockHeight": 2815727, - "blockTime": 1671054330981668, - "prevBlockHash": "asisNr3nuU0t357i2bxMVUiIWDVHAMtncJHtmyENbio", "publicMeta": { - "chainId": "0", - "creationTime": 1667249173, + "creationTime": 1671462208, + "ttl": 28800, "gasLimit": 1000, + "chainId": "1", "gasPrice": 1.0e-6, - "sender": "k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94", - "ttl": 28800 - } - }, - "reqKey": "-1npoTU2Mi71pKE_oteJiJuHuXTXxoObJm8zzVRK2pk", - "result": { - "data": 11, - "status": "success" + "sender": "k:d1a361d721cf81dbc21f676e6897f7e7a336671c0d5d25f87c10933cac6d8cf7" + }, + "blockTime": 1671546034831940, + "prevBlockHash": "Y6Wj0sxJpdV8M-3ihAbzUka57Wv5ZV2Uez6H_6_WeeY", + "blockHeight": 3303861 }, + "continuation": null, "txId": null }