feat(rpc): OpenAPI spec (#8342)

* add an openapi generator

* fix run command

Co-authored-by: Marek <mail@marek.onl>

---------

Co-authored-by: Marek <mail@marek.onl>
This commit is contained in:
Alfredo Garcia 2024-03-14 12:04:19 -03:00 committed by GitHub
parent 5d6fe9134f
commit f2be73eebb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1934 additions and 61 deletions

View File

@ -239,7 +239,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -250,7 +250,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -409,7 +409,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.50",
"syn 2.0.52",
"which",
]
@ -805,7 +805,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1066,7 +1066,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1090,7 +1090,7 @@ dependencies = [
"codespan-reporting",
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1107,7 +1107,7 @@ checksum = "2fa16a70dd58129e4dfffdff535fb1bce66673f7bbeec4a5a1765a504e1ccd84"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1155,7 +1155,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1177,7 +1177,7 @@ checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be"
dependencies = [
"darling_core 0.20.6",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1250,7 +1250,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -1546,7 +1546,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -2500,7 +2500,7 @@ checksum = "38b4faf00617defe497754acde3024865bc143d44a86799b24e191ecff91354f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -2935,7 +2935,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -2976,7 +2976,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -3077,7 +3077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
dependencies = [
"proc-macro2",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -3200,7 +3200,7 @@ dependencies = [
"prost",
"prost-types",
"regex",
"syn 2.0.50",
"syn 2.0.52",
"tempfile",
"which",
]
@ -3215,7 +3215,7 @@ dependencies = [
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -3923,7 +3923,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -4008,7 +4008,20 @@ dependencies = [
"darling 0.20.6",
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
name = "serde_yaml"
version = "0.9.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f"
dependencies = [
"indexmap 2.2.3",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
@ -4224,9 +4237,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.50"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
@ -4325,7 +4338,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -4448,7 +4461,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -4632,7 +4645,7 @@ dependencies = [
"proc-macro2",
"prost-build",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -4645,7 +4658,7 @@ dependencies = [
"proc-macro2",
"prost-build",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -4775,7 +4788,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -4996,6 +5009,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]]
name = "untrusted"
version = "0.7.1"
@ -5207,7 +5226,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
"wasm-bindgen-shared",
]
@ -5241,7 +5260,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -6029,10 +6048,14 @@ dependencies = [
"hex",
"itertools 0.12.1",
"jsonrpc",
"quote",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"structopt",
"syn 2.0.52",
"thiserror",
"tinyvec",
"tokio",
@ -6133,7 +6156,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]
[[package]]
@ -6153,5 +6176,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
"syn 2.0.52",
]

47
book/src/user/openapi.md Normal file
View File

@ -0,0 +1,47 @@
# Zebra OpenAPI specification
The Zebra RPC methods are a collection of endpoints used for interacting with the Zcash blockchain. These methods are utilized by wallets, block explorers, web and mobile applications, and more, for retrieving and sending information to the blockchain.
While the Zebra source code and RPC methods are well-documented, accessing this information typically involves searching for each function within the [Zebra crate documentation](https://docs.rs/zebrad/latest/zebrad/#zebra-crates), which may be inconvenient for users who are not familiar with Rust development.
To address this issue, the Zebra team has created an [OpenAPI](https://www.openapis.org/) specification in the [YAML](https://en.wikipedia.org/wiki/YAML) format.
The Zebra OpenAPI specification is stored in a file named openapi.yaml, located at the root of the project. The latest version of this specification will always be available [here](https://github.com/ZcashFoundation/zebra/blob/main/openapi.yaml).
## Usage
There are several ways to utilize the specification. For users unfamiliar with OpenAPI and Swagger, simply navigate to the [Swagger Editor](https://editor.swagger.io/) and paste the specification there.
![image info](openapi1.png)
To send and receive data directly from/to the blockchain within the Swagger web app, you'll need a Zebra node with the RPC endpoint enabled.
To enable this functionality, start zebrad with a custom configuration. Generate a default configuration by running the following command:
```console
mkdir -p ~/.config
zebrad generate -o ~/.config/zebrad.toml
```
Then, add the IP address and port to the `rpc` section of the configuration:
```
[rpc]
listen_addr = "127.0.0.1:8232"
```
If you modify the address and port in the Zebra configuration, ensure to update it in the `openapi.yaml` specification as well.
Start Zebra with the following command:
```console
zebrad
```
You should now be able to send requests and receive responses within Swagger.
![image info](openapi2.png)
![image info](openapi3.png)

BIN
book/src/user/openapi1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
book/src/user/openapi2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
book/src/user/openapi3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

1007
openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
//! A root of a note commitment tree is associated with each treestate.
use std::{
default::Default,
fmt,
hash::{Hash, Hasher},
io,
@ -710,7 +711,7 @@ impl From<Vec<pallas::Base>> for NoteCommitmentTree {
/// It is likely that the dense format will be used in future RPCs, in which
/// case the current implementation will have to change and use the format
/// compatible with [`Frontier`](bridgetree::Frontier) instead.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
pub struct SerializedTree(Vec<u8>);
impl From<&NoteCommitmentTree> for SerializedTree {

View File

@ -128,7 +128,7 @@ const FAKE_TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] = &[
/// The Consensus Branch Id, used to bind transactions and blocks to a
/// particular network upgrade.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct ConsensusBranchId(u32);
impl ConsensusBranchId {

View File

@ -11,6 +11,7 @@
//! A root of a note commitment tree is associated with each treestate.
use std::{
default::Default,
fmt,
hash::{Hash, Hasher},
io,
@ -692,7 +693,7 @@ impl From<Vec<jubjub::Fq>> for NoteCommitmentTree {
/// It is likely that the dense format will be used in future RPCs, in which
/// case the current implementation will have to change and use the format
/// compatible with [`Frontier`] instead.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
pub struct SerializedTree(Vec<u8>);
impl From<&NoteCommitmentTree> for SerializedTree {

View File

@ -6,7 +6,7 @@
//! Some parts of the `zcashd` RPC documentation are outdated.
//! So this implementation follows the `zcashd` server and `lightwalletd` client implementations.
use std::{collections::HashSet, fmt::Debug, sync::Arc};
use std::{collections::HashSet, default::Default, fmt::Debug, sync::Arc};
use chrono::Utc;
use futures::{FutureExt, TryFutureExt};
@ -53,9 +53,12 @@ mod tests;
#[rpc(server)]
/// RPC method signatures.
pub trait Rpc {
#[rpc(name = "getinfo")]
/// Returns software information from the RPC server, as a [`GetInfo`] JSON struct.
///
/// zcashd reference: [`getinfo`](https://zcash.github.io/rpc/getinfo.html)
/// method: post
/// tags: control
///
/// # Notes
///
@ -65,12 +68,13 @@ pub trait Rpc {
///
/// Some fields from the zcashd reference are missing from Zebra's [`GetInfo`]. It only contains the fields
/// [required for lightwalletd support.](https://github.com/zcash/lightwalletd/blob/v0.4.9/common/common.go#L91-L95)
#[rpc(name = "getinfo")]
fn get_info(&self) -> Result<GetInfo>;
/// Returns blockchain state information, as a [`GetBlockChainInfo`] JSON struct.
///
/// zcashd reference: [`getblockchaininfo`](https://zcash.github.io/rpc/getblockchaininfo.html)
/// method: post
/// tags: blockchain
///
/// # Notes
///
@ -82,11 +86,13 @@ pub trait Rpc {
/// Returns the total balance of a provided `addresses` in an [`AddressBalance`] instance.
///
/// zcashd reference: [`getaddressbalance`](https://zcash.github.io/rpc/getaddressbalance.html)
/// method: post
/// tags: address
///
/// # Parameters
///
/// - `address_strings`: (map) A JSON map with a single entry
/// - `addresses`: (array of strings) A list of base-58 encoded addresses.
/// - `address_strings`: (object, example={"addresses": ["tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ"]}) A JSON map with a single entry
/// - `addresses`: (array of strings) A list of base-58 encoded addresses.
///
/// # Notes
///
@ -109,10 +115,12 @@ pub trait Rpc {
/// Returns the [`SentTransactionHash`] for the transaction, as a JSON string.
///
/// zcashd reference: [`sendrawtransaction`](https://zcash.github.io/rpc/sendrawtransaction.html)
/// method: post
/// tags: transaction
///
/// # Parameters
///
/// - `raw_transaction_hex`: (string, required) The hex-encoded raw transaction bytes.
/// - `raw_transaction_hex`: (string, required, example="signedhex") The hex-encoded raw transaction bytes.
///
/// # Notes
///
@ -129,12 +137,13 @@ pub trait Rpc {
/// [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
///
/// zcashd reference: [`getblock`](https://zcash.github.io/rpc/getblock.html)
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `hash | height`: (string, required) The hash or height for the block to be returned.
/// - `verbosity`: (numeric, optional, default=1) 0 for hex encoded data, 1 for a json object,
/// and 2 for json object with transaction data.
/// - `hash_or_height`: (string, required, example="1") The hash or height for the block to be returned.
/// - `verbosity`: (number, optional, default=1, example=1) 0 for hex encoded data, 1 for a json object, and 2 for json object with transaction data.
///
/// # Notes
///
@ -154,22 +163,28 @@ pub trait Rpc {
/// Returns the hash of the current best blockchain tip block, as a [`GetBlockHash`] JSON string.
///
/// zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html)
/// method: post
/// tags: blockchain
#[rpc(name = "getbestblockhash")]
fn get_best_block_hash(&self) -> Result<GetBlockHash>;
/// Returns all transaction ids in the memory pool, as a JSON array.
///
/// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html)
/// method: post
/// tags: blockchain
#[rpc(name = "getrawmempool")]
fn get_raw_mempool(&self) -> BoxFuture<Result<Vec<String>>>;
/// Returns information about the given block's Sapling & Orchard tree state.
///
/// zcashd reference: [`z_gettreestate`](https://zcash.github.io/rpc/z_gettreestate.html)
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `hash | height`: (string, required) The block hash or height.
/// - `hash | height`: (string, required, example="00000000febc373a1da2bd9f887b105ad79ddc26ac26c2b28652d64e5207c5b5") The block hash or height.
///
/// # Notes
///
@ -182,14 +197,15 @@ pub trait Rpc {
/// Returns information about a range of Sapling or Orchard subtrees.
///
/// zcashd reference: [`z_getsubtreesbyindex`](https://zcash.github.io/rpc/z_getsubtreesbyindex.html)
/// zcashd reference: [`z_getsubtreesbyindex`](https://zcash.github.io/rpc/z_getsubtreesbyindex.html) - TODO: fix link
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `pool`: (string, required) The pool from which subtrees should be returned.
/// Either "sapling" or "orchard".
/// - `start_index`: (numeric, required) The index of the first 2^16-leaf subtree to return.
/// - `limit`: (numeric, optional) The maximum number of subtree values to return.
/// - `pool`: (string, required) The pool from which subtrees should be returned. Either "sapling" or "orchard".
/// - `start_index`: (number, required) The index of the first 2^16-leaf subtree to return.
/// - `limit`: (number, optional) The maximum number of subtree values to return.
///
/// # Notes
///
@ -208,11 +224,13 @@ pub trait Rpc {
/// Returns the raw transaction data, as a [`GetRawTransaction`] JSON string or structure.
///
/// zcashd reference: [`getrawtransaction`](https://zcash.github.io/rpc/getrawtransaction.html)
/// method: post
/// tags: transaction
///
/// # Parameters
///
/// - `txid`: (string, required) The transaction ID of the transaction to be returned.
/// - `verbose`: (numeric, optional, default=0) If 0, return a string of hex-encoded data, otherwise return a JSON object.
/// - `txid`: (string, required, example="mytxid") The transaction ID of the transaction to be returned.
/// - `verbose`: (number, optional, default=0, example=1) If 0, return a string of hex-encoded data, otherwise return a JSON object.
///
/// # Notes
///
@ -232,13 +250,15 @@ pub trait Rpc {
/// Returns the transaction ids made by the provided transparent addresses.
///
/// zcashd reference: [`getaddresstxids`](https://zcash.github.io/rpc/getaddresstxids.html)
/// method: post
/// tags: address
///
/// # Parameters
///
/// A [`GetAddressTxIdsRequest`] struct with the following named fields:
/// - `addresses`: (json array of string, required) The addresses to get transactions from.
/// - `start`: (numeric, required) The lower height to start looking for transactions (inclusive).
/// - `end`: (numeric, required) The top height to stop looking for transactions (inclusive).
/// - `request`: (object, required, example={\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"], \"start\": 1000, \"end\": 2000}) A struct with the following named fields:
/// - `addresses`: (json array of string, required) The addresses to get transactions from.
/// - `start`: (numeric, required) The lower height to start looking for transactions (inclusive).
/// - `end`: (numeric, required) The top height to stop looking for transactions (inclusive).
///
/// # Notes
///
@ -251,10 +271,12 @@ pub trait Rpc {
/// Returns all unspent outputs for a list of addresses.
///
/// zcashd reference: [`getaddressutxos`](https://zcash.github.io/rpc/getaddressutxos.html)
/// method: post
/// tags: address
///
/// # Parameters
///
/// - `addresses`: (json array of string, required) The addresses to get outputs from.
/// - `addresses`: (array, required, example={\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"]}) The addresses to get outputs from.
///
/// # Notes
///
@ -1428,6 +1450,15 @@ pub struct GetInfo {
subversion: String,
}
impl Default for GetInfo {
fn default() -> Self {
GetInfo {
build: "some build version".to_string(),
subversion: "some subversion".to_string(),
}
}
}
/// Response to a `getblockchaininfo` RPC request.
///
/// See the notes for the [`Rpc::get_blockchain_info` method].
@ -1456,6 +1487,22 @@ pub struct GetBlockChainInfo {
consensus: TipConsensusBranch,
}
impl Default for GetBlockChainInfo {
fn default() -> Self {
GetBlockChainInfo {
chain: "main".to_string(),
blocks: Height(1),
best_block_hash: block::Hash([0; 32]),
estimated_height: Height(1),
upgrades: IndexMap::new(),
consensus: TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(ConsensusBranchId::default()),
next_block: ConsensusBranchIdHex(ConsensusBranchId::default()),
},
}
}
}
/// A wrapper type with a list of transparent address strings.
///
/// This is used for the input parameter of [`Rpc::get_address_balance`],
@ -1590,6 +1637,18 @@ pub enum GetBlock {
},
}
impl Default for GetBlock {
fn default() -> Self {
GetBlock::Object {
hash: GetBlockHash::default(),
confirmations: 0,
height: None,
tx: Vec::new(),
trees: GetBlockTrees::default(),
}
}
}
/// Response to a `getbestblockhash` and `getblockhash` RPC request.
///
/// Contains the hex-encoded hash of the requested block.
@ -1599,6 +1658,12 @@ pub enum GetBlock {
#[serde(transparent)]
pub struct GetBlockHash(#[serde(with = "hex")] pub block::Hash);
impl Default for GetBlockHash {
fn default() -> Self {
GetBlockHash(block::Hash([0; 32]))
}
}
/// Response to a `z_gettreestate` RPC request.
///
/// Contains the hex-encoded Sapling & Orchard note commitment trees, and their
@ -1627,6 +1692,26 @@ pub struct GetTreestate {
orchard: Treestate<orchard::tree::SerializedTree>,
}
impl Default for GetTreestate {
fn default() -> Self {
GetTreestate {
hash: block::Hash([0; 32]),
height: Height(0),
time: 0,
sapling: Treestate {
commitments: Commitments {
final_state: sapling::tree::SerializedTree::default(),
},
},
orchard: Treestate {
commitments: Commitments {
final_state: orchard::tree::SerializedTree::default(),
},
},
}
}
}
/// A treestate that is included in the [`z_gettreestate`][1] RPC response.
///
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
@ -1758,6 +1843,15 @@ pub struct GetBlockTrees {
orchard: OrchardTrees,
}
impl Default for GetBlockTrees {
fn default() -> Self {
GetBlockTrees {
sapling: SaplingTrees { size: 0 },
orchard: OrchardTrees { size: 0 },
}
}
}
/// Sapling note commitment tree information.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SaplingTrees {

View File

@ -69,6 +69,8 @@ pub trait GetBlockTemplateRpc {
/// the number of blocks in this chain excluding the genesis block).
///
/// zcashd reference: [`getblockcount`](https://zcash.github.io/rpc/getblockcount.html)
/// method: post
/// tags: blockchain
///
/// # Notes
///
@ -80,10 +82,12 @@ pub trait GetBlockTemplateRpc {
/// to a block in the best chain.
///
/// zcashd reference: [`getblockhash`](https://zcash-rpc.github.io/getblockhash.html)
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `index`: (numeric, required) The block index.
/// - `index`: (numeric, required, example=1) The block index.
///
/// # Notes
///
@ -100,6 +104,8 @@ pub trait GetBlockTemplateRpc {
/// - `jsonrequestobject`: (string, optional) A JSON object containing arguments.
///
/// zcashd reference: [`getblocktemplate`](https://zcash-rpc.github.io/getblocktemplate.html)
/// method: post
/// tags: mining
///
/// # Notes
///
@ -124,11 +130,17 @@ pub trait GetBlockTemplateRpc {
/// Returns the [`submit_block::Response`] for the operation, as a JSON string.
///
/// zcashd reference: [`submitblock`](https://zcash.github.io/rpc/submitblock.html)
/// method: post
/// tags: mining
///
/// # Parameters
/// - `hexdata` (string, required)
/// - `jsonparametersobject` (string, optional) - currently ignored
/// - holds a single field, workid, that must be included in submissions if provided by the server.
///
/// - `hexdata`: (string, required)
/// - `jsonparametersobject`: (string, optional) - currently ignored
///
/// # Notes
///
/// - `jsonparametersobject` holds a single field, workid, that must be included in submissions if provided by the server.
#[rpc(name = "submitblock")]
fn submit_block(
&self,
@ -139,6 +151,8 @@ pub trait GetBlockTemplateRpc {
/// Returns mining-related information.
///
/// zcashd reference: [`getmininginfo`](https://zcash.github.io/rpc/getmininginfo.html)
/// method: post
/// tags: mining
#[rpc(name = "getmininginfo")]
fn get_mining_info(&self) -> BoxFuture<Result<get_mining_info::Response>>;
@ -150,6 +164,8 @@ pub trait GetBlockTemplateRpc {
/// If `height` is not supplied or is -1, uses the tip height.
///
/// zcashd reference: [`getnetworksolps`](https://zcash.github.io/rpc/getnetworksolps.html)
/// method: post
/// tags: mining
#[rpc(name = "getnetworksolps")]
fn get_network_sol_ps(
&self,
@ -164,6 +180,8 @@ pub trait GetBlockTemplateRpc {
/// See that method for details.
///
/// zcashd reference: [`getnetworkhashps`](https://zcash.github.io/rpc/getnetworkhashps.html)
/// method: post
/// tags: mining
#[rpc(name = "getnetworkhashps")]
fn get_network_hash_ps(
&self,
@ -176,6 +194,8 @@ pub trait GetBlockTemplateRpc {
/// Returns data about each connected network node.
///
/// zcashd reference: [`getpeerinfo`](https://zcash.github.io/rpc/getpeerinfo.html)
/// method: post
/// tags: network
#[rpc(name = "getpeerinfo")]
fn get_peer_info(&self) -> BoxFuture<Result<Vec<PeerInfo>>>;
@ -183,6 +203,16 @@ pub trait GetBlockTemplateRpc {
/// Returns information about the given address if valid.
///
/// zcashd reference: [`validateaddress`](https://zcash.github.io/rpc/validateaddress.html)
/// method: post
/// tags: util
///
/// # Parameters
///
/// - `address`: (string, required) The zcash address to validate.
///
/// # Notes
///
/// - No notes
#[rpc(name = "validateaddress")]
fn validate_address(&self, address: String) -> BoxFuture<Result<validate_address::Response>>;
@ -190,6 +220,16 @@ pub trait GetBlockTemplateRpc {
/// Returns information about the given address if valid.
///
/// zcashd reference: [`z_validateaddress`](https://zcash.github.io/rpc/z_validateaddress.html)
/// method: post
/// tags: util
///
/// # Parameters
///
/// - `address`: (string, required) The zcash address to validate.
///
/// # Notes
///
/// - No notes
#[rpc(name = "z_validateaddress")]
fn z_validate_address(
&self,
@ -199,22 +239,41 @@ pub trait GetBlockTemplateRpc {
/// Returns the block subsidy reward of the block at `height`, taking into account the mining slow start.
/// Returns an error if `height` is less than the height of the first halving for the current network.
///
/// `height` can be any valid current or future height.
/// If `height` is not supplied, uses the tip height.
///
/// zcashd reference: [`getblocksubsidy`](https://zcash.github.io/rpc/getblocksubsidy.html)
/// method: post
/// tags: mining
///
/// # Parameters
///
/// - `height`: (numeric, optional, example=1) Can be any valid current or future height.
///
/// # Notes
///
/// If `height` is not supplied, uses the tip height.
#[rpc(name = "getblocksubsidy")]
fn get_block_subsidy(&self, height: Option<u32>) -> BoxFuture<Result<BlockSubsidy>>;
/// Returns the proof-of-work difficulty as a multiple of the minimum difficulty.
///
/// zcashd reference: [`getdifficulty`](https://zcash.github.io/rpc/getdifficulty.html)
/// method: post
/// tags: blockchain
#[rpc(name = "getdifficulty")]
fn get_difficulty(&self) -> BoxFuture<Result<f64>>;
/// Returns the list of individual payment addresses given a unified address.
///
/// zcashd reference: [`z_listunifiedreceivers`](https://zcash.github.io/rpc/z_listunifiedreceivers.html)
/// method: post
/// tags: wallet
///
/// # Parameters
///
/// - `address`: (string, required) The zcash unified address to get the list from.
///
/// # Notes
///
/// - No notes
#[rpc(name = "z_listunifiedreceivers")]
fn z_list_unified_receivers(
&self,

View File

@ -41,6 +41,11 @@ name = "scanning-results-reader"
path = "src/bin/scanning-results-reader/main.rs"
required-features = ["shielded-scan"]
[[bin]]
name = "openapi-generator"
path = "src/bin/openapi-generator/main.rs"
required-features = ["openapi-generator"]
[features]
default = []
@ -74,6 +79,14 @@ shielded-scan = [
"zebra-scan"
]
openapi-generator = [
"zebra-rpc",
"syn",
"quote",
"serde_yaml",
"serde"
]
[dependencies]
color-eyre = "0.6.2"
# This is a transitive dependency via color-eyre.
@ -109,3 +122,9 @@ jsonrpc = { version = "0.17.0", optional = true }
zcash_primitives = { version = "0.13.0-rc.1", optional = true }
zcash_client_backend = {version = "0.10.0-rc.1", optional = true}
# For the openapi generator
syn = { version = "2.0.52", features = ["full"], optional = true }
quote = { version = "1.0.35", optional = true }
serde_yaml = { version = "0.9.32", optional = true }
serde = { version = "1.0.196", features = ["serde_derive"], optional = true }

View File

@ -7,6 +7,7 @@ Tools for maintaining and testing Zebra:
- [zebrad-log-filter](#zebrad-log-filter)
- [zcash-rpc-diff](#zcash-rpc-diff)
- [scanning-results-reader](#scanning-results-reader)
- [openapi-generator](#openapi-generator)
Binaries are easier to use if they are located in your system execution path.
@ -232,3 +233,80 @@ A utility for displaying Zebra's scanning results.
``` bash
cargo run --release --features shielded-scan --bin scanning-results-reader
```
## OpenAPI generator
This utility generates an `openapi.yaml` specification by extracting information from RPC method documentation in the `zebra-rpc` crate code.
### Usage
To use the generator tool, build and run it with the following command:
```console
cargo run --bin openapi-generator --features="openapi-generator"
```
This command will create or update an `openapi.yaml` file at the root of the Zebra project repository.
The latest specification generated using this utility can be found [here](https://github.com/ZcashFoundation/zebra/blob/main/openapi.yaml).
### Documentation standard
In order for the script to work, each RPC method documentation needs to follow a specific well-defined format. For example, here is the in-code documentation for the `getblock` method, which takes arguments:
```rust
/// Returns the requested block by hash or height, as a [`GetBlock`] JSON string.
/// If the block is not in Zebra's state, returns
/// [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
///
/// zcashd reference: [`getblock`](https://zcash.github.io/rpc/getblock.html)
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `hash_or_height`: (string, required, example="1") The hash or height for the block to be returned.
/// - `verbosity`: (number, optional, default=1, example=1) 0 for hex encoded data, 1 for a json object, and 2 for json object with transaction data.
///
/// # Notes
///
/// With verbosity=1, [`lightwalletd` only reads the `tx` field of the
/// result](https://github.com/zcash/lightwalletd/blob/dfac02093d85fb31fb9a8475b884dd6abca966c7/common/common.go#L152),
/// and other clients only read the `hash` and `confirmations` fields,
/// so we only return a few fields for now.
///
/// `lightwalletd` and mining clients also do not use verbosity=2, so we don't support it.
#[rpc(name = "getblock")]
fn get_block(
&self,
hash_or_height: String,
verbosity: Option<u8>,
) -> BoxFuture<Result<GetBlock>>;
```
An example of a method with no arguments can be the `getinfo` call:
```rust
#[rpc(name = "getinfo")]
/// Returns software information from the RPC server, as a [`GetInfo`] JSON struct.
///
/// zcashd reference: [`getinfo`](https://zcash.github.io/rpc/getinfo.html)
/// method: post
/// tags: control
///
/// # Notes
///
/// [The zcashd reference](https://zcash.github.io/rpc/getinfo.html) might not show some fields
/// in Zebra's [`GetInfo`]. Zebra uses the field names and formats from the
/// [zcashd code](https://github.com/zcash/zcash/blob/v4.6.0-1/src/rpc/misc.cpp#L86-L87).
///
/// Some fields from the zcashd reference are missing from Zebra's [`GetInfo`]. It only contains the fields
/// [required for lightwalletd support.](https://github.com/zcash/lightwalletd/blob/v0.4.9/common/common.go#L91-L95)
fn get_info(&self) -> Result<GetInfo>;
```
Find more examples inside the `zebra-rpc/src/methods.rs` and the `zebra-rpc/src/methods/get_block_template_rpcs.rs` files.
The generator will detect new methods added if they are members of the `Rpc` trait for the `zebra-rpc/src/methods.rs` file and inside the `GetBlockTemplateRpc` in the file `zebra-rpc/src/methods/get_block_template_rpcs.rs`.

View File

@ -0,0 +1,544 @@
//! Generate an openapi.yaml file from the Zebra RPC methods
use std::{collections::HashMap, error::Error, fs::File, io::Write};
use quote::ToTokens;
use serde::Serialize;
use syn::LitStr;
use zebra_rpc::methods::*;
// The API server
const SERVER: &str = "http://localhost:8232";
// The API methods
#[derive(Serialize, Debug)]
struct Methods {
paths: HashMap<String, HashMap<String, MethodConfig>>,
}
// The configuration for each method
#[derive(Serialize, Clone, Debug)]
struct MethodConfig {
tags: Vec<String>,
description: String,
#[serde(rename = "requestBody")]
request_body: RequestBody,
responses: HashMap<String, Response>,
}
// The request body
#[derive(Serialize, Clone, Debug)]
struct RequestBody {
required: bool,
content: Content,
}
// The content of the request body
#[derive(Serialize, Clone, Debug)]
struct Content {
#[serde(rename = "application/json")]
application_json: Application,
}
// The application of the request body
#[derive(Serialize, Clone, Debug)]
struct Application {
schema: Schema,
}
// The schema of the request body
#[derive(Serialize, Clone, Debug)]
struct Schema {
#[serde(rename = "type")]
type_: String,
properties: HashMap<String, Property>,
}
// The properties of the request body
#[derive(Serialize, Clone, Debug)]
struct Property {
#[serde(rename = "type")]
type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
items: Option<ArrayItems>,
default: String,
}
// The response
#[derive(Serialize, Clone, Debug)]
struct Response {
description: String,
content: Content,
}
// The array items
#[derive(Serialize, Clone, Debug)]
struct ArrayItems {}
fn main() -> Result<(), Box<dyn Error>> {
let current_path = env!("CARGO_MANIFEST_DIR");
// Define the paths to the Zebra RPC methods
let paths = vec![
(
format!("{}/../zebra-rpc/src/methods.rs", current_path),
"Rpc",
),
(
format!(
"{}/../zebra-rpc/src/methods/get_block_template_rpcs.rs",
current_path
),
"GetBlockTemplateRpc",
),
];
// Create a hashmap to store the method names and configuration
let mut methods = HashMap::new();
for zebra_rpc_methods_path in paths {
// Read the source code from the file
let source_code = std::fs::read_to_string(zebra_rpc_methods_path.0)?;
// Parse the source code into a syn AST
let syn_file = syn::parse_file(&source_code)?;
// Create a hashmap to store the methods configuration
let mut methods_config = HashMap::new();
// Iterate over items in the file looking for traits
for item in &syn_file.items {
if let syn::Item::Trait(trait_item) = item {
// Check if this trait is the one we're interested in
if trait_item.ident == zebra_rpc_methods_path.1 {
// Iterate over the trait items looking for methods
for trait_item in &trait_item.items {
// Extract method name
let method_name = method_name(trait_item)?;
// Extract method documentation and description
let (method_doc, mut description) = method_doc(trait_item)?;
// Request type. TODO: All methods are POST so we just hardcode it
let request_type = "post".to_string();
// Tags. TODO: We are assuming 1 tag per call for now
let tags = tags(&method_doc)?;
// Parameters
let mut parameters_example = "[]".to_string();
if let Ok((params_description, params_example)) = get_params(&method_doc) {
// Add parameters to method description:
description =
add_params_to_description(&description, &params_description);
// The Zebra API uses a `params` array to pass arguments to the RPC methods,
// so we need to add this to the OpenAPI spec instead of `parameters`
parameters_example = params_example;
}
// Create the request body
let request_body = create_request_body(&method_name, &parameters_example);
// Check if we have parameters
let mut have_parameters = true;
if parameters_example == "[]" {
have_parameters = false;
}
// Create the responses
let responses = create_responses(&method_name, have_parameters)?;
// Add the method configuration to the hashmap
methods_config.insert(
request_type,
MethodConfig {
tags,
description,
request_body,
responses,
},
);
// Add the method name and configuration to the hashmap
methods.insert(format!("/{}", method_name), methods_config.clone());
}
}
}
}
}
// Create a struct to hold all the methods
let all_methods = Methods { paths: methods };
// Add openapi header and write to file
let yaml_string = serde_yaml::to_string(&all_methods)?;
let mut w = File::create("openapi.yaml")?;
w.write_all(format!("{}{}", create_yaml(), yaml_string).as_bytes())?;
Ok(())
}
// Create the openapi.yaml header
fn create_yaml() -> String {
format!("openapi: 3.0.3
info:
title: Swagger Zebra API - OpenAPI 3.0
version: 0.0.1
description: |-
This is the Zebra API. It is a JSON-RPC 2.0 API that allows you to interact with the Zebra node.
Useful links:
- [The Zebra repository](https://github.com/ZcashFoundation/zebra)
- [The latests API spec](https://github.com/ZcashFoundation/zebra/blob/main/openapi.yaml)
servers:
- url: {}
", SERVER)
}
// Extract the method name from the trait item
fn method_name(trait_item: &syn::TraitItem) -> Result<String, Box<dyn Error>> {
let mut method_name = "".to_string();
if let syn::TraitItem::Fn(method) = trait_item {
method_name = method.sig.ident.to_string();
// Refine name if needed
method.attrs.iter().for_each(|attr| {
if attr.path().is_ident("rpc") {
let _ = attr.parse_nested_meta(|meta| {
method_name = meta.value()?.parse::<LitStr>()?.value();
Ok(())
});
}
});
}
Ok(method_name)
}
// Return the method docs array and the description of the method
fn method_doc(method: &syn::TraitItem) -> Result<(Vec<String>, String), Box<dyn Error>> {
let mut method_doc = vec![];
if let syn::TraitItem::Fn(method) = method {
// Filter only doc attributes
let doc_attrs: Vec<_> = method
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
// If no doc attributes found, return an error
if doc_attrs.is_empty() {
return Err("No documentation attribute found for the method".into());
}
method.attrs.iter().for_each(|attr| {
if attr.path().is_ident("doc") {
method_doc.push(attr.to_token_stream().to_string());
}
});
}
// Extract the description from the first line of documentation
let description = match method_doc[0].split_once('"') {
Some((_, desc)) => desc.trim().to_string().replace('\'', "''"),
None => return Err("Description not found in method documentation".into()),
};
Ok((method_doc, description))
}
// Extract the tags from the method documentation. TODO: Assuming 1 tag per method for now
fn tags(method_doc: &[String]) -> Result<Vec<String>, Box<dyn Error>> {
// Find the line containing tags information
let tags_line = method_doc
.iter()
.find(|line| line.contains("tags:"))
.ok_or("Tags not found in method documentation")?;
// Extract tags from the tags line
let mut tags = Vec::new();
let tags_str = tags_line
.split(':')
.nth(1)
.ok_or("Invalid tags line")?
.trim();
// Split the tags string into individual tags
for tag in tags_str.split(',') {
let trimmed_tag = tag.trim_matches(|c: char| !c.is_alphanumeric());
if !trimmed_tag.is_empty() {
tags.push(trimmed_tag.to_string());
}
}
Ok(tags)
}
// Extract the parameters from the method documentation
fn get_params(method_doc: &[String]) -> Result<(String, String), Box<dyn Error>> {
// Find the start and end index of the parameters
let params_start_index = method_doc
.iter()
.enumerate()
.find(|(_, line)| line.contains("# Parameters"));
let notes_start_index = method_doc
.iter()
.enumerate()
.find(|(_, line)| line.contains("# Notes"));
// If start and end indices of parameters are found, extract them
if let (Some((params_index, _)), Some((notes_index, _))) =
(params_start_index, notes_start_index)
{
let params = &method_doc[params_index + 2..notes_index - 1];
// Initialize variables to store parameter descriptions and examples
let mut param_descriptions = Vec::new();
let mut param_examples = Vec::new();
// Iterate over the parameters and extract information
for param_line in params {
// Check if the line starts with the expected format
if param_line.trim().starts_with("# [doc = \" -") {
// Extract parameter name and description
if let Some((name, description)) = extract_param_info(param_line) {
param_descriptions.push(format!("- `{}` - {}", name, description));
// Extract parameter example if available
if let Some(example) = extract_param_example(param_line) {
param_examples.push(example);
}
}
}
}
// Format parameters and examples
let params_formatted = format!("[{}]", param_examples.join(", "));
let params_description = param_descriptions.join("\n");
return Ok((params_description, params_formatted));
}
Err("No parameters found".into())
}
// Extract parameter name and description
fn extract_param_info(param_line: &str) -> Option<(String, String)> {
let start_idx = param_line.find('`')?;
let end_idx = param_line.rfind('`')?;
let name = param_line[start_idx + 1..end_idx].trim().to_string();
let description_starts = param_line.find(") ")?;
let description_ends = param_line.rfind("\"]")?;
let description = param_line[description_starts + 2..description_ends]
.trim()
.to_string();
Some((name, description))
}
// Extract parameter example if available
fn extract_param_example(param_line: &str) -> Option<String> {
if let Some(example_start) = param_line.find("example=") {
let example_ends = param_line.rfind(')')?;
let example = param_line[example_start + 8..example_ends].trim();
Some(example.to_string())
} else {
None
}
}
// Create the request body
fn create_request_body(method_name: &str, parameters_example: &str) -> RequestBody {
// Add the method name to the request body
let method_name_prop = Property {
type_: "string".to_string(),
items: None,
default: method_name.to_string(),
};
// Add a hardcoded request_id to the request body
let request_id_prop = Property {
type_: "number".to_string(),
items: None,
default: "123".to_string(),
};
// Create the schema and add the first 2 properties
let mut schema = HashMap::new();
schema.insert("method".to_string(), method_name_prop);
schema.insert("id".to_string(), request_id_prop);
// Add the parameters with the extracted examples
let default = parameters_example.replace('\\', "");
schema.insert(
"params".to_string(),
Property {
type_: "array".to_string(),
items: Some(ArrayItems {}),
default,
},
);
// Create the request body
let content = Content {
application_json: Application {
schema: Schema {
type_: "object".to_string(),
properties: schema,
},
},
};
RequestBody {
required: true,
content,
}
}
// Create the responses
fn create_responses(
method_name: &str,
have_parameters: bool,
) -> Result<HashMap<String, Response>, Box<dyn Error>> {
let mut responses = HashMap::new();
let properties = get_default_properties(method_name)?;
let res_ok = Response {
description: "OK".to_string(),
content: Content {
application_json: Application {
schema: Schema {
type_: "object".to_string(),
properties,
},
},
},
};
responses.insert("200".to_string(), res_ok);
let mut properties = HashMap::new();
if have_parameters {
properties.insert(
"error".to_string(),
Property {
type_: "string".to_string(),
items: None,
default: "Invalid parameters".to_string(),
},
);
let res_bad_request = Response {
description: "Bad request".to_string(),
content: Content {
application_json: Application {
schema: Schema {
type_: "object".to_string(),
properties,
},
},
},
};
responses.insert("400".to_string(), res_bad_request);
}
Ok(responses)
}
// Add the parameters to the method description
fn add_params_to_description(description: &str, params_description: &str) -> String {
let mut new_description = description.to_string();
new_description.push_str("\n\n**Request body `params` arguments:**\n\n");
new_description.push_str(params_description);
new_description
}
// Get requests examples by using defaults from the Zebra RPC methods
fn get_default_properties(method_name: &str) -> Result<HashMap<String, Property>, Box<dyn Error>> {
// TODO: Complete the list of methods
let type_ = "object".to_string();
let items = None;
let mut props = HashMap::new();
let properties = match method_name {
"getinfo" => {
props.insert(
"result".to_string(),
Property {
type_,
items,
default: serde_json::to_string(&GetInfo::default())?,
},
);
props
}
"getbestblockhash" => {
props.insert(
"result".to_string(),
Property {
type_,
items,
default: serde_json::to_string(&GetBlockHash::default())?,
},
);
props
}
"getblockchaininfo" => {
props.insert(
"result".to_string(),
Property {
type_,
items,
default: serde_json::to_string(&GetBlockChainInfo::default())?,
},
);
props
}
"getblock" => {
props.insert(
"result".to_string(),
Property {
type_,
items,
default: serde_json::to_string(&GetBlock::default())?,
},
);
props
}
"getblockhash" => {
props.insert(
"result".to_string(),
Property {
type_,
items,
default: serde_json::to_string(&GetBlockHash::default())?,
},
);
props
}
"z_gettreestate" => {
props.insert(
"result".to_string(),
Property {
type_,
items,
default: serde_json::to_string(&GetTreestate::default())?,
},
);
props
}
_ => {
props.insert(
"result".to_string(),
Property {
type_,
items: None,
default: "{}".to_string(),
},
);
props
}
};
Ok(properties)
}