use koios_sdk::{models::requests::TransactionInfoRequest, Client};
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::{
matchers::{body_json, header, method, path},
Mock, MockServer, ResponseTemplate,
};
async fn setup_test_client() -> (MockServer, Client) {
let mock_server = MockServer::start().await;
let client = Client::builder()
.base_url(mock_server.uri())
.build()
.unwrap();
(mock_server, client)
}
#[tokio::test]
async fn test_get_utxo_info() {
let (mock_server, client) = setup_test_client().await;
let utxo_refs =
vec!["1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef#1".to_string()];
let mock_response = json!([{
"tx_hash": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"tx_index": 1,
"address": "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz",
"value": "42000000",
"stake_address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc",
"payment_cred": "a2944a17b8d8b7ede6e365432cf59ff5276c7c3a99e2b89d47c87661",
"epoch_no": 321,
"block_height": 7017300,
"block_time": 1630106091,
"datum_hash": null,
"inline_datum": null,
"reference_script": null,
"asset_list": [],
"is_spent": false
}]);
Mock::given(method("POST"))
.and(path("/utxo_info"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!({
"_utxo_refs": utxo_refs,
"_extended": true
})))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_utxo_info(&utxo_refs, Some(true)).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(
response[0].tx_hash,
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
);
assert_eq!(response[0].tx_index, 1);
assert_eq!(response[0].value, "42000000");
assert!(!response[0].is_spent);
}
#[tokio::test]
async fn test_get_transaction_info() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let options = TransactionInfoRequest {
tx_hashes: vec![tx_hash.to_string()],
inputs: Some(true),
metadata: Some(true),
assets: Some(true),
withdrawals: Some(true),
certs: Some(true),
scripts: Some(true),
bytecode: Some(true),
governance: Some(true),
};
let mock_response = json!([{
"tx_hash": tx_hash,
"block_hash": "43c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e683",
"block_height": 7017300,
"epoch_no": 321,
"epoch_slot": 85691,
"absolute_slot": 53384091,
"tx_timestamp": 1630106091,
"tx_block_index": 0,
"tx_size": 289,
"total_output": "42000000",
"fee": "168273",
"treasury_donation": "0",
"deposit": "0",
"invalid_before": null,
"invalid_after": null,
"collateral_inputs": null,
"collateral_output": null,
"reference_inputs": null,
"inputs": [{
"payment_addr": {
"bech32": "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz",
"cred": "a2944a17b8d8b7ede6e365432cf59ff5276c7c3a99e2b89d47c87661"
},
"stake_addr": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc",
"tx_hash": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"tx_index": 0,
"value": "42000000",
"datum_hash": null,
"inline_datum": null,
"reference_script": null,
"asset_list": []
}],
"outputs": [],
"withdrawals": null,
"assets_minted": null,
"metadata": null,
"certificates": null,
"native_scripts": null,
"plutus_contracts": null,
"voting_procedures": null,
"proposal_procedures": null
}]);
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!(options)))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_transaction_info(&options).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].tx_hash, tx_hash);
assert_eq!(response[0].block_height, 7017300);
assert_eq!(response[0].total_output, "42000000");
assert_eq!(response[0].fee, "168273");
}
#[tokio::test]
async fn test_get_transaction_metadata() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let mock_response = json!([{
"tx_hash": tx_hash,
"metadata": {
"1234": {
"key": "value",
"array": [1, 2, 3],
"nested": {
"field": true
}
}
}
}]);
Mock::given(method("POST"))
.and(path("/tx_metadata"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!({
"_tx_hashes": [tx_hash]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client
.get_transaction_metadata(&[tx_hash.to_string()])
.await
.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].tx_hash, tx_hash);
assert!(response[0].metadata.is_some());
}
#[tokio::test]
async fn test_get_transaction_metalabels() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"key": "1234"
}]);
Mock::given(method("GET"))
.and(path("/tx_metalabels"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_transaction_metalabels().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].key, "1234");
}
#[tokio::test]
async fn test_submit_transaction() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let cbor_data = [0x84, 0x00];
Mock::given(method("POST"))
.and(path("/submittx"))
.and(header("Content-Type", "application/cbor"))
.respond_with(ResponseTemplate::new(202).set_body_string(tx_hash))
.mount(&mock_server)
.await;
let response = client.submit_transaction(&cbor_data).await.unwrap();
assert_eq!(response, tx_hash);
}
#[tokio::test]
async fn test_get_transaction_status() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let mock_response = json!([{
"tx_hash": tx_hash,
"num_confirmations": 100
}]);
Mock::given(method("POST"))
.and(path("/tx_status"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!({
"_tx_hashes": [tx_hash]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client
.get_transaction_status(&[tx_hash.to_string()])
.await
.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].tx_hash, tx_hash);
assert_eq!(response[0].num_confirmations.unwrap(), 100);
}
#[tokio::test]
async fn test_get_transaction_cbor() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let mock_response = json!([{
"tx_hash": tx_hash,
"block_hash": "43c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e683",
"block_height": 7017300,
"epoch_no": 321,
"absolute_slot": 53384091,
"tx_timestamp": 1630106091,
"cbor": "84a400828258206ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541..."
}]);
Mock::given(method("POST"))
.and(path("/tx_cbor"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!({
"_tx_hashes": [tx_hash]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client
.get_transaction_cbor(&[tx_hash.to_string()])
.await
.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].tx_hash, tx_hash);
assert_eq!(response[0].block_height, 7017300);
}
#[tokio::test]
async fn test_invalid_transaction_hash() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "invalid_tx_hash";
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(body_json(json!({
"_tx_hashes": [tx_hash],
})))
.respond_with(ResponseTemplate::new(400).set_body_string("Invalid transaction hash format"))
.mount(&mock_server)
.await;
let options = TransactionInfoRequest {
tx_hashes: vec![tx_hash.to_string()],
..Default::default()
};
let error = client.get_transaction_info(&options).await.unwrap_err();
match error {
koios_sdk::error::Error::Api { status, message } => {
assert_eq!(status, 400);
assert_eq!(message, "Invalid transaction hash format");
}
_ => panic!("Expected API error"),
}
}
#[tokio::test]
async fn test_transaction_not_found() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(body_json(json!({
"_tx_hashes": [tx_hash],
})))
.respond_with(ResponseTemplate::new(404).set_body_string("Transaction not found"))
.mount(&mock_server)
.await;
let options = TransactionInfoRequest {
tx_hashes: vec![tx_hash.to_string()],
..Default::default()
};
let error = client.get_transaction_info(&options).await.unwrap_err();
match error {
koios_sdk::error::Error::Api { status, message } => {
assert_eq!(status, 404);
assert_eq!(message, "Transaction not found");
}
_ => panic!("Expected API error"),
}
}
#[tokio::test]
async fn test_invalid_cbor_submission() {
let (mock_server, client) = setup_test_client().await;
let invalid_cbor = vec![0xFF, 0xFF];
Mock::given(method("POST"))
.and(path("/submittx"))
.and(header("Content-Type", "application/cbor"))
.respond_with(ResponseTemplate::new(400).set_body_string("Invalid transaction CBOR format"))
.mount(&mock_server)
.await;
let error = client.submit_transaction(&invalid_cbor).await.unwrap_err();
match error {
koios_sdk::error::Error::Api { status, message } => {
assert_eq!(status, 400);
assert_eq!(message, "Invalid transaction CBOR format");
}
_ => panic!("Expected API error"),
}
}
#[tokio::test]
async fn test_transaction_submission_and_status() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let cbor_data = [0x84, 0x00];
Mock::given(method("POST"))
.and(path("/submittx"))
.and(header("Content-Type", "application/cbor"))
.respond_with(ResponseTemplate::new(202).set_body_string(tx_hash))
.mount(&mock_server)
.await;
let status_response = json!([{
"tx_hash": tx_hash,
"num_confirmations": 1
}]);
Mock::given(method("POST"))
.and(path("/tx_status"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!({
"_tx_hashes": [tx_hash]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(&status_response))
.mount(&mock_server)
.await;
let submit_result = client.submit_transaction(&cbor_data).await.unwrap();
assert_eq!(submit_result, tx_hash);
let status = client
.get_transaction_status(&[tx_hash.to_string()])
.await
.unwrap();
assert_eq!(status.len(), 1);
assert_eq!(status[0].tx_hash, tx_hash);
assert_eq!(status[0].num_confirmations.unwrap(), 1);
}
#[tokio::test]
async fn test_transaction_info_and_metadata_integration() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let info_response = json!([{
"tx_hash": tx_hash,
"block_hash": "43c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e683",
"block_height": 7017300,
"epoch_no": 321,
"epoch_slot": 85691,
"absolute_slot": 53384091,
"tx_timestamp": 1630106091,
"tx_block_index": 0,
"tx_size": 289,
"total_output": "42000000",
"fee": "168273",
"treasury_donation": "0",
"deposit": "0",
"inputs": [],
"outputs": []
}]);
let options = TransactionInfoRequest {
tx_hashes: vec![tx_hash.to_string()],
metadata: Some(true),
..Default::default()
};
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!(options)))
.respond_with(ResponseTemplate::new(200).set_body_json(&info_response))
.mount(&mock_server)
.await;
let metadata_response = json!([{
"tx_hash": tx_hash,
"metadata": {
"1234": {
"key": "value",
"array": [1, 2, 3]
}
}
}]);
Mock::given(method("POST"))
.and(path("/tx_metadata"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!({
"_tx_hashes": [tx_hash]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(&metadata_response))
.mount(&mock_server)
.await;
let tx_hashes = vec![tx_hash.to_string()];
let (info, metadata) = tokio::join!(
client.get_transaction_info(&options),
client.get_transaction_metadata(&tx_hashes)
);
let info = info.unwrap();
let metadata = metadata.unwrap();
assert_eq!(info.len(), 1);
assert_eq!(metadata.len(), 1);
assert_eq!(info[0].tx_hash, tx_hash);
assert_eq!(metadata[0].tx_hash, tx_hash);
assert_eq!(info[0].block_height, 7017300);
assert_eq!(info[0].total_output, "42000000");
assert!(metadata[0].metadata.is_some());
let metadata_value = metadata[0].metadata.as_ref().unwrap();
assert!(metadata_value.get("1234").is_some());
}
#[tokio::test]
async fn test_batch_transaction_info() {
let (mock_server, client) = setup_test_client().await;
let tx_hashes = vec![
"6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541".to_string(),
"7ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6542".to_string(),
];
let mock_response = json!([
{
"tx_hash": &tx_hashes[0],
"block_hash": "43c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e683",
"block_height": 7017300,
"epoch_no": 321,
"epoch_slot": 85691, "absolute_slot": 53384091, "tx_timestamp": 1630106091, "tx_block_index": 0, "tx_size": 289, "total_output": "42000000",
"fee": "168273",
"treasury_donation": "0", "deposit": "0", "inputs": [],
"outputs": []
},
{
"tx_hash": &tx_hashes[1],
"block_hash": "53c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e684",
"block_height": 7017301,
"epoch_no": 321,
"epoch_slot": 85692,
"absolute_slot": 53384092,
"tx_timestamp": 1630106092,
"tx_block_index": 0,
"tx_size": 290,
"total_output": "50000000",
"fee": "168274",
"treasury_donation": "0",
"deposit": "0",
"inputs": [],
"outputs": []
}
]);
let options = TransactionInfoRequest {
tx_hashes: tx_hashes.clone(),
..Default::default()
};
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!(options)))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_transaction_info(&options).await.unwrap();
assert_eq!(response.len(), 2);
assert_eq!(response[0].tx_hash, tx_hashes[0]);
assert_eq!(response[0].block_height, 7017300);
assert_eq!(response[0].total_output, "42000000");
assert_eq!(response[1].tx_hash, tx_hashes[1]);
assert_eq!(response[1].block_height, 7017301);
assert_eq!(response[1].total_output, "50000000");
}
#[tokio::test]
async fn test_transaction_with_certificates() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let mock_response = json!([{
"tx_hash": tx_hash,
"block_hash": "43c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e683",
"block_height": 7017300,
"epoch_no": 321, "epoch_slot": 85691, "absolute_slot": 53384091, "tx_timestamp": 1630106091, "tx_block_index": 0, "tx_size": 289, "total_output": "42000000", "fee": "168273", "treasury_donation": "0", "deposit": "0", "certificates": [
{
"index": 0,
"type": "stake_registration",
"info": {
"stake_address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc"
}
}
],
"inputs": [],
"outputs": []
}]);
let options = TransactionInfoRequest {
tx_hashes: vec![tx_hash.to_string()],
certs: Some(true),
..Default::default()
};
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!(options)))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_transaction_info(&options).await.unwrap();
assert_eq!(response.len(), 1);
let certificates = response[0].certificates.as_ref().unwrap();
assert_eq!(certificates.len(), 1);
assert_eq!(certificates[0].cert_type, "stake_registration");
assert_eq!(certificates[0].index, Some(0));
}
#[tokio::test]
async fn test_transaction_with_withdrawals() {
let (mock_server, client) = setup_test_client().await;
let tx_hash = "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541";
let mock_response = json!([{
"tx_hash": tx_hash,
"block_hash": "43c66ecb78f5938d7a3bf2cef6b575acda9c86a7c0c27dd91cdcd9e2f0f6e683",
"block_height": 7017300,
"epoch_no": 321, "epoch_slot": 85691, "absolute_slot": 53384091, "tx_timestamp": 1630106091, "tx_block_index": 0, "tx_size": 289, "total_output": "42000000", "fee": "168273", "treasury_donation": "0", "deposit": "0", "withdrawals": [
{
"amount": "1000000",
"stake_addr": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc"
}
],
"inputs": [],
"outputs": []
}]);
let options = TransactionInfoRequest {
tx_hashes: vec![tx_hash.to_string()],
withdrawals: Some(true),
..Default::default()
};
Mock::given(method("POST"))
.and(path("/tx_info"))
.and(header("Content-Type", "application/json"))
.and(body_json(json!(options)))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_transaction_info(&options).await.unwrap();
assert_eq!(response.len(), 1);
let withdrawals = response[0].withdrawals.as_ref().unwrap();
assert_eq!(withdrawals.len(), 1);
assert_eq!(withdrawals[0].amount, "1000000");
assert_eq!(
withdrawals[0].stake_addr,
"stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc"
);
}