use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use prost::Message;
use zera_proto::zera_api::{
BalanceResponse, BaseFeeResponse, NonceResponse, TokenFeeInfo, TokenFeeInfoResponse,
};
use zera_proto::zera_txn::{ContractFeeType, ContractFees, TransactionType};
use zera_sdk::api::{
get_balance_with_client, get_base_fee_with_client, get_nonce_with_client,
get_nonces_with_client, get_token_fee_info_with_client, GetTokenFeeInfoParams,
};
use zera_sdk::grpc::{TransportResponse, UnaryTransport, ValidatorApiClient};
use zera_sdk::Result;
type QueuedTransportResult = std::result::Result<TransportResponse, String>;
type ResponseQueueMap = HashMap<String, VecDeque<QueuedTransportResult>>;
#[derive(Clone, Default)]
struct MockTransport {
responses: Arc<Mutex<ResponseQueueMap>>,
}
impl MockTransport {
fn push_response<MessageType: Message>(&self, path: &str, message: &MessageType) {
self.responses
.lock()
.expect("responses")
.entry(path.to_string())
.or_default()
.push_back(Ok(grpc_ok(message)));
}
fn push_error(&self, path: &str, message: &str) {
self.responses
.lock()
.expect("responses")
.entry(path.to_string())
.or_default()
.push_back(Err(message.to_string()));
}
}
#[async_trait]
impl UnaryTransport for MockTransport {
async fn unary_bytes(&self, path: &str, _framed_request: Vec<u8>) -> Result<TransportResponse> {
let mut responses = self.responses.lock().expect("responses");
let queue = responses.get_mut(path).expect("missing path");
match queue.pop_front().expect("missing queued response") {
Ok(response) => Ok(response),
Err(message) => Err(zera_sdk::ZeraError::Rpc(message)),
}
}
}
fn grpc_ok<MessageType: Message>(message: &MessageType) -> TransportResponse {
let payload = message.encode_to_vec();
let mut body = vec![0];
body.extend_from_slice(&(payload.len() as u32).to_be_bytes());
body.extend_from_slice(&payload);
let trailers = b"grpc-status: 0\r\n";
body.push(0x80);
body.extend_from_slice(&(trailers.len() as u32).to_be_bytes());
body.extend_from_slice(trailers);
TransportResponse::ok(body)
}
fn test_address() -> String {
bs58::encode([1u8, 2, 3, 4]).into_string()
}
#[tokio::test]
async fn nonce_wrapper_adds_one_and_defaults_to_one() {
let transport = MockTransport::default();
transport.push_response("/api/Nonce", &NonceResponse { nonce: 100 });
transport.push_response("/api/Nonce", &NonceResponse { nonce: 0 });
let client = ValidatorApiClient::with_transport(transport);
let nonce = get_nonce_with_client(&test_address(), &client)
.await
.expect("nonce");
let default_nonce = get_nonce_with_client(&test_address(), &client)
.await
.expect("default nonce");
assert_eq!(nonce, 101);
assert_eq!(default_nonce, 1);
}
#[tokio::test]
async fn nonce_wrapper_handles_multiple_addresses() {
let transport = MockTransport::default();
transport.push_response("/api/Nonce", &NonceResponse { nonce: 1 });
transport.push_response("/api/Nonce", &NonceResponse { nonce: 2 });
let client = ValidatorApiClient::with_transport(transport);
let addresses = vec![test_address(), test_address()];
let nonces = get_nonces_with_client(&addresses, &client)
.await
.expect("nonces");
assert_eq!(nonces, vec![2, 3]);
}
#[tokio::test]
async fn balance_wrapper_normalizes_invalid_wallets() {
let transport = MockTransport::default();
transport.push_error("/api/Balance", "Invalid Wallet");
let client = ValidatorApiClient::with_transport(transport);
let response = get_balance_with_client(&test_address(), "$ZRA+0000", &client)
.await
.expect("normalized invalid wallet");
assert_eq!(response.balance, "0");
assert_eq!(response.denomination, "1");
assert_eq!(response.rate, "0");
assert_eq!(response.balance_nice, "0");
}
#[tokio::test]
async fn base_fee_wrapper_adds_human_readable_fields() {
let transport = MockTransport::default();
transport.push_response(
"/api/BaseFee",
&BaseFeeResponse {
key_fee: "2000000000000000000".to_string(),
byte_fee: "500000000000000000".to_string(),
new_wallet_fee: "0".to_string(),
},
);
let client = ValidatorApiClient::with_transport(transport);
let response = get_base_fee_with_client(TransactionType::CoinType, None, &client)
.await
.expect("base fee");
assert_eq!(response.key_fee_usd, "2");
assert_eq!(response.byte_fee_usd, "0.5");
}
#[tokio::test]
async fn token_fee_info_wrapper_defaults_empty_strings() {
let transport = MockTransport::default();
transport.push_response(
"/api/GetTokenFeeInfo",
&TokenFeeInfoResponse {
tokens: vec![TokenFeeInfo {
contract_id: "$ZRA+0000".to_string(),
rate: String::new(),
authorized: true,
denomination: String::new(),
contract_fees: Some(ContractFees {
fee: String::new(),
burn: String::new(),
validator: String::new(),
contract_fee_type: ContractFeeType::Fixed as i32,
..Default::default()
}),
allowed_fees: String::new(),
used_fees: String::new(),
}],
},
);
let client = ValidatorApiClient::with_transport(transport);
let response = get_token_fee_info_with_client(
GetTokenFeeInfoParams {
contract_ids: vec!["$ZRA+0000".to_string()],
},
&client,
)
.await
.expect("token fee info");
let token = &response.tokens[0];
assert_eq!(token.rate, "0");
assert_eq!(token.denomination, "1");
assert_eq!(token.allowed_fees, "0");
assert_eq!(token.used_fees, "0");
let fees = token.contract_fees.as_ref().expect("contract fees");
assert_eq!(fees.fee, "0");
assert_eq!(fees.burn, "0");
assert_eq!(fees.validator, "0");
}
#[tokio::test]
async fn validator_client_decodes_balance_response() {
let transport = MockTransport::default();
transport.push_response(
"/api/Balance",
&BalanceResponse {
balance: "1000".to_string(),
denomination: "100".to_string(),
rate: "2000000000000000000".to_string(),
},
);
let client = ValidatorApiClient::with_transport(transport);
let response = get_balance_with_client(&test_address(), "$ZRA+0000", &client)
.await
.expect("balance");
assert_eq!(response.balance_nice, "10");
assert_eq!(response.rate_nice, "2");
}