Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Generate the openrpc.json specification file in this repo #692

Merged
merged 11 commits into from
Jan 10, 2024
Merged
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vercel

# non-production openrpc.json files
/openrpc/*openrpc.json
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y gpg curl git ma
COPY . /app/

RUN yarn install
RUN yarn rpcspec:build
RUN yarn build

FROM nginx:1.17
Expand Down
138 changes: 138 additions & 0 deletions openrpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Soroban RPC API Specification <!-- omit in toc -->

## Table of Contents <!-- omit in toc -->

- [JSON-RPC](#json-rpc)
- [Building](#building)
- [Testing](#testing)
- [JSON `$ref`s](#json-refs)
- [Keeping Things Up-to-Date](#keeping-things-up-to-date)
- [Methods (`/openrpc/src/methods/*`)](#methods-openrpcsrcmethods)
- [Content Descriptors (`/openrpc/src/contentDescriptors/*`)](#content-descriptors-openrpcsrccontentdescriptors)
- [Schemas (`/openrpc/src/schemas/*`)](#schemas-openrpcsrcschemas)
- [Examples (`/openrpc/src/examples/*`)](#examples-openrpcsrcexamples)
- [Example Pairings (`/openrpc/src/examplePairingObjects/*`)](#example-pairings-openrpcsrcexamplepairingobjects)

## JSON-RPC

This is a specification of the API presented by Soroban RPC.

### Building

> _Note:_ The build process will provide an output file at
> `/static/openrpc.json`. This file should be included in any commits. However,
> this build process is re-run as part of our docusaurus deployment. So, it's
> necessary to update the actual source JSON files, and not just this built file
> as it will be overwritten at deploy time.

The specification is split into multiple files to improve readability. The
complete spec can be compiled into a single document as follows. (Run this
command from the root `soroban-docs` directory.)

```bash
yarn rpcspec:build
# Build successful.
```

This will output the file to `/static/openrpc.json`. This file will have all
schema `$ref`s resolved.

### Testing

We have included a script which will test and validate the generated
specification file.

```bash
yarn rpcspec:validate
# OpenRPC spec validated successfully.
```

### JSON `$ref`s

These files make extensive use of `$ref` objects for improved readability and
maintainability. In the separate files, the references don't mean much, but when
things are generated they'll be resolved. If you are going to reference
something in the specification, you will need to use the following format:
`#/components/{schemas,examples,etc.}/NameOfComponentToReference`.

The items broken out into objects that will be referenced are not held
individually in their own files. Instead, they are grouped into similar and
related files. For example: `/src/examples/Transactions.json` hold several
`example` components that are related to transactions, such as transaction
hashes, results from the `getTransaction` or `sendTransaction` methods,
transactions parameters that were sent using the `sendTransaction` method, etc.

## Keeping Things Up-to-Date

**Don't making any changes to `openrpc.json` or `refs-openrpc.json`!** Any
changes you make there, will not be actually reflected in the generated
specification file. Instead, any changes should be made in the files contained
in the `/openrpc/src` directory.

This directory follows a structure similar to the schema defined in the OpenRPC
specification. Here are the pieces you'll need to know about:

### Methods (`/openrpc/src/methods/*`)

This collection of JSON files define the [method objects] that will go into the
generated specification file. The methods can be considered the container that
will ultimately hold _all_ of the details about how the method works (parameter
types, return types, examples, etc.). The following properties are required in
the method object:

- `name` (string) - The canonical name for the method. The name MUST be unique
within the methods array
- `params` (list) - A list of parameters that are applicable for this method

### Content Descriptors (`/openrpc/src/contentDescriptors/*`)

This collection of JSON files define the [contentDescriptor objects] that will
go into the generated specification file. A content descriptor is a reusable way
of describing either parameters or results. (Though, I've found they're best
used as items in a method's `params` list). The following property are required
in the content descriptor object:

- `name` (string) - Name of the content that is being described. If this object
is defining a parameter, the `name` field will define the parameter's key
- `schema` (object) - A schema that describes the content

### Schemas (`/openrpc/src/schemas/*`)

This collection of JSON files define the [schema objects] that will go into the
generated specification file. These schemas allow us to define input and output
data types. These schemas **MUST** follow the [JSON Schema Specification 7]

### Examples (`/openrpc/src/examples/*`)

This collection of JSON files define the [example objects] that will go into the
generated specification file. These objects define an example that is consistent
and matches the `schema` of a given content descriptor. These example objects
can act as either a parameter or result. The `value` property of the example
object allows us to embed a literal example of what the schema can look like.

### Example Pairings (`/openrpc/src/examplePairingObjects/*`)

This collection of JSON files define the [example pairing objects] that will go
into the generated specification file. The example pairing objects make up a
complete example request to the Soroban RPC service. This is where you can
specify a set of `params` that were supplied in the request, as well as the
value(s) returned in the `result` from the node. The following properties are
required in the example pairing objects:

- `name` (string) - Name for the example pairing
- `params` (list) - A list of example parameters (or `$ref`s to example objects)
- `result` (example object) - Example result received from the node

> _Note:_ The `result` property is not technically _required_ by the open-rpc
> specification if the method is to be represented as a notification. However,
> Soroban RPC doesn't make use of any methods as notifications, so we've listed
> it as required here.

[method objects]: <https://spec.open-rpc.org/#method-object>
[contentDescriptor objects]:
<https://spec.open-rpc.org/#content-descriptor-object>
[schema objects]: <https://spec.open-rpc.org/#schema-object>
[JSON Schema Specification 7]:
<https://json-schema.org/draft-07/json-schema-release-notes.html>
[example objects]: <https://spec.open-rpc.org/#example-object>
[example pairing objects]: <https://spec.open-rpc.org/#example-pairing-object>
137 changes: 137 additions & 0 deletions openrpc/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import fs from "fs";
import mergeAllOf from "json-schema-merge-allof";
import { dereferenceDocument } from "@open-rpc/schema-utils-js";
import defaultResolver from "@json-schema-tools/reference-resolver";

function sortByMethodName(methods) {
return methods.slice().sort((a, b) => {
if (a['name'] > b['name']) {
return 1;
} else if (a['name'] < b['name']) {
return -1;
} else {
return 0;
}
})
}

let methods = [];
let methodsBase = "openrpc/src/methods/";
let methodFiles = fs.readdirSync(methodsBase);
methodFiles.forEach(file => {
let raw = fs.readFileSync(methodsBase + file);
let parsed = JSON.parse(raw);
methods = [
...methods,
parsed,
];
});

let contentDescriptors = {};
let cdBase = "openrpc/src/contentDescriptors/";
let cdFiles = fs.readdirSync(cdBase);
cdFiles.forEach(file => {
let raw = fs.readFileSync(cdBase + file);
let parsed = JSON.parse(raw);
contentDescriptors = {
...contentDescriptors,
...parsed,
};
});

let schemas = {};
let schemasBase = "openrpc/src/schemas/"
let schemaFiles = fs.readdirSync(schemasBase);
schemaFiles.forEach(file => {
let raw = fs.readFileSync(schemasBase + file);
let parsed = JSON.parse(raw);
schemas = {
...schemas,
...parsed,
};
});

let examples = {}
let examplesBase = "openrpc/src/examples/"
let examplesFiles = fs.readdirSync(examplesBase)
examplesFiles.forEach(file => {
let raw = fs.readFileSync(examplesBase + file)
let parsed = JSON.parse(raw)
examples = {
...examples,
...parsed,
}
})

let examplePairingObjects = {}
let epoBase = "openrpc/src/examplePairingObjects/"
let epoFiles = fs.readdirSync(epoBase)
epoFiles.forEach(file => {
let raw = fs.readFileSync(epoBase + file)
let parsed = JSON.parse(raw)
examplePairingObjects = {
...examplePairingObjects,
...parsed,
}
})

const doc = {
openrpc: "1.2.4",
info: {
title: "Soroban RPC",
description: "Soroban-RPC allows you to communicate directly with Soroban via a JSON RPC interface.",
termsOfService: "https://stellar.org/terms-of-service",
contact: {
name: "Stellar Development Foundation",
url: "https://stellar.org/connect",
email: "[email protected]"
},
license: {
name: "Apache 2.0",
url: "https://www.apache.org/licenses/LICENSE-2.0.html"
},
version: "20.1.0"
},
servers: [
{
name: "Testnet",
url: "https://soroban-testnet.stellar.org:443",
summary: "Publicly available RPC server maintained by SDF, operating on the Testnet test network.",
description: "Testnet is meant to be a stable network that runs a production (or near-production) version of the Stellar network."
},
{
name: "Futurenet",
url: "https://rpc-futurenet.stellar.org:443",
summary: "Publicly available RPC server maintained by SDF, operating on the Futurenet test network.",
description: "Futurenet is meant to be a bleeding-edge, experimental network that runs an early, test version of the Stellar network."
}
],
methods: sortByMethodName(methods),
components: {
contentDescriptors: contentDescriptors,
schemas: schemas,
examples: examples,
examplePairingObjects: examplePairingObjects,
},
}

fs.writeFileSync('openrpc/refs-openrpc.json', JSON.stringify(doc, null, 2));

let spec = await dereferenceDocument(doc, defaultResolver.default)

spec.components = {};

// Merge instances of `allOf` in methods.
for (var i=0; i < spec.methods.length; i++) {
for (var j=0; j < spec.methods[i].params.length; j++) {
spec.methods[i].params[j].schema = mergeAllOf(spec.methods[i].params[j].schema);
}
spec.methods[i].result.schema = mergeAllOf(spec.methods[i].result.schema);
}

let data = JSON.stringify(spec, null, 2);
fs.writeFileSync('openrpc/openrpc.json', data);
fs.writeFileSync('static/openrpc.json', data);

console.log();
console.log("Build successful.");
27 changes: 27 additions & 0 deletions openrpc/scripts/validate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from "fs";
import {
parseOpenRPCDocument,
validateOpenRPCDocument
} from "@open-rpc/schema-utils-js";

let rawdata = fs.readFileSync("openrpc/openrpc.json");
let openrpc = JSON.parse(rawdata);

const error = validateOpenRPCDocument(openrpc);
if (error != true) {
console.log(error.name);
console.log(error.message);
process.exit(1);
}

try {
await Promise.resolve(parseOpenRPCDocument(openrpc));
} catch(e) {
console.log(e.name);
let end = e.message.indexOf("schema in question");
let msg = e.message.substring(0, end);
console.log(msg);
process.exit(1);
}

console.log("OpenRPC spec validated successfully.");
10 changes: 10 additions & 0 deletions openrpc/src/contentDescriptors/EventFilters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"EventFilters": {
"name": "filters",
"summary": "filters to narrow events search",
"description": "List of filters for the returned events. Events matching any of the filters are included. To match a filter, an event must match both a contractId and a topic. Maximum 5 filters are allowed per request.",
"schema": {
"$ref": "#/components/schemas/EventFilters"
}
}
}
11 changes: 11 additions & 0 deletions openrpc/src/contentDescriptors/LedgerKeys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"LedgerKeys": {
"name": "keys",
"summary": "array of ledger keys",
"description": "Array containing the keys of the ledger entries you wish to retrieve. (an array of serialized base64 strings)",
"required": true,
"schema": {
"$ref": "#/components/schemas/LedgerKeys"
}
}
}
11 changes: 11 additions & 0 deletions openrpc/src/contentDescriptors/Pagination.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Pagination": {
"name": "pagination",
"summary": "pagination options",
"description": "Pagination in soroban-rpc is similar to pagination in Horizon. See [Pagination](https://soroban.stellar.org/api/pagination).",
"required": false,
"schema": {
"$ref": "#/components/schemas/Pagination"
}
}
}
11 changes: 11 additions & 0 deletions openrpc/src/contentDescriptors/ResourceConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"ResourceConfig": {
"name": "resourceConfig",
"summary": "configuration for how resources will be calculated",
"description": "Contains configuration for how resources will be calculated when simulating transactions.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ResourceConfig"
}
}
}
11 changes: 11 additions & 0 deletions openrpc/src/contentDescriptors/StartLedger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"StartLedger": {
"name": "startLedger",
"summary": "ledger to begin searching from",
"description": "Ledger sequence number to fetch events after (inclusive). This method will return an error if `startLedger` is less than the oldest ledger stored in this node, or greater than the latest ledger seen by this node. If a cursor is included in the request, `startLedger` must be omitted.",
"required": true,
"schema": {
"$ref": "#/components/schemas/LedgerSequence"
}
}
}
20 changes: 20 additions & 0 deletions openrpc/src/contentDescriptors/Transaction.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"SignedTransaction": {
"name": "transaction",
"summary": "transaction to be submitted to the network",
"description": "The signed transaction to broadcast for inclusion in a ledger.",
"required": true,
"schema": {
"$ref": "#/components/schemas/Transaction"
}
},
"UnsignedTransaction": {
"name": "transaction",
"summary": "transaction to be simulated",
"description": "In order for the RPC server to successfully simulate a Stellar transaction, the provided transaction must contain only a single operation of the type `invokeHostFunction`.",
"required": true,
"schema": {
"$ref": "#/components/schemas/Transaction"
}
}
}
Loading