View Source Icon.RPC.Request (ICON 2.0 SDK v0.2.3)

This module defines a basic JSON RPC request payloads.

building-a-request

Building a Request

The requests built using build/3 are prepared to be encoded as a JSON accepted by the ICON 2.0 JSON RPC v3. For building a request we need three things:

  • The name of the method we're calling.
  • The parameters we're sending along the method.
  • Some options to both validate the parameters, sign any transaction and build the actual request.

e.g. let's say we want to query a block by its height, we would do the following:

iex> method = "icx_getBlockByHeight"
iex> params = %{height: 42}
iex> schema = %{height: :pos_integer}
iex> Icon.RPC.Request.build(method, params, schema: schema)
%Icon.RPC.Request{
  id: 1_639_382_704_065_742_380,
  method: "icx_getBlockByHeight",
  options: %{
    schema: %{height: :pos_integer},
    identity: #Identity<
      node: "https://ctz.solidwallet.io",
      network_id: "0x01 (Mainnet)",
      debug: false
    >,
    url: "https://ctz.solidwallet.io/api/v3"
  },
  params: %{height: 42}
}

Note: The previous example is for documentation purpose. This functionality is already present in Icon.RPC.Request.Goloop, so no need to build the request ourselves.

And, when using the function Jason.encode!, we'll get the following JSON:

{
  "id": 1639382704065742380,
  "jsonrpc": "2.0",
  "method": "icx_getBlockByHeight",
  "params": {
    "height": "0x2a"
  }
}

signing-a-transaction

Signing a Transaction

Transactions need to be signed before sending them to the node. With the signature is possible to verify it comes from the wallet requesting it e.g. let's say we want to sent 1 ICX from one wallet to another:

iex> request = Icon.RPC.Request.build("icx_sendTransaction", ...)
%Icon.RPC.Request{
  id: 1_639_382_704_065_742_380,
  method: "icx_sendTransaction",
  options: ...,
  params: %{
    from: "hx2e243ad926ac48d15156756fce28314357d49d83",
    to: "hxdd3ead969f0dfb0b72265ca584092a3fb25d27e0",
    value: 1_000_000_000_000_000_000,
    ...
  }
}

then we can sign it as follows:

iex> {:ok, request} = Icon.RPC.Request.sign(request)
{
  :ok,
  %Icon.RPC.Request{
    id: 1_639_382_704_065_742_380,
    method: "icx_sendTransaction",
    options: ...,
    params: %{
      from: "hx2e243ad926ac48d15156756fce28314357d49d83",
      to: "hxdd3ead969f0dfb0b72265ca584092a3fb25d27e0",
      value: 1_000_000_000_000_000_000,
      signature: "Kut8d4uXzy0UPIU13l3OW5Ba3WNuq6B6w7+0v4XR4qQNv1Cy3qOmn7ih4TZrXZGT3qhkaRM/WCL+qmWyh86/tgA="
      ...
    }
  }
}

and also verify it to check everything is correct after signing it:

iex> Icon.RPC.Request.verify(request)
true

Note: In order to sign a transaction, the option identity is mandatory and it needs to be built using a private_key e.g.

iex> Icon.RPC.Identity.new(private_key: "8ad9...")
#Identity<[
  node: "https://ctz.solidwallet.io",
  network_id: "0x1 (Mainnet)",
  debug: false,
  address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
  private_key: "8ad9..."
]>

sending-the-request

Sending the Request

Once we're satisfied with our request, we can send it to the ICON 2.0 blockchain using the send/1 function:

iex> Icon.RPC.Request.send(request)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}

where the response() will depend on the actual method being called.

Link to this section Summary

Types

RPC ID.

RPC Method.

RPC option.

RPC options.

RPC call parameters.

A JSON RPC response.

t()

A JSON RPC request.

Functions

Adds step limit to a transaction. If no value is provided, then it will request for a node estimation.

Builds an RPC request given a method, some parameters and general options.

Sends a remote procedure call to an ICON 2.0 node.

Serializes a transaction request.

Signs request.

Whether a request is signed correctly or not.

Link to this section Types

@type id() :: pos_integer()

RPC ID.

@type method() :: binary()

RPC Method.

@type option() ::
  {:schema, module() | Icon.Schema.t()}
  | {:timeout, non_neg_integer()}
  | {:identity, Icon.RPC.Identity.t()}
  | {:url, binary()}

RPC option.

@type options() :: [option()]

RPC options.

@type params() :: map()

RPC call parameters.

@type response() :: binary() | list() | map()

A JSON RPC response.

@type t() :: %Icon.RPC.Request{
  id: id :: pos_integer(),
  method: method :: method(),
  options:
    options :: %{
      :identity => Icon.RPC.Identity.t(),
      :url => binary(),
      optional(:schema) => module() | Icon.Schema.t(),
      optional(:timeout) => non_neg_integer()
    },
  params: params :: params()
}

A JSON RPC request.

Link to this section Functions

Link to this function

add_step_limit(request, step_limit \\ nil)

View Source
@spec add_step_limit(t(), nil | Icon.Schema.Types.Loop.t()) ::
  {:ok, t()} | {:error, Icon.Schema.Error.t()}

Adds step limit to a transaction. If no value is provided, then it will request for a node estimation.

Link to this function

build(method, params, options)

View Source
@spec build(method(), params(), options()) :: t()

Builds an RPC request given a method, some parameters and general options.

The method and parameters depend on the JSON RPC method we're calling, while the options have extra instructions for the actual request.

Options:

  • timeout - Whether we should have a timeout on the call or not. This timeout only applies to methods we can wait on the result e.g. icx_waitTransactionResult and icx_sendTransactionAndWait.
  • schema - Icon.Schema for verifying the call parameters.
  • identity - Icon.RPC.Identity of the wallet performing the action. A full identity is not required for most readonly calls. However, it is necessary to have a full wallet configure for sending transactions to the ICON 2.0 blockchain.
  • url - This endpoint is set automatically by the request builder.

encoding

Encoding

Though having the schema option is not mandatory, it is necessary whenever we're calling a method with parameters in order to convert them to the ICON 2.0 type representation as well as serializing transactions. For more information, check Icon.Schema module.

example

Example

The following example shows how to build a request for querying a block by height:

iex> method = "icx_getBlockByHeight"
iex> params = %{height: 42}
iex> schema = %{height: :pos_integer}
iex> Icon.RPC.Request.build(method, params, schema: schema)
%Icon.RPC.Request{
  id: 1_639_382_704_065_742_380,
  method: "icx_getBlockByHeight",
  options: %{
    schema: %{height: :pos_integer},
    identity: #Identity<
      node: "https://ctz.solidwallet.io",
      network_id: "0x01 (Mainnet)",
      debug: false
    >,
    url: "https://ctz.solidwallet.io/api/v3"
  },
  params: %{height: 42}
}
@spec send(t()) :: {:ok, response()} | {:error, Icon.Schema.Error.t()}

Sends a remote procedure call to an ICON 2.0 node.

@spec serialize(t()) :: {:ok, binary()} | {:error, Icon.Schema.Error.t()}

Serializes a transaction request.

When building a transaction signature, one of the steps of the process is serializing the transaction. In general, the serialization process goes as follows:

  1. Convert the JSON RPC method parameters to the ICON representation.
  2. Serialize them.

E.g. a request like the following:

%Icon.RPC.Request{
  id: 1_641_400_211_292_452_380,
  method: "icx_sendTransaction",
  options: ...,
  params: %{
    from: "hx2e243ad926ac48d15156756fce28314357d49d83",
    to: "hxdd3ead969f0dfb0b72265ca584092a3fb25d27e0",
    nid: 1,
    version: 3,
    timestamp: ~U[2022-01-05 16:30:11.292452Z],
    stepLimit: 100_000,
    value: 1_000_000_000_000_000_000
  }
}

would be serialized as follows:

icx_sendTransaction.from.hx2e243ad926ac48d15156756fce28314357d49d83.nid.0x1.stepLimit.0x186a0.timestamp.0x5d4d844874124.to.hxdd3ead969f0dfb0b72265ca584092a3fb25d27e0.value.0xde0b6b3a7640000.version.0x3

The serialization rules are simple:

  • Values should be encoded to the ICONs encoding e.g. the integer 1 would be converted to "0x1".
  • <key>/<value> pairs in maps should be converted to "<key>.<value>" string e.g. {:a, 1} would be converted to "a.0x1"
  • All keys in a map should be in alphabetical order.
  • All maps except the top level one should be surrounded by braces e.g. %{a: 1} would be converted to "{a.0x1}".
  • Lists should be surrounded by brackets and its elements should be separated by . e.g. [1,2,3] would be converted to "[0x1.0x2.0x3]".
  • The top level map should be preceded by "icx_sendTransaction." prefix e.g. %{from: "hx...", ...} would be converted to "icx_sendTransaction.from.hx..."
  • Any of the characters \, {, }, [, ] and . should be escaped by adding a \ before them e.g. %{message: "..."} would be encoded as {message.\.\.\.}.
@spec sign(t()) :: {:ok, t()} | {:error, Icon.Schema.Error.t()}

Signs request.

Signing a request does the following:

  1. Serializes the parameters see serialize/1,
  2. Hash the serialized parameters with SHA3_256 digest algorithm twice.
  3. Generate a SECP256K1 signature with the hash.
  4. Encode the signature in Base 64.
  5. Add the encoded signature to the transaction parameters.

Note: Curvy is the library used by this API for the signature. It generates compact signatures in the form of VRS while ICON expects RSV signatures. This modules handles the conversion between these formats transparently.

@spec verify(t()) :: boolean()

Whether a request is signed correctly or not.