Skip to content

Commit 81aa153

Browse files
committed
Add tx output route to API server
1 parent 41b564d commit 81aa153

File tree

5 files changed

+263
-46
lines changed

5 files changed

+263
-46
lines changed

Cargo.lock

Lines changed: 13 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api-server/stack-test-suite/tests/v2/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod token_ids;
3838
mod token_ticker;
3939
mod transaction;
4040
mod transaction_merkle_path;
41+
mod transaction_output;
4142
mod transaction_submit;
4243
mod transactions;
4344

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright (c) 2025 RBB S.r.l
2+
3+
// SPDX-License-Identifier: MIT
4+
// Licensed under the MIT License;
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
use api_web_server::api::json_helpers::tx_input_to_json;
17+
18+
use super::*;
19+
20+
#[tokio::test]
21+
async fn invalid_transaction_id() {
22+
let (task, response) =
23+
spawn_webserver("/api/v2/transaction/invalid-transaction-id/output/1").await;
24+
25+
assert_eq!(response.status(), 400);
26+
27+
let body = response.text().await.unwrap();
28+
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
29+
30+
assert_eq!(body["error"].as_str().unwrap(), "Invalid transaction Id");
31+
32+
task.abort();
33+
}
34+
35+
#[tokio::test]
36+
async fn transaction_not_found() {
37+
let (task, response) = spawn_webserver(
38+
"/api/v2/transaction/0000000000000000000000000000000000000000000000000000000000000001/output/1",
39+
)
40+
.await;
41+
42+
assert_eq!(response.status(), 404);
43+
44+
let body = response.text().await.unwrap();
45+
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
46+
47+
assert_eq!(
48+
body["error"].as_str().unwrap(),
49+
"Transaction output not found"
50+
);
51+
52+
task.abort();
53+
}
54+
55+
#[rstest]
56+
#[trace]
57+
#[case(Seed::from_entropy())]
58+
#[tokio::test]
59+
async fn ok(#[case] seed: Seed) {
60+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
61+
let addr = listener.local_addr().unwrap();
62+
63+
let (tx, rx) = tokio::sync::oneshot::channel();
64+
65+
let task = tokio::spawn(async move {
66+
let web_server_state = {
67+
let mut rng = make_seedable_rng(seed);
68+
let block_height = rng.gen_range(2..50);
69+
let n_blocks = rng.gen_range(block_height..100);
70+
71+
let chain_config = create_unit_test_config();
72+
73+
let chainstate_blocks = {
74+
let mut tf = TestFramework::builder(&mut rng)
75+
.with_chain_config(chain_config.clone())
76+
.build();
77+
78+
let chainstate_block_ids = tf
79+
.create_chain_return_ids(&tf.genesis().get_id().into(), n_blocks, &mut rng)
80+
.unwrap();
81+
82+
// Need the "- 1" to account for the genesis block not in the vec
83+
let block_id = chainstate_block_ids[block_height - 1];
84+
let block = tf.block(tf.to_chain_block_id(&block_id));
85+
let prev_block =
86+
tf.block(tf.to_chain_block_id(&chainstate_block_ids[block_height - 2]));
87+
let prev_tx = &prev_block.transactions()[0];
88+
89+
let transaction_index = rng.gen_range(0..block.transactions().len());
90+
let transaction = block.transactions()[transaction_index].transaction();
91+
let transaction_id = transaction.get_id();
92+
93+
let utxos = transaction.inputs().iter().map(|inp| match inp {
94+
TxInput::Utxo(outpoint) => {
95+
Some(prev_tx.outputs()[outpoint.output_index() as usize].clone())
96+
}
97+
TxInput::Account(_)
98+
| TxInput::AccountCommand(_, _)
99+
| TxInput::OrderAccountCommand(_) => None,
100+
});
101+
102+
let expected_transaction = json!({
103+
"block_id": block_id.to_hash().encode_hex::<String>(),
104+
"timestamp": block.timestamp().to_string(),
105+
"confirmations": BlockHeight::new((n_blocks - block_height) as u64).to_string(),
106+
"version_byte": transaction.version_byte(),
107+
"is_replaceable": transaction.is_replaceable(),
108+
"flags": transaction.flags(),
109+
"inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({
110+
"input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config),
111+
"utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))),
112+
})).collect::<Vec<_>>(),
113+
"outputs": transaction.outputs()
114+
.iter()
115+
.map(|out| txoutput_to_json(out, &chain_config, &TokenDecimals::Single(None)))
116+
.collect::<Vec<_>>(),
117+
});
118+
119+
_ = tx.send((
120+
block_id.to_hash().encode_hex::<String>(),
121+
transaction_id.to_hash().encode_hex::<String>(),
122+
expected_transaction,
123+
));
124+
125+
chainstate_block_ids
126+
.iter()
127+
.map(|id| tf.block(tf.to_chain_block_id(id)))
128+
.collect::<Vec<_>>()
129+
};
130+
131+
let storage = {
132+
let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config);
133+
134+
let mut db_tx = storage.transaction_rw().await.unwrap();
135+
db_tx.reinitialize_storage(&chain_config).await.unwrap();
136+
db_tx.commit().await.unwrap();
137+
138+
storage
139+
};
140+
141+
let chain_config = Arc::new(chain_config);
142+
let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage);
143+
local_node.scan_genesis(chain_config.genesis_block()).await.unwrap();
144+
local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap();
145+
146+
ApiServerWebServerState {
147+
db: Arc::new(local_node.storage().clone_storage().await),
148+
chain_config: Arc::clone(&chain_config),
149+
rpc: Arc::new(DummyRPC {}),
150+
cached_values: Arc::new(CachedValues {
151+
feerate_points: RwLock::new((get_time(), vec![])),
152+
}),
153+
time_getter: Default::default(),
154+
}
155+
};
156+
157+
web_server(listener, web_server_state, true).await
158+
});
159+
160+
let (block_id, transaction_id, expected_transaction) = rx.await.unwrap();
161+
let url = format!("/api/v2/transaction/{transaction_id}");
162+
163+
// Given that the listener port is open, this will block until a
164+
// response is made (by the web server, which takes the listener
165+
// over)
166+
let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port()))
167+
.await
168+
.unwrap();
169+
170+
assert_eq!(response.status(), 200);
171+
172+
let body = response.text().await.unwrap();
173+
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
174+
let body = body.as_object().unwrap();
175+
176+
assert_eq!(body.get("block_id").unwrap(), &block_id);
177+
assert_eq!(
178+
body.get("version_byte").unwrap(),
179+
&expected_transaction["version_byte"]
180+
);
181+
assert_eq!(
182+
body.get("is_replaceable").unwrap(),
183+
&expected_transaction["is_replaceable"]
184+
);
185+
assert_eq!(body.get("flags").unwrap(), &expected_transaction["flags"]);
186+
assert_eq!(body.get("inputs").unwrap(), &expected_transaction["inputs"]);
187+
assert_eq!(
188+
body.get("outputs").unwrap(),
189+
&expected_transaction["outputs"]
190+
);
191+
assert_eq!(
192+
body.get("timestamp").unwrap(),
193+
&expected_transaction["timestamp"]
194+
);
195+
assert_eq!(
196+
body.get("confirmations").unwrap(),
197+
&expected_transaction["confirmations"]
198+
);
199+
200+
task.abort();
201+
}

0 commit comments

Comments
 (0)