Tezos via JSOO - 03, Structured value

This mini tutorial series shows how to access Tezos network from web browsers using JSOO, js_of_ocaml library. This is the 3rd tutorial. The previous tutorials are available at:

Structured value from Tezos RPC

In the last tutorial, we have got the balance of an account. Tezos account balance is returned as a JSON string like "12345678". We used JS’s JSON parser _JSON##parse to convert it to a JS string, then to an arbitrary precision integer Z.t.

This time, let’s get more structured data from Tezos node, the account information which includes this balance. The RPC endpoint is:

GET ../<block_id>/context/contracts/<contract_id>

which is documented here. Let’s get the account info of tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9 via this RPC. Its URL is https://mainnet.smartpy.io/chains/main/blocks/head/context/contracts/tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9:

$ curl https://mainnet.smartpy.io/chains/main/blocks/head/context/contracts/tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9
{"balance":"424555322368","delegate":"tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9","counter":"11"}

This time the RPC returns a JSON record.

For KT1 account, for example KT1BGQR7t4izzKZ7eRodKWTodAsM23P38v7N, the JSON response contains field script with long data of its smart contract:

$ curl https://mainnet.smartpy.io/chains/main/blocks/head/context/contracts/KT1BGQR7t4izzKZ7eRodKWTodAsM23P38v7N
{"balance":"1210133578","delegate":"tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q","script":{"code":[{"prim":"parameter","args":[{"prim":"or","args":[{"prim":"or","args":[{"prim":"or","args"
...

JSON schema

Tezos RPC reference shows the schema of the JSON return value of the RPC in “Json output” tab:

This is not a JSON schema but you should be able to understand what it means:

  • balance is a required field of type $012-Psithaca.mutez
  • delegate is an optional field of type $Signature.Public_key_hash
  • script is an optional field of type $012-Psithaca.scripted.contracts
  • counter is an optional field of type $positive_bignum

The definitions of the field types are followed. You will find they are all strings except $012-Psithaca.scripted.contracts if you track them down.

OCaml class type for JSON

In JSOO, the JSON value can be parsed by _JSON##parse as a JS object. We need to prepare the OCaml type for the result. We define the following object type of properties:

open Js

type unknown

type contract =
  <  (* balance : required string field *)
     balance : js_string t readonly_prop;

     (* delegate : optional string field *)
     delegate : js_string t optdef readonly_prop;

     (* script : optional.  We ignore the contents for now *)
     script : unknown t optdef readonly_prop;

     (* counter : optional string field *)
     counter : js_string t optdef readonly_prop
  >

Several things we have to note:

  • JS’s string type is not string but js_string t. It is common to misuse string here since no static typing helps you.
  • For optional field use optdef.
  • We use an abstract type unknown for the contents of script field which we are not interested in this tutorial.

Using this object type contract now we can parse the JSON in JSOO:

let c : contract Js.t = Js._JSON##parse (Js.string http_frame.content)

Now we can access the fields via ##.name JSOO property accessor:

report "%s : @[<v>balance: %s;@ delegate: %s;@ script: %s;@ counter: %s@]"
  address
  (Js.to_string c##.balance)
  (Js.Optdef.case c##.delegate (fun () -> "none") Js.to_string)
  (Js.Optdef.case c##.script (fun () -> "none") (fun x ->
       (* It is an abstract value in OCaml,
          but we can print it using _JSON##stringify *)
       (Js.to_string (Js._JSON##stringify x))))
  (Js.Optdef.case c##.counter (fun () -> "none") Js.to_string)

The values of the optinal fields are handled by Js.Optdef.case. The script value unknown t is abstracted in OCaml, but it is still encodable to JSON using _JSON##stringify.

Full OCaml code

open Js_of_ocaml
open Js_of_ocaml_lwt
open Lwt.Syntax

module Html = Dom_html

(* Append the format result to "result" HTML element *)
let report fmt =
  let elem = Html.getElementById "result" in
  Format.kasprintf
    (fun s ->
       elem##.innerText := elem##.innerText##concat (Js.string (s ^ "\n")))
    fmt

(* The node.  It must be CORS enabled. *)
let node = "https://mainnet.smartpy.io"

(* This time, we get more complex data of a contract *)
let url address =
  Option.get
  @@ Url.url_of_string
  @@ node ^ "/chains/main/blocks/head/context/contracts/" ^ address

(* Type for the data we skip parsing this time (script) *)
type unknown

(* The data returned from the RPC is an JSON object, specified at
   https://tezos.gitlab.io/active/rpc.html#get-block-id-context-contracts-contract-id

   Here is the OCaml object type for the JS object isomorphic to it.
*)
open Js
type contract =
  <  (* balance : required string field *)
     balance : js_string t readonly_prop;

     (* delegate : optional string field *)
     delegate : js_string t optdef readonly_prop;

     (* script : optional.  We ignore the contents for now *)
     script : unknown t optdef readonly_prop;

     (* counter : optional string field *)
     counter : js_string t optdef readonly_prop
  >

let query_contract address =
  let+ http_frame = XmlHttpRequest.perform (url address) in
  match http_frame.code with
  | 200 ->
      (* Parse the string as an JSON of type contract *)
      let c : contract Js.t = Js._JSON##parse (Js.string http_frame.content)
      in
      report "%s : @[<v>balance: %s;@ delegate: %s;@ script: %s;@ counter: %s@]"
        address
        (Js.to_string c##.balance)
        (Js.Optdef.case c##.delegate (fun () -> "none") Js.to_string)
        (Js.Optdef.case c##.script (fun () -> "none") (fun x ->
             (* It is an abstract value in OCaml,
                but we can print it using _JSON##stringify *)
             (Js.to_string (Js._JSON##stringify x))))
        (Js.Optdef.case c##.counter (fun () -> "none") Js.to_string)
  | _ ->
      report "HTTP code %d" http_frame.code

let _ =
  Html.window##.onload := Html.handler (fun _ ->
      ignore @@ query_contract "tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9";
      ignore @@ query_contract "KT1BGQR7t4izzKZ7eRodKWTodAsM23P38v7N";
      Js._false)

The entire code set (this code + dune + index.html) is available here. How to compile the tutorial examples are explained in the Appendix of the first tutorial.

Conclusion

In this 3rd tutorial of Tezos via JSOO, we have seen how to parse complex JSON data returned from Tezos RPC:

  • Check the JSON schema in the RPC reference.
  • Define an OCaml class type to match the returned JSON record.
    • Each field becomes readonly_prop
    • Use Js.optdef for optional fields
    • Abstract types are usable for fields you are not interested in.
  • Once parsed as a JS object, fields can be accessed via ##.name JSOO property accessor.

The next tutorial will conclude the parsing of JSON response from the Tezos node.