use std::{collections::HashSet, sync::Arc};
use anyhow::*;
use bitcoin_hashes::hex::ToHex;
use bitcoincash::Network;
use futures::stream::StreamExt;
use futures::TryStreamExt;
use serde::Serialize;
use serde_json::Value;
use tokio::join;
use crate::{
chaindef::{ScriptHash, TokenID, TxIn},
encode::outpoint_hash,
errors::rpc_invalid_params,
indexes::outputtokenindex::OutputTokenIndexRow,
mempool::MEMPOOL_HEIGHT,
query::{
self,
balance::balance_at_tip,
queryfilter::QueryFilter,
status::{
confirmed_scripthash_token_balance, scripthash_history, scripthash_listunspent,
unconfirmed_history, unconfirmed_scripthash_token_balance, HistoryItem,
},
token::find_token_genesis,
Query,
},
scripthash::addr_to_scripthash,
};
use anyhow::Result;
use crate::rpc::parseutil::{str_from_value, tokenid_from_value};
use super::parseutil::{commitment_from_value, cursor_from_value, hash_from_value};
#[cfg(bch)]
use crate::utilserialize::as_hex;
#[derive(Serialize)]
struct NFTListItem {
#[cfg(nexa)]
pub token_id: TokenID,
#[cfg(bch)]
#[serde(skip_serializing)]
#[allow(dead_code)]
token_id: TokenID,
#[cfg(bch)]
#[serde(serialize_with = "as_hex")]
commitment: Vec<u8>,
}
impl NFTListItem {
#[cfg(nexa)]
pub fn new(parent_id: [u8; 32], subgroup: Vec<u8>) -> Self {
let token_id = TokenID::from_parent_and_subgroup(parent_id, subgroup);
NFTListItem { token_id }
}
#[cfg(bch)]
pub fn new(parent_id: [u8; 32], commitment: Vec<u8>) -> Self {
use crate::bitcoin_hashes::Hash;
NFTListItem {
token_id: TokenID::from_inner(parent_id),
commitment,
}
}
}
pub struct TokenRPC {
query: Arc<Query>,
#[allow(dead_code)] network: Network,
}
impl TokenRPC {
pub fn new(query: Arc<Query>, network: Network) -> Self {
Self { query, network }
}
pub async fn address_get_balance(&self, params: &[Value]) -> Result<Value> {
let addr = str_from_value(params.first(), "address")?;
let scripthash = addr_to_scripthash(&addr)?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
let filter = QueryFilter::from_param_custom_default(
params.get(3),
QueryFilter::filter_token_only(),
)?;
Ok(self.get_balance(scripthash, filter, token).await?)
}
pub async fn address_get_history(&self, params: &[Value]) -> Result<Value> {
let addr = str_from_value(params.first(), "address")?;
let scripthash = addr_to_scripthash(&addr)?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
let filter = QueryFilter::from_param_custom_default(
params.get(3),
QueryFilter::filter_token_only(),
)?;
self.get_scripthash_history(scripthash, token, filter).await
}
pub(crate) async fn address_get_mempool(&self, params: &[Value]) -> Result<Value> {
let addr = str_from_value(params.first(), "address")?;
let scripthash = addr_to_scripthash(&addr)?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
let filter = QueryFilter::from_param_custom_default(
params.get(3),
QueryFilter::filter_token_only(),
)?;
self.get_scripthash_mempool(scripthash, token, filter).await
}
pub(crate) async fn address_listunspent(&self, params: &[Value]) -> Result<Value> {
let addr = str_from_value(params.first(), "address")?;
let scripthash = addr_to_scripthash(&addr)?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
self.listunspent(scripthash, token).await
}
pub(crate) async fn scripthash_get_balance(&self, params: &[Value]) -> Result<Value> {
let scripthash = hash_from_value::<ScriptHash>(params.first())?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
let filter = QueryFilter::from_param_custom_default(
params.get(3),
QueryFilter::filter_token_only(),
)?;
self.get_balance(scripthash, filter, token).await
}
pub(crate) async fn scripthash_get_history(&self, params: &[Value]) -> Result<Value> {
let scripthash: ScriptHash = hash_from_value(params.first())?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
let filter = QueryFilter::from_param_custom_default(
params.get(3),
QueryFilter::filter_token_only(),
)?;
self.get_scripthash_history(scripthash, token, filter).await
}
pub(crate) async fn scripthash_get_mempool(&self, params: &[Value]) -> Result<Value> {
let scripthash = hash_from_value::<ScriptHash>(params.first())?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
let filter = QueryFilter::from_param_custom_default(
params.get(3),
QueryFilter::filter_token_only(),
)?;
self.get_scripthash_mempool(scripthash, token, filter).await
}
pub(crate) async fn scripthash_listunspent(&self, params: &[Value]) -> Result<Value> {
let scripthash = hash_from_value::<ScriptHash>(params.first())?;
let _cursor = cursor_from_value(params.get(1))?;
let token = tokenid_from_value(params.get(2))?;
self.listunspent(scripthash, token).await
}
pub(crate) async fn token_transaction_history(&self, params: &[Value]) -> Result<Value> {
let token_id = match tokenid_from_value(params.first())? {
Some(t) => t,
None => return Err(rpc_invalid_params("token_id missing".to_string())),
};
let _cursor = cursor_from_value(params.get(1))?;
let commitment = commitment_from_value(params.get(2))?;
let confirmed_history =
query::token::get_token_history(&token_id, &commitment, self.query.confirmed_index());
let mempool_history = query::token::get_token_history(
&token_id,
&commitment,
self.query.unconfirmed_index().index(),
);
let (confirmed_history, mempool_history) = join!(confirmed_history, mempool_history);
let (confirmed_history, mempool_history) = (confirmed_history?, mempool_history?);
let as_history_items = mempool_history
.iter()
.chain(confirmed_history.iter())
.map(|h| HistoryItem {
tx_hash: h.0,
height: if h.1 == MEMPOOL_HEIGHT { 0 } else { h.1 as i32 },
fee: None,
});
Ok(json!({
"cursor": None::<String>,
"history": as_history_items.collect::<Vec<HistoryItem>>()
}))
}
#[cfg(nexa)]
fn get_token_in_output(out: &crate::nexa::transaction::TxOut) -> Option<TokenID> {
use crate::nexa::token::parse_token_from_scriptpubkey;
parse_token_from_scriptpubkey(&out.script_pubkey).map(|(token_id, _)| token_id)
}
pub(crate) async fn genesis_info(&self, params: &[Value]) -> Result<Value> {
use crate::indexes::DBRow;
let token_id = match tokenid_from_value(params.first())? {
Some(t) => t,
None => return Err(rpc_invalid_params("token_id missing".to_string())),
};
let (tx, height) = {
#[cfg(nexa)]
{
find_token_genesis(&token_id, self.query.confirmed_index(), self.query.tx()).await
}
#[cfg(bch)]
{
find_token_genesis(
&token_id,
self.query.confirmed_index(),
self.query.unconfirmed_index().index(),
self.query.tx(),
)
.await
}
}?;
let fetch_token_if_any = |i: TxIn| async move {
let prefix =
OutputTokenIndexRow::filter_by_outpointhash(&outpoint_hash(&i.previous_output));
let (task, stream) = self
.query
.confirmed_index()
.scan(OutputTokenIndexRow::CF, prefix, None)
.await;
let token = stream
.map(|r| OutputTokenIndexRow::from_row(&r))
.map(|r| r.into_token_id())
.next()
.await;
task.await??;
Ok(token)
};
let tokens_in: Vec<Option<TokenID>> = futures::stream::iter(tx.clone().input.into_iter())
.then(|i| async move { fetch_token_if_any(i).await })
.try_collect()
.await?;
let tokens_in: HashSet<TokenID> = tokens_in.into_iter().flatten().collect();
if tokens_in.contains(&token_id) {
bail!(
"Expected {} to be genesis tx for {}, but it also had token as input.",
tx.txid(),
token_id,
);
}
let mut tx_details = json!({
"height": height,
});
#[cfg(bch)]
{
use crate::bch::bcmr::get_token_bcmr;
use crate::bch::crc20::get_token_crc20;
use futures_util::join;
let d = tx_details.as_object_mut().unwrap();
d.insert("tx_hash".to_string(), json!(tx.txid().to_hex()));
d.insert("token_id".to_string(), json!(token_id));
let txid = tx.txid();
let bcmr = get_token_bcmr(
&txid,
self.query.confirmed_index(),
self.query.unconfirmed_index().index(),
self.query.tx(),
);
let crc20 = get_token_crc20(&tx, self.query.confirmed_index(), self.query.tx());
let (bcmr, crc20) = join!(bcmr, crc20);
d.insert("bcmr".to_string(), json!(bcmr?));
d.insert("crc20".to_string(), json!(crc20?));
}
#[cfg(nexa)]
{
use crate::nexa::token::parse_token_description;
let tokens_out: HashSet<TokenID> = tx
.output
.iter()
.filter_map(Self::get_token_in_output)
.collect();
let genesis_outputs = tokens_out.difference(&tokens_in);
let (op_return, token) = if genesis_outputs.count() == 1 {
let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return());
let token = op_return
.map(|o| parse_token_description(&o.script_pubkey).unwrap_or_default());
(op_return.map(|o| o.script_pubkey.as_bytes()), token)
} else {
(None, None)
};
let d = tx_details.as_object_mut().unwrap();
d.insert("txid".to_string(), json!(tx.txid().to_hex()));
d.insert("txidem".to_string(), json!(tx.txidem().to_hex()));
d.insert(
"group".to_string(),
json!(token_id.as_cashaddr(self.network)),
);
d.insert("token_id_hex".to_string(), json!(token_id));
d.insert(
"ticker".to_string(),
json!(token.as_ref().map(|t| t.ticker.clone())),
);
d.insert(
"name".to_string(),
json!(token.as_ref().map(|t| t.name.clone())),
);
d.insert(
"document_hash".to_string(),
json!(token.as_ref().map(|t| t.document_hash.map(|d| d.to_hex()))),
);
d.insert(
"document_url".to_string(),
json!(token.as_ref().map(|t| t.document_url.clone())),
);
d.insert(
"decimal_places".to_string(),
json!(token.as_ref().map(|t| t.decimal_places)),
);
d.insert(
"op_return".to_string(),
json!(op_return.map(|o| o.to_hex())),
);
d.insert(
"op_return_id".to_string(),
json!(token.map(|t| t.op_return_id)),
);
}
Ok(tx_details)
}
async fn get_balance(
&self,
scripthash: ScriptHash,
filter: QueryFilter,
token: Option<TokenID>,
) -> Result<Value> {
if !filter.has_height_filter() {
let (_cbch, confirmed, _ubch, unconfirmed) = balance_at_tip(
self.query.confirmed_index(),
self.query.unconfirmed_index().index(),
&scripthash,
&filter,
&token,
)
.await?;
return Ok(json!({
"confirmed": confirmed,
"unconfirmed": unconfirmed,
"cursor": None::<String>,
}));
}
let (confirmed, outputs) = confirmed_scripthash_token_balance(
self.query.confirmed_index(),
scripthash,
token.clone(),
)
.await?;
let unconfirmed = unconfirmed_scripthash_token_balance(
self.query.unconfirmed_index().index(),
self.query.confirmed_index(),
outputs,
scripthash,
token,
)
.await?;
Ok(json!({
"confirmed": confirmed,
"unconfirmed": unconfirmed,
"cursor": None::<String>,
}))
}
async fn get_scripthash_history(
&self,
scripthash: ScriptHash,
token: Option<TokenID>,
filter: QueryFilter,
) -> Result<Value> {
let history = scripthash_history(
self.query.confirmed_index(),
self.query.unconfirmed_index(),
scripthash,
filter,
token,
)
.await?;
Ok(json!({
"transactions": history,
"cursor": None::<String>,
}))
}
async fn get_scripthash_mempool(
&self,
scripthash: ScriptHash,
token: Option<TokenID>,
filter: QueryFilter,
) -> Result<Value> {
let mut history =
unconfirmed_history(self.query.unconfirmed_index(), scripthash, filter, token).await?;
filter.filter_limit_offset(&mut history);
Ok(json!({
"transactions": history,
"cursor": None::<String>,
}))
}
async fn listunspent(&self, scripthash: ScriptHash, token: Option<TokenID>) -> Result<Value> {
let unspent = scripthash_listunspent(
self.query.confirmed_index(),
self.query.unconfirmed_index().index(),
scripthash,
QueryFilter::filter_token_only(),
token,
)
.await?;
#[cfg(nexa)]
let unspent = {
unspent
.into_iter()
.map(|u| {
json!({
"tx_hash": u.txid,
"tx_pos": u.vout,
"outpoint_hash": u.outpoint_hash,
"height": if u.height == MEMPOOL_HEIGHT { 0 } else { u.height },
"value": u.value,
"group": u.token_id.as_ref().map(|t| t.as_cashaddr(self.network)),
"token_id_hex": u.token_id.map(|t| t.to_hex()),
"token_amount": u.token_amount,
})
})
.collect::<Vec<Value>>()
};
Ok(json!({
"cursor": None::<String>,
"unspent": unspent,
}))
}
pub(crate) async fn list_nft(&self, params: &[Value]) -> Result<Value> {
let token_id = match tokenid_from_value(params.first())? {
Some(t) => t,
None => return Err(rpc_invalid_params("token_id missing".to_string())),
};
let nfts = query::token::list_nft(&token_id, self.query.confirmed_index())
.await?
.into_iter()
.chain(query::token::list_nft(&token_id, self.query.unconfirmed_index().index()).await?)
.map(|(token_id, subgroup)| NFTListItem::new(token_id, subgroup));
#[cfg(nexa)]
let nfts = {
nfts.into_iter()
.map(|nft| {
let hex = nft.token_id.to_hex();
let cashaddr = nft.token_id.as_cashaddr(self.network);
json!({
"group": cashaddr,
"token_id_hex": hex
})
})
.collect::<Vec<Value>>()
};
#[cfg(bch)]
let nfts = { nfts.collect::<Vec<NFTListItem>>() };
Ok(json!({
"cursor": None::<String>,
"nft": nfts,
}))
}
}