1037 lines
32 KiB
Rust
1037 lines
32 KiB
Rust
use crate::chain::{Network, OutPoint, Transaction, TxIn, TxOut};
|
|
use crate::config::Config;
|
|
use crate::daemon::Daemon;
|
|
use crate::errors;
|
|
use crate::new_index::{compute_script_hash, Query, SpendingInput, Utxo};
|
|
use crate::util::Address;
|
|
use crate::util::{
|
|
full_hash, get_script_asm, has_prevout, is_coinbase, script_to_address, BlockHeaderMeta,
|
|
BlockId, FullHash, TransactionStatus,
|
|
};
|
|
|
|
#[cfg(feature = "liquid")]
|
|
use crate::util::{BlockProofValue, IssuanceValue, PegOutRequest};
|
|
|
|
use bitcoin::consensus::encode::{self, serialize};
|
|
use bitcoin::util::hash::{HexError, Sha256dHash};
|
|
use bitcoin::{BitcoinHash, Script};
|
|
use futures::sync::oneshot;
|
|
use hex::{self, FromHexError};
|
|
use hyper::rt::{self, Future};
|
|
use hyper::service::service_fn_ok;
|
|
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
|
|
|
#[cfg(feature = "liquid")]
|
|
use elements::confidential::{Asset, Value};
|
|
|
|
use serde::Serialize;
|
|
use serde_json;
|
|
use std::collections::HashMap;
|
|
use std::num::ParseIntError;
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use url::form_urlencoded;
|
|
|
|
const CHAIN_TXS_PER_PAGE: usize = 25;
|
|
const MAX_MEMPOOL_TXS: usize = 50;
|
|
const BLOCK_LIMIT: usize = 10;
|
|
|
|
const TTL_LONG: u32 = 157784630; // ttl for static resources (5 years)
|
|
const TTL_SHORT: u32 = 10; // ttl for volatie resources
|
|
const CONF_FINAL: usize = 10; // reorgs deeper than this are considered unlikely
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct BlockValue {
|
|
id: String,
|
|
height: u32,
|
|
version: u32,
|
|
timestamp: u32,
|
|
tx_count: u32,
|
|
size: u32,
|
|
weight: u32,
|
|
merkle_root: String,
|
|
previousblockhash: Option<String>,
|
|
#[cfg(not(feature = "liquid"))]
|
|
nonce: u32,
|
|
#[cfg(not(feature = "liquid"))]
|
|
bits: u32,
|
|
#[cfg(feature = "liquid")]
|
|
proof: Option<BlockProofValue>,
|
|
}
|
|
|
|
impl From<BlockHeaderMeta> for BlockValue {
|
|
fn from(blockhm: BlockHeaderMeta) -> Self {
|
|
let header = blockhm.header_entry.header();
|
|
BlockValue {
|
|
id: header.bitcoin_hash().be_hex_string(),
|
|
height: blockhm.header_entry.height() as u32,
|
|
version: header.version,
|
|
timestamp: header.time,
|
|
tx_count: blockhm.meta.tx_count,
|
|
size: blockhm.meta.size,
|
|
weight: blockhm.meta.weight,
|
|
merkle_root: header.merkle_root.be_hex_string(),
|
|
previousblockhash: if &header.prev_blockhash != &Sha256dHash::default() {
|
|
Some(header.prev_blockhash.be_hex_string())
|
|
} else {
|
|
None
|
|
},
|
|
|
|
#[cfg(not(feature = "liquid"))]
|
|
bits: header.bits,
|
|
#[cfg(not(feature = "liquid"))]
|
|
nonce: header.nonce,
|
|
|
|
#[cfg(feature = "liquid")]
|
|
proof: Some(BlockProofValue::from(&header.proof)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct TransactionValue {
|
|
txid: Sha256dHash,
|
|
version: u32,
|
|
locktime: u32,
|
|
vin: Vec<TxInValue>,
|
|
vout: Vec<TxOutValue>,
|
|
size: u32,
|
|
weight: u32,
|
|
fee: Option<u64>,
|
|
status: Option<TransactionStatus>,
|
|
}
|
|
|
|
impl
|
|
From<(
|
|
Transaction,
|
|
Option<BlockId>,
|
|
&HashMap<OutPoint, TxOut>,
|
|
&Config,
|
|
)> for TransactionValue
|
|
{
|
|
fn from(
|
|
(tx, blockid, prevouts, config): (
|
|
Transaction,
|
|
Option<BlockId>,
|
|
&HashMap<OutPoint, TxOut>,
|
|
&Config,
|
|
),
|
|
) -> Self {
|
|
let vins: Vec<TxInValue> = tx
|
|
.input
|
|
.iter()
|
|
.map(|txin| {
|
|
let prevout = prevouts.get(&txin.previous_output);
|
|
TxInValue::from((txin.clone(), prevout, config)) // TODO avoid clone
|
|
})
|
|
.collect();
|
|
let vouts: Vec<TxOutValue> = tx
|
|
.output
|
|
.iter()
|
|
.map(|txout| TxOutValue::from((txout.clone(), config))) // TODO avoid clone
|
|
.collect();
|
|
let bytes = serialize(&tx);
|
|
|
|
#[cfg(not(feature = "liquid"))]
|
|
let fee = if config.prevout_enabled && !vins.iter().any(|vin| vin.prevout.is_none()) {
|
|
let total_in: u64 = vins
|
|
.iter()
|
|
.map(|vin| vin.prevout.as_ref().unwrap().value)
|
|
.sum();
|
|
let total_out: u64 = vouts.iter().map(|vout| vout.value).sum();
|
|
Some(total_in - total_out)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
#[cfg(feature = "liquid")]
|
|
let fee = vouts
|
|
.iter()
|
|
.find(|vout| vout.scriptpubkey_type == "fee")
|
|
.map(|vout| vout.value.unwrap())
|
|
.or_else(|| Some(0));
|
|
|
|
TransactionValue {
|
|
txid: tx.txid(),
|
|
version: tx.version,
|
|
locktime: tx.lock_time,
|
|
vin: vins,
|
|
vout: vouts,
|
|
size: bytes.len() as u32,
|
|
weight: tx.get_weight() as u32,
|
|
fee,
|
|
status: Some(TransactionStatus::from(blockid)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
struct TxInValue {
|
|
txid: Sha256dHash,
|
|
vout: u32,
|
|
prevout: Option<TxOutValue>,
|
|
scriptsig: Script,
|
|
scriptsig_asm: String,
|
|
witness: Option<Vec<String>>,
|
|
is_coinbase: bool,
|
|
sequence: u32,
|
|
|
|
#[cfg(feature = "liquid")]
|
|
is_pegin: bool,
|
|
#[cfg(feature = "liquid")]
|
|
issuance: Option<IssuanceValue>,
|
|
}
|
|
|
|
impl From<(TxIn, Option<&TxOut>, &Config)> for TxInValue {
|
|
fn from((txin, prevout, config): (TxIn, Option<&TxOut>, &Config)) -> Self {
|
|
#[cfg(not(feature = "liquid"))]
|
|
let witness = if txin.witness.len() > 0 {
|
|
Some(txin.witness.iter().map(|w| hex::encode(w)).collect())
|
|
} else {
|
|
None
|
|
};
|
|
#[cfg(feature = "liquid")]
|
|
let witness = None; // @TODO
|
|
|
|
let is_coinbase = is_coinbase(&txin);
|
|
|
|
TxInValue {
|
|
txid: txin.previous_output.txid,
|
|
vout: txin.previous_output.vout,
|
|
prevout: prevout.map(|prevout| TxOutValue::from((prevout.clone(), config))), // TODO avoid clone()
|
|
scriptsig_asm: get_script_asm(&txin.script_sig),
|
|
witness,
|
|
is_coinbase,
|
|
sequence: txin.sequence,
|
|
#[cfg(feature = "liquid")]
|
|
is_pegin: txin.is_pegin,
|
|
#[cfg(feature = "liquid")]
|
|
issuance: if txin.has_issuance() {
|
|
Some(IssuanceValue::from(&txin.asset_issuance))
|
|
} else {
|
|
None
|
|
},
|
|
|
|
scriptsig: txin.script_sig,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
struct TxOutValue {
|
|
scriptpubkey: Script,
|
|
scriptpubkey_asm: String,
|
|
scriptpubkey_address: Option<String>,
|
|
scriptpubkey_type: String,
|
|
|
|
#[cfg(not(feature = "liquid"))]
|
|
value: u64,
|
|
|
|
#[cfg(feature = "liquid")]
|
|
value: Option<u64>,
|
|
#[cfg(feature = "liquid")]
|
|
valuecommitment: Option<String>,
|
|
#[cfg(feature = "liquid")]
|
|
asset: Option<String>,
|
|
#[cfg(feature = "liquid")]
|
|
assetcommitment: Option<String>,
|
|
#[cfg(feature = "liquid")]
|
|
pegout: Option<PegOutRequest>,
|
|
}
|
|
|
|
impl From<(TxOut, &Config)> for TxOutValue {
|
|
fn from((txout, config): (TxOut, &Config)) -> Self {
|
|
#[cfg(not(feature = "liquid"))]
|
|
let value = txout.value;
|
|
|
|
#[cfg(feature = "liquid")]
|
|
let value = match txout.value {
|
|
Value::Explicit(value) => Some(value),
|
|
_ => None,
|
|
};
|
|
#[cfg(feature = "liquid")]
|
|
let valuecommitment = match txout.value {
|
|
Value::Confidential(..) => Some(hex::encode(serialize(&txout.value))),
|
|
_ => None,
|
|
};
|
|
#[cfg(feature = "liquid")]
|
|
let asset = match txout.asset {
|
|
Asset::Explicit(value) => Some(value.be_hex_string()),
|
|
_ => None,
|
|
};
|
|
#[cfg(feature = "liquid")]
|
|
let assetcommitment = match txout.asset {
|
|
Asset::Confidential(..) => Some(hex::encode(serialize(&txout.asset))),
|
|
_ => None,
|
|
};
|
|
|
|
#[cfg(not(feature = "liquid"))]
|
|
let is_fee = false;
|
|
#[cfg(feature = "liquid")]
|
|
let is_fee = txout.is_fee();
|
|
|
|
let script = txout.script_pubkey;
|
|
let script_asm = get_script_asm(&script);
|
|
let script_addr = script_to_address(&script, &config.network_type);
|
|
|
|
// TODO should the following something to put inside rust-elements lib?
|
|
let script_type = if is_fee {
|
|
"fee"
|
|
} else if script.is_empty() {
|
|
"empty"
|
|
} else if script.is_op_return() {
|
|
"op_return"
|
|
} else if script.is_p2pk() {
|
|
"p2pk"
|
|
} else if script.is_p2pkh() {
|
|
"p2pkh"
|
|
} else if script.is_p2sh() {
|
|
"p2sh"
|
|
} else if script.is_v0_p2wpkh() {
|
|
"v0_p2wpkh"
|
|
} else if script.is_v0_p2wsh() {
|
|
"v0_p2wsh"
|
|
} else if script.is_provably_unspendable() {
|
|
"provably_unspendable"
|
|
} else {
|
|
"unknown"
|
|
};
|
|
|
|
#[cfg(feature = "liquid")]
|
|
let pegout =
|
|
PegOutRequest::parse(&script, &config.parent_network, &config.parent_genesis_hash);
|
|
|
|
TxOutValue {
|
|
scriptpubkey: script,
|
|
scriptpubkey_asm: script_asm,
|
|
scriptpubkey_address: script_addr,
|
|
scriptpubkey_type: script_type.to_string(),
|
|
value,
|
|
#[cfg(feature = "liquid")]
|
|
valuecommitment,
|
|
#[cfg(feature = "liquid")]
|
|
asset,
|
|
#[cfg(feature = "liquid")]
|
|
assetcommitment,
|
|
#[cfg(feature = "liquid")]
|
|
pegout,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct UtxoValue {
|
|
txid: Sha256dHash,
|
|
vout: u32,
|
|
status: TransactionStatus,
|
|
#[cfg(not(feature = "liquid"))]
|
|
value: u64,
|
|
#[cfg(feature = "liquid")]
|
|
value: Option<u64>,
|
|
#[cfg(feature = "liquid")]
|
|
valuecommitment: Option<String>,
|
|
}
|
|
impl From<Utxo> for UtxoValue {
|
|
fn from(utxo: Utxo) -> Self {
|
|
#[cfg(not(feature = "liquid"))]
|
|
let value = utxo.value;
|
|
|
|
#[cfg(feature = "liquid")]
|
|
let value = match utxo.value {
|
|
Value::Explicit(value) => Some(value),
|
|
_ => None,
|
|
};
|
|
#[cfg(feature = "liquid")]
|
|
let valuecommitment = match utxo.value {
|
|
Value::Confidential(..) => Some(hex::encode(serialize(&utxo.value))),
|
|
_ => None,
|
|
};
|
|
|
|
UtxoValue {
|
|
txid: utxo.txid,
|
|
vout: utxo.vout,
|
|
value,
|
|
status: TransactionStatus::from(utxo.confirmed),
|
|
#[cfg(feature = "liquid")]
|
|
valuecommitment,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct SpendingValue {
|
|
spent: bool,
|
|
txid: Option<Sha256dHash>,
|
|
vin: Option<u32>,
|
|
status: Option<TransactionStatus>,
|
|
}
|
|
impl From<SpendingInput> for SpendingValue {
|
|
fn from(spend: SpendingInput) -> Self {
|
|
SpendingValue {
|
|
spent: true,
|
|
txid: Some(spend.txid),
|
|
vin: Some(spend.vin),
|
|
status: Some(TransactionStatus::from(spend.confirmed)),
|
|
}
|
|
}
|
|
}
|
|
impl Default for SpendingValue {
|
|
fn default() -> Self {
|
|
SpendingValue {
|
|
spent: false,
|
|
txid: None,
|
|
vin: None,
|
|
status: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn ttl_by_depth(height: Option<usize>, query: &Query) -> u32 {
|
|
height.map_or(TTL_SHORT, |height| {
|
|
if query.chain().best_height() - height >= CONF_FINAL {
|
|
TTL_LONG
|
|
} else {
|
|
TTL_SHORT
|
|
}
|
|
})
|
|
}
|
|
|
|
fn prepare_txs(
|
|
txs: Vec<(Transaction, Option<BlockId>)>,
|
|
query: &Query,
|
|
config: &Config,
|
|
) -> Vec<TransactionValue> {
|
|
let prevouts = if config.prevout_enabled {
|
|
let outpoints = txs
|
|
.iter()
|
|
.flat_map(|(tx, _)| {
|
|
tx.input
|
|
.iter()
|
|
.filter(|txin| has_prevout(txin))
|
|
.map(|txin| txin.previous_output)
|
|
})
|
|
.collect();
|
|
|
|
query.lookup_txos(&outpoints)
|
|
} else {
|
|
HashMap::new()
|
|
};
|
|
|
|
txs.into_iter()
|
|
.map(|(tx, blockid)| TransactionValue::from((tx, blockid, &prevouts, config)))
|
|
.collect()
|
|
}
|
|
|
|
pub fn run_server(config: Arc<Config>, query: Arc<Query>, daemon: Arc<Daemon>) -> Handle {
|
|
let addr = &config.http_addr;
|
|
info!("REST server running on {}", addr);
|
|
|
|
let config = Arc::new(config.clone());
|
|
|
|
let new_service = move || {
|
|
let query = Arc::clone(&query);
|
|
let config = Arc::clone(&config);
|
|
let daemon = Arc::clone(&daemon);
|
|
|
|
service_fn_ok(move |req: Request<Body>| {
|
|
let mut resp = handle_request(req, &query, &config, &daemon).unwrap_or_else(|err| {
|
|
warn!("{:?}", err);
|
|
Response::builder()
|
|
.status(err.0)
|
|
.header("Content-Type", "text/plain")
|
|
.body(Body::from(err.1))
|
|
.unwrap()
|
|
});
|
|
if let Some(ref origins) = config.cors {
|
|
resp.headers_mut()
|
|
.insert("Access-Control-Allow-Origin", origins.parse().unwrap());
|
|
}
|
|
resp
|
|
})
|
|
};
|
|
|
|
let (tx, rx) = oneshot::channel::<()>();
|
|
let server = Server::bind(&addr)
|
|
.serve(new_service)
|
|
.with_graceful_shutdown(rx)
|
|
.map_err(|e| eprintln!("server error: {}", e));
|
|
|
|
Handle {
|
|
tx,
|
|
thread: thread::spawn(move || {
|
|
rt::run(server);
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub struct Handle {
|
|
tx: oneshot::Sender<()>,
|
|
thread: thread::JoinHandle<()>,
|
|
}
|
|
|
|
impl Handle {
|
|
pub fn stop(self) {
|
|
self.tx.send(()).expect("failed to send shutdown signal");
|
|
self.thread.join().expect("REST server failed");
|
|
}
|
|
}
|
|
|
|
fn handle_request(
|
|
req: Request<Body>,
|
|
query: &Query,
|
|
config: &Config,
|
|
daemon: &Daemon,
|
|
) -> Result<Response<Body>, HttpError> {
|
|
// TODO it looks hyper does not have routing and query parsing :(
|
|
let uri = req.uri();
|
|
let path: Vec<&str> = uri.path().split('/').skip(1).collect();
|
|
let query_params = match uri.query() {
|
|
Some(value) => form_urlencoded::parse(&value.as_bytes())
|
|
.into_owned()
|
|
.collect::<HashMap<String, String>>(),
|
|
None => HashMap::new(),
|
|
};
|
|
|
|
info!("path {:?}", path);
|
|
match (
|
|
req.method(),
|
|
path.get(0),
|
|
path.get(1),
|
|
path.get(2),
|
|
path.get(3),
|
|
path.get(4),
|
|
) {
|
|
(&Method::GET, Some(&"blocks"), Some(&"tip"), Some(&"hash"), None, None) => http_message(
|
|
StatusCode::OK,
|
|
query.chain().best_hash().be_hex_string(),
|
|
TTL_SHORT,
|
|
),
|
|
|
|
(&Method::GET, Some(&"blocks"), Some(&"tip"), Some(&"height"), None, None) => http_message(
|
|
StatusCode::OK,
|
|
query.chain().best_height().to_string(),
|
|
TTL_SHORT,
|
|
),
|
|
|
|
(&Method::GET, Some(&"blocks"), start_height, None, None, None) => {
|
|
let start_height = start_height.and_then(|height| height.parse::<usize>().ok());
|
|
blocks(&query, start_height)
|
|
}
|
|
(&Method::GET, Some(&"block-height"), Some(height), None, None, None) => {
|
|
let height = height.parse::<usize>()?;
|
|
let header = query
|
|
.chain()
|
|
.header_by_height(height)
|
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
|
let ttl = ttl_by_depth(Some(height), query);
|
|
http_message(StatusCode::OK, header.hash().be_hex_string(), ttl)
|
|
}
|
|
(&Method::GET, Some(&"block"), Some(hash), None, None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let blockhm = query
|
|
.chain()
|
|
.get_block_with_meta(&hash)
|
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
|
let block_value = BlockValue::from(blockhm);
|
|
json_response(block_value, TTL_LONG)
|
|
}
|
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"status"), None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let status = query.chain().get_block_status(&hash);
|
|
let ttl = ttl_by_depth(status.height, query);
|
|
json_response(status, ttl)
|
|
}
|
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"txids"), None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let txids = query
|
|
.chain()
|
|
.get_block_txids(&hash)
|
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
|
json_response(txids, TTL_LONG)
|
|
}
|
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"txs"), start_index, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let txids = query
|
|
.chain()
|
|
.get_block_txids(&hash)
|
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
|
|
|
let start_index = start_index
|
|
.map_or(0u32, |el| el.parse().unwrap_or(0))
|
|
.max(0u32) as usize;
|
|
if start_index >= txids.len() {
|
|
bail!(HttpError::not_found("start index out of range".to_string()));
|
|
} else if start_index % CHAIN_TXS_PER_PAGE != 0 {
|
|
bail!(HttpError::from(format!(
|
|
"start index must be a multipication of {}",
|
|
CHAIN_TXS_PER_PAGE
|
|
)));
|
|
}
|
|
|
|
let txs = txids
|
|
.iter()
|
|
.skip(start_index)
|
|
.take(CHAIN_TXS_PER_PAGE)
|
|
.map(|txid| {
|
|
query
|
|
.lookup_txn(&txid)
|
|
// FIXME: set blockid correctly, only when the block is non-orphaned
|
|
// alternatively, don't set "status" at all?
|
|
.map(|tx| (tx, None))
|
|
.ok_or_else(|| "missing tx".to_string())
|
|
})
|
|
.collect::<Result<Vec<(Transaction, Option<BlockId>)>, _>>()?;
|
|
|
|
json_response(prepare_txs(txs, query, config), TTL_LONG)
|
|
}
|
|
(&Method::GET, Some(script_type @ &"address"), Some(script_str), None, None, None)
|
|
| (&Method::GET, Some(script_type @ &"scripthash"), Some(script_str), None, None, None) => {
|
|
let script_hash = to_scripthash(script_type, script_str, &config.network_type)?;
|
|
let stats = query.stats(&script_hash[..]);
|
|
json_response(
|
|
json!({
|
|
*script_type: script_str,
|
|
"chain_stats": stats.0,
|
|
"mempool_stats": stats.1,
|
|
}),
|
|
TTL_SHORT,
|
|
)
|
|
}
|
|
(
|
|
&Method::GET,
|
|
Some(script_type @ &"address"),
|
|
Some(script_str),
|
|
Some(&"txs"),
|
|
None,
|
|
None,
|
|
)
|
|
| (
|
|
&Method::GET,
|
|
Some(script_type @ &"scripthash"),
|
|
Some(script_str),
|
|
Some(&"txs"),
|
|
None,
|
|
None,
|
|
) => {
|
|
let script_hash = to_scripthash(script_type, script_str, &config.network_type)?;
|
|
|
|
let mut txs = vec![];
|
|
|
|
txs.extend(
|
|
query
|
|
.mempool()
|
|
.history(&script_hash[..], MAX_MEMPOOL_TXS)
|
|
.into_iter()
|
|
.map(|tx| (tx, None)),
|
|
);
|
|
|
|
txs.extend(
|
|
query
|
|
.chain()
|
|
.history(&script_hash[..], None, CHAIN_TXS_PER_PAGE)
|
|
.into_iter(),
|
|
);
|
|
|
|
json_response(prepare_txs(txs, query, config), TTL_SHORT)
|
|
}
|
|
|
|
(
|
|
&Method::GET,
|
|
Some(script_type @ &"address"),
|
|
Some(script_str),
|
|
Some(&"txs"),
|
|
Some(&"chain"),
|
|
last_seen_txid,
|
|
)
|
|
| (
|
|
&Method::GET,
|
|
Some(script_type @ &"scripthash"),
|
|
Some(script_str),
|
|
Some(&"txs"),
|
|
Some(&"chain"),
|
|
last_seen_txid,
|
|
) => {
|
|
let script_hash = to_scripthash(script_type, script_str, &config.network_type)?;
|
|
let last_seen_txid = last_seen_txid.and_then(|txid| Sha256dHash::from_hex(txid).ok());
|
|
|
|
let txs = query.chain().history(
|
|
&script_hash[..],
|
|
last_seen_txid.as_ref(),
|
|
CHAIN_TXS_PER_PAGE,
|
|
);
|
|
|
|
json_response(prepare_txs(txs, query, config), TTL_SHORT)
|
|
}
|
|
(
|
|
&Method::GET,
|
|
Some(script_type @ &"address"),
|
|
Some(script_str),
|
|
Some(&"txs"),
|
|
Some(&"mempool"),
|
|
None,
|
|
)
|
|
| (
|
|
&Method::GET,
|
|
Some(script_type @ &"scripthash"),
|
|
Some(script_str),
|
|
Some(&"txs"),
|
|
Some(&"mempool"),
|
|
None,
|
|
) => {
|
|
let script_hash = to_scripthash(script_type, script_str, &config.network_type)?;
|
|
|
|
let txs = query
|
|
.mempool()
|
|
.history(&script_hash[..], MAX_MEMPOOL_TXS)
|
|
.into_iter()
|
|
.map(|tx| (tx, None))
|
|
.collect();
|
|
|
|
json_response(prepare_txs(txs, query, config), TTL_SHORT)
|
|
}
|
|
|
|
(
|
|
&Method::GET,
|
|
Some(script_type @ &"address"),
|
|
Some(script_str),
|
|
Some(&"utxo"),
|
|
None,
|
|
None,
|
|
)
|
|
| (
|
|
&Method::GET,
|
|
Some(script_type @ &"scripthash"),
|
|
Some(script_str),
|
|
Some(&"utxo"),
|
|
None,
|
|
None,
|
|
) => {
|
|
let script_hash = to_scripthash(script_type, script_str, &config.network_type)?;
|
|
let utxos: Vec<UtxoValue> = query
|
|
.utxo(&script_hash[..])
|
|
.into_iter()
|
|
.map(UtxoValue::from)
|
|
.collect();
|
|
// XXX paging?
|
|
json_response(utxos, TTL_SHORT)
|
|
}
|
|
(&Method::GET, Some(&"tx"), Some(hash), None, None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let tx = query
|
|
.lookup_txn(&hash)
|
|
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
|
let confirmation = query.chain().tx_confirming_block(&hash);
|
|
let ttl = ttl_by_depth(confirmation.as_ref().map(|c| c.height), query);
|
|
|
|
let tx = prepare_txs(vec![(tx, confirmation)], query, config).remove(0);
|
|
|
|
json_response(tx, ttl)
|
|
}
|
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"hex"), None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let rawtx = query
|
|
.lookup_raw_txn(&hash)
|
|
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
|
let ttl = ttl_by_depth(query.get_tx_status(&hash).block_height, query);
|
|
http_message(StatusCode::OK, hex::encode(rawtx), ttl)
|
|
}
|
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"status"), None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let status = query.get_tx_status(&hash);
|
|
let ttl = ttl_by_depth(status.block_height, query);
|
|
json_response(status, ttl)
|
|
}
|
|
// TODO: implement merkle proof
|
|
/*
|
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"merkle-proof"), None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let status = query.get_tx_status(&hash);
|
|
if !status.confirmed {
|
|
bail!("Transaction is unconfirmed".to_string())
|
|
};
|
|
let proof = query.get_merkle_proof(&hash, &status.block_hash.unwrap())?;
|
|
let ttl = ttl_by_depth(status.block_height, query);
|
|
json_response(
|
|
json!({ "block_height": status.block_height, "merkle": proof.0, "pos": proof.1 }),
|
|
ttl,
|
|
)
|
|
}
|
|
*/
|
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"outspend"), Some(index), None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let outpoint = OutPoint {
|
|
txid: hash,
|
|
vout: index.parse::<u32>()?,
|
|
};
|
|
let spend = query
|
|
.lookup_spend(&outpoint)
|
|
.map_or_else(SpendingValue::default, SpendingValue::from);
|
|
let ttl = ttl_by_depth(
|
|
spend
|
|
.status
|
|
.as_ref()
|
|
.and_then(|ref status| status.block_height),
|
|
query,
|
|
);
|
|
json_response(spend, ttl)
|
|
}
|
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"outspends"), None, None) => {
|
|
let hash = Sha256dHash::from_hex(hash)?;
|
|
let tx = query
|
|
.lookup_txn(&hash)
|
|
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
|
let spends: Vec<SpendingValue> = query
|
|
.lookup_tx_spends(tx)
|
|
.into_iter()
|
|
.map(|spend| {
|
|
spend.map_or_else(
|
|
|| SpendingValue::default(),
|
|
|spend| SpendingValue::from(spend),
|
|
)
|
|
})
|
|
.collect();
|
|
// @TODO long ttl if all outputs are either spent long ago or unspendable
|
|
json_response(spends, TTL_SHORT)
|
|
}
|
|
(&Method::GET, Some(&"broadcast"), None, None, None, None) => {
|
|
// FIXME read txhex from post body
|
|
let txhex = query_params
|
|
.get("tx")
|
|
.ok_or_else(|| HttpError::from("Missing tx".to_string()))?;
|
|
let txid = daemon
|
|
.broadcast_raw(&txhex)
|
|
.map_err(|err| HttpError::from(err.description().to_string()))?;
|
|
query.mempool_write().add_by_txid(daemon, &txid);
|
|
http_message(StatusCode::OK, txid.be_hex_string(), 0)
|
|
}
|
|
|
|
_ => Err(HttpError::not_found(format!(
|
|
"endpoint does not exist {:?}",
|
|
uri.path()
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn http_message(
|
|
status: StatusCode,
|
|
message: String,
|
|
ttl: u32,
|
|
) -> Result<Response<Body>, HttpError> {
|
|
Ok(Response::builder()
|
|
.status(status)
|
|
.header("Content-Type", "text/plain")
|
|
.header("Cache-Control", format!("public, max-age={:}", ttl))
|
|
.body(Body::from(message))
|
|
.unwrap())
|
|
}
|
|
|
|
fn json_response<T: Serialize>(value: T, ttl: u32) -> Result<Response<Body>, HttpError> {
|
|
let value = serde_json::to_string(&value)?;
|
|
Ok(Response::builder()
|
|
.header("Content-Type", "application/json")
|
|
.header("Cache-Control", format!("public, max-age={:}", ttl))
|
|
.body(Body::from(value))
|
|
.unwrap())
|
|
}
|
|
|
|
fn blocks(query: &Query, start_height: Option<usize>) -> Result<Response<Body>, HttpError> {
|
|
let mut values = Vec::new();
|
|
let mut current_hash = match start_height {
|
|
Some(height) => query
|
|
.chain()
|
|
.header_by_height(height)
|
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?
|
|
.hash()
|
|
.clone(),
|
|
None => query.chain().best_header().hash().clone(),
|
|
};
|
|
|
|
let zero = [0u8; 32];
|
|
for _ in 0..BLOCK_LIMIT {
|
|
let blockhm = query
|
|
.chain()
|
|
.get_block_with_meta(¤t_hash)
|
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
|
current_hash = blockhm.header_entry.header().prev_blockhash.clone();
|
|
|
|
#[allow(unused_mut)]
|
|
let mut value = BlockValue::from(blockhm);
|
|
|
|
#[cfg(feature = "liquid")]
|
|
{
|
|
// exclude proof in block list view
|
|
value.proof = None;
|
|
}
|
|
values.push(value);
|
|
|
|
if ¤t_hash[..] == &zero[..] {
|
|
break;
|
|
}
|
|
}
|
|
json_response(values, TTL_SHORT)
|
|
}
|
|
|
|
fn to_scripthash(
|
|
script_type: &str,
|
|
script_str: &str,
|
|
network: &Network,
|
|
) -> Result<FullHash, HttpError> {
|
|
match script_type {
|
|
"address" => address_to_scripthash(script_str, network),
|
|
"scripthash" => parse_scripthash(script_str),
|
|
_ => bail!("Invalid script type".to_string()),
|
|
}
|
|
}
|
|
|
|
fn address_to_scripthash(addr: &str, network: &Network) -> Result<FullHash, HttpError> {
|
|
let addr = Address::from_str(addr)?;
|
|
|
|
#[cfg(not(feature = "liquid"))]
|
|
let regtest_net = Network::Regtest;
|
|
#[cfg(feature = "liquid")]
|
|
let regtest_net = Network::LiquidRegtest;
|
|
|
|
if addr.network != *network && !(addr.network == Network::Testnet && *network == regtest_net) {
|
|
bail!(HttpError::from("Address on invalid network".to_string()))
|
|
}
|
|
Ok(compute_script_hash(&addr.script_pubkey()))
|
|
}
|
|
|
|
fn parse_scripthash(scripthash: &str) -> Result<FullHash, HttpError> {
|
|
let bytes = hex::decode(scripthash)?;
|
|
if bytes.len() != 32 {
|
|
Err(HttpError::from("Invalid scripthash".to_string()))
|
|
} else {
|
|
Ok(full_hash(&bytes))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct HttpError(StatusCode, String);
|
|
|
|
impl HttpError {
|
|
fn not_found(msg: String) -> Self {
|
|
HttpError(StatusCode::NOT_FOUND, msg)
|
|
}
|
|
fn generic() -> Self {
|
|
HttpError::from("We encountered an error. Please try again later.".to_string())
|
|
}
|
|
}
|
|
|
|
impl From<String> for HttpError {
|
|
fn from(msg: String) -> Self {
|
|
HttpError(StatusCode::BAD_REQUEST, msg)
|
|
}
|
|
}
|
|
impl From<ParseIntError> for HttpError {
|
|
fn from(_e: ParseIntError) -> Self {
|
|
//HttpError::from(e.description().to_string())
|
|
HttpError::from("Invalid number".to_string())
|
|
}
|
|
}
|
|
impl From<HexError> for HttpError {
|
|
fn from(_e: HexError) -> Self {
|
|
//HttpError::from(e.description().to_string())
|
|
HttpError::from("Invalid hex string".to_string())
|
|
}
|
|
}
|
|
impl From<FromHexError> for HttpError {
|
|
fn from(_e: FromHexError) -> Self {
|
|
//HttpError::from(e.description().to_string())
|
|
HttpError::from("Invalid hex string".to_string())
|
|
}
|
|
}
|
|
impl From<errors::Error> for HttpError {
|
|
fn from(e: errors::Error) -> Self {
|
|
warn!("errors::Error: {:?}", e);
|
|
match e.description().to_string().as_ref() {
|
|
"getblock RPC error: {\"code\":-5,\"message\":\"Block not found\"}" => {
|
|
HttpError::not_found("Block not found".to_string())
|
|
}
|
|
_ => HttpError::generic(),
|
|
}
|
|
}
|
|
}
|
|
impl From<serde_json::Error> for HttpError {
|
|
fn from(_e: serde_json::Error) -> Self {
|
|
//HttpError::from(e.description().to_string())
|
|
HttpError::generic()
|
|
}
|
|
}
|
|
impl From<encode::Error> for HttpError {
|
|
fn from(_e: encode::Error) -> Self {
|
|
//HttpError::from(e.description().to_string())
|
|
HttpError::generic()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::rest::HttpError;
|
|
use serde_json::Value;
|
|
use std::collections::HashMap;
|
|
|
|
#[test]
|
|
fn test_parse_query_param() {
|
|
let mut query_params = HashMap::new();
|
|
|
|
query_params.insert("limit", "10");
|
|
let limit = query_params
|
|
.get("limit")
|
|
.map_or(10u32, |el| el.parse().unwrap_or(10u32))
|
|
.min(30u32);
|
|
assert_eq!(10, limit);
|
|
|
|
query_params.insert("limit", "100");
|
|
let limit = query_params
|
|
.get("limit")
|
|
.map_or(10u32, |el| el.parse().unwrap_or(10u32))
|
|
.min(30u32);
|
|
assert_eq!(30, limit);
|
|
|
|
query_params.insert("limit", "5");
|
|
let limit = query_params
|
|
.get("limit")
|
|
.map_or(10u32, |el| el.parse().unwrap_or(10u32))
|
|
.min(30u32);
|
|
assert_eq!(5, limit);
|
|
|
|
query_params.insert("limit", "aaa");
|
|
let limit = query_params
|
|
.get("limit")
|
|
.map_or(10u32, |el| el.parse().unwrap_or(10u32))
|
|
.min(30u32);
|
|
assert_eq!(10, limit);
|
|
|
|
query_params.remove("limit");
|
|
let limit = query_params
|
|
.get("limit")
|
|
.map_or(10u32, |el| el.parse().unwrap_or(10u32))
|
|
.min(30u32);
|
|
assert_eq!(10, limit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_value_param() {
|
|
let v: Value = json!({ "confirmations": 10 });
|
|
|
|
let confirmations = v
|
|
.get("confirmations")
|
|
.and_then(|el| el.as_u64())
|
|
.ok_or(HttpError::from(
|
|
"confirmations absent or not a u64".to_string(),
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(10, confirmations);
|
|
|
|
let err = v
|
|
.get("notexist")
|
|
.and_then(|el| el.as_u64())
|
|
.ok_or(HttpError::from("notexist absent or not a u64".to_string()));
|
|
|
|
assert!(err.is_err());
|
|
}
|
|
}
|