1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
#![deny(missing_docs)]
//! # Essential REST Client
//!
//! This library provides a client for interacting with the Essential REST Server.
//!
//! ## Deploy Contract
//! Contracts must be signed before they can be deployed.
//! `essential-wallet` can be used to sign contracts.
//! Deploying the same contract multiple times is a no-op.
//!
//! ## Check Solution
//! This allows checking a lotion without it actually being included in a block.
//! The solution with use the pre-state that is currently on the server.
//! Any contracts that the solution solves must be deployed already.
//!
//! This is useful when you want to check a solution before submitting it
//! and want to use the state that is currently on the server.
//!
//! ## Check Solution With Contracts
//! This allows checking a solution with the set of contracts it is solving.
//! All contracts that the solution solves must be included in the set of contracts.
//! The solution will use the state that is currently on the server.
//!
//! This is useful when you are building contracts that aren't ready to be deployed
//! and you want to test them with a solution.
//!
//! ## Submit Solution
//! This allows submitting a solution to be included in an upcoming block.
//! Once a solution is submitted it is added to the pool.
//! The block builder runs on a regular loop interval and will include the solution in a block
//! in FIFO order if it satisfies the constraints.
//!
//! The block builder is likely to become more sophisticated in the future.
//!
//! Note that currently if you submit a solution that conflicts with another solution then
//! which ever solution is submitted first will be included in the block and the other solution
//! will fail. Failed solutions are not retried and will eventually be pruned.
//!
//! A solution can conflict with another solution when one solution is built on top of pre state
//! that the other solution changes. For example if a counter can only increment by 1 and is
//! currently set to 5 then you submit a solution setting it to 6 but another solution is submitted
//! before yours that sets the counter to 6 then your solution will fail to satisfy the constraints.
//! In fact in this example your solution will never satisfy again unless you update the state mutation
//! to the current count + 1. But to do this you have to resubmit your solution.
//!
//! Submitting the same solution twice (even by different user) is idempotent.
//!
//! ## Solution Outcome
//! This allows querying the outcome of a solution.
//! A solution is either successfully included in a block or it fails with a reason.
//!
//! One thing to keep in mind is solutions aren't necessarily unique.
//! It's possible for the same solution to be submitted multiple times.
//! For example if the counter example also allowed decrementing by 1 then
//! a solution could increment the count from 4 to 5 and another solution could decrement the count from 5 to 4.
//! Then a solution that increments the count from 4 to 5 could be submitted again.
//! These two solutions would have the exact same content address.
//! This results in the same solution hash returning multiple outcomes.
//!
//! This might make it difficult to know if it was the solution that you submitted that
//! was successful or failed. But actually it doesn't really matter because there is no
//! real ownership over a solution. Remember if two of the same solution are submitted
//! at the same time then it is as if only one was submitted.
//!
//! If you are interested in "has my solution worked" then it probably makes more
//! sense to query the state of the contract that you were trying to change.
//!
//! Keep in mind this is all very application specific.
//!
//! ## Get Predicate
//! This allows retrieving a deployed predicate.
//! It might be useful to do this if you want to debug a solution.
//!
//! ## Get Contract
//! This allows retrieving a deployed contract.
//! Very similar to `Get Predicate` but gets you the entire contract.
//!
//! ## List Contracts
//! This allows listing all deployed contracts.
//! The results are paged so you can only get a maximum number of contracts per query.
//! The contracts can also be filtered by the time range that they were deployed.
//!
//! ## List Solutions Pool
//! This allows listing all solutions currently in the pool.
//! The results are also paged.
//! Depending on the backlog of solutions an individual solution might not be in the pool for long.
//!
//! ## List Winning Blocks
//! This allows listing all blocks that have been successfully created.
//! The results are also paged.
//! The blocks can also be filtered by time.
//! Blocks are only created if there are solutions in the pool.
//! Blocks are created on a regular interval.
//!
//! ## Query State
//! This allows querying the state of a contract.
//! It is the main way the front end application will interact with state.
//! It only really makes sense to query state where you know what the abi of the contract is.
//! The state that's returned is a list of words of variable size.
//! The keys are also variable sized lists of words.
//! To make use of this api you need to know what type of contract you are querying.
//!
//! ## Query State Reads
//! This allows querying the state of a contract using state read programs.
//! This is a more advanced way of querying state.
//! It allows you to query the state of a contract using the state read programs from a predicate.
//! Custom state read programs can be also be written.
//! Pint can be used to create custom state reads.
//!
//! This api is also very useful if you are trying to solve a predicate but need to know what the pre-state
//! that the solution will read is.
//! For example if you want to run a debugger you will need this pre-state.
//!
//! The api can return which keys were read and which values were returned.
//! It can also return that values that were read into state slots on the pre-state read
//! and post-state read.
//!
//! Note that it doesn't return the keys and values that were read on the post-state read
//! because it is trivial to compute this locally using the state mutations in the solution.
use essential_server_types::{
CheckSolution, CheckSolutionOutput, QueryStateReads, QueryStateReadsOutput, SolutionOutcome,
};
use essential_types::{
contract::{Contract, SignedContract},
convert::bytes_from_word,
predicate::Predicate,
solution::Solution,
Block, ContentAddress, Hash, Key, PredicateAddress, Word,
};
use reqwest::{Client, ClientBuilder, Response};
use std::{ops::Range, time::Duration};
/// Client library for sending requests to the Essential REST Server.
pub struct EssentialClient {
/// Async reqwest client to make requests with.
client: Client,
/// The url to make requests to.
url: reqwest::Url,
}
impl EssentialClient {
/// Create a new client with the given address.
pub fn new(addr: String) -> anyhow::Result<Self> {
let client = ClientBuilder::new().http2_prior_knowledge().build()?;
let url = reqwest::Url::parse(&addr)?;
Ok(Self { client, url })
}
/// Deploy a signed contract to the server.
pub async fn deploy_contract(
&self,
signed_contract: SignedContract,
) -> anyhow::Result<ContentAddress> {
let url = self.url.join("/deploy-contract")?;
let response =
handle_error(self.client.post(url).json(&signed_contract).send().await?).await?;
Ok(response.json::<ContentAddress>().await?)
}
/// Check a solution with the server.
/// Contracts that this solves must be deployed.
pub async fn check_solution(&self, solution: Solution) -> anyhow::Result<CheckSolutionOutput> {
let url = self.url.join("/check-solution")?;
let response = handle_error(self.client.post(url).json(&solution).send().await?).await?;
Ok(response.json::<CheckSolutionOutput>().await?)
}
/// Check a solution with these contracts.
/// This uses the state on the server.
pub async fn check_solution_with_contracts(
&self,
solution: Solution,
contracts: Vec<Contract>,
) -> anyhow::Result<CheckSolutionOutput> {
let url = self.url.join("/check-solution-with-contracts")?;
let input = CheckSolution {
solution,
contracts,
};
let response = handle_error(self.client.post(url).json(&input).send().await?).await?;
Ok(response.json::<CheckSolutionOutput>().await?)
}
/// Submit a solution to be included in an upcoming block.
pub async fn submit_solution(&self, solution: Solution) -> anyhow::Result<ContentAddress> {
let url = self.url.join("/submit-solution")?;
let response = handle_error(self.client.post(url).json(&solution).send().await?).await?;
Ok(response.json::<essential_types::ContentAddress>().await?)
}
/// Get the outcome of a solution.
///
/// Note that a solution can have multiple outcomes because the
/// same solution can be submitted multiple times.
pub async fn solution_outcome(
&self,
solution_hash: &Hash,
) -> anyhow::Result<Vec<SolutionOutcome>> {
let ca = ContentAddress(*solution_hash);
let url = self.url.join(&format!("/solution-outcome/{ca}"))?;
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Vec<SolutionOutcome>>().await?)
}
/// Get a deployed predicate.
pub async fn get_predicate(
&self,
address: &PredicateAddress,
) -> anyhow::Result<Option<Predicate>> {
let url = self.url.join(&format!(
"/get-predicate/{}/{}",
address.contract, address.predicate,
))?;
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Option<Predicate>>().await?)
}
/// Get a deployed contract.
pub async fn get_contract(
&self,
address: &ContentAddress,
) -> anyhow::Result<Option<SignedContract>> {
let url = self.url.join(&format!("/get-contract/{address}"))?;
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Option<SignedContract>>().await?)
}
/// List deployed contracts.
pub async fn list_contracts(
&self,
time_range: Option<Range<Duration>>,
page: Option<u64>,
) -> anyhow::Result<Vec<Contract>> {
let mut url = self.url.join("/list-contracts")?;
if let Some(time_range) = time_range {
url.query_pairs_mut()
.append_pair("start", time_range.start.as_secs().to_string().as_str())
.append_pair("end", time_range.end.as_secs().to_string().as_str());
}
if let Some(page) = page {
url.query_pairs_mut()
.append_pair("page", page.to_string().as_str());
}
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Vec<Contract>>().await?)
}
/// List solutions currently in the pool.
pub async fn list_solutions_pool(&self, page: Option<u64>) -> anyhow::Result<Vec<Solution>> {
let mut url = self.url.join("list-solutions-pool")?;
if let Some(page) = page {
url.query_pairs_mut()
.append_pair("page", page.to_string().as_str());
}
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Vec<Solution>>().await?)
}
/// List blocks that have been successfully created.
pub async fn list_winning_blocks(
&self,
time_range: Option<Range<Duration>>,
page: Option<u64>,
) -> anyhow::Result<Vec<Block>> {
let mut url = self.url.join("/list-winning-blocks")?;
if let Some(time_range) = time_range {
url.query_pairs_mut()
.append_pair("start", time_range.start.as_secs().to_string().as_str())
.append_pair("end", time_range.end.as_secs().to_string().as_str());
}
if let Some(page) = page {
url.query_pairs_mut()
.append_pair("page", page.to_string().as_str());
}
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Vec<Block>>().await?)
}
/// Query the state of a contract.
pub async fn query_state(
&self,
address: &ContentAddress,
key: &Key,
) -> anyhow::Result<Vec<Word>> {
let url = self.url.join(&format!(
"/query-state/{address}/{}",
hex::encode_upper(
key.iter()
.flat_map(|w: &i64| bytes_from_word(*w))
.collect::<Vec<u8>>()
),
))?;
let response = handle_error(self.client.get(url).send().await?).await?;
Ok(response.json::<Vec<Word>>().await?)
}
/// Query the state of a contract using state read programs.
pub async fn query_state_reads(
&self,
query: QueryStateReads,
) -> anyhow::Result<QueryStateReadsOutput> {
let url = self.url.join("/query-state-reads")?;
let response = handle_error(self.client.post(url).json(&query).send().await?).await?;
Ok(response.json::<QueryStateReadsOutput>().await?)
}
}
async fn handle_error(response: Response) -> anyhow::Result<Response> {
let status = response.status();
if status.is_success() {
Ok(response)
} else {
let text = response.text().await?;
Err(anyhow::anyhow!("{}: {}", status, text))
}
}