diff --git a/Cargo.lock b/Cargo.lock index 0dd4ad8d58..9ef1fb14e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,7 @@ dependencies = [ "common", "constraints-value-accumulator", "crypto", + "futures", "logging", "mempool", "node-comm", diff --git a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs index e0d90d4f67..f5689b5b8e 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs @@ -16,7 +16,7 @@ pub mod transactional; use crate::storage::storage_api::{ - block_aux_data::BlockAuxData, ApiServerStorageError, Delegation, FungibleTokenData, + block_aux_data::BlockAuxData, ApiServerStorageError, BlockInfo, Delegation, FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, }; use common::{ @@ -108,13 +108,18 @@ impl ApiServerInMemoryStorage { })) } - fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { + fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { let block_result = self.block_table.get(&block_id); let block = match block_result { Some(blk) => blk, None => return Ok(None), }; - Ok(Some(block.clone())) + let height = self.block_aux_data_table.get(&block_id).map(|data| data.block_height()); + + Ok(Some(BlockInfo { + block: block.clone(), + height, + })) } #[allow(clippy::type_complexity)] @@ -503,6 +508,10 @@ impl ApiServerInMemoryStorage { block: &Block, ) -> Result<(), ApiServerStorageError> { self.block_table.insert(block_id, block.clone()); + self.block_aux_data_table.insert( + block_id, + BlockAuxData::new(block_id, block_height, block.timestamp()), + ); self.main_chain_blocks_table.insert(block_height, block_id); self.best_block = (block_height, block_id.into()); Ok(()) @@ -545,7 +554,14 @@ impl ApiServerInMemoryStorage { &mut self, block_height: BlockHeight, ) -> Result<(), ApiServerStorageError> { - self.main_chain_blocks_table.retain(|k, _| k <= &block_height); + self.main_chain_blocks_table.retain(|k, id| { + if k <= &block_height { + true + } else { + self.block_aux_data_table.remove(id); + false + } + }); Ok(()) } diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs index 5d6d438453..62b2360a33 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs @@ -26,8 +26,8 @@ use common::{ use pos_accounting::PoolData; use crate::storage::storage_api::{ - block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, Delegation, - FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, + block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, BlockInfo, + Delegation, FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, }; use super::ApiServerInMemoryStorageTransactionalRo; @@ -53,7 +53,10 @@ impl<'t> ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'t> { self.transaction.get_address_transactions(address) } - async fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { + async fn get_block( + &self, + block_id: Id, + ) -> Result, ApiServerStorageError> { self.transaction.get_block(block_id) } diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs index 8e3cf925cc..a786d71055 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs @@ -28,7 +28,8 @@ use pos_accounting::PoolData; use crate::storage::storage_api::{ block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, - ApiServerStorageWrite, Delegation, FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, + ApiServerStorageWrite, BlockInfo, Delegation, FungibleTokenData, PoolBlockStats, + TransactionInfo, Utxo, }; use super::ApiServerInMemoryStorageTransactionalRw; @@ -233,7 +234,10 @@ impl<'t> ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'t> { self.transaction.get_best_block() } - async fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { + async fn get_block( + &self, + block_id: Id, + ) -> Result, ApiServerStorageError> { self.transaction.get_block(block_id) } diff --git a/api-server/api-server-common/src/storage/impls/postgres/queries.rs b/api-server/api-server-common/src/storage/impls/postgres/queries.rs index 71a7d64290..0d39f0197c 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/queries.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/queries.rs @@ -34,8 +34,8 @@ use tokio_postgres::NoTls; use crate::storage::{ impls::CURRENT_STORAGE_VERSION, storage_api::{ - block_aux_data::BlockAuxData, ApiServerStorageError, Delegation, FungibleTokenData, - PoolBlockStats, TransactionInfo, Utxo, + block_aux_data::BlockAuxData, ApiServerStorageError, BlockInfo, Delegation, + FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, }, }; @@ -592,22 +592,24 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { pub async fn get_block( &mut self, block_id: Id, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { let row = self .tx .query_opt( - "SELECT block_data FROM ml_blocks WHERE block_id = $1;", + "SELECT block_data, block_height FROM ml_blocks WHERE block_id = $1;", &[&block_id.encode()], ) .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; - let data = match row { + let row = match row { Some(d) => d, None => return Ok(None), }; - let data: Vec = data.get(0); + let data: Vec = row.get(0); + let height: Option = row.get(1); + let height = height.map(|h| BlockHeight::new(h as u64)); let block = Block::decode_all(&mut data.as_slice()).map_err(|e| { ApiServerStorageError::DeserializationError(format!( @@ -616,7 +618,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { )) })?; - Ok(Some(block)) + Ok(Some(BlockInfo { block, height })) } pub async fn get_block_range_from_time_range( diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/mod.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/mod.rs index a9d108175d..1997ac7757 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/mod.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/mod.rs @@ -28,7 +28,7 @@ use tokio_postgres::NoTls; use crate::storage::storage_api::{ block_aux_data::BlockAuxData, ApiServerStorage, ApiServerStorageError, ApiServerTransactionRo, - ApiServerTransactionRw, TransactionInfo, Transactional, + ApiServerTransactionRw, BlockInfo, TransactionInfo, Transactional, }; use super::{queries::QueryFromConnection, TransactionalApiServerPostgresStorage}; @@ -110,7 +110,7 @@ impl<'a> ApiServerPostgresTransactionalRo<'a> { pub async fn get_block( &mut self, block_id: Id, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); let res = conn.get_block(block_id).await?; diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs index 1bd5acb639..2996631b40 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs @@ -25,8 +25,8 @@ use common::{ use crate::storage::{ impls::postgres::queries::QueryFromConnection, storage_api::{ - block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, Delegation, - FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, + block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, BlockInfo, + Delegation, FungibleTokenData, PoolBlockStats, TransactionInfo, Utxo, }, }; use std::collections::BTreeMap; @@ -83,7 +83,7 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRo<'a> { async fn get_block( &self, block_id: Id, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); let res = conn.get_block(block_id).await?; diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs index f0fde73ed6..dbb5a50b24 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs @@ -30,8 +30,8 @@ use crate::storage::{ impls::postgres::queries::QueryFromConnection, storage_api::{ block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, - ApiServerStorageWrite, Delegation, FungibleTokenData, PoolBlockStats, TransactionInfo, - Utxo, + ApiServerStorageWrite, BlockInfo, Delegation, FungibleTokenData, PoolBlockStats, + TransactionInfo, Utxo, }, }; @@ -309,7 +309,10 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRw<'a> { Ok(res) } - async fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { + async fn get_block( + &self, + block_id: Id, + ) -> Result, ApiServerStorageError> { let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); let res = conn.get_block(block_id).await?; diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index a8b74d2ef9..98ea25292d 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -193,12 +193,19 @@ impl FungibleTokenData { pub struct TransactionInfo { pub tx: SignedTransaction, pub fee: Amount, + pub input_utxos: Vec>, } pub struct PoolBlockStats { pub block_count: u64, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockInfo { + pub block: Block, + pub height: Option, +} + #[async_trait::async_trait] pub trait ApiServerStorageRead: Sync { async fn is_initialized(&self) -> Result; @@ -218,7 +225,10 @@ pub trait ApiServerStorageRead: Sync { async fn get_best_block(&self) -> Result<(BlockHeight, Id), ApiServerStorageError>; - async fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError>; + async fn get_block( + &self, + block_id: Id, + ) -> Result, ApiServerStorageError>; async fn get_block_aux_data( &self, diff --git a/api-server/scanner-lib/Cargo.toml b/api-server/scanner-lib/Cargo.toml index cd0b95758b..77ce37d659 100644 --- a/api-server/scanner-lib/Cargo.toml +++ b/api-server/scanner-lib/Cargo.toml @@ -19,6 +19,7 @@ tx-verifier = { path = "../../chainstate/tx-verifier" } constraints-value-accumulator = { path = "../../chainstate/constraints-value-accumulator" } mempool = { path = "../../mempool" } +futures = { workspace = true, default-features = false } async-trait.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["full"] } diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 7b6282c273..9518da76e1 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -33,6 +33,7 @@ use common::{ }, primitives::{id::WithId, Amount, BlockHeight, CoinOrTokenId, Fee, Id, Idable}, }; +use futures::{stream::FuturesUnordered, TryStreamExt}; use pos_accounting::{make_delegation_id, PoSAccountingView, PoolData}; use std::{ collections::{BTreeMap, BTreeSet}, @@ -400,9 +401,14 @@ async fn calculate_fees( let fee = tx_fees(chain_config, block_height, tx, db_tx, &new_outputs).await?; total_fees = total_fees.combine(fee.clone()).expect("no overflow"); + let input_tasks: FuturesUnordered<_> = + tx.inputs().iter().map(|input| fetch_utxo(input, db_tx)).collect(); + let input_utxos: Vec> = input_tasks.try_collect().await?; + let tx_info = TransactionInfo { tx: tx.clone(), fee: fee.map_into_block_fees(chain_config, block_height).expect("no overflow").0, + input_utxos, }; db_tx @@ -414,6 +420,18 @@ async fn calculate_fees( Ok(total_fees.map_into_block_fees(chain_config, block_height).expect("no overflow")) } +async fn fetch_utxo( + input: &TxInput, + db_tx: &T, +) -> Result, ApiServerStorageError> { + match input { + TxInput::Utxo(outpoint) => { + Ok(db_tx.get_utxo(outpoint.clone()).await?.map(|utxo| utxo.into_output())) + } + TxInput::Account(_) | TxInput::AccountCommand(_, _) => Ok(None), + } +} + async fn tx_fees( chain_config: &ChainConfig, block_height: BlockHeight, diff --git a/api-server/stack-test-suite/tests/v1/block.rs b/api-server/stack-test-suite/tests/v1/block.rs index f09f105768..5db6fb7007 100644 --- a/api-server/stack-test-suite/tests/v1/block.rs +++ b/api-server/stack-test-suite/tests/v1/block.rs @@ -143,6 +143,7 @@ async fn ok(#[case] seed: Seed) { .unwrap(); let old_expected_block = json!({ + "height": None::, "header": block_header_to_json(&block), "body": { "reward": block.block_reward() @@ -192,6 +193,7 @@ async fn ok(#[case] seed: Seed) { .unwrap(); let new_expected_block = json!({ + "height": block_height, "header": block_header_to_json(&block), "body": { "reward": block.block_reward() diff --git a/api-server/stack-test-suite/tests/v1/transaction.rs b/api-server/stack-test-suite/tests/v1/transaction.rs index 7abc8ac182..b4f0f8934c 100644 --- a/api-server/stack-test-suite/tests/v1/transaction.rs +++ b/api-server/stack-test-suite/tests/v1/transaction.rs @@ -61,7 +61,7 @@ async fn ok(#[case] seed: Seed) { let task = tokio::spawn(async move { let web_server_state = { let mut rng = make_seedable_rng(seed); - let block_height = rng.gen_range(1..50); + let block_height = rng.gen_range(2..50); let n_blocks = rng.gen_range(block_height..100); let chain_config = create_unit_test_config(); @@ -78,11 +78,21 @@ async fn ok(#[case] seed: Seed) { // Need the "- 1" to account for the genesis block not in the vec let block_id = chainstate_block_ids[block_height - 1]; let block = tf.block(tf.to_chain_block_id(&block_id)); + let prev_block = + tf.block(tf.to_chain_block_id(&chainstate_block_ids[block_height - 2])); + let prev_tx = &prev_block.transactions()[0]; let transaction_index = rng.gen_range(0..block.transactions().len()); let transaction = block.transactions()[transaction_index].transaction(); let transaction_id = transaction.get_id(); + let utxos = transaction.inputs().iter().map(|inp| match inp { + TxInput::Utxo(outpoint) => { + Some(prev_tx.outputs()[outpoint.output_index() as usize].clone()) + } + TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + }); + let expected_transaction = json!({ "block_id": block_id.to_hash().encode_hex::(), "timestamp": block.timestamp().to_string(), @@ -90,7 +100,10 @@ async fn ok(#[case] seed: Seed) { "version_byte": transaction.version_byte(), "is_replaceable": transaction.is_replaceable(), "flags": transaction.flags(), - "inputs": transaction.inputs(), + "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ + "input": inp, + "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config)), + })).collect::>(), "outputs": transaction.outputs() .iter() .map(|out| txoutput_to_json(out, &chain_config)) diff --git a/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs b/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs index 42712721bb..546ebe683d 100644 --- a/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs +++ b/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs @@ -112,6 +112,7 @@ async fn get_block_failed(#[case] seed: Seed) { let tx_info = TransactionInfo { tx: signed_transaction, fee: Amount::from_atoms(rng.gen_range(0..100)), + input_utxos: vec![], }; db_tx.set_transaction(transaction_id, Some(block_id), &tx_info).await.unwrap(); @@ -237,6 +238,7 @@ async fn transaction_not_part_of_block(#[case] seed: Seed) { let tx_info = TransactionInfo { tx: signed_transaction, fee: Amount::from_atoms(rng.gen_range(0..100)), + input_utxos: vec![], }; db_tx.set_transaction(transaction_id, None, &tx_info).await.unwrap(); db_tx.commit().await.unwrap(); diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index ab68f69259..7b84fe5c45 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -24,7 +24,7 @@ use api_server_common::storage::{ impls::CURRENT_STORAGE_VERSION, storage_api::{ block_aux_data::BlockAuxData, ApiServerStorage, ApiServerStorageRead, - ApiServerStorageWrite, ApiServerTransactionRw, Delegation, FungibleTokenData, + ApiServerStorageWrite, ApiServerTransactionRw, BlockInfo, Delegation, FungibleTokenData, TransactionInfo, Utxo, }, }; @@ -129,6 +129,10 @@ where test_framework.block_id(1).classify(&chain_config).chain_block_id().unwrap(); let block1 = test_framework.block(block_id1); let block_height = BlockHeight::new(1); + let block_info1 = BlockInfo { + block: block1.clone(), + height: Some(block_height), + }; { let block_id = db_tx.get_block(block_id1).await.unwrap(); @@ -140,7 +144,7 @@ where db_tx.set_mainchain_block(block_id1, block_height, &block1).await.unwrap(); let block = db_tx.get_block(block_id1).await.unwrap(); - assert_eq!(block.unwrap(), block1); + assert_eq!(block.unwrap(), block_info1); let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); assert_eq!(block_id.unwrap(), block_id1); @@ -154,8 +158,12 @@ where let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); assert!(block_id.is_none()); // but the block is still there just not on main chain + let block_info1 = BlockInfo { + block: block1.clone(), + height: None, + }; let block = db_tx.get_block(block_id1).await.unwrap(); - assert_eq!(block.unwrap(), block1); + assert_eq!(block.unwrap(), block_info1); } { @@ -225,8 +233,12 @@ where let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); assert!(block_id.is_none()); + let block_info1 = BlockInfo { + block: block1.clone(), + height: None, + }; let block = db_tx.get_block(block_id1).await.unwrap(); - assert_eq!(block.unwrap(), block1); + assert_eq!(block.unwrap(), block_info1); } } @@ -250,6 +262,10 @@ where empty_witness(&mut rng), ) .build(); + let tx1_input_utxos = vec![Some(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen_range(1..100))), + Destination::AnyoneCanSpend, + ))]; // before storage let tx_and_block_id = db_tx.get_transaction(tx1.transaction().get_id()).await.unwrap(); @@ -264,6 +280,7 @@ where let tx_info = TransactionInfo { tx: tx1.clone(), fee: Amount::from_atoms(rng.gen_range(0..100)), + input_utxos: tx1_input_utxos.clone(), }; db_tx.set_transaction(tx1.transaction().get_id(), None, &tx_info).await.unwrap(); @@ -280,6 +297,7 @@ where let tx_info = TransactionInfo { tx: tx1.clone(), fee: Amount::from_atoms(rng.gen_range(0..100)), + input_utxos: tx1_input_utxos.clone(), }; db_tx .set_transaction(tx1.transaction().get_id(), Some(owning_block1), &tx_info) diff --git a/api-server/web-server/src/api/v1.rs b/api-server/web-server/src/api/v1.rs index 03b8f02607..9d36e1b123 100644 --- a/api-server/web-server/src/api/v1.rs +++ b/api-server/web-server/src/api/v1.rs @@ -22,7 +22,8 @@ use crate::{ TxSubmitClient, }; use api_server_common::storage::storage_api::{ - block_aux_data::BlockAuxData, ApiServerStorage, ApiServerStorageRead, TransactionInfo, + block_aux_data::BlockAuxData, ApiServerStorage, ApiServerStorageRead, BlockInfo, + TransactionInfo, }; use axum::{ extract::{DefaultBodyLimit, Path, Query, State}, @@ -113,7 +114,7 @@ async fn forbidden_request() -> Result<(), ApiServerWebServerError> { async fn get_block( block_id: &str, state: &ApiServerWebServerState, Arc>, -) -> Result { +) -> Result { let block_id: Id = H256::from_str(block_id) .map_err(|_| { ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidBlockId) @@ -144,17 +145,18 @@ pub async fn block( Path(block_id): Path, State(state): State, Arc>>, ) -> Result { - let block = get_block(&block_id, &state).await?; + let block_info = get_block(&block_id, &state).await?; Ok(Json(json!({ - "header": block_header_to_json(&block), + "height": block_info.height, + "header": block_header_to_json(&block_info.block), "body": { - "reward": block.block_reward() + "reward": block_info.block.block_reward() .outputs() .iter() .map(|out| txoutput_to_json(out, &state.chain_config)) .collect::>(), - "transactions": block.transactions() + "transactions": block_info.block.transactions() .iter() .map(|tx| tx_to_json(tx.transaction(), &state.chain_config)) .collect::>(), @@ -167,7 +169,7 @@ pub async fn block_header( Path(block_id): Path, State(state): State, Arc>>, ) -> Result { - let block = get_block(&block_id, &state).await?; + let block = get_block(&block_id, &state).await?.block; Ok(Json(block_header_to_json(&block))) } @@ -177,7 +179,7 @@ pub async fn block_reward( Path(block_id): Path, State(state): State, Arc>>, ) -> Result { - let block = get_block(&block_id, &state).await?; + let block = get_block(&block_id, &state).await?.block; Ok(Json(json!(block .block_reward() @@ -192,7 +194,7 @@ pub async fn block_transaction_ids( Path(block_id): Path, State(state): State, Arc>>, ) -> Result { - let block = get_block(&block_id, &state).await?; + let block = get_block(&block_id, &state).await?.block; let transaction_ids = block .transactions() @@ -409,7 +411,14 @@ pub async fn transaction( Path(transaction_id): Path, State(state): State, Arc>>, ) -> Result { - let (block, TransactionInfo { tx, fee }) = get_transaction(&transaction_id, &state).await?; + let ( + block, + TransactionInfo { + tx, + fee, + input_utxos, + }, + ) = get_transaction(&transaction_id, &state).await?; let confirmations = if let Some(block) = &block { let (tip_height, _) = best_block(&state).await?; @@ -426,7 +435,10 @@ pub async fn transaction( "is_replaceable": tx.is_replaceable(), "flags": tx.flags(), "fee": amount_to_json(fee), - "inputs": tx.inputs(), + "inputs": tx.inputs().iter().zip(input_utxos.iter()).map(|(inp, utxo)| json!({ + "input": inp, + "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &state.chain_config)), + })).collect::>(), "outputs": tx.outputs() .iter() .map(|out| txoutput_to_json(out, &state.chain_config)) @@ -444,7 +456,8 @@ pub async fn transaction_merkle_path( &block_data.block_id().to_hash().encode_hex::(), &state, ) - .await?; + .await? + .block; (block, tx_info.tx.transaction().clone()) } (None, _) => {