use super::*;
use snarkvm::{
prelude::{ConsensusVersion, Identifier, LimitedWriter, Plaintext, Program, ToBytes, Transaction, VM, Value},
synthesizer::program::{FinalizeGlobalState, StackTrait},
};
use axum::{Json, extract::rejection::JsonRejection};
use anyhow::{Context, anyhow};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::atomic::Ordering;
use rayon::prelude::*;
fn de_csv<'de, D>(de: D) -> std::result::Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(de)?;
Ok(if s.trim().is_empty() { Vec::new() } else { s.split(',').map(|x| x.trim().to_string()).collect() })
}
type ViewFunctionRoute<N> = (ProgramID<N>, Identifier<N>, u32);
fn parse_view_inputs<N: Network>(inputs: &[String]) -> Result<Vec<Value<N>>, RestError> {
inputs
.iter()
.enumerate()
.map(|(index, input)| {
input.parse::<Value<N>>().map_err(|err| {
RestError::unprocessable_entity(err.context(format!("Invalid input at index {index}: {input}")))
})
})
.collect::<Result<Vec<_>, _>>()
}
#[derive(Deserialize, Serialize)]
pub(crate) struct BlockRange {
start: u32,
end: u32,
}
#[derive(Copy, Clone, Deserialize, Serialize)]
pub(crate) struct Metadata {
metadata: Option<bool>,
all: Option<bool>,
}
#[derive(Copy, Clone, Deserialize, Serialize)]
pub(crate) struct CheckTransaction {
check_transaction: Option<bool>,
}
#[derive(Clone, Deserialize, Serialize)]
pub(crate) struct Commitments {
#[serde(deserialize_with = "de_csv")]
commitments: Vec<String>,
}
#[derive(Clone, Deserialize, Serialize)]
pub(crate) struct CreateBlockRequest {
pub num_blocks: Option<u32>,
}
impl<N: Network, C: ConsensusStorage<N>> Rest<N, C> {
pub(crate) async fn get_consensus_version(State(rest): State<Self>) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(N::CONSENSUS_VERSION(rest.ledger.latest_height())? as u16))
}
pub(crate) async fn get_block_height_latest(State(rest): State<Self>) -> ErasedJson {
ErasedJson::new(rest.ledger.latest_height())
}
pub(crate) async fn get_block_hash_latest(State(rest): State<Self>) -> ErasedJson {
ErasedJson::new(rest.ledger.latest_hash())
}
pub(crate) async fn get_block_latest(State(rest): State<Self>) -> ErasedJson {
ErasedJson::new(rest.ledger.latest_block())
}
pub(crate) async fn get_block(
State(rest): State<Self>,
Path(height_or_hash): Path<String>,
) -> Result<ErasedJson, RestError> {
let block = if let Ok(height) = height_or_hash.parse::<u32>() {
rest.ledger.get_block(height).with_context(|| "Failed to get block by height")?
} else if let Ok(hash) = height_or_hash.parse::<N::BlockHash>() {
rest.ledger.get_block_by_hash(&hash).with_context(|| "Failed to get block by hash")?
} else {
return Err(RestError::bad_request(anyhow!(
"invalid input, it is neither a block height nor a block hash"
)));
};
Ok(ErasedJson::new(block))
}
pub(crate) async fn get_blocks(
State(rest): State<Self>,
Query(block_range): Query<BlockRange>,
) -> Result<ErasedJson, RestError> {
let start_height = block_range.start;
let end_height = block_range.end;
const MAX_BLOCK_RANGE: u32 = 50;
if start_height > end_height {
return Err(RestError::bad_request(anyhow!("Invalid block range")));
}
if end_height - start_height > MAX_BLOCK_RANGE {
return Err(RestError::bad_request(anyhow!(
"Cannot request more than {MAX_BLOCK_RANGE} blocks per call (requested {})",
end_height - start_height
)));
}
let get_json_blocks = move || -> Result<ErasedJson, RestError> {
let blocks = (start_height..end_height)
.into_par_iter()
.map(|height| rest.ledger.get_block(height))
.collect::<Result<Vec<_>, _>>()?;
Ok(ErasedJson::new(blocks))
};
match tokio::task::spawn_blocking(get_json_blocks).await {
Ok(json) => json,
Err(err) => {
let err: anyhow::Error = err.into();
Err(RestError::internal_server_error(
err.context(format!("Failed to get blocks '{start_height}..{end_height}'")),
))
}
}
}
pub(crate) async fn get_height(
State(rest): State<Self>,
Path(hash): Path<N::BlockHash>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_height(&hash)?))
}
pub(crate) async fn get_block_header(
State(rest): State<Self>,
Path(height): Path<u32>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_header(height)?))
}
pub(crate) async fn get_block_transactions(
State(rest): State<Self>,
Path(height): Path<u32>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_transactions(height)?))
}
pub(crate) async fn get_transaction(
State(rest): State<Self>,
Path(tx_id): Path<N::TransactionID>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_transaction(tx_id).map_err(|err| {
if err.to_string().contains("Missing") { RestError::not_found(err) } else { RestError::from(err) }
})?))
}
pub(crate) async fn get_confirmed_transaction(
State(rest): State<Self>,
Path(tx_id): Path<N::TransactionID>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_confirmed_transaction(tx_id).map_err(|err| {
if err.to_string().contains("Missing") { RestError::not_found(err) } else { RestError::from(err) }
})?))
}
pub(crate) async fn get_unconfirmed_transaction(
State(rest): State<Self>,
Path(tx_id): Path<N::TransactionID>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_unconfirmed_transaction(&tx_id).map_err(|err| {
if err.to_string().contains("Missing") { RestError::not_found(err) } else { RestError::from(err) }
})?))
}
pub(crate) async fn get_program(
State(rest): State<Self>,
Path(id): Path<ProgramID<N>>,
metadata: Query<Metadata>,
) -> Result<ErasedJson, RestError> {
let program = rest.ledger.get_program(id).with_context(|| format!("Failed to find program `{id}`"))?;
if metadata.metadata.unwrap_or(false) {
let edition = rest.ledger.get_latest_edition_for_program(&id)?;
return rest.return_program_with_metadata(program, edition);
}
Ok(ErasedJson::new(program))
}
pub(crate) async fn get_program_for_edition(
State(rest): State<Self>,
Path((id, edition)): Path<(ProgramID<N>, u16)>,
metadata: Query<Metadata>,
) -> Result<ErasedJson, RestError> {
match rest
.ledger
.try_get_program_for_edition(&id, edition)
.with_context(|| format!("Failed get program `{id}` for edition {edition}"))?
{
Some(program) => {
if metadata.metadata.unwrap_or(false) {
rest.return_program_with_metadata(program, edition)
} else {
Ok(ErasedJson::new(program))
}
}
None => Err(RestError::not_found(anyhow!("No program `{id}` exists for edition {edition}"))),
}
}
fn return_program_with_metadata(&self, program: Program<N>, edition: u16) -> Result<ErasedJson, RestError> {
let id = program.id();
let tx_id = self.ledger.find_latest_transaction_id_from_program_id_and_edition(id, edition)?;
let program_owner = match &tx_id {
Some(tid) => self
.ledger
.vm()
.block_store()
.transaction_store()
.deployment_store()
.get_deployment(tid)?
.and_then(|deployment| deployment.program_owner()),
None => None,
};
Ok(ErasedJson::new(json!({
"program": program,
"edition": edition,
"transaction_id": tx_id,
"program_owner": program_owner,
})))
}
pub(crate) async fn get_latest_program_edition(
State(rest): State<Self>,
Path(id): Path<ProgramID<N>>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_latest_edition_for_program(&id)?))
}
pub(crate) async fn get_mapping_names(
State(rest): State<Self>,
Path(id): Path<ProgramID<N>>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.vm().finalize_store().get_mapping_names_confirmed(&id)?))
}
pub(crate) async fn get_mapping_value(
State(rest): State<Self>,
Path((id, name, key)): Path<(ProgramID<N>, Identifier<N>, Plaintext<N>)>,
metadata: Query<Metadata>,
) -> Result<ErasedJson, RestError> {
let mapping_value = rest.ledger.vm().finalize_store().get_value_confirmed(id, name, &key)?;
if metadata.metadata.unwrap_or(false) {
return Ok(ErasedJson::new(json!({
"data": mapping_value,
"height": rest.ledger.latest_height(),
})));
}
Ok(ErasedJson::new(mapping_value))
}
pub(crate) async fn get_mapping_values(
State(rest): State<Self>,
Path((id, name)): Path<(ProgramID<N>, Identifier<N>)>,
metadata: Query<Metadata>,
) -> Result<ErasedJson, RestError> {
if metadata.all != Some(true) {
return Err(RestError::bad_request(anyhow!(
"Invalid query parameter. At this time, 'all=true' must be included"
)));
}
let height = rest.ledger.latest_height();
match tokio::task::spawn_blocking(move || rest.ledger.vm().finalize_store().get_mapping_confirmed(id, name))
.await
{
Ok(Ok(mapping_values)) => {
if metadata.metadata.unwrap_or(false) {
return Ok(ErasedJson::new(json!({
"data": mapping_values,
"height": height,
})));
}
Ok(ErasedJson::new(mapping_values))
}
Ok(Err(err)) => Err(RestError::internal_server_error(err.context("Unable to read mapping"))),
Err(err) => Err(RestError::internal_server_error(anyhow!("Tokio error: {err}"))),
}
}
pub(crate) async fn get_state_path_for_commitment(
State(rest): State<Self>,
Path(commitment): Path<Field<N>>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_state_path_for_commitment(&commitment)?))
}
pub(crate) async fn get_state_paths_for_commitments(
State(rest): State<Self>,
Query(commitments): Query<Commitments>,
) -> Result<ErasedJson, RestError> {
let num_commitments = commitments.commitments.len();
if num_commitments == 0 {
return Err(RestError::unprocessable_entity(anyhow!("No commitments provided")));
}
if num_commitments > N::MAX_INPUTS {
return Err(RestError::unprocessable_entity(anyhow!(
"Too many commitments provided (max: {}, got: {})",
N::MAX_INPUTS,
num_commitments
)));
}
let commitments = commitments
.commitments
.iter()
.map(|s| {
s.parse::<Field<N>>()
.map_err(|err| RestError::unprocessable_entity(err.context(format!("Invalid commitment: {s}"))))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(ErasedJson::new(rest.ledger.get_state_paths_for_commitments(&commitments)?))
}
pub(crate) async fn get_state_root_latest(State(rest): State<Self>) -> ErasedJson {
ErasedJson::new(rest.ledger.latest_state_root())
}
pub(crate) async fn get_state_root(
State(rest): State<Self>,
Path(height): Path<u32>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.get_state_root(height)?))
}
pub(crate) async fn find_block_hash(
State(rest): State<Self>,
Path(tx_id): Path<N::TransactionID>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.find_block_hash(&tx_id)?))
}
pub(crate) async fn find_block_height_from_state_root(
State(rest): State<Self>,
Path(state_root): Path<N::StateRoot>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.find_block_height_from_state_root(state_root)?))
}
pub(crate) async fn find_latest_transaction_id_from_program_id(
State(rest): State<Self>,
Path(program_id): Path<ProgramID<N>>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.find_latest_transaction_id_from_program_id(&program_id)?))
}
pub(crate) async fn find_transaction_id_from_program_id_and_edition(
State(rest): State<Self>,
Path((program_id, edition)): Path<(ProgramID<N>, u16)>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.find_latest_transaction_id_from_program_id_and_edition(&program_id, edition)?))
}
pub(crate) async fn find_transaction_id_from_transition_id(
State(rest): State<Self>,
Path(transition_id): Path<N::TransitionID>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.find_transaction_id_from_transition_id(&transition_id)?))
}
pub(crate) async fn find_transition_id(
State(rest): State<Self>,
Path(input_or_output_id): Path<Field<N>>,
) -> Result<ErasedJson, RestError> {
Ok(ErasedJson::new(rest.ledger.find_transition_id(&input_or_output_id)?))
}
pub(crate) async fn transaction_broadcast(
State(rest): State<Self>,
check_transaction: Query<CheckTransaction>,
json_result: Result<Json<Transaction<N>>, JsonRejection>,
) -> Result<impl axum::response::IntoResponse, RestError> {
let Json(tx) = match json_result {
Ok(json) => json,
Err(JsonRejection::JsonDataError(err)) => {
return Err(RestError::unprocessable_entity(anyhow!("Invalid transaction data: {err}")));
}
Err(other_rejection) => return Err(other_rejection.into()),
};
let tx_id = tx.id();
let buffer = Vec::with_capacity(3000);
if tx.write_le(LimitedWriter::new(buffer, N::LATEST_MAX_TRANSACTION_SIZE())).is_err() {
return Err(RestError::bad_request(anyhow!("Transaction size exceeds the byte limit")));
}
let check_transaction = check_transaction.check_transaction.unwrap_or(true);
if check_transaction {
let (counter, limit, err_msg) = if tx.is_execute() {
(
&rest.num_verifying_executions,
VM::<N, C>::MAX_PARALLEL_EXECUTE_VERIFICATIONS,
"Too many execution verifications in progress",
)
} else {
(
&rest.num_verifying_deploys,
VM::<N, C>::MAX_PARALLEL_DEPLOY_VERIFICATIONS,
"Too many deploy verifications in progress",
)
};
if counter
.fetch_update(
Ordering::Relaxed,
Ordering::Relaxed,
|val| {
if val < limit { Some(val + 1) } else { None }
},
)
.is_err()
{
return Err(RestError::too_many_requests(anyhow!("{err_msg}")));
}
let res = rest
.ledger
.check_transaction_basic(&tx, None, &mut rand::rng())
.map_err(|err| RestError::unprocessable_entity(err.context("Invalid transaction")));
counter.fetch_sub(1, Ordering::Relaxed);
res?;
}
if !rest.manual_block_creation {
let ledger = rest.ledger.clone();
let new_block = tokio::task::spawn_blocking(move || {
ledger
.prepare_advance_to_next_beacon_block(&rest.private_key, vec![], vec![], vec![tx], &mut rand::rng())
.map_err(|e| anyhow!("{e}"))
})
.await
.map_err(|e| RestError::internal_server_error(anyhow!("Task panicked: {}", e)))??;
tokio::task::spawn_blocking(move || {
rest.ledger.advance_to_next_block(&new_block).map_err(|e| anyhow!("{e}"))
})
.await
.map_err(|e| RestError::internal_server_error(anyhow!("Task panicked: {}", e)))??;
return Ok((StatusCode::OK, ErasedJson::new(tx_id)));
}
{
let mut buffer = rest.buffer.lock();
buffer.push(tx);
}
Ok((StatusCode::OK, ErasedJson::new(tx_id)))
}
pub(crate) async fn evaluate_view(
State(rest): State<Self>,
Path((program_id, view_name, height)): Path<ViewFunctionRoute<N>>,
json_result: Result<Json<Vec<String>>, JsonRejection>,
) -> Result<ErasedJson, RestError> {
let Json(raw_inputs) = match json_result {
Ok(json) => json,
Err(err) => return Err(RestError::unprocessable_entity(anyhow!("Invalid request body: {err}"))),
};
let inputs = parse_view_inputs(&raw_inputs)?;
let outputs = match tokio::task::spawn_blocking(move || {
rest.ledger.vm().evaluate_view_at_height(program_id, view_name, inputs, height)
})
.await
{
Ok(Ok(outputs)) => outputs,
Ok(Err(err)) => {
return Err(RestError::bad_request(
err.context(format!("Failed to evaluate view '{view_name}' for '{program_id}' at height {height}")),
));
}
Err(err) => return Err(RestError::internal_server_error(anyhow!("Tokio error: {err}"))),
};
let output_strings: Vec<String> = outputs.iter().map(|v| v.to_string()).collect();
Ok(ErasedJson::new(output_strings))
}
pub(crate) async fn evaluate_view_latest(
State(rest): State<Self>,
Path((program_id, view_name)): Path<(ProgramID<N>, Identifier<N>)>,
metadata: Query<Metadata>,
json_result: Result<Json<Vec<String>>, JsonRejection>,
) -> Result<ErasedJson, RestError> {
let Json(raw_inputs) = match json_result {
Ok(json) => json,
Err(err) => return Err(RestError::unprocessable_entity(anyhow!("Invalid request body: {err}"))),
};
let inputs = parse_view_inputs(&raw_inputs)?;
let (outputs, height) = match tokio::task::spawn_blocking(move || {
let block = rest.ledger.latest_block();
let height = block.height();
let block_timestamp =
(height >= N::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default()).then_some(block.timestamp());
let state = FinalizeGlobalState::new::<N>(
block.round(),
height,
block_timestamp,
block.cumulative_weight(),
block.cumulative_proof_target(),
block.previous_hash(),
None,
None,
)?;
let stack = rest.ledger.vm().process().get_stack(program_id)?;
let outputs = stack.evaluate_view(state, rest.ledger.vm().finalize_store(), &view_name, inputs)?;
Ok::<_, anyhow::Error>((outputs, height))
})
.await
{
Ok(Ok(result)) => result,
Ok(Err(err)) => {
return Err(RestError::bad_request(err.context(format!(
"Failed to evaluate view '{view_name}' for '{program_id}' at the latest height"
))));
}
Err(err) => return Err(RestError::internal_server_error(anyhow!("Tokio error: {err}"))),
};
let output_strings: Vec<String> = outputs.iter().map(|v| v.to_string()).collect();
if metadata.metadata.unwrap_or(false) {
return Ok(ErasedJson::new(json!({
"data": output_strings,
"height": height,
})));
}
Ok(ErasedJson::new(output_strings))
}
pub(crate) async fn create_block(
State(rest): State<Self>,
Json(req): Json<CreateBlockRequest>,
) -> Result<ErasedJson, RestError> {
let num_blocks = req.num_blocks.unwrap_or(1);
let last_block = tokio::task::spawn_blocking(move || -> Result<ErasedJson, RestError> {
let mut last_block = None;
let mut unconfirmed_txs = Some({
let mut buffer = rest.buffer.lock();
buffer.drain(..).collect()
});
for _ in 0..num_blocks {
let txs = unconfirmed_txs.take().unwrap_or_default();
let new_block = rest
.ledger
.prepare_advance_to_next_beacon_block(&rest.private_key, vec![], vec![], txs, &mut rand::rng())
.map_err(|e| RestError::internal_server_error(anyhow!("Failed to prepare block: {}", e)))?;
rest.ledger
.advance_to_next_block(&new_block)
.map_err(|e| RestError::internal_server_error(anyhow!("Failed to advance block: {}", e)))?;
last_block = Some(new_block);
}
Ok(ErasedJson::new(last_block.unwrap()))
})
.await
.map_err(|e| RestError::internal_server_error(anyhow!("Task panicked: {}", e)))??;
Ok(last_block)
}
}