diff --git a/Cargo.lock b/Cargo.lock index b27cb740f5760..28090f2fec09e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1177,6 +1177,17 @@ dependencies = [ "syn", ] +[[package]] +name = "derive-syn-parse" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79116f119dd1dba1abf1f3405f03b9b0e79a27a3883864bfebded8a3dc768cd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_builder" version = "0.11.2" @@ -4997,6 +5008,7 @@ dependencies = [ "node", "num_cpus", "once_cell", + "pretty_assertions", "rand 0.8.5", "rayon", "reqwest", @@ -5014,6 +5026,8 @@ dependencies = [ "sui-adapter", "sui-framework", "sui-network", + "sui-open-rpc", + "sui-open-rpc-macros", "sui-types", "sui-verifier", "sui_core", @@ -5106,6 +5120,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "sui-open-rpc" +version = "0.1.0" +dependencies = [ + "schemars", + "serde 1.0.136", +] + +[[package]] +name = "sui-open-rpc-macros" +version = "0.1.0" +dependencies = [ + "derive-syn-parse", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sui-types" version = "0.1.0" @@ -5132,6 +5165,7 @@ dependencies = [ "opentelemetry", "parking_lot 0.12.0", "rand 0.7.3", + "schemars", "serde 1.0.136", "serde-name", "serde_bytes", @@ -5184,6 +5218,7 @@ dependencies = [ "pretty_assertions", "rand 0.7.3", "rocksdb", + "schemars", "scopeguard", "serde 1.0.136", "serde-reflection", diff --git a/doc/src/build/json-rpc.md b/doc/src/build/json-rpc.md new file mode 100644 index 0000000000000..cb81c07543144 --- /dev/null +++ b/doc/src/build/json-rpc.md @@ -0,0 +1,302 @@ +--- +title: Local RPC Server & JSON-RPC API Quick Start +--- + +Welcome to the Sui RPC server quick start. + +This document walks you through setting up your own local Sui RPC Server and using the Sui JSON-RPC API to interact with a local Sui network. This guide is useful for developers interested in Sui network interactions via API. For a similar guide on Sui network interactions via CLI, refer to the [wallet](wallet.md) documentation. + + +## Local RPC server setup +Follow the instructions to [install Sui binaries](install.md). + +### Start local Sui network +Follow the instructions to [create](wallet.md#genesis) and [start](wallet.md#starting-the-network) the Sui network. +The genesis process will create a `gateway.conf` configuration file that will be used by the RPC server. + +### Start local RPC server + +Use the following command to start a local server: +```shell +$ rpc-server +``` +You will see output resembling: +``` +2022-04-25T11:06:40.147259Z INFO rpc_server: Gateway config file path: ".sui/sui_config/gateway.conf" +2022-04-25T11:06:40.147277Z INFO rpc_server: AccessControl { allowed_hosts: Any, allowed_origins: None, allowed_headers: Any, continue_on_invalid_cors: false } +2022-04-25T11:06:40.163568Z INFO rpc_server: Available JSON-RPC methods : ["sui_moveCall", "sui_getTransaction", "sui_getObjectTypedInfo", "sui_getTotalTransactionNumber", "sui_getOwnedObjects", "sui_getObjectInfoRaw", "sui_transferCoin", "sui_executeTransaction", "sui_mergeCoins", "sui_getRecentTransactions", "sui_getTransactionsInRange", "rpc.discover", "sui_splitCoin", "sui_publish", "sui_syncAccountState"] +2022-04-25T11:06:40.163590Z INFO rpc_server: Sui RPC Gateway listening on local_addr:127.0.0.1:5001 +``` + +> **Note:** For additional logs, set `RUST_LOG=debug` before invoking `rpc-server`. + +Export a local user variable to store the hardcoded hostname + port that the local RPC server starts with to be used when issuing the `curl` commands that follow. +```shell +export SUI_RPC_HOST=http://127.0.0.1:5001 +``` + +## Sui JSON-RPC API + +In the following sections we will show how to use Sui's JSON-RPC API with +the `curl` command. + +## Sui JSON-RPC methods + +### rpc.discover + +Sui RPC server supports OpenRPC’s [service discovery method](https://spec.open-rpc.org/#service-discovery-method). +A `rpc.discover` method is added to provide documentation describing our JSON-RPC APIs service. + +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", "method":"rpc.discover","id":1}' +``` + +You can see an example of the discovery service in the [OpenRPC Playground](https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/MystenLabs/sui/189d61df846f7c3676c1215cc41fb970ee9e22b5/sui/open_rpc/spec/openrpc.json). + +### sui_syncAccountState + +Synchronize client state with validators with the following command, +replacing `{{address}}` with an actual address value, for example one obtained from `wallet.conf`: + +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", "method":"sui_syncAccountState", "params":["{{address}}"], "id":1}' +``` + +This will fetch the latest information on all objects owned by each +address that is managed by this server. This command has no output. + +### sui_getOwnedObjects + +Return the list of objects owned by an address: +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", "method":"sui_getOwnedObjects", "params":["{{address}}"], "id":1}' | json_pp +``` + +You should replace `{{address}}` in the command above with an actual +address value, you can retrieve the list of the addresses created during +genesis from `wallet.conf`. Ensure you have run [`sui_syncAccountState`](#sui_syncaccountstate) + +The output you see should resemble the following (abbreviated to show only two objects): + +```shell +{ + "id" : 1, + "jsonrpc" : "2.0", + "result" : { + "objects" : [ + { + "digest" : "zpa45U9ANfA9A6iS01NvAoVH0RbYB6a5rjhgh2Hb/GE=", + "objectId" : "17b348903b0cfb75fc9ab5426bb69d83d1e756a5", + "version" : 1 + }, + { + "digest" : "8SPi0h6xVMVNBvGzzF4RfuOoaXISdtiB5aT7+BYDbxg=", + "objectId" : "7599d8ea1de4c9616d077f16ca0eb38cdecacc07", + "version" : 1 + }, + ... + ] + } +} + +``` + +### GET sui_getObjectInfoRaw + +Return the object information for a specified object, for example: + +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", "method":"sui_getObjectInfoRaw", "params":["{{object_id}}"], "id":1}' | json_pp +``` + +Replace `{{object_id}}` in the command above with an +actual object ID, for example one obtained from [`sui_getOwnedObjects`](#sui_getownedobjects) (without quotes). + +### sui_transferCoin +#### 1, Create a transaction to transfer a Sui coin from one address to another: +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", + "method":"sui_transferCoin", + "params":["{{owner_address}}", + "{{coin_object_id}}", + "{{gas_object_id}}", + {{gas_budget}}, + "{{to_address}}"], + "id":1}' | json_pp +``` +A transaction data response will be returned from the gateway server. +```json +{ + "id" : 1, + "jsonrpc" : "2.0", + "result" : { + "tx_bytes" : "VHJhbnNhY3Rpb25EYXRhOjoAAFHe8jecgzoGWyGlZ1sJ2KBFN8aZF7NIkDsM+3X8mrVCa7adg9HnVqUBAAAAAAAAACDOlrjlT0A18D0DqJLTU28ChUfRFtgHprmuOGCHYdv8YVHe8jecgzoGWyGlZ1sJ2KBFN8aZdZnY6h3kyWFtB38Wyg6zjN7KzAcBAAAAAAAAACDxI+LSHrFUxU0G8bPMXhF+46hpchJ22IHlpPv4FgNvGOgDAAAAAAAA" + } +} + +``` +#### 2, Sign the transaction using the Sui signtool +```shell +sui signtool --address --data +``` +The signing tool will create and print out the signature and public key information. +You will see output resembling: +```shell +2022-04-25T18:50:06.031722Z INFO sui::sui_commands: Data to sign : VHJhbnNhY3Rpb25EYXRhOjoAAFHe8jecgzoGWyGlZ1sJ2KBFN8aZF7NIkDsM+3X8mrVCa7adg9HnVqUBAAAAAAAAACDOlrjlT0A18D0DqJLTU28ChUfRFtgHprmuOGCHYdv8YVHe8jecgzoGWyGlZ1sJ2KBFN8aZdZnY6h3kyWFtB38Wyg6zjN7KzAcBAAAAAAAAACDxI+LSHrFUxU0G8bPMXhF+46hpchJ22IHlpPv4FgNvGOgDAAAAAAAA +2022-04-25T18:50:06.031765Z INFO sui::sui_commands: Address : 51DEF2379C833A065B21A5675B09D8A04537C699 +2022-04-25T18:50:06.031911Z INFO sui::sui_commands: Public Key Base64: H82FDLUZN1u0+6UdZilxu9HDT5rPd3khKo2UJoCPJFo= +2022-04-25T18:50:06.031925Z INFO sui::sui_commands: Signature : 6vc+ku0RsMKdky8DRfoy/hw6eCQ3YsadH6rZ9WUCwGTAumuWER3TOJRw7u7F4QaHkqUsIPfJN9GRraSX+N8ADQ== +``` + +#### 3, Execute the transaction using the transaction data, signature and public key. +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", + "method":"sui_executeTransaction", + "params":[{ + "tx_bytes" : "{{tx_bytes}}", + "signature" : "{{signature}}", + "pub_key" : "{{pub_key}}"}], + "id":1}' | json_pp +``` + +Native transfer by `sui_transferCoin` is supported for coin +objects only (including gas objects). Refer to +[transactions](transactions.md#native-transaction) documentation for +more information about a native transfer. Non-coin objects cannot be +transferred natively and require a [Move call](#sui_movecall). + +You should replace `{{owner_address}}` and `{{to_address}}` in the +command above with an actual address values, for example one obtained +from `wallet.conf`. You should also replace +`{{coin_object_id}}` and `{{gas_object_id}}` in the command above with +an actual object ID, for example one obtained from `objectId` in the output +of [`sui_getOwnedObjects`](#sui_getownedobjects). You can see that all gas objects generated +during genesis are of `Coin/SUI` type). For this call to work, objects +represented by both `{{coin_object_id}}` and `{{gas_object_id}}` must +be owned by the address represented by `{{owner_address}}`. + + +### sui_moveCall + +#### 1, Execute a Move call transaction by calling the specified function in +the module of a given package (smart contracts in Sui are written in +the [Move](move.md#move) language): + +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", + "method":"sui_moveCall", + "params":["{{owner_address}}", + "0x2", + "GAS", + "transfer", + [], + ["Pure": "{{coin_object_id_base64}}", "Pure": "{{to_address_base64}}"}], + "{{gas_object_id}}", + 2000], + "id":1}' | json_pp +``` + +#### 2, Sign the transaction +Follow the instructions to [sign the transaction](rest-api.md#2-sign-the-transaction-using-the-sui-signtool). + +#### 3, Execute the transaction +Follow the instructions to [execute the transaction](rest-api.md#3-execute-the-transaction-using-the-transaction-data-signature-and-public-key). + +Arguments are passed in, and type will be inferred from function +signature. Gas usage is capped by the gas_budget. The `transfer` +function is described in more detail in +the [Sui Wallet](wallet.md#calling-move-code) documentation. + +Calling the `transfer` function in the `GAS` module serves the same +purpose as the native coin transfer ([`sui_transferCoin`](#sui_transfercoin)), and is mostly used for illustration +purposes as native transfer is more efficient when it's applicable +(i.e., we are transferring coins rather than non-coin +objects). Consequently, you should fill out argument placeholders +(`{{owner_address}}`, `{{coin_object_id}`, etc.) the same way you +would for [`sui_transferCoin`](#sui_transfercoin) - please not additional +`0x` prepended to function arguments. + +To learn more about what `args` are accepted in a Move call, refer to the [SuiJSON](sui-json.md) documentation. + +### sui_publish + +Publish Move module. + +```shell +curl --location --request POST $SUI_RPC_HOST \ +--header 'Content-Type: application/json' \ +--data-raw '{ "jsonrpc":"2.0", + "method":"sui_publish", + "params":[ "{{owner_address}}", + {{vector_of_compiled_modules}}, + "{{gas_object_id}}", + 10000], + "id":1}' | json_pp +``` + +This endpoint will perform proper verification and linking to make +sure the package is valid. If some modules have [initializers](move.md#module-initializers), these initializers +will also be executed in Move (which means new Move objects can be created in +the process of publishing a Move package). Gas budget is required because of the +need to execute module initializers. + +You should replace `{{owner_address}}` in the +command above with an actual address values, for example one obtained +from `wallet.conf`. You should also replace `{{gas_object_id}}` in the command above with +an actual object ID, for example one obtained from `objectId` in the output +of [`sui_getownedobjects`](#sui_getownedobjects). You can see that all gas objects generated +during genesis are of `Coin/SUI` type). For this call to work, the object +represented by `{{gas_object_id}}` must be owned by the address represented by +`{{owner_address}}`. + +To publish a Move module, you also need `{{vector_of_compiled_modules}}`. To generate the value of this field, use the `sui-move` command. The `sui-move` command supports printing the bytecodes as base64 with the following option + +``` +sui-move --path build --dump-bytecode-as-base64 +``` + +Assuming that the location of the package's sources is in the `PATH_TO_PACKAGE` environment variable an example command would resemble the following + +``` +sui-move --path $PATH_TO_PACKAGE/my_move_package build --dump-bytecode-as-base64 + +["oRzrCwUAAAAJAQAIAggUAxw3BFMKBV1yB88BdAjDAigK6wIFDPACQgAAAQEBAgEDAAACAAEEDAEAAQEBDAEAAQMDAgAABQABAAAGAgEAAAcDBAAACAUBAAEFBwEBAAEKCQoBAgMLCwwAAgwNAQEIAQcODwEAAQgQAQEABAYFBgcICAYJBgMHCwEBCAALAgEIAAcIAwABBwgDAwcLAQEIAAMHCAMBCwIBCAADCwEBCAAFBwgDAQgAAgsCAQkABwsBAQkAAQsBAQgAAgkABwgDAQsBAQkAAQYIAwEFAgkABQMDBwsBAQkABwgDAQsCAQkAAgsBAQkABQdNQU5BR0VEBENvaW4IVHJhbnNmZXIJVHhDb250ZXh0C1RyZWFzdXJ5Q2FwBGJ1cm4EaW5pdARtaW50DHRyYW5zZmVyX2NhcAtkdW1teV9maWVsZA9jcmVhdGVfY3VycmVuY3kGc2VuZGVyCHRyYW5zZmVyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAQkBAAEAAAEECwELADgAAgEAAAAICwkSAAoAOAEMAQsBCwAuEQY4AgICAQAAAQULAQsACwI4AwIDAQAAAQQLAAsBOAQCAA==", "oRzrCwUAAAALAQAOAg4kAzJZBIsBHAWnAasBB9IC6QEIuwQoBuMECgrtBB0MigWzAQ29BgYAAAABAQIBAwEEAQUBBgAAAgAABwgAAgIMAQABBAQCAAEBAgAGBgIAAxAEAAISDAEAAQAIAAEAAAkCAwAACgQFAAALBgcAAAwEBQAADQQFAAIVCgUBAAIICwMBAAIWDQ4BAAIXERIBAgYYAhMAAhkCDgEABRoVAwEIAhsWAwEAAgsXDgEAAg0YBQEABgkHCQgMCA8JCQsMCw8MFAYPBgwNDA0PDgkPCQMHCAELAgEIAAcIBQILAgEIAwsCAQgEAQcIBQABBggBAQMEBwgBCwIBCAMLAgEIBAcIBQELAgEIAAMLAgEIBAMLAgEIAwEIAAEGCwIBCQACCwIBCQAHCwcBCQABCAMDBwsCAQkAAwcIBQELAgEJAAEIBAELBwEIAAIJAAcIBQELBwEJAAEIBgEIAQEJAAIHCwIBCQALAgEJAAMDBwsHAQkABwgFAQYLBwEJAAZCQVNLRVQHTUFOQUdFRARDb2luAklEA1NVSQhUcmFuc2ZlcglUeENvbnRleHQHUmVzZXJ2ZQRidXJuBGluaXQObWFuYWdlZF9zdXBwbHkEbWludApzdWlfc3VwcGx5DHRvdGFsX3N1cHBseQtkdW1teV9maWVsZAJpZAtWZXJzaW9uZWRJRAx0cmVhc3VyeV9jYXALVHJlYXN1cnlDYXADc3VpB21hbmFnZWQFdmFsdWUId2l0aGRyYXcPY3JlYXRlX2N1cnJlbmN5Bm5ld19pZAR6ZXJvDHNoYXJlX29iamVjdARqb2luAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgMIAAAAAAAAAAAAAgEOAQECBA8IBhELBwEIABMLAgEIAxQLAgEIBAABAAAIFg4BOAAMBAsBCgAPADgBCgAPAQoECgI4AgwFCwAPAgsECwI4AwwDCwULAwIBAAAAEA8JEgAKADgEDAEKABEKCwEKADgFCwA4BhIBOAcCAgEAAAMECwAQAjgIAgMBAAAFHA4BOAkMBAoEDgI4CCEDDgsAAQsDAQcAJwoADwELATgKCgAPAgsCOAsLBAsADwALAzgMAgQBAAADBAsAEAE4CQIFAQAAAwQLABAAOA0CAQEBAgEDAA=="] +Build Successful +``` + +Copy the outputting base64 representation of the compiled Move module into the +REST publish endpoint. + +#### 2, Sign the transaction +Follow the instructions to [sign the transaction](json-rpc.md#2-sign-the-transaction-using-the-sui-signtool). + +#### 3, Execute the transaction +Follow the instructions to [execute the transaction](rest-api.md#3-execute-the-transaction-using-the-transaction-data-signature-and-public-key). + +Below you can see a truncated sample output of [sui_publish](#sui_publish). One of the results of executing this command is generation of a package object representing the published Move code. An ID of the package object can be used as an argument for subsequent Move calls to functions defined in this package. + +``` +{ + "package": [ + "13e3ec7279060663e1bbc45aaf5859113fc164d2", + ... +} +``` + +## Connect to remote JSON-RPC server + +Coming soon - alternative ways of working with Sui's JSON-RPC API. Connect to Sui devnet, testnet, or mainnet! diff --git a/sui/Cargo.toml b/sui/Cargo.toml index 9eeeeff2d57e9..4e3f23e3d9186 100644 --- a/sui/Cargo.toml +++ b/sui/Cargo.toml @@ -41,6 +41,8 @@ sui-framework = { path = "../sui_programmability/framework" } sui-network = { path = "../network_utils" } sui-types = { path = "../sui_types" } sui-verifier = { path = "../sui_programmability/verifier" } +sui-open-rpc = { path = "open_rpc" } +sui-open-rpc-macros = { path = "open_rpc/macros" } rustyline = "9.1.2" rustyline-derive = "0.6.0" @@ -68,11 +70,11 @@ once_cell = "1.10.0" reqwest = { version = "0.11.10", features = ["json", "serde_json", "blocking"] } jsonrpsee = { version = "0.11.0", features = ["full"] } - jsonrpsee-proc-macros = "0.11.0" [dev-dependencies] tracing-test = "0.2.1" +pretty_assertions = "1.2.0" tokio-util = { version = "0.7.1", features = ["codec"] } test_utils = { path = "../test_utils" } @@ -80,3 +82,8 @@ sui-network = { path = "../network_utils" } [features] benchmark = ["narwhal-node/benchmark"] + +[[example]] +name = "generate-json-rpc-spec" +path = "src/generate_json_rpc_spec.rs" +test = false diff --git a/sui/open_rpc/Cargo.toml b/sui/open_rpc/Cargo.toml new file mode 100644 index 0000000000000..8332f45c8c922 --- /dev/null +++ b/sui/open_rpc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sui-open-rpc" +version = "0.1.0" +authors = ["Mysten Labs "] +license = "Apache-2.0" +publish = false +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +schemars = "0.8.8" +serde = "1.0.136" \ No newline at end of file diff --git a/sui/open_rpc/macros/Cargo.toml b/sui/open_rpc/macros/Cargo.toml new file mode 100644 index 0000000000000..cb40bdb810934 --- /dev/null +++ b/sui/open_rpc/macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sui-open-rpc-macros" +version = "0.1.0" +authors = ["Mysten Labs "] +license = "Apache-2.0" +publish = false +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +itertools = "0.10.3" +derive-syn-parse = "0.1.5" \ No newline at end of file diff --git a/sui/open_rpc/macros/src/lib.rs b/sui/open_rpc/macros/src/lib.rs new file mode 100644 index 0000000000000..19529ffdbba78 --- /dev/null +++ b/sui/open_rpc/macros/src/lib.rs @@ -0,0 +1,309 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use proc_macro::TokenStream; + +use derive_syn_parse::Parse; +use itertools::Itertools; +use proc_macro2::{Ident, TokenTree}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Paren; +use syn::{ + parse, parse_macro_input, Attribute, GenericArgument, LitStr, PatType, Path, PathArguments, + Token, TraitItem, Type, +}; + +#[proc_macro_attribute] +/// Add a OpenRpc struct and implementation providing access to Open RPC doc builder. +/// This proc macro must be use in conjunction with `jsonrpsee_proc_macro::rpc` +/// +/// The generated method `open_rpc` is added to OpenRpc, +/// ideally we want to add this to the trait generated by jsonrpsee framework, creating a new struct +/// to provide access to the method is a workaround. +/// +/// TODO: consider contributing the open rpc doc macro to jsonrpsee to simplify the logics. +pub fn open_rpc(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr: OpenRpcAttributes = parse_macro_input!(attr); + + let mut trait_data: syn::ItemTrait = syn::parse(item).unwrap(); + let rpc_definition = parse_rpc_method(&mut trait_data).unwrap(); + let mut methods = Vec::new(); + for method in rpc_definition.methods { + let name = method.name; + let doc = method.doc; + let mut inputs = Vec::new(); + for (name, ty) in method.params { + let (ty, required) = extract_type_from_option(ty); + inputs.push(quote! { + let des = builder.create_content_descriptor::<#ty>(#name, "", "", #required); + inputs.push(des); + }) + } + let returns_ty = if let Some(ty) = method.returns { + let (ty, required) = extract_type_from_option(ty); + let name = quote! {#ty}.to_string(); + quote! {Some(builder.create_content_descriptor::<#ty>(#name, "", "", #required));} + } else { + quote! {None;} + }; + methods.push(quote! { + let mut inputs: Vec = Vec::new(); + #(#inputs)* + let result = #returns_ty + builder.add_method(#name, inputs, result, #doc); + }) + } + let open_rpc_name = quote::format_ident!("{}OpenRpc", &rpc_definition.name); + + let url = attr.find_attr("license_url").to_quote(); + + let license = attr + .find_attr("license") + .unwrap_quote(|license| quote! (builder.set_license(#license, #url);)); + + let contact_url = attr.find_attr("contact_url").to_quote(); + let contact_email = attr.find_attr("contact_email").to_quote(); + let contact = attr.find_attr("contact_name").unwrap_quote( + |contact| quote! (builder.set_contact(#contact, #contact_url, #contact_email);), + ); + let description = attr + .find_attr("description") + .unwrap_quote(|description| quote! (builder.set_description(#description);)); + + let proj_name = attr + .find_attr("name") + .map(|str| str.value()) + .unwrap_or_default(); + let namespace = attr + .find_attr("namespace") + .map(|str| str.value()) + .unwrap_or_default(); + + quote! { + #trait_data + pub struct #open_rpc_name; + impl #open_rpc_name { + pub fn open_rpc() -> sui_open_rpc::Project{ + let mut builder = sui_open_rpc::ProjectBuilder::new(#proj_name, #namespace); + #license + #contact + #description + #(#methods)* + builder.build() + } + } + } + .into() +} + +trait OptionalQuote { + fn to_quote(&self) -> TokenStream2; + + fn unwrap_quote(&self, quote: F) -> TokenStream2 + where + F: FnOnce(LitStr) -> TokenStream2; +} + +impl OptionalQuote for Option { + fn to_quote(&self) -> TokenStream2 { + if let Some(value) = self { + quote!(Some(#value.to_string())) + } else { + quote!(None) + } + } + + fn unwrap_quote(&self, quote: F) -> TokenStream2 + where + F: FnOnce(LitStr) -> TokenStream2, + { + if let Some(lit_str) = self { + quote(lit_str.clone()) + } else { + quote!() + } + } +} + +struct RpcDefinition { + name: Ident, + methods: Vec, +} +struct Method { + name: String, + params: Vec<(String, Type)>, + returns: Option, + doc: String, +} + +fn parse_rpc_method(trait_data: &mut syn::ItemTrait) -> Result { + let mut methods = Vec::new(); + for trait_item in &mut trait_data.items { + if let TraitItem::Method(method) = trait_item { + let method_name = if let Some(attr) = find_attr(&method.attrs, "method").cloned() { + let token: TokenStream = attr.tokens.clone().into(); + parse::(token)?.value.value() + } else { + "Unknown method name".to_string() + }; + + let doc = extract_doc_comments(&method.attrs).to_string(); + + let params: Vec<_> = method + .sig + .inputs + .iter_mut() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(arg) => match *arg.pat.clone() { + syn::Pat::Ident(name) => { + Some(get_type(arg).map(|ty| (name.ident.to_string(), ty))) + } + syn::Pat::Wild(wild) => Some(Err(syn::Error::new( + wild.underscore_token.span(), + "Method argument names must be valid Rust identifiers; got `_` instead", + ))), + _ => Some(Err(syn::Error::new( + arg.span(), + format!("Unexpected method signature input; got {:?} ", *arg.pat), + ))), + }, + }) + .collect::>()?; + + let returns = match &method.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, output) => extract_type_from(&*output, "RpcResult"), + }; + methods.push(Method { + name: method_name, + params, + returns, + doc, + }); + } + } + Ok(RpcDefinition { + name: trait_data.ident.clone(), + methods, + }) +} + +fn extract_type_from(ty: &Type, from_ty: &str) -> Option { + fn path_is(path: &Path, from_ty: &str) -> bool { + path.leading_colon.is_none() + && path.segments.len() == 1 + && path.segments.iter().next().unwrap().ident == from_ty + } + + if let Type::Path(p) = ty { + if p.qself.is_none() && path_is(&p.path, from_ty) { + if let PathArguments::AngleBracketed(a) = &p.path.segments[0].arguments { + if let Some(GenericArgument::Type(ty)) = a.args.first() { + return Some(ty.clone()); + } + } + } + } + None +} + +fn extract_type_from_option(ty: Type) -> (Type, bool) { + if let Some(ty) = extract_type_from(&ty, "Option") { + (ty, false) + } else { + (ty, true) + } +} + +fn get_type(pat_type: &mut PatType) -> Result { + Ok( + if let Some((pos, attr)) = pat_type + .attrs + .iter() + .find_position(|a| a.path.is_ident("schemars")) + { + let attribute = parse::(attr.tokens.clone().into())?; + + let stream = syn::parse_str(&attribute.value.value())?; + let tokens = respan_token_stream(stream, attribute.value.span()); + + let path = syn::parse2(tokens)?; + pat_type.attrs.remove(pos); + path + } else { + pat_type.ty.as_ref().clone() + }, + ) +} + +fn find_attr<'a>(attrs: &'a [Attribute], ident: &str) -> Option<&'a Attribute> { + attrs.iter().find(|a| a.path.is_ident(ident)) +} + +fn respan_token_stream(stream: TokenStream2, span: Span) -> TokenStream2 { + stream + .into_iter() + .map(|mut token| { + if let TokenTree::Group(g) = &mut token { + *g = proc_macro2::Group::new(g.delimiter(), respan_token_stream(g.stream(), span)); + } + token.set_span(span); + token + }) + .collect() +} + +fn extract_doc_comments(attrs: &[syn::Attribute]) -> String { + attrs + .iter() + .filter(|attr| { + attr.path.is_ident("doc") + && match attr.parse_meta() { + Ok(syn::Meta::NameValue(meta)) => matches!(&meta.lit, syn::Lit::Str(_)), + _ => false, + } + }) + .map(|attr| { + let s = attr.tokens.to_string(); + s[4..s.len() - 1].to_string() + }) + .join(" ") +} + +#[derive(Parse, Debug)] +struct OpenRpcAttributes { + #[parse_terminated(OpenRpcAttribute::parse)] + fields: Punctuated, +} + +impl OpenRpcAttributes { + fn find_attr(&self, name: &str) -> Option { + self.fields + .iter() + .find(|attr| attr.label == name) + .map(|attr| attr.value.clone()) + } +} + +#[derive(Parse, Debug)] +struct OpenRpcAttribute { + label: syn::Ident, + _eq_token: Token![=], + value: syn::LitStr, +} + +#[derive(Parse, Debug)] +struct NamedAttribute { + #[paren] + _paren_token: Paren, + #[inside(_paren_token)] + _ident: Ident, + #[inside(_paren_token)] + _eq_token: Token![=], + #[inside(_paren_token)] + value: syn::LitStr, +} diff --git a/sui/open_rpc/spec/openrpc.json b/sui/open_rpc/spec/openrpc.json new file mode 100644 index 0000000000000..1d414cf37ce71 --- /dev/null +++ b/sui/open_rpc/spec/openrpc.json @@ -0,0 +1,2107 @@ +{ + "openrpc": "1.2.6", + "info": { + "title": "Sui JSON-RPC", + "description": "Sui JSON-RPC API for interaction with the Sui network gateway.", + "contact": { + "name": "Mysten Labs", + "url": "https://mystenlabs.com", + "email": "build@mystenlabs.com" + }, + "license": { + "name": "Apache-2.0", + "url": "https://raw.githubusercontent.com/MystenLabs/sui/main/LICENSE" + }, + "version": "0.1.0" + }, + "methods": [ + { + "name": "sui_getObjectTypedInfo", + "description": "Return the object information for a specified object", + "params": [ + { + "name": "object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + } + ], + "result": { + "name": "GetObjectInfoResponse", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/GetObjectInfoResponse" + } + } + }, + { + "name": "sui_transferCoin", + "description": "Create a transaction to transfer a Sui coin from one address to another.", + "params": [ + { + "name": "signer", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_payment", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_budget", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "recipient", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + } + ], + "result": { + "name": "TransactionBytes", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, + { + "name": "sui_moveCall", + "description": "Execute a Move call transaction by calling the specified function in the module of a given package.", + "params": [ + { + "name": "signer", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "package_object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "module", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/Identifier" + } + }, + { + "name": "function", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/Identifier" + } + }, + { + "name": "type_arguments", + "summary": "", + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeTag" + } + } + }, + { + "name": "arguments", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RpcCallArg" + } + } + }, + { + "name": "gas_object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_budget", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "TransactionBytes", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, + { + "name": "sui_publish", + "description": "Publish Move module.", + "params": [ + { + "name": "sender", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "compiled_modules", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Base64" + } + } + }, + { + "name": "gas_object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_budget", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "TransactionBytes", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, + { + "name": "sui_splitCoin", + "description": "", + "params": [ + { + "name": "signer", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "coin_object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "split_amounts", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "gas_payment", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_budget", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "TransactionBytes", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, + { + "name": "sui_mergeCoins", + "description": "", + "params": [ + { + "name": "signer", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "primary_coin", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "coin_to_merge", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_payment", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + }, + { + "name": "gas_budget", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "TransactionBytes", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, + { + "name": "sui_executeTransaction", + "description": "Execute the transaction using the transaction data, signature and public key.", + "params": [ + { + "name": "signed_transaction", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SignedTransaction" + } + } + ], + "result": { + "name": "TransactionResponse", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionResponse" + } + } + }, + { + "name": "sui_syncAccountState", + "description": "Synchronize client state with validators.", + "params": [ + { + "name": "address", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + } + ], + "result": { + "name": "()", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "null" + } + } + }, + { + "name": "sui_getOwnedObjects", + "description": "Return the list of objects owned by an address.", + "params": [ + { + "name": "owner", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + } + ], + "result": { + "name": "ObjectResponse", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectResponse" + } + } + }, + { + "name": "sui_getTotalTransactionNumber", + "description": "", + "params": [], + "result": { + "name": "u64", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "sui_getTransactionsInRange", + "description": "", + "params": [ + { + "name": "start", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "end", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "Vec < (GatewayTxSeqNumber, TransactionDigest) >", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "$ref": "#/components/schemas/TransactionDigest" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + { + "name": "sui_getRecentTransactions", + "description": "", + "params": [ + { + "name": "count", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "Vec < (GatewayTxSeqNumber, TransactionDigest) >", + "summary": "", + "description": "", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "$ref": "#/components/schemas/TransactionDigest" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + { + "name": "sui_getTransaction", + "description": "", + "params": [ + { + "name": "digest", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionDigest" + } + } + ], + "result": { + "name": "CertifiedTransaction", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/CertifiedTransaction" + } + } + }, + { + "name": "sui_getObjectInfoRaw", + "description": "Low level API to get object info. Client Applications should prefer to use `get_object_typed_info` instead.", + "params": [ + { + "name": "object_id", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + } + ], + "result": { + "name": "ObjectRead", + "summary": "", + "description": "", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectRead" + } + } + } + ], + "components": { + "contentDescriptors": {}, + "schemas": { + "AccountAddress": { + "$ref": "#/components/schemas/Hex" + }, + "AuthoritySignature": { + "description": "A signature emitted by an authority. It's useful to decouple this from user signatures, as their set of supported schemes will probably diverge", + "allOf": [ + { + "$ref": "#/components/schemas/Base64" + } + ] + }, + "Base64": { + "type": "string" + }, + "CallArg": { + "oneOf": [ + { + "type": "object", + "required": [ + "Pure" + ], + "properties": { + "Pure": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ImmOrOwnedObject" + ], + "properties": { + "ImmOrOwnedObject": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "SharedObject" + ], + "properties": { + "SharedObject": { + "$ref": "#/components/schemas/ObjectID" + } + }, + "additionalProperties": false + } + ] + }, + "CertifiedTransaction": { + "description": "An transaction signed by a quorum of authorities\n\nNote: the signature set of this data structure is not necessarily unique in the system, i.e. there can be several valid certificates per transaction.\n\nAs a consequence, we check this struct does not implement Hash or Eq, see the note below.", + "type": "object", + "required": [ + "epoch", + "signatures", + "transaction" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "signatures": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/PublicKeyBytes" + }, + { + "$ref": "#/components/schemas/AuthoritySignature" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "transaction": { + "$ref": "#/components/schemas/TransactionEnvelope_for_EmptySignInfo" + } + } + }, + "Data": { + "oneOf": [ + { + "description": "An object whose governing logic lives in a published Move module", + "type": "object", + "required": [ + "Move" + ], + "properties": { + "Move": { + "$ref": "#/components/schemas/MoveObject" + } + }, + "additionalProperties": false + }, + { + "description": "Map from each module name to raw serialized Move module bytes", + "type": "object", + "required": [ + "Package" + ], + "properties": { + "Package": { + "$ref": "#/components/schemas/MovePackage" + } + }, + "additionalProperties": false + } + ] + }, + "EmptySignInfo": { + "type": "object" + }, + "Event": { + "description": "User-defined event emitted by executing Move code. Executing a transaction produces an ordered log of these", + "type": "object", + "required": [ + "contents", + "type_" + ], + "properties": { + "contents": { + "type": "string" + }, + "type_": { + "$ref": "#/components/schemas/StructTag" + } + } + }, + "ExecutionStatus": { + "oneOf": [ + { + "type": "object", + "required": [ + "Success" + ], + "properties": { + "Success": { + "type": "object", + "required": [ + "gas_cost" + ], + "properties": { + "gas_cost": { + "$ref": "#/components/schemas/GasCostSummary" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Failure" + ], + "properties": { + "Failure": { + "type": "object", + "required": [ + "error", + "gas_cost" + ], + "properties": { + "error": { + "type": "string" + }, + "gas_cost": { + "$ref": "#/components/schemas/GasCostSummary" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "GasCostSummary": { + "type": "object", + "required": [ + "computation_cost", + "storage_cost", + "storage_rebate" + ], + "properties": { + "computation_cost": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "storage_cost": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "storage_rebate": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "GetObjectInfoResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "details", + "status" + ], + "properties": { + "details": { + "$ref": "#/components/schemas/ObjectExistsResponse" + }, + "status": { + "type": "string", + "enum": [ + "Exists" + ] + } + } + }, + { + "type": "object", + "required": [ + "details", + "status" + ], + "properties": { + "details": { + "$ref": "#/components/schemas/ObjectNotExistsResponse" + }, + "status": { + "type": "string", + "enum": [ + "NotExists" + ] + } + } + }, + { + "type": "object", + "required": [ + "details", + "status" + ], + "properties": { + "details": { + "$ref": "#/components/schemas/NamedObjectRef" + }, + "status": { + "type": "string", + "enum": [ + "Deleted" + ] + } + } + } + ] + }, + "Hex": { + "type": "string" + }, + "Identifier": { + "type": "string" + }, + "MergeCoinResponse": { + "type": "object", + "required": [ + "certificate", + "updated_coin", + "updated_gas" + ], + "properties": { + "certificate": { + "description": "Certificate of the transaction", + "allOf": [ + { + "$ref": "#/components/schemas/CertifiedTransaction" + } + ] + }, + "updated_coin": { + "description": "The updated original coin object after merge", + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + }, + "updated_gas": { + "description": "The updated gas payment object after deducting payment", + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + } + }, + "MoveCall": { + "type": "object", + "required": [ + "arguments", + "function", + "module", + "package", + "type_arguments" + ], + "properties": { + "arguments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CallArg" + } + }, + "function": { + "$ref": "#/components/schemas/Identifier" + }, + "module": { + "$ref": "#/components/schemas/Identifier" + }, + "package": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + "type_arguments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeTag" + } + } + } + }, + "MoveFieldLayout": { + "type": "object", + "required": [ + "layout", + "name" + ], + "properties": { + "layout": { + "$ref": "#/components/schemas/MoveTypeLayout" + }, + "name": { + "$ref": "#/components/schemas/Identifier" + } + } + }, + "MoveModulePublish": { + "type": "object", + "required": [ + "modules" + ], + "properties": { + "modules": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "MoveObject": { + "type": "object", + "required": [ + "contents", + "type_" + ], + "properties": { + "contents": { + "$ref": "#/components/schemas/Base64" + }, + "type_": { + "$ref": "#/components/schemas/StructTag" + } + } + }, + "MoveObjectType": { + "type": "string", + "enum": [ + "moveObject", + "movePackage" + ] + }, + "MovePackage": { + "type": "object", + "required": [ + "id", + "module_map" + ], + "properties": { + "id": { + "$ref": "#/components/schemas/ObjectID" + }, + "module_map": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "MoveStructLayout": { + "oneOf": [ + { + "type": "object", + "required": [ + "Runtime" + ], + "properties": { + "Runtime": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoveTypeLayout" + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "WithFields" + ], + "properties": { + "WithFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoveFieldLayout" + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "WithTypes" + ], + "properties": { + "WithTypes": { + "type": "object", + "required": [ + "fields", + "type_" + ], + "properties": { + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoveFieldLayout" + } + }, + "type_": { + "$ref": "#/components/schemas/StructTag" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "MoveTypeLayout": { + "oneOf": [ + { + "type": "string", + "enum": [ + "bool", + "u8", + "u64", + "u128", + "address", + "signer" + ] + }, + { + "type": "object", + "required": [ + "vector" + ], + "properties": { + "vector": { + "$ref": "#/components/schemas/MoveTypeLayout" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "struct" + ], + "properties": { + "struct": { + "$ref": "#/components/schemas/MoveStructLayout" + } + }, + "additionalProperties": false + } + ] + }, + "NamedObjectRef": { + "type": "object", + "required": [ + "digest", + "objectId", + "version" + ], + "properties": { + "digest": { + "description": "Base64 string representing the object digest", + "type": "string" + }, + "objectId": { + "description": "Hex code as string representing the object id", + "type": "string" + }, + "version": { + "description": "Object version.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "Object": { + "type": "object", + "required": [ + "data", + "owner", + "previous_transaction", + "storage_rebate" + ], + "properties": { + "data": { + "description": "The meat of the object", + "allOf": [ + { + "$ref": "#/components/schemas/Data" + } + ] + }, + "owner": { + "description": "The owner that unlocks this object", + "allOf": [ + { + "$ref": "#/components/schemas/Owner" + } + ] + }, + "previous_transaction": { + "description": "The digest of the transaction that created or last mutated this object", + "allOf": [ + { + "$ref": "#/components/schemas/TransactionDigest" + } + ] + }, + "storage_rebate": { + "description": "The amount of SUI we would rebate if this object gets deleted. This number is re-calculated each time the object is mutated based on the present storage gas price.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "ObjectDigest": { + "$ref": "#/components/schemas/Base64" + }, + "ObjectExistsResponse": { + "type": "object", + "required": [ + "object", + "objectRef", + "objectType" + ], + "properties": { + "object": true, + "objectRef": { + "$ref": "#/components/schemas/NamedObjectRef" + }, + "objectType": { + "$ref": "#/components/schemas/MoveObjectType" + } + } + }, + "ObjectID": { + "$ref": "#/components/schemas/Hex" + }, + "ObjectNotExistsResponse": { + "type": "object", + "required": [ + "objectId" + ], + "properties": { + "objectId": { + "type": "string" + } + } + }, + "ObjectRead": { + "oneOf": [ + { + "type": "object", + "required": [ + "details", + "status" + ], + "properties": { + "details": { + "$ref": "#/components/schemas/ObjectID" + }, + "status": { + "type": "string", + "enum": [ + "NotExists" + ] + } + } + }, + { + "type": "object", + "required": [ + "details", + "status" + ], + "properties": { + "details": { + "type": "array", + "items": [ + { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + { + "$ref": "#/components/schemas/Object" + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/MoveStructLayout" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 3, + "minItems": 3 + }, + "status": { + "type": "string", + "enum": [ + "Exists" + ] + } + } + }, + { + "type": "object", + "required": [ + "details", + "status" + ], + "properties": { + "details": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + "status": { + "type": "string", + "enum": [ + "Deleted" + ] + } + } + } + ] + }, + "ObjectResponse": { + "type": "object", + "required": [ + "objects" + ], + "properties": { + "objects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NamedObjectRef" + } + } + } + }, + "Owner": { + "oneOf": [ + { + "type": "string", + "enum": [ + "Shared", + "Immutable" + ] + }, + { + "description": "Object is exclusively owned by a single address, and is mutable.", + "type": "object", + "required": [ + "AddressOwner" + ], + "properties": { + "AddressOwner": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + "additionalProperties": false + }, + { + "description": "Object is exclusively owned by a single object, and is mutable. The object ID is converted to SuiAddress as SuiAddress is universal.", + "type": "object", + "required": [ + "ObjectOwner" + ], + "properties": { + "ObjectOwner": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + "additionalProperties": false + } + ] + }, + "PublicKeyBytes": { + "$ref": "#/components/schemas/Base64" + }, + "PublishResponse": { + "type": "object", + "required": [ + "certificate", + "created_objects", + "package", + "updated_gas" + ], + "properties": { + "certificate": { + "description": "Certificate of the transaction", + "allOf": [ + { + "$ref": "#/components/schemas/CertifiedTransaction" + } + ] + }, + "created_objects": { + "description": "List of Move objects created as part of running the module initializers in the package", + "type": "array", + "items": { + "$ref": "#/components/schemas/Object" + } + }, + "package": { + "description": "The newly published package object reference.", + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + "updated_gas": { + "description": "The updated gas payment object after deducting payment", + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + } + }, + "RpcCallArg": { + "oneOf": [ + { + "type": "object", + "required": [ + "Pure" + ], + "properties": { + "Pure": { + "$ref": "#/components/schemas/Base64" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ImmOrOwnedObject" + ], + "properties": { + "ImmOrOwnedObject": { + "$ref": "#/components/schemas/ObjectID" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "SharedObject" + ], + "properties": { + "SharedObject": { + "$ref": "#/components/schemas/ObjectID" + } + }, + "additionalProperties": false + } + ] + }, + "SequenceNumber": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Signature": { + "$ref": "#/components/schemas/Base64" + }, + "SignedTransaction": { + "type": "object", + "required": [ + "pub_key", + "signature", + "tx_bytes" + ], + "properties": { + "pub_key": { + "$ref": "#/components/schemas/Base64" + }, + "signature": { + "$ref": "#/components/schemas/Base64" + }, + "tx_bytes": { + "$ref": "#/components/schemas/Base64" + } + } + }, + "SingleTransactionKind": { + "oneOf": [ + { + "description": "Initiate an object transfer between addresses", + "type": "object", + "required": [ + "Transfer" + ], + "properties": { + "Transfer": { + "$ref": "#/components/schemas/Transfer" + } + }, + "additionalProperties": false + }, + { + "description": "Publish a new Move module", + "type": "object", + "required": [ + "Publish" + ], + "properties": { + "Publish": { + "$ref": "#/components/schemas/MoveModulePublish" + } + }, + "additionalProperties": false + }, + { + "description": "Call a function in a published Move module", + "type": "object", + "required": [ + "Call" + ], + "properties": { + "Call": { + "$ref": "#/components/schemas/MoveCall" + } + }, + "additionalProperties": false + } + ] + }, + "SplitCoinResponse": { + "type": "object", + "required": [ + "certificate", + "new_coins", + "updated_coin", + "updated_gas" + ], + "properties": { + "certificate": { + "description": "Certificate of the transaction", + "allOf": [ + { + "$ref": "#/components/schemas/CertifiedTransaction" + } + ] + }, + "new_coins": { + "description": "All the newly created coin objects generated from the split", + "type": "array", + "items": { + "$ref": "#/components/schemas/Object" + } + }, + "updated_coin": { + "description": "The updated original coin object after split", + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + }, + "updated_gas": { + "description": "The updated gas payment object after deducting payment", + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + } + }, + "StructTag": { + "type": "object", + "required": [ + "address", + "module", + "name", + "type_args" + ], + "properties": { + "address": { + "$ref": "#/components/schemas/AccountAddress" + }, + "module": { + "$ref": "#/components/schemas/Identifier" + }, + "name": { + "$ref": "#/components/schemas/Identifier" + }, + "type_args": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeTag" + } + } + } + }, + "SuiAddress": { + "$ref": "#/components/schemas/Hex" + }, + "TransactionBytes": { + "type": "object", + "required": [ + "tx_bytes" + ], + "properties": { + "tx_bytes": { + "$ref": "#/components/schemas/Base64" + } + } + }, + "TransactionData": { + "type": "object", + "required": [ + "gas_budget", + "gas_payment", + "kind", + "sender" + ], + "properties": { + "gas_budget": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "gas_payment": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + "kind": { + "$ref": "#/components/schemas/TransactionKind" + }, + "sender": { + "$ref": "#/components/schemas/SuiAddress" + } + } + }, + "TransactionDigest": { + "description": "A transaction will have a (unique) digest.", + "allOf": [ + { + "$ref": "#/components/schemas/Base64" + } + ] + }, + "TransactionEffects": { + "description": "The response from processing a transaction or a certified transaction", + "type": "object", + "required": [ + "created", + "deleted", + "dependencies", + "events", + "gas_object", + "mutated", + "shared_objects", + "status", + "transaction_digest", + "unwrapped", + "wrapped" + ], + "properties": { + "created": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + { + "$ref": "#/components/schemas/Owner" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "deleted": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "dependencies": { + "description": "The set of transaction digests this transaction depends on.", + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionDigest" + } + }, + "events": { + "description": "The events emitted during execution. Note that only successful transactions emit events", + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + }, + "gas_object": { + "type": "array", + "items": [ + { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + { + "$ref": "#/components/schemas/Owner" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "mutated": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + { + "$ref": "#/components/schemas/Owner" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "shared_objects": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "status": { + "$ref": "#/components/schemas/ExecutionStatus" + }, + "transaction_digest": { + "$ref": "#/components/schemas/TransactionDigest" + }, + "unwrapped": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + { + "$ref": "#/components/schemas/Owner" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "wrapped": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + } + } + } + }, + "TransactionEnvelope_for_EmptySignInfo": { + "description": "A transaction signed by a client, optionally signed by an authority (depending on `S`). `S` indicates the authority signing state. It can be either empty or signed. We make the authority signature templated so that `TransactionEnvelope` can be used universally in the transactions storage in `SuiDataStore`, shared by both authorities and non-authorities: authorities store signed transactions, while non-authorities store unsigned transactions.", + "type": "object", + "required": [ + "auth_signature", + "data", + "tx_signature" + ], + "properties": { + "auth_signature": { + "description": "auth_signature, if available, is signed by an authority, applied on `data`.", + "allOf": [ + { + "$ref": "#/components/schemas/EmptySignInfo" + } + ] + }, + "data": { + "$ref": "#/components/schemas/TransactionData" + }, + "tx_signature": { + "description": "tx_signature is signed by the transaction sender, applied on `data`.", + "allOf": [ + { + "$ref": "#/components/schemas/Signature" + } + ] + } + } + }, + "TransactionKind": { + "oneOf": [ + { + "description": "A single transaction.", + "type": "object", + "required": [ + "Single" + ], + "properties": { + "Single": { + "$ref": "#/components/schemas/SingleTransactionKind" + } + }, + "additionalProperties": false + }, + { + "description": "A batch of single transactions.", + "type": "object", + "required": [ + "Batch" + ], + "properties": { + "Batch": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SingleTransactionKind" + } + } + }, + "additionalProperties": false + } + ] + }, + "TransactionResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "EffectResponse" + ], + "properties": { + "EffectResponse": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/CertifiedTransaction" + }, + { + "$ref": "#/components/schemas/TransactionEffects" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "PublishResponse" + ], + "properties": { + "PublishResponse": { + "$ref": "#/components/schemas/PublishResponse" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "MergeCoinResponse" + ], + "properties": { + "MergeCoinResponse": { + "$ref": "#/components/schemas/MergeCoinResponse" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "SplitCoinResponse" + ], + "properties": { + "SplitCoinResponse": { + "$ref": "#/components/schemas/SplitCoinResponse" + } + }, + "additionalProperties": false + } + ] + }, + "Transfer": { + "type": "object", + "required": [ + "object_ref", + "recipient" + ], + "properties": { + "object_ref": { + "type": "array", + "items": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "$ref": "#/components/schemas/SequenceNumber" + }, + { + "$ref": "#/components/schemas/ObjectDigest" + } + ], + "maxItems": 3, + "minItems": 3 + }, + "recipient": { + "$ref": "#/components/schemas/SuiAddress" + } + } + }, + "TypeTag": { + "oneOf": [ + { + "type": "string", + "enum": [ + "bool", + "u8", + "u64", + "u128", + "address", + "signer" + ] + }, + { + "type": "object", + "required": [ + "vector" + ], + "properties": { + "vector": { + "$ref": "#/components/schemas/TypeTag" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "struct" + ], + "properties": { + "struct": { + "$ref": "#/components/schemas/StructTag" + } + }, + "additionalProperties": false + } + ] + } + } + } +} diff --git a/sui/open_rpc/src/lib.rs b/sui/open_rpc/src/lib.rs new file mode 100644 index 0000000000000..6dd5f30496fbd --- /dev/null +++ b/sui/open_rpc/src/lib.rs @@ -0,0 +1,201 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use schemars::gen::{SchemaGenerator, SchemaSettings}; +use schemars::schema::SchemaObject; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// OPEN-RPC documentation following the OpenRPC specification https://spec.open-rpc.org +/// The implementation is partial, only required fields and subset of optional fields +/// in the specification are implemented catered to Sui's need. +#[derive(Serialize, Deserialize, Clone)] +pub struct Project { + openrpc: String, + info: Info, + methods: Vec, + components: Components, +} + +pub struct ProjectBuilder { + proj_name: String, + namespace: String, + version: String, + openrpc: String, + schema_generator: SchemaGenerator, + methods: Vec, + content_descriptors: BTreeMap, + license: Option, + contact: Option, + description: Option, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct ContentDescriptor { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(skip_serializing_if = "default")] + required: bool, + schema: SchemaObject, + #[serde(skip_serializing_if = "default")] + deprecated: bool, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +struct Method { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + params: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "camelCase")] +struct Info { + title: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + terms_of_service: Option, + #[serde(skip_serializing_if = "Option::is_none")] + contact: Option, + #[serde(skip_serializing_if = "Option::is_none")] + license: Option, + version: String, +} + +fn default(value: &T) -> bool +where + T: Default + PartialEq, +{ + value == &T::default() +} + +#[derive(Serialize, Deserialize, Default, Clone)] +struct Contact { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, +} +#[derive(Serialize, Deserialize, Default, Clone)] +struct License { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, +} + +impl ProjectBuilder { + pub fn new(proj_name: &str, namespace: &str) -> Self { + let schema_generator = SchemaSettings::default() + .with(|s| { + s.definitions_path = "#/components/schemas/".to_string(); + }) + .into_generator(); + + Self { + proj_name: proj_name.to_string(), + namespace: namespace.to_string(), + version: env!("CARGO_PKG_VERSION").to_owned(), + openrpc: "1.2.6".to_string(), + schema_generator, + methods: vec![], + content_descriptors: BTreeMap::new(), + license: None, + contact: None, + description: None, + } + } + + pub fn build(mut self) -> Project { + Project { + openrpc: self.openrpc, + info: Info { + title: self.proj_name, + version: self.version, + license: self.license, + contact: self.contact, + description: self.description, + ..Default::default() + }, + methods: self.methods, + components: Components { + content_descriptors: self.content_descriptors, + schemas: self + .schema_generator + .root_schema_for::() + .definitions + .into_iter() + .map(|(name, schema)| (name, schema.into_object())) + .collect::>(), + }, + } + } + + pub fn set_description(&mut self, description: &str) { + self.description = Some(description.to_string()); + } + + pub fn set_contact(&mut self, name: &str, url: Option, email: Option) { + self.contact = Some(Contact { + name: name.to_string(), + url, + email, + }); + } + + pub fn set_license(&mut self, license: &str, url: Option) { + self.license = Some(License { + name: license.to_string(), + url, + }); + } + + pub fn add_method( + &mut self, + name: &str, + params: Vec, + result: Option, + doc: &str, + ) { + self.methods.push(Method { + name: format!("{}_{}", self.namespace, name), + description: Some(doc.to_string()), + params, + result, + }) + } + + pub fn create_content_descriptor( + &mut self, + name: &str, + summary: &str, + description: &str, + required: bool, + ) -> ContentDescriptor { + let schema = self.schema_generator.subschema_for::().into_object(); + ContentDescriptor { + name: name.to_string(), + summary: Some(summary.to_string()), + description: Some(description.to_string()), + required, + schema, + deprecated: false, + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct Components { + content_descriptors: BTreeMap, + schemas: BTreeMap, +} diff --git a/sui/src/bin/rpc-server.rs b/sui/src/bin/rpc-server.rs index 7c1f116ea514b..141057177837e 100644 --- a/sui/src/bin/rpc-server.rs +++ b/sui/src/bin/rpc-server.rs @@ -1,21 +1,24 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use clap::Parser; -use jsonrpsee::{ - http_server::{AccessControlBuilder, HttpServerBuilder}, - RpcModule, -}; use std::{ env, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, }; + +use clap::Parser; +use jsonrpsee::{ + http_server::{AccessControlBuilder, HttpServerBuilder}, + RpcModule, +}; +use tracing::info; + +use sui::rpc_gateway::RpcGatewayOpenRpc; use sui::{ rpc_gateway::{RpcGatewayImpl, RpcGatewayServer}, sui_config_dir, }; -use tracing::info; const DEFAULT_RPC_SERVER_PORT: &str = "5001"; const DEFAULT_RPC_SERVER_ADDR_IPV4: &str = "127.0.0.1"; @@ -66,12 +69,17 @@ async fn main() -> anyhow::Result<()> { ac_builder = ac_builder.set_allowed_origins(list)?; } + let acl = ac_builder.build(); + info!("{:?}", acl); + let server = server_builder - .set_access_control(ac_builder.build()) + .set_access_control(acl) .build(SocketAddr::new(IpAddr::V4(options.host), options.port)) .await?; let mut module = RpcModule::new(()); + let open_rpc = RpcGatewayOpenRpc::open_rpc(); + module.register_method("rpc.discover", move |_, _| Ok(open_rpc.clone()))?; module.merge(RpcGatewayImpl::new(&config_path)?.into_rpc())?; info!( diff --git a/sui/src/generate_json_rpc_spec.rs b/sui/src/generate_json_rpc_spec.rs new file mode 100644 index 0000000000000..a304b1e1f489f --- /dev/null +++ b/sui/src/generate_json_rpc_spec.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::io::Write; + +use clap::ArgEnum; +use clap::Parser; +use pretty_assertions::assert_str_eq; + +use sui::rpc_gateway::RpcGatewayOpenRpc; + +#[derive(Debug, Parser, Clone, Copy, ArgEnum)] +enum Action { + Print, + Test, + Record, +} + +#[derive(Debug, Parser)] +#[clap( + name = "Sui format generator", + about = "Trace serde (de)serialization to generate format descriptions for Sui types" +)] +struct Options { + #[clap(arg_enum, default_value = "Print", ignore_case = true)] + action: Action, +} + +const FILE_PATH: &str = "sui/open_rpc/spec/openrpc.json"; + +fn main() { + let options = Options::parse(); + let open_rpc = RpcGatewayOpenRpc::open_rpc(); + match options.action { + Action::Print => { + let content = serde_json::to_string_pretty(&open_rpc).unwrap(); + println!("{content}"); + } + Action::Record => { + let content = serde_json::to_string_pretty(&open_rpc).unwrap(); + let mut f = File::create(FILE_PATH).unwrap(); + writeln!(f, "{}", content).unwrap(); + } + Action::Test => { + let reference = std::fs::read_to_string(FILE_PATH).unwrap(); + let content = serde_json::to_string_pretty(&open_rpc).unwrap() + "\n"; + assert_str_eq!(&reference, &content); + } + } +} diff --git a/sui/src/rpc_gateway.rs b/sui/src/rpc_gateway.rs index 7424c24a28ef6..fbf93db85317c 100644 --- a/sui/src/rpc_gateway.rs +++ b/sui/src/rpc_gateway.rs @@ -1,7 +1,6 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::ops::Deref; use std::path::Path; use anyhow::anyhow; @@ -11,38 +10,54 @@ use jsonrpsee::core::RpcResult; use jsonrpsee_proc_macros::rpc; use move_core_types::identifier::Identifier; use move_core_types::language_storage::TypeTag; +use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use serde_with::base64; use serde_with::serde_as; -use sui_types::messages::CallArg; -use tracing::debug; - -use serde_with::base64::Base64; use sui_core::gateway_state::gateway_responses::TransactionResponse; use sui_core::gateway_state::{GatewayClient, GatewayState, GatewayTxSeqNumber}; +use sui_open_rpc_macros::open_rpc; use sui_types::base_types::{ObjectID, SuiAddress, TransactionDigest}; use sui_types::crypto; use sui_types::crypto::SignableBytes; +use sui_types::messages::CallArg; use sui_types::messages::{CertifiedTransaction, Transaction, TransactionData}; use sui_types::object::ObjectRead; +use tracing::debug; + +use sui_types::json_schema; +use sui_types::json_schema::Base64; use crate::config::PersistedConfig; use crate::gateway_config::GatewayConfig; use crate::rest_gateway::responses::GetObjectInfoResponse; use crate::rest_gateway::responses::{NamedObjectRef, ObjectResponse}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, JsonSchema)] pub enum RpcCallArg { - Pure(Base64EncodedBytes), + Pure(json_schema::Base64), ImmOrOwnedObject(ObjectID), SharedObject(ObjectID), } +#[open_rpc( + name = "Sui JSON-RPC", + namespace = "sui", + contact_name = "Mysten Labs", + contact_url = "https://mystenlabs.com", + contact_email = "build@mystenlabs.com", + license = "Apache-2.0", + license_url = "https://raw.githubusercontent.com/MystenLabs/sui/main/LICENSE", + description = "Sui JSON-RPC API for interaction with the Sui network gateway." +)] #[rpc(server, client, namespace = "sui")] pub trait RpcGateway { + /// Return the object information for a specified object #[method(name = "getObjectTypedInfo")] async fn get_object_typed_info(&self, object_id: ObjectID) -> RpcResult; + /// Create a transaction to transfer a Sui coin from one address to another. #[method(name = "transferCoin")] async fn transfer_coin( &self, @@ -53,24 +68,28 @@ pub trait RpcGateway { recipient: SuiAddress, ) -> RpcResult; + /// Execute a Move call transaction by calling the specified function in the module of a given package. #[method(name = "moveCall")] async fn move_call( &self, signer: SuiAddress, package_object_id: ObjectID, - module: Identifier, - function: Identifier, - type_arguments: Vec, + #[schemars(with = "json_schema::Identifier")] module: Identifier, + #[schemars(with = "json_schema::Identifier")] function: Identifier, + #[schemars(with = "Option>")] type_arguments: Option< + Vec, + >, arguments: Vec, gas_object_id: ObjectID, gas_budget: u64, ) -> RpcResult; + /// Publish Move module. #[method(name = "publish")] async fn publish( &self, sender: SuiAddress, - compiled_modules: Vec, + compiled_modules: Vec, gas_object_id: ObjectID, gas_budget: u64, ) -> RpcResult; @@ -95,15 +114,18 @@ pub trait RpcGateway { gas_budget: u64, ) -> RpcResult; + /// Execute the transaction using the transaction data, signature and public key. #[method(name = "executeTransaction")] async fn execute_transaction( &self, signed_transaction: SignedTransaction, ) -> RpcResult; + /// Synchronize client state with validators. #[method(name = "syncAccountState")] async fn sync_account_state(&self, address: SuiAddress) -> RpcResult<()>; + /// Return the list of objects owned by an address. #[method(name = "getOwnedObjects")] async fn get_owned_objects(&self, owner: SuiAddress) -> RpcResult; @@ -178,7 +200,7 @@ impl RpcGatewayServer for RpcGatewayImpl { async fn publish( &self, sender: SuiAddress, - compiled_modules: Vec, + compiled_modules: Vec, gas_object_id: ObjectID, gas_budget: u64, ) -> RpcResult { @@ -289,7 +311,7 @@ impl RpcGatewayServer for RpcGatewayImpl { package_object_id: ObjectID, module: Identifier, function: Identifier, - type_arguments: Vec, + type_arguments: Option>, rpc_arguments: Vec, gas_object_id: ObjectID, gas_budget: u64, @@ -326,7 +348,7 @@ impl RpcGatewayServer for RpcGatewayImpl { package_object_ref, module, function, - type_arguments, + type_arguments.unwrap_or_default(), gas_obj_ref, arguments, gas_budget, @@ -370,13 +392,16 @@ impl RpcGatewayServer for RpcGatewayImpl { } #[serde_as] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, JsonSchema)] pub struct SignedTransaction { - #[serde_as(as = "Base64")] + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "base64::Base64")] pub tx_bytes: Vec, - #[serde_as(as = "Base64")] + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "base64::Base64")] pub signature: Vec, - #[serde_as(as = "Base64")] + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "base64::Base64")] pub pub_key: Vec, } @@ -391,9 +416,10 @@ impl SignedTransaction { } #[serde_as] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, JsonSchema)] pub struct TransactionBytes { - #[serde_as(as = "Base64")] + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "base64::Base64")] pub tx_bytes: Vec, } @@ -402,15 +428,3 @@ impl TransactionBytes { TransactionData::from_signable_bytes(&self.tx_bytes) } } - -#[serde_as] -#[derive(Serialize, Deserialize)] -pub struct Base64EncodedBytes(#[serde_as(as = "serde_with::base64::Base64")] pub Vec); - -impl Deref for Base64EncodedBytes { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/sui/src/rpc_gateway_client.rs b/sui/src/rpc_gateway_client.rs index a09f3f6a1930a..b3d0902dc8114 100644 --- a/sui/src/rpc_gateway_client.rs +++ b/sui/src/rpc_gateway_client.rs @@ -3,8 +3,7 @@ use crate::rest_gateway::responses::ObjectResponse; use crate::rpc_gateway::{ - Base64EncodedBytes, RpcCallArg, RpcGatewayClient as RpcGateway, SignedTransaction, - TransactionBytes, + RpcCallArg, RpcGatewayClient as RpcGateway, SignedTransaction, TransactionBytes, }; use anyhow::Error; use async_trait::async_trait; @@ -14,6 +13,7 @@ use move_core_types::language_storage::TypeTag; use sui_core::gateway_state::gateway_responses::TransactionResponse; use sui_core::gateway_state::{GatewayAPI, GatewayTxSeqNumber}; use sui_types::base_types::{ObjectID, ObjectRef, SuiAddress, TransactionDigest}; +use sui_types::json_schema::Base64; use sui_types::messages::{CallArg, CertifiedTransaction, Transaction, TransactionData}; use sui_types::object::ObjectRead; use tokio::runtime::Handle; @@ -76,7 +76,7 @@ impl GatewayAPI for RpcGatewayClient { let arguments = arguments .into_iter() .map(|arg| match arg { - CallArg::Pure(bytes) => RpcCallArg::Pure(Base64EncodedBytes(bytes)), + CallArg::Pure(bytes) => RpcCallArg::Pure(Base64(bytes)), CallArg::ImmOrOwnedObject((id, _, _)) => RpcCallArg::ImmOrOwnedObject(id), CallArg::SharedObject(id) => RpcCallArg::SharedObject(id), }) @@ -89,7 +89,7 @@ impl GatewayAPI for RpcGatewayClient { package_object_ref.0, module, function, - type_arguments, + Some(type_arguments), arguments, gas_object_ref.0, gas_budget, @@ -105,7 +105,7 @@ impl GatewayAPI for RpcGatewayClient { gas_object_ref: ObjectRef, gas_budget: u64, ) -> Result { - let package_bytes = package_bytes.into_iter().map(Base64EncodedBytes).collect(); + let package_bytes = package_bytes.into_iter().map(Base64).collect(); let bytes: TransactionBytes = self .client .publish(signer, package_bytes, gas_object_ref.0, gas_budget) diff --git a/sui/src/unit_tests/rpc_server_tests.rs b/sui/src/unit_tests/rpc_server_tests.rs index 83f628d8b2c01..af78fe0f0ee92 100644 --- a/sui/src/unit_tests/rpc_server_tests.rs +++ b/sui/src/unit_tests/rpc_server_tests.rs @@ -12,8 +12,8 @@ use move_core_types::identifier::Identifier; use sui::config::{PersistedConfig, WalletConfig}; use sui::keystore::{Keystore, SuiKeystore}; use sui::rest_gateway::responses::ObjectResponse; +use sui::rpc_gateway::RpcGatewayClient; use sui::rpc_gateway::TransactionBytes; -use sui::rpc_gateway::{Base64EncodedBytes, RpcGatewayClient}; use sui::rpc_gateway::{RpcCallArg, RpcGatewayServer}; use sui::rpc_gateway::{RpcGatewayImpl, SignedTransaction}; use sui::sui_commands::SuiNetwork; @@ -22,6 +22,7 @@ use sui::{SUI_GATEWAY_CONFIG, SUI_WALLET_CONFIG}; use sui_core::gateway_state::gateway_responses::TransactionResponse; use sui_framework::build_move_package_to_bytes; use sui_types::base_types::{ObjectID, SuiAddress}; +use sui_types::json_schema::Base64; use sui_types::object::ObjectRead; use sui_types::SUI_FRAMEWORK_ADDRESS; @@ -103,7 +104,7 @@ async fn test_publish() -> Result<(), anyhow::Error> { false, )? .into_iter() - .map(Base64EncodedBytes) + .map(Base64) .collect::>(); let tx_data: TransactionBytes = http_client @@ -155,7 +156,7 @@ async fn test_move_call() -> Result<(), anyhow::Error> { let mut args = Vec::with_capacity(json_args.len()); for json_arg in json_args { args.push(match json_arg { - SuiJsonCallArg::Pure(bytes) => RpcCallArg::Pure(Base64EncodedBytes(bytes)), + SuiJsonCallArg::Pure(bytes) => RpcCallArg::Pure(Base64(bytes)), SuiJsonCallArg::Object(id) => match http_client.get_object_info(id).await? { ObjectRead::Exists(_, obj, _) if obj.is_shared() => RpcCallArg::SharedObject(id), _ => RpcCallArg::ImmOrOwnedObject(id), @@ -165,14 +166,7 @@ async fn test_move_call() -> Result<(), anyhow::Error> { let tx_data: TransactionBytes = http_client .move_call( - *address, - package_id, - module, - function, - Vec::new(), - args, - gas.0, - 1000, + *address, package_id, module, function, None, args, gas.0, 1000, ) .await?; diff --git a/sui/tests/generate_json_rpc_spec.rs b/sui/tests/generate_json_rpc_spec.rs new file mode 100644 index 0000000000000..744e038a0d5af --- /dev/null +++ b/sui/tests/generate_json_rpc_spec.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test] +fn test_json_rpc_spec() { + // If this test breaks and you intended a json rpc schema change, you need to run to get the fresh schema: + // # cargo -q run --example generate-json-rpc-spec -- record + let status = std::process::Command::new("cargo") + .current_dir("..") + .args(&["run", "--example", "generate-json-rpc-spec", "--"]) + .arg("test") + .status() + .expect("failed to execute process"); + assert!(status.success()); +} diff --git a/sui_core/Cargo.toml b/sui_core/Cargo.toml index b5cd3bd3eb57b..b488cea67cdea 100644 --- a/sui_core/Cargo.toml +++ b/sui_core/Cargo.toml @@ -42,6 +42,7 @@ move-vm-types = { git = "https://github.com/move-language/move", rev = "4e025186 typed-store = { git = "https://github.com/MystenLabs/mysten-infra", rev = "d2976a45420147ad821baae96e6fe4b12215f743"} narwhal-executor = { git = "https://github.com/MystenLabs/narwhal", rev = "8ae2164f0510349cbac2770e50e853bce5ab0e02", package = "executor" } +schemars = "0.8.8" [dev-dependencies] serde-reflection = "0.3.5" diff --git a/sui_core/src/gateway_state/gateway_responses.rs b/sui_core/src/gateway_state/gateway_responses.rs index 2cb6e5d30d1d3..792f4fbe80db9 100644 --- a/sui_core/src/gateway_state/gateway_responses.rs +++ b/sui_core/src/gateway_state/gateway_responses.rs @@ -8,6 +8,7 @@ use std::fmt::{Display, Formatter}; use serde::ser::Error; use serde::Serialize; +use schemars::JsonSchema; use serde::Deserialize; use sui_types::base_types::{ObjectRef, SuiAddress}; use sui_types::error::SuiError; @@ -15,7 +16,7 @@ use sui_types::gas_coin::GasCoin; use sui_types::messages::{CertifiedTransaction, TransactionEffects}; use sui_types::object::Object; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] pub enum TransactionResponse { EffectResponse(CertifiedTransaction, TransactionEffects), PublishResponse(PublishResponse), @@ -55,7 +56,7 @@ impl TransactionResponse { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct SplitCoinResponse { /// Certificate of the transaction pub certificate: CertifiedTransaction, @@ -92,7 +93,7 @@ impl Display for SplitCoinResponse { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct MergeCoinResponse { /// Certificate of the transaction pub certificate: CertifiedTransaction, @@ -117,7 +118,7 @@ impl Display for MergeCoinResponse { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct PublishResponse { /// Certificate of the transaction pub certificate: CertifiedTransaction, diff --git a/sui_types/Cargo.toml b/sui_types/Cargo.toml index cd909cdf07e69..e7d93e1f55a5d 100644 --- a/sui_types/Cargo.toml +++ b/sui_types/Cargo.toml @@ -44,3 +44,4 @@ move-vm-types = { git = "https://github.com/move-language/move", rev = "4e025186 narwhal-executor = { git = "https://github.com/MystenLabs/narwhal", rev = "8ae2164f0510349cbac2770e50e853bce5ab0e02", package = "executor" } narwhal-crypto = { git = "https://github.com/MystenLabs/narwhal", rev = "8ae2164f0510349cbac2770e50e853bce5ab0e02", package = "crypto" } +schemars ="0.8.8" \ No newline at end of file diff --git a/sui_types/src/base_types.rs b/sui_types/src/base_types.rs index 97fccc2a4aa3f..f79ae8b44e32b 100644 --- a/sui_types/src/base_types.rs +++ b/sui_types/src/base_types.rs @@ -1,16 +1,18 @@ // Copyright (c) 2021, Facebook, Inc. and its affiliates // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use base64ct::Encoding; + use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; use std::fmt; use crate::crypto::PublicKeyBytes; use crate::error::SuiError; +use crate::json_schema; use crate::readable_serde::encoding::Base64; use crate::readable_serde::encoding::Hex; use crate::readable_serde::Readable; +use base64ct::Encoding; use digest::Digest; use hex::FromHex; use move_core_types::account_address::AccountAddress; @@ -18,6 +20,7 @@ use move_core_types::ident_str; use move_core_types::identifier::IdentStr; use opentelemetry::{global, Context}; use rand::Rng; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use serde_with::Bytes; @@ -28,7 +31,18 @@ use sha3::Sha3_256; mod base_types_tests; #[derive( - Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Default, Debug, Serialize, Deserialize, + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Hash, + Default, + Debug, + Serialize, + Deserialize, + JsonSchema, )] pub struct SequenceNumber(u64); @@ -40,15 +54,25 @@ pub struct UserData(pub Option<[u8; 32]>); pub type AuthorityName = PublicKeyBytes; #[serde_as] -#[derive(Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct ObjectID(#[serde_as(as = "Readable")] AccountAddress); +#[derive(Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)] +pub struct ObjectID( + #[schemars(with = "json_schema::Hex")] + #[serde_as(as = "Readable")] + AccountAddress, +); pub type ObjectRef = (ObjectID, SequenceNumber, ObjectDigest); pub const SUI_ADDRESS_LENGTH: usize = ObjectID::LENGTH; #[serde_as] -#[derive(Eq, Default, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize)] -pub struct SuiAddress(#[serde_as(as = "Readable")] [u8; SUI_ADDRESS_LENGTH]); +#[derive( + Eq, Default, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema, +)] +pub struct SuiAddress( + #[schemars(with = "json_schema::Hex")] + #[serde_as(as = "Readable")] + [u8; SUI_ADDRESS_LENGTH], +); impl SuiAddress { pub fn to_vec(&self) -> Vec { @@ -137,14 +161,20 @@ pub const OBJECT_DIGEST_LENGTH: usize = 32; /// A transaction will have a (unique) digest. #[serde_as] -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)] pub struct TransactionDigest( - #[serde_as(as = "Readable")] [u8; TRANSACTION_DIGEST_LENGTH], + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "Readable")] + [u8; TRANSACTION_DIGEST_LENGTH], ); // Each object has a unique digest #[serde_as] -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize)] -pub struct ObjectDigest(#[serde_as(as = "Readable")] pub [u8; 32]); // We use SHA3-256 hence 32 bytes here +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)] +pub struct ObjectDigest( + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "Readable")] + pub [u8; 32], +); // We use SHA3-256 hence 32 bytes here pub const TX_CONTEXT_MODULE_NAME: &IdentStr = ident_str!("TxContext"); pub const TX_CONTEXT_STRUCT_NAME: &IdentStr = TX_CONTEXT_MODULE_NAME; diff --git a/sui_types/src/crypto.rs b/sui_types/src/crypto.rs index 34dc3e3c16992..bf49feaf89f81 100644 --- a/sui_types/src/crypto.rs +++ b/sui_types/src/crypto.rs @@ -3,6 +3,7 @@ use crate::base_types::{AuthorityName, SuiAddress}; use crate::committee::EpochId; use crate::error::{SuiError, SuiResult}; +use crate::json_schema; use crate::readable_serde::encoding::Base64; use crate::readable_serde::Readable; use anyhow::anyhow; @@ -16,6 +17,7 @@ use narwhal_crypto::ed25519::Ed25519PrivateKey; use narwhal_crypto::ed25519::Ed25519PublicKey; use once_cell::sync::OnceCell; use rand::rngs::OsRng; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use serde_with::Bytes; @@ -129,9 +131,13 @@ impl signature::Signer for KeyPair { } #[serde_as] -#[derive(Eq, Default, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize)] +#[derive( + Eq, Default, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema, +)] pub struct PublicKeyBytes( - #[serde_as(as = "Readable")] [u8; dalek::PUBLIC_KEY_LENGTH], + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "Readable")] + [u8; dalek::PUBLIC_KEY_LENGTH], ); impl PublicKeyBytes { @@ -209,8 +215,12 @@ pub const SUI_SIGNATURE_LENGTH: usize = ed25519_dalek::PUBLIC_KEY_LENGTH + ed25519_dalek::SIGNATURE_LENGTH; #[serde_as] -#[derive(Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] -pub struct Signature(#[serde_as(as = "Readable")] [u8; SUI_SIGNATURE_LENGTH]); +#[derive(Eq, PartialEq, Copy, Clone, Serialize, Deserialize, JsonSchema)] +pub struct Signature( + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "Readable")] + [u8; SUI_SIGNATURE_LENGTH], +); impl AsRef<[u8]> for Signature { fn as_ref(&self) -> &[u8] { @@ -342,8 +352,12 @@ impl Signature { /// A signature emitted by an authority. It's useful to decouple this from user signatures, /// as their set of supported schemes will probably diverge #[serde_as] -#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] -pub struct AuthoritySignature(#[serde_as(as = "Readable")] pub dalek::Signature); +#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize, JsonSchema)] +pub struct AuthoritySignature( + #[schemars(with = "json_schema::Base64")] + #[serde_as(as = "Readable")] + pub dalek::Signature, +); impl AsRef<[u8]> for AuthoritySignature { fn as_ref(&self) -> &[u8] { self.0.as_ref() @@ -443,7 +457,7 @@ impl AuthoritySignature { /// This will make CertifiedTransaction also an instance of the same struct. pub trait AuthoritySignInfoTrait: private::SealedAuthoritySignInfoTrait {} -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct EmptySignInfo {} impl AuthoritySignInfoTrait for EmptySignInfo {} diff --git a/sui_types/src/error.rs b/sui_types/src/error.rs index a677156f60772..bab2c6d022692 100644 --- a/sui_types/src/error.rs +++ b/sui_types/src/error.rs @@ -7,6 +7,7 @@ use crate::committee::EpochId; use move_binary_format::errors::{PartialVMError, VMError}; use narwhal_executor::ExecutionStateError; use narwhal_executor::SubscriberError; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use thiserror::Error; @@ -30,7 +31,7 @@ macro_rules! fp_ensure { pub(crate) use fp_ensure; /// Custom error type for Sui. -#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Error, Hash)] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Error, Hash, JsonSchema)] #[allow(clippy::large_enum_variant)] pub enum SuiError { // Object misuse issues @@ -251,7 +252,11 @@ pub enum SuiError { error: Box, }, #[error("Storage error")] - StorageError(#[from] TypedStoreError), + StorageError( + #[from] + #[schemars(with = "String")] + TypedStoreError, + ), #[error("Batch error: cannot send transaction to batch.")] BatchErrorSender, #[error("Authority Error: {error:?}")] diff --git a/sui_types/src/event.rs b/sui_types/src/event.rs index e2732dae068f1..c2064cf15f1c2 100644 --- a/sui_types/src/event.rs +++ b/sui_types/src/event.rs @@ -1,16 +1,20 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::json_schema; use move_core_types::language_storage::StructTag; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; /// User-defined event emitted by executing Move code. /// Executing a transaction produces an ordered log of these #[serde_as] -#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash, JsonSchema)] pub struct Event { + #[schemars(with = "json_schema::StructTag")] pub type_: StructTag, + #[schemars(with = "String")] #[serde_as(as = "Bytes")] pub contents: Vec, } diff --git a/sui_types/src/gas.rs b/sui_types/src/gas.rs index bd5d0d1693d54..961fc595a9ba0 100644 --- a/sui_types/src/gas.rs +++ b/sui_types/src/gas.rs @@ -12,6 +12,7 @@ use move_core_types::gas_schedule::{ }; use move_vm_types::gas_schedule::{GasStatus, INITIAL_COST_SCHEDULE}; use once_cell::sync::Lazy; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -25,7 +26,7 @@ macro_rules! ok_or_gas_error { }; } -#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct GasCostSummary { pub computation_cost: u64, pub storage_cost: u64, diff --git a/sui_types/src/json_schema.rs b/sui_types/src/json_schema.rs new file mode 100644 index 0000000000000..d4e5d65cf0760 --- /dev/null +++ b/sui_types/src/json_schema.rs @@ -0,0 +1,165 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This file contains JsonSchema implementation for a few of the move types that is exposed through the GatewayAPI +/// These types are being use by `schemars` to create schema using the `#[schemars(with = "")]` tag. +use crate::readable_serde::encoding; +use crate::readable_serde::Readable; +use schemars::gen::SchemaGenerator; +use schemars::schema::Schema; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_with::serde_as; +use std::ops::Deref; +#[derive(Deserialize, Serialize)] +pub struct StructTag; + +impl JsonSchema for StructTag { + fn schema_name() -> String { + "StructTag".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Deserialize, Serialize, JsonSchema)] + struct StructTag { + pub address: AccountAddress, + pub module: Identifier, + pub name: Identifier, + pub type_args: Vec, + } + StructTag::json_schema(gen) + } +} +#[derive(Deserialize, Serialize)] +pub struct TypeTag; + +impl JsonSchema for TypeTag { + fn schema_name() -> String { + "TypeTag".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Deserialize, Serialize, JsonSchema)] + #[serde(rename_all = "camelCase")] + enum TypeTag { + Bool, + U8, + U64, + U128, + Address, + Signer, + Vector(Box), + Struct(StructTag), + } + TypeTag::json_schema(gen) + } +} + +#[derive(Deserialize, Serialize)] +pub struct Identifier; + +impl JsonSchema for Identifier { + fn schema_name() -> String { + "Identifier".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Serialize, Deserialize, JsonSchema)] + struct Identifier(Box); + Identifier::json_schema(gen) + } +} +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct AccountAddress(Hex); + +#[serde_as] +#[derive(Deserialize, Serialize)] +pub struct Base64(#[serde_as(as = "Readable")] pub Vec); + +impl JsonSchema for Base64 { + fn schema_name() -> String { + "Base64".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Serialize, Deserialize, JsonSchema)] + struct Base64(String); + Base64::json_schema(gen) + } +} + +impl Deref for Base64 { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Hex(String); + +#[derive(Deserialize, Serialize)] +pub struct MoveStructLayout; + +impl JsonSchema for MoveStructLayout { + fn schema_name() -> String { + "MoveStructLayout".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Deserialize, Serialize, JsonSchema)] + enum MoveStructLayout { + Runtime(Vec), + WithFields(Vec), + WithTypes { + type_: StructTag, + fields: Vec, + }, + } + MoveStructLayout::json_schema(gen) + } +} + +#[derive(Deserialize, Serialize)] +struct MoveTypeLayout; + +impl JsonSchema for MoveTypeLayout { + fn schema_name() -> String { + "MoveTypeLayout".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Deserialize, Serialize, JsonSchema)] + #[serde(rename_all = "camelCase")] + enum MoveTypeLayout { + Bool, + U8, + U64, + U128, + Address, + Vector(Box), + Struct(MoveStructLayout), + Signer, + } + MoveTypeLayout::json_schema(gen) + } +} +#[derive(Serialize, Deserialize)] +pub struct MoveFieldLayout; + +impl JsonSchema for MoveFieldLayout { + fn schema_name() -> String { + "MoveFieldLayout".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(Deserialize, Serialize, JsonSchema)] + struct MoveFieldLayout { + name: Identifier, + layout: MoveTypeLayout, + } + MoveFieldLayout::json_schema(gen) + } +} diff --git a/sui_types/src/lib.rs b/sui_types/src/lib.rs index 34de2c9d948e2..13f195bc6cd98 100644 --- a/sui_types/src/lib.rs +++ b/sui_types/src/lib.rs @@ -22,6 +22,7 @@ pub mod event; pub mod gas; pub mod gas_coin; pub mod id; +pub mod json_schema; pub mod messages; pub mod move_package; pub mod object; diff --git a/sui_types/src/messages.rs b/sui_types/src/messages.rs index f42b310d65586..7bad77970af09 100644 --- a/sui_types/src/messages.rs +++ b/sui_types/src/messages.rs @@ -9,6 +9,7 @@ use crate::crypto::{ EmptySignInfo, Signable, Signature, VerificationObligation, }; use crate::gas::GasCostSummary; +use crate::json_schema; use crate::object::{Object, ObjectFormatOptions, Owner, OBJECT_START_VERSION}; use crate::readable_serde::encoding::Base64; use crate::readable_serde::Readable; @@ -22,6 +23,7 @@ use move_core_types::{ }; use name_variant::NamedVariant; use once_cell::sync::OnceCell; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_name::{DeserializeNameAdapter, SerializeNameAdapter}; use serde_with::serde_as; @@ -36,7 +38,7 @@ use std::{ #[path = "unit_tests/messages_tests.rs"] mod messages_tests; -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub enum CallArg { // contains no structs or objects Pure(Vec), @@ -47,13 +49,13 @@ pub enum CallArg { SharedObject(ObjectID), } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub struct Transfer { pub recipient: SuiAddress, pub object_ref: ObjectRef, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub struct MoveCall { // Although `package` represents a read-only Move package, // we still want to use a reference instead of just object ID. @@ -61,20 +63,24 @@ pub struct MoveCall { // used in an order (through the object digest) without having to // re-execute the order on a quorum of authorities. pub package: ObjectRef, + #[schemars(with = "json_schema::Identifier")] pub module: Identifier, + #[schemars(with = "json_schema::Identifier")] pub function: Identifier, + #[schemars(with = "Vec")] pub type_arguments: Vec, pub arguments: Vec, } #[serde_as] -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub struct MoveModulePublish { + #[schemars(with = "Vec")] #[serde_as(as = "Vec>")] pub modules: Vec>, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub enum SingleTransactionKind { /// Initiate an object transfer between addresses Transfer(Transfer), @@ -188,7 +194,7 @@ impl Display for SingleTransactionKind { // TODO: Make SingleTransactionKind a Box #[allow(clippy::large_enum_variant)] -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, NamedVariant)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, NamedVariant, JsonSchema)] pub enum TransactionKind { /// A single transaction. Single(SingleTransactionKind), @@ -216,7 +222,7 @@ impl Display for TransactionKind { } } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub struct TransactionData { pub kind: TransactionKind, sender: SuiAddress, @@ -318,7 +324,7 @@ where /// universally in the transactions storage in `SuiDataStore`, shared by both authorities /// and non-authorities: authorities store signed transactions, while non-authorities /// store unsigned transactions. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(remote = "TransactionEnvelope")] pub struct TransactionEnvelope { // Deserialization sets this to "false" @@ -588,7 +594,7 @@ impl PartialEq for SignedTransaction { /// /// As a consequence, we check this struct does not implement Hash or Eq, see the note below. /// -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct CertifiedTransaction { // This is a cache of an otherwise expensive to compute value. // DO NOT serialize or deserialize from the network or disk. @@ -774,7 +780,7 @@ pub enum CallResult { AddrVecVec(Vec>), } -#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub enum ExecutionStatus { // Gas used in the success case. Success { @@ -783,6 +789,7 @@ pub enum ExecutionStatus { // Gas used in the failed case, and the error. Failure { gas_cost: GasCostSummary, + #[schemars(with = "String")] error: Box, }, } @@ -838,7 +845,7 @@ impl ExecutionStatus { } /// The response from processing a transaction or a certified transaction -#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct TransactionEffects { // The status of the execution pub status: ExecutionStatus, diff --git a/sui_types/src/move_package.rs b/sui_types/src/move_package.rs index 156144297da51..efb5e5d50e72a 100644 --- a/sui_types/src/move_package.rs +++ b/sui_types/src/move_package.rs @@ -9,6 +9,7 @@ use crate::{ }; use move_binary_format::file_format::CompiledModule; use move_core_types::identifier::Identifier; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use serde_with::Bytes; @@ -21,10 +22,11 @@ use std::collections::BTreeMap; // serde_bytes::ByteBuf is an analog of Vec with built-in fast serialization. #[serde_as] -#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash, JsonSchema)] pub struct MovePackage { id: ObjectID, // TODO use session cache + #[schemars(with = "BTreeMap")] #[serde_as(as = "BTreeMap<_, Readable>")] module_map: BTreeMap>, } diff --git a/sui_types/src/object.rs b/sui_types/src/object.rs index 145011481ccc4..afd0cfb288247 100644 --- a/sui_types/src/object.rs +++ b/sui_types/src/object.rs @@ -5,6 +5,19 @@ use std::convert::{TryFrom, TryInto}; use std::fmt::{Debug, Display, Formatter}; use std::mem::size_of; +use crate::coin::Coin; +use crate::crypto::{sha3_hash, BcsSignable}; +use crate::error::{SuiError, SuiResult}; +use crate::json_schema; +use crate::move_package::MovePackage; +use crate::readable_serde::encoding::Base64; +use crate::readable_serde::Readable; +use crate::{ + base_types::{ + ObjectDigest, ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest, + }, + gas_coin::GasCoin, +}; use move_binary_format::binary_views::BinaryIndexedView; use move_binary_format::CompiledModule; use move_bytecode_utils::layout::TypeLayoutBuilder; @@ -14,31 +27,21 @@ use move_core_types::language_storage::TypeTag; use move_core_types::value::{MoveStruct, MoveStructLayout, MoveTypeLayout}; use move_disassembler::disassembler::Disassembler; use move_ir_types::location::Spanned; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use serde_with::serde_as; use serde_with::Bytes; -use crate::coin::Coin; -use crate::crypto::{sha3_hash, BcsSignable}; -use crate::error::{SuiError, SuiResult}; -use crate::move_package::MovePackage; -use crate::readable_serde::encoding::Base64; -use crate::readable_serde::Readable; -use crate::{ - base_types::{ - ObjectDigest, ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest, - }, - gas_coin::GasCoin, -}; - pub const GAS_VALUE_FOR_TESTING: u64 = 100000_u64; pub const OBJECT_START_VERSION: SequenceNumber = SequenceNumber::from_u64(1); #[serde_as] -#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash, JsonSchema)] pub struct MoveObject { + #[schemars(with = "json_schema::StructTag")] pub type_: StructTag, + #[schemars(with = "json_schema::Base64")] #[serde_as(as = "Readable")] contents: Vec, } @@ -177,7 +180,7 @@ impl MoveObject { } } -#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash, JsonSchema)] #[allow(clippy::large_enum_variant)] pub enum Data { /// An object whose governing logic lives in a published Move module @@ -271,7 +274,7 @@ impl Data { } } -#[derive(Eq, PartialEq, Debug, Clone, Copy, Deserialize, Serialize, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Copy, Deserialize, Serialize, Hash, JsonSchema)] pub enum Owner { /// Object is exclusively owned by a single address, and is mutable. AddressOwner(SuiAddress), @@ -327,7 +330,7 @@ impl std::cmp::PartialEq for Owner { } } -#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash, JsonSchema)] pub struct Object { /// The meat of the object pub data: Data, @@ -573,10 +576,15 @@ impl Object { } #[allow(clippy::large_enum_variant)] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(tag = "status", content = "details")] pub enum ObjectRead { NotExists(ObjectID), - Exists(ObjectRef, Object, Option), + Exists( + ObjectRef, + Object, + #[schemars(with = "Option")] Option, + ), Deleted(ObjectRef), }