Skip to content

Commit

Permalink
Merge pull request #94 from mycognosist/jsonrpc_client
Browse files Browse the repository at this point in the history
Add a JSON-RPC client library
  • Loading branch information
mycognosist authored Feb 22, 2024
2 parents 30f244b + 84c5ebe commit 07a44f6
Show file tree
Hide file tree
Showing 29 changed files with 827 additions and 129 deletions.
268 changes: 251 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
members = [
"solar",
"solar_cli",
"solar_client",
]
resolver = "2"
61 changes: 1 addition & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ or embedded into another Rust application as a [library](https://github.com/myco

:warning: **Solar is alpha software; expect breaking changes** :construction:

[Background](#background) | [Features](#features) | [Installation](#installation) | [Usage](#usage) | [Examples](#examples) | [CLI Options](#options) | [Configuration](#configuration) | [JSON-RPC API](#json-rpc) | [License](#license)
[Background](#background) | [Features](#features) | [Installation](#installation) | [Usage](#usage) | [Examples](#examples) | [CLI Options](#options) | [Configuration](#configuration) | [License](#license)

## Background

Expand Down Expand Up @@ -126,65 +126,6 @@ Alternatively, peers can be added to the replication configuration via CLI optio

Log-level can be defined by setting the `RUST_LOG` environment variable.

## JSON-RPC API

While running, a solar node can be queried using JSON-RPC over HTTP.

| Method | Parameters | Response | Description |
| --- | --- | --- | --- |
| `blocks` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `blockers` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `descriptions` | `{ "pub_key": "<@...=.ed25519>" }` | `[<description>]` | Returns an array of descriptions |
| `self_descriptions` | `{ "pub_key": "<@...=.ed25519>" }` | `[<description>]` | Returns an array of descriptions |
| `latest_description` | `{ "pub_key": "<@...=.ed25519>" }` | `<description>` | Returns a single description |
| `latest_self_description` | `{ "pub_key": "<@...=.ed25519>" }` | `<description>` | Returns a single description |
| `feed` | `{ "pub_key": "<@...=.ed25519>" }` | `[{ "key": "<%...=.sha256>", "value": <value>, "timestamp": <timestamp>, "rts": null }]` | Returns an array of message KVTs (key, value, timestamp) from the local database |
| `follows` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `followers` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `is_following` | `{ "peer_a": "<@...=.ed25519>", "peer_b": "<@...=.ed25519>" }` | `<bool>` | Returns a boolean |
| `friends` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `images` | `{ "pub_key": "<@...=.ed25519>" }` | `[<&...=.sha256>]` | Returns an array of image references |
| `self_images` | `{ "pub_key": "<@...=.ed25519>" }` | `[<&...=.sha256>]` | Returns an array of image references |
| `latest_image` | `{ "pub_key": "<@...=.ed25519>" }` | `<&...=.sha256>` | Returns a single image reference |
| `latest_self_image` | `{ "pub_key": "<@...=.ed25519>" }` | `<&...=.sha256>` | Returns a single image reference |
| `message` | `{ "msg_ref": "<%...=.sha256>" }` | `{ "key": "<%...=.sha256>", "value": <value>, "timestamp": <timestamp>, "rts": null }` | Returns a single message KVT (key, value, timestamp) from the local database |
| `names` | `{ "pub_key": "<@...=.ed25519>" }` | `[<name>]` | Returns an array of names |
| `self_names` | `{ "pub_key": "<@...=.ed25519>" }` | `[<name>]` | Returns an array of names |
| `latest_name` | `{ "pub_key": "<@...=.ed25519>" }` | `<name>` | Returns a single name |
| `latest_self_name` | `{ "pub_key": "<@...=.ed25519>" }` | `<name>` | Returns a single name |
| `peers` | | `[{ "pub_key": "<@...=.ed25519>", "seq_num": <int> }` | Returns an array of public key and latest sequence number for each peer in the local database |
| `ping` | | `pong!` | Responds if the JSON-RPC server is running |
| `publish` | `<content>` | `{ "msg_ref": "<%...=.sha256>", "seq_num": <int> }` | Publishes a message and returns the reference (message hash) and sequence number |
| `subscribers` | `{ "channel": "<channel_name>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `subscriptions` | `{ "pub_key": "<@...=.ed25519>" }` | `[<channel>]` | Returns an array of channel names |
| `whoami` | | `<@...=.ed25519>` | Returns the public key of the local node |

### Examples

`curl` can be used to invoke the available methods from the commandline.

Request:

`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ping", "id":1 }' 127.0.0.1:3030`

Response:

`{"jsonrpc":"2.0","result":"pong!","id":1}`

Request:

`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "publish", "params": {"type": "about", "about": "@o8lWpyLeSqV/BJV9pbxFhKpwm6Lw5k+sqexYK+zT9Tc=.ed25519", "name": "solar_glyph", "description": "glyph's experimental solar (rust) node"}, "id":1 }' 127.0.0.1:3030`

Response:

`{"jsonrpc":"2.0","result":{"msg_ref":"%ZwYwLxMHgU8eC43HOziJvYURjZzAzwFk3v5RYS/NbQY=.sha256","seq": 3,"id":1}`

_Note: You might find it easier to save your JSON to file and pass that to `curl` instead._

```
curl -X POST -H "Content-Type: application/json" --data @publish.json 127.0.0.1:3030
```

## License

AGPL-3.0
49 changes: 25 additions & 24 deletions solar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,33 +67,34 @@ While running, a solar node can be queried using JSON-RPC over HTTP.

| Method | Parameters | Response | Description |
| --- | --- | --- | --- |
| `blocks` | `"<@...=.ed25519>"` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `blockers` | `"<@...=.ed25519>"` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `descriptions` | `"<@...=.ed25519>"` | `[<description>]` | Returns an array of descriptions |
| `self_descriptions` | `"<@...=.ed25519>"` | `[<description>]` | Returns an array of descriptions |
| `latest_description` | `"<@...=.ed25519>"` | `<description>` | Returns a single description |
| `latest_self_description` | `"<@...=.ed25519>"` | `<description>` | Returns a single description |
| `feed` | `"<@...=.ed25519>"` | `[{ "key": "<%...=.sha256>", "value": <value>, "timestamp": <timestamp>, "rts": null }]` | Returns an array of message KVTs (key, value, timestamp) from the local database |
| `follows` | `"<@...=.ed25519>"` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `followers` | `"<@...=.ed25519>"` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `is_following` | `{ "peer_a": "<@...=.ed25519>", "peer_b": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns a boolean |
| `friends` | `"<@...=.ed25519>"` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `images` | `"<@...=.ed25519>"` | `[<&...=.sha256>]` | Returns an array of image references |
| `self_images` | `"<@...=.ed25519>"` | `[<&...=.sha256>]` | Returns an array of image references |
| `latest_image` | `"<@...=.ed25519>"` | `<&...=.sha256>` | Returns a single image reference |
| `latest_self_image` | `"<@...=.ed25519>"` | `<&...=.sha256>` | Returns a single image reference |
| `message` | `"<%...=.sha256>"` | `{ "key": "<%...=.sha256>", "value": <value>, "timestamp": <timestamp>, "rts": null }` | Returns a single message KVT (key, value, timestamp) from the local database |
| `names` | `"<@...=.ed25519>"` | `[<name>]` | Returns an array of names |
| `self_names` | `"<@...=.ed25519>"` | `[<name>]` | Returns an array of names |
| `latest_name` | `"<@...=.ed25519>"` | `<name>` | Returns a single name |
| `latest_self_name` | `"<@...=.ed25519>"` | `<name>` | Returns a single name |
| `blocks` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `blockers` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `descriptions` | `{ "pub_key": "<@...=.ed25519>" }` | `[(<@...=.ed25519>, <description>)]` | Returns an array of tuples, each containing a public key and a description |
| `self_descriptions` | `{ "pub_key": "<@...=.ed25519>" }` | `[<description>]` | Returns an array of descriptions |
| `latest_description` | `{ "pub_key": "<@...=.ed25519>" }` | `<description>` | Returns a single description |
| `latest_self_description` | `{ "pub_key": "<@...=.ed25519>" }` | `<description>` | Returns a single description |
| `feed` | `{ "pub_key": "<@...=.ed25519>" }` | `[{ "key": "<%...=.sha256>", "value": <value>, "timestamp": <timestamp>, "rts": null }]` | Returns an array of message KVTs (key, value, timestamp) from the local database |
| `follows` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `followers` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `is_following` | `{ "peer_a": "<@...=.ed25519>", "peer_b": "<@...=.ed25519>" }` | `<bool>` | Returns a boolean |
| `friends` | `{ "pub_key": "<@...=.ed25519>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `images` | `{ "pub_key": "<@...=.ed25519>" }` | `[(<@...=.ed25519>, <&...=.sha256>)]` | Returns an array of tuples, each containing a public key and an image reference |
| `self_images` | `{ "pub_key": "<@...=.ed25519>" }` | `[<&...=.sha256>]` | Returns an array of image references |
| `latest_image` | `{ "pub_key": "<@...=.ed25519>" }` | `<&...=.sha256>` | Returns a single image reference |
| `latest_self_image` | `{ "pub_key": "<@...=.ed25519>" }` | `<&...=.sha256>` | Returns a single image reference |
| `message` | `{ "msg_ref": "<%...=.sha256>" }` | `{ "key": "<%...=.sha256>", "value": <value>, "timestamp": <timestamp>, "rts": null }` | Returns a single message KVT (key, value, timestamp) from the local database |
| `names` | `{ "pub_key": "<@...=.ed25519>" }` | `[<name>]` | Returns an array of names |
| `self_names` | `{ "pub_key": "<@...=.ed25519>" }` | `[<name>]` | Returns an array of names |
| `latest_name` | `{ "pub_key": "<@...=.ed25519>" }` | `<name>` | Returns a single name |
| `latest_self_name` | `{ "pub_key": "<@...=.ed25519>" }` | `<name>` | Returns a single name |
| `peers` | | `[{ "pub_key": "<@...=.ed25519>", "seq_num": <int> }` | Returns an array of public key and latest sequence number for each peer in the local database |
| `ping` | | `pong!` | Responds if the JSON-RPC server is running |
| `publish` | `<content>` | `{ "msg_ref": "<%...=.sha256>", "seq_num": <int> }` | Publishes a message and returns the reference (message hash) and sequence number |
| `subscribers` | `"<channel>"` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `subscriptions` | `"<@...=.ed25519>"` | `[<channel>]` | Returns an array of channel names |
| `publish` | `{ "msg": {<content>} }` | `("<%...=.sha256>", <int>)` | Returns a tuple of the reference (message hash) and sequence number |
| `subscribers` | `{ "channel": "<channel_name>" }` | `[<@...=.ed25519>]` | Returns an array of public keys |
| `subscriptions` | `{ "pub_key": "<@...=.ed25519>" }` | `[<channel>]` | Returns an array of channel names |
| `whoami` | | `<@...=.ed25519>` | Returns the public key of the local node |


### Examples

`curl` can be used to invoke the available methods from the commandline.
Expand All @@ -108,7 +109,7 @@ Response:

Request:

`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "publish", "params": {"type": "about", "about": "@o8lWpyLeSqV/BJV9pbxFhKpwm6Lw5k+sqexYK+zT9Tc=.ed25519", "name": "solar_glyph", "description": "glyph's experimental solar (rust) node"}, "id":1 }' 127.0.0.1:3030`
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "publish", "params": {"msg": {"type": "about", "about": "@o8lWpyLeSqV/BJV9pbxFhKpwm6Lw5k+sqexYK+zT9Tc=.ed25519", "name": "solar_glyph", "description": "glyph's experimental solar (rust) node"} }, "id":1 }' 127.0.0.1:3030`

Response:

Expand Down
18 changes: 13 additions & 5 deletions solar/src/actors/jsonrpc/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ struct IsFollowing {
peer_b: String,
}

/// The contents of a raw message (of any supported type).
#[derive(Debug, Deserialize)]
struct Msg {
msg: TypedMessage,
}

/// Message reference containing the key (sha256 hash) of a message.
/// Used to parse the key from the parameters supplied to the `message`
/// endpoint.
Expand Down Expand Up @@ -474,8 +480,9 @@ pub async fn actor(server_id: OwnedIdentity, server_addr: SocketAddr) -> Result<
// Returns the key (hash) and sequence number of the published message.
rpc_module.register_method("publish", move |params: Params, _| {
task::block_on(async {
// Parse the parameter containing the post content.
let post_content: TypedMessage = params.parse()?;
// Parse the parameter containing the message content.
let msg_object: Msg = params.parse()?;
let msg_content: TypedMessage = msg_object.msg;

// Open the primary KV database for writing.
let db = KV_STORE.write().await;
Expand All @@ -484,8 +491,8 @@ pub async fn actor(server_id: OwnedIdentity, server_addr: SocketAddr) -> Result<
// Return `None` if no messages have yet been published on the feed.
let last_msg = db.get_latest_msg_val(&server_id.id)?;

// Instantiate and cryptographically-sign a new message using `post`.
let msg = Message::sign(last_msg.as_ref(), &server_id, json!(post_content))
// Instantiate and cryptographically-sign a new message.
let msg = Message::sign(last_msg.as_ref(), &server_id, json!(msg_content))
.map_err(Error::Validation)?;

// Append the signed message to the feed.
Expand All @@ -497,7 +504,8 @@ pub async fn actor(server_id: OwnedIdentity, server_addr: SocketAddr) -> Result<
seq
);

let response = json![{ "msg_ref": msg.id().to_string(), "seq_num": seq }];
// Return a tuple of message reference and sequence number.
let response = json!((msg.id().to_string(), seq));

Ok::<Value, JsonRpcError>(response)
})
Expand Down
42 changes: 24 additions & 18 deletions solar/src/storage/indexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,27 +299,31 @@ impl Indexes {
}

/// Return all indexed self-assigned descriptions for the given public key.
pub fn get_self_assigned_descriptions(&self, ssb_id: &str) -> Result<Vec<(String, String)>> {
let mut descriptions = self.get_descriptions(ssb_id)?;
descriptions.retain(|(author, _description)| author == ssb_id);
pub fn get_self_assigned_descriptions(&self, ssb_id: &str) -> Result<Vec<String>> {
let descriptions = self
.get_descriptions(ssb_id)?
.into_iter()
.filter(|(author, _description)| author == ssb_id)
.map(|(_ssb_id, description)| description)
.collect();

Ok(descriptions)
}

/// Return the most recently indexed description for the given public key.
pub fn get_latest_description(&self, ssb_id: &str) -> Result<Option<(String, String)>> {
let descriptions = self.get_descriptions(ssb_id)?;
let description = descriptions.last().cloned();
pub fn get_latest_description(&self, ssb_id: &str) -> Result<Option<String>> {
let description = self
.get_descriptions(ssb_id)?
.last()
.map(|(_ssb_id, description)| description)
.cloned();

Ok(description)
}

/// Return the most recently indexed self-assigned description for the given
/// public key.
pub fn get_latest_self_assigned_description(
&self,
ssb_id: &str,
) -> Result<Option<(String, String)>> {
pub fn get_latest_self_assigned_description(&self, ssb_id: &str) -> Result<Option<String>> {
let self_descriptions = self.get_self_assigned_descriptions(ssb_id)?;
let description = self_descriptions.last().cloned();

Expand Down Expand Up @@ -460,9 +464,13 @@ impl Indexes {

/// Return all indexed self-assigned image references for the given public
/// key.
pub fn get_self_assigned_images(&self, ssb_id: &str) -> Result<Vec<(String, String)>> {
let mut images = self.get_images(ssb_id)?;
images.retain(|(author, _image)| author == ssb_id);
pub fn get_self_assigned_images(&self, ssb_id: &str) -> Result<Vec<String>> {
let images = self
.get_images(ssb_id)?
.into_iter()
.filter(|(author, _image)| author == ssb_id)
.map(|(_ssb_id, image)| image)
.collect();

Ok(images)
}
Expand All @@ -478,7 +486,7 @@ impl Indexes {

/// Return the most recently indexed self-assigned image reference for the
/// given public key.
pub fn get_latest_self_assigned_image(&self, ssb_id: &str) -> Result<Option<(String, String)>> {
pub fn get_latest_self_assigned_image(&self, ssb_id: &str) -> Result<Option<String>> {
let images = self.get_self_assigned_images(ssb_id)?;
let image = images.last().cloned();

Expand Down Expand Up @@ -595,7 +603,7 @@ mod test {

indexes.index_msg(&keypair.id, first_msg)?;

if let Some((_author, description)) = indexes.get_latest_description(&keypair.id)? {
if let Some(description) = indexes.get_latest_description(&keypair.id)? {
assert_eq!(description, first_description);
}

Expand Down Expand Up @@ -632,9 +640,7 @@ mod test {
assert_eq!(lastest_name, second_name);
}

if let Some((_author, latest_description)) =
indexes.get_latest_description(&keypair.id)?
{
if let Some(latest_description) = indexes.get_latest_description(&keypair.id)? {
assert_eq!(latest_description, second_description);
}
}
Expand Down
9 changes: 4 additions & 5 deletions solar/src/storage/kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ impl KvStorage {

/// Return the public key and latest sequence number for all peers in the
/// database.
pub async fn get_peers(&self) -> Result<Vec<PubKeyAndSeqNum>> {
pub async fn get_peers(&self) -> Result<Vec<(String, u64)>> {
let db = self.db.as_ref().ok_or(Error::OptionIsNone)?;
let mut peers = Vec::new();

Expand All @@ -235,8 +235,7 @@ impl KvStorage {
// Get the latest sequence number for the peer.
// Fallback to a value of 0 if a `None` value is returned.
let seq_num = self.get_latest_seq(&pub_key)?.unwrap_or(0);
let peer_latest_sequence = PubKeyAndSeqNum { pub_key, seq_num };
peers.push(peer_latest_sequence)
peers.push((pub_key, seq_num))
}

Ok(peers)
Expand Down Expand Up @@ -479,8 +478,8 @@ mod test {
assert_eq!(peers.len(), 1);
// Ensure the public key of the peer matches expectations and that
// the sequence number is correct.
assert_eq!(peers[0].pub_key, keypair.id);
assert_eq!(peers[0].seq_num, 1);
assert_eq!(peers[0].0, keypair.id);
assert_eq!(peers[0].1, 1);

// Create, sign and append a second post-type message.
let msg_content_2 = TypedMessage::Post {
Expand Down
13 changes: 13 additions & 0 deletions solar_client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "solar_client"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.79"
jsonrpc_client = { version = "0.7.1", features = ["macros", "reqwest"] }
reqwest = { version = "0.11", default-features = false, features = [ "json" ] }
serde_json = { version = "1", features = ["preserve_order", "arbitrary_precision"] }

[dev-dependencies]
tokio = { version = "1.36.0", features = [ "macros", "rt-multi-thread" ] }
9 changes: 9 additions & 0 deletions solar_client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 🌞 Solar JSON-RPC Client

An HTTP JSON-RPC client for the Solar node.

See `src/lib.rs` and `examples/` for API details and usage examples.

## License

AGPL-3.0
21 changes: 21 additions & 0 deletions solar_client/examples/blocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use anyhow::Result;
use solar_client::{Client, SolarClient};

const SERVER_ADDR: &str = "http://127.0.0.1:3030";
const PUB_KEY: &str = "@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519";

#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(SERVER_ADDR.to_owned())?;

let blocks = client.blocks(PUB_KEY).await?;
println!("{:#?}", blocks);
// [
// "@dW5ch5miTnxLJDVDtB4ZCvrVxh+S8kGCQIBbd5paLhw=.ed25519",
// "@QIlKZ8DMw9XpjpRZ96RBLpfkLnOUZSqamC6WMddGh3I=.ed25519",
// ...
// "@+rMXLy1md42gvbBq+6l6rp95/drh6QyACO1ZZMMnWI0=.ed25519",
// ]

Ok(())
}
Loading

0 comments on commit 07a44f6

Please sign in to comment.