use super::{
error::{Error, ErrorKind, Result},
results::AuthInfo,
session::Session,
transport::Transport,
};
use crate::protocol::osr1::{TYPED_PREFIX, decode_typed_result};
use crate::protocol::target::ReadMode;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::time::Duration;
pub(super) fn view_with<T: Transport>(
transport: &T,
session: &Session,
method: &str,
params: Vec<Value>,
) -> Result<Value> {
match session.target().read_mode {
ReadMode::Public => return public_view_with(transport, session, method, params),
ReadMode::Auto if circle_is_public_read(&circle_info_with(transport, session)?) => {
return public_view_with(transport, session, method, params);
}
_ => {}
}
signed_view_with(transport, session, method, params)
}
fn signed_view_with<T: Transport>(
transport: &T,
session: &Session,
method: &str,
params: Vec<Value>,
) -> Result<Value> {
let params_value = Value::Array(params.clone());
let params_json = compact_json(¶ms_value)?;
let params_hash = sha256_hex(params_json.as_bytes());
let message = format!(
"octra_circle_view|{}|{}|{}|{}|0",
session.target().circle,
session.caller(),
method,
params_hash
);
let signature = session.sign_view_auth_b64(&message)?;
let result = rpc_call(
transport,
session,
"octra_circleViewAuth",
json!([
session.target().circle,
method,
params,
session.caller(),
session.public_key_b64()?,
signature,
false
]),
)?;
decode_rpc_result(result)
}
fn public_view_with<T: Transport>(
transport: &T,
session: &Session,
method: &str,
params: Vec<Value>,
) -> Result<Value> {
let result = rpc_call(
transport,
session,
"octra_circleView",
json!([
session.target().circle,
method,
params,
session.caller(),
false
]),
)?;
decode_rpc_result(result)
}
pub(super) fn circle_info_with<T: Transport>(transport: &T, session: &Session) -> Result<Value> {
rpc_call(
transport,
session,
"octra_circleInfo",
json!([session.target().circle]),
)
}
fn circle_is_public_read(info: &Value) -> bool {
info.get("privacy_class").and_then(Value::as_str) == Some("public")
&& info.get("browser_mode").and_then(Value::as_str) == Some("gateway_allowed")
&& info.get("resource_mode").and_then(Value::as_str) == Some("public_resources")
}
pub(super) fn query_typed_with<T: Transport>(
transport: &T,
session: &Session,
sql: &str,
) -> Result<Value> {
view_with(
transport,
session,
"query_typed",
vec![Value::String(sql.to_string())],
)
}
pub(super) fn auth_info_with<T: Transport>(transport: &T, session: &Session) -> Result<AuthInfo> {
let value = view_with(transport, session, "auth_info", vec![])?;
let configured = value
.get("configured")
.and_then(Value::as_bool)
.unwrap_or(false);
let db_id = value
.get("db_id")
.and_then(Value::as_str)
.ok_or_else(|| Error::with_kind(ErrorKind::Decode, "auth_info missing db_id"))?
.to_string();
let owner_pubkey = value
.get("owner_pubkey")
.and_then(Value::as_str)
.map(str::to_string);
let owner_sequence = value.get("owner_sequence").and_then(Value::as_u64);
Ok(AuthInfo {
configured,
db_id,
owner_pubkey,
owner_sequence,
})
}
pub(super) fn program_info_with<T: Transport>(transport: &T, session: &Session) -> Result<Value> {
let message = format!(
"octra_circle_program_info|{}|{}",
session.target().circle,
session.caller()
);
let signature = session.sign_program_info_b64(&message)?;
rpc_call(
transport,
session,
"octra_circleProgramInfoAuth",
json!([
session.target().circle,
session.caller(),
session.public_key_b64()?,
signature
]),
)
}
pub(super) fn next_nonce_with<T: Transport>(transport: &T, session: &Session) -> Result<i64> {
let balance = rpc_call(
transport,
session,
"octra_balance",
json!([session.caller()]),
)?;
Ok(balance
.get("pending_nonce")
.or_else(|| balance.get("nonce"))
.and_then(Value::as_i64)
.unwrap_or(0)
+ 1)
}
pub(super) fn rpc_call<T: Transport>(
transport: &T,
session: &Session,
method: &str,
params: Value,
) -> Result<Value> {
transport.call(session.rpc(), method, params)
}
pub(super) fn wait_for_receipt_with<T: Transport>(
transport: &T,
session: &Session,
tx_hash: &str,
) -> Result<Value> {
for _ in 0..45 {
let result = rpc_call(transport, session, "contract_receipt", json!([tx_hash]));
if let Ok(receipt) = result
&& !receipt.is_null()
{
return Ok(receipt);
}
std::thread::sleep(Duration::from_secs(2));
}
Err(Error::with_kind(
ErrorKind::Timeout,
format!("timed out waiting for receipt {tx_hash}"),
))
}
#[cfg(feature = "http")]
pub(super) fn wait_for_transaction_with<T: Transport>(
transport: &T,
session: &Session,
tx_hash: &str,
) -> Result<Value> {
for _ in 0..60 {
let result = rpc_call(transport, session, "octra_transaction", json!([tx_hash]));
if let Ok(transaction) = result {
let status = transaction
.get("status")
.and_then(Value::as_str)
.unwrap_or_default();
match status {
"confirmed" | "accepted" => return Ok(transaction),
"rejected" | "failed" => {
return Err(Error::with_kind(
ErrorKind::Receipt,
format!("transaction {tx_hash} {status}: {transaction}"),
));
}
_ => {}
}
}
std::thread::sleep(Duration::from_secs(2));
}
Err(Error::with_kind(
ErrorKind::Timeout,
format!("timed out waiting for transaction {tx_hash}"),
))
}
pub(super) fn compact_json(value: &Value) -> Result<String> {
Ok(serde_json::to_string(value)?)
}
fn decode_rpc_result(result: Value) -> Result<Value> {
if let Some(text) = result.get("result").and_then(Value::as_str) {
return decode_method_result(text);
}
if let Some(text) = result.as_str() {
return decode_method_result(text);
}
Ok(result)
}
fn decode_method_result(text: &str) -> Result<Value> {
if let Some(encoded) = text.strip_prefix(TYPED_PREFIX) {
return Ok(decode_typed_result(encoded)?);
}
let value = serde_json::from_str(text).unwrap_or_else(|_| Value::String(text.to_string()));
if let Some(error) = contract_error_text(&value) {
return Err(Error::with_kind(ErrorKind::Rpc, error));
}
Ok(value)
}
fn contract_error_text(value: &Value) -> Option<String> {
let object = value.as_object()?;
let failed = object.get("ok").and_then(Value::as_bool) == Some(false);
let code = object.get("error").and_then(Value::as_str);
if !failed && code.is_none() {
return None;
}
let code = code.unwrap_or("contract_error");
match object.get("detail").and_then(Value::as_str) {
Some(detail) if !detail.is_empty() => Some(format!("database error ({code}): {detail}")),
_ => Some(format!("database error ({code})")),
}
}
fn sha256_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}