use crate::client::SorobanRpcClient;
use crate::error::RpcError;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TtlInfo {
pub contract_id: String,
pub entries: Vec<TtlEntry>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TtlEntry {
pub key: String,
pub current_ttl: u32,
pub expiration_time: String,
pub days_remaining: u32,
pub extension_cost_stroops: u64,
}
pub fn calculate_extension_cost(ledger_delta: u32) -> u64 {
ledger_delta as u64 * 100
}
pub(crate) fn instance_ledger_key(contract_id: &str) -> Result<String, RpcError> {
let contract_id_hex = crate::inspect::contract_id_to_hex(contract_id)?;
sdkt_xdr::encode_ledger_key(&sdkt_xdr::LedgerKeyParams::ContractData(contract_id_hex))
.map_err(|e| RpcError::Rpc(format!("Failed to encode instance ledger key: {e}")))
}
pub async fn get_ttl_info(
client: &SorobanRpcClient,
contract_id: &str,
) -> Result<TtlInfo, RpcError> {
let ledger_info = client.get_ledger().await?;
let current_ledger = ledger_info.sequence;
let instance_key = instance_ledger_key(contract_id)?;
let storage_resp = client
.get_contract_storage(contract_id, &[instance_key])
.await?;
let mut entries = Vec::new();
for entry in storage_resp.entries {
let current_ttl = if let Some(live_until) = entry.live_until_ledger_seq {
live_until.saturating_sub(current_ledger)
} else {
0
};
let days_remaining = (current_ttl * 5) / (24 * 3600);
let extension_cost_stroops = calculate_extension_cost(current_ttl);
entries.push(TtlEntry {
key: entry.key,
current_ttl,
expiration_time: format!("~{} days", days_remaining),
days_remaining,
extension_cost_stroops,
});
}
Ok(TtlInfo {
contract_id: contract_id.to_string(),
entries,
})
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use stellar_xdr::{LedgerKey, ReadXdr};
#[test]
fn test_calculate_extension_cost() {
const SINGLE_ENTRY_TTL_30: u32 = 30;
const SINGLE_ENTRY_TTL_100: u32 = 100;
assert_eq!(calculate_extension_cost(SINGLE_ENTRY_TTL_30), 3000);
assert_eq!(calculate_extension_cost(SINGLE_ENTRY_TTL_100), 10000);
assert_eq!(calculate_extension_cost(0), 0);
}
#[test]
fn test_instance_ledger_key_is_valid_and_stable() {
let c = "CAE3U7JKESRWZHPEQ72DVNGOQ6WPA7HSPQZL5YV46NPCE4TMUPAGYMEC";
let hex = "09ba7d2a24a36c9de487f43ab4ce87acf07cf27c32bee2bcf35e22726ca3c06c";
let key_from_strkey = instance_ledger_key(c).expect("StrKey C... should derive a key");
let key_from_hex = instance_ledger_key(hex).expect("hex should derive a key");
assert!(!key_from_strkey.is_empty());
assert_eq!(
key_from_strkey, key_from_hex,
"StrKey and hex must map to the same contract instance key"
);
let bytes = base64::engine::general_purpose::STANDARD
.decode(key_from_strkey.trim())
.expect("key must be valid base64");
let mut cursor = std::io::Cursor::new(bytes);
let mut limited = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
let ledger_key = LedgerKey::read_xdr(&mut limited).expect("key must decode to a LedgerKey");
match ledger_key {
LedgerKey::ContractData(cd) => {
assert!(
matches!(cd.key, stellar_xdr::ScVal::LedgerKeyContractInstance),
"instance key must target the contract instance singleton"
);
}
other => panic!("expected LedgerKey::ContractData, got {other:?}"),
}
}
#[test]
fn test_instance_ledger_key_rejects_garbage() {
assert!(instance_ledger_key("not-a-contract-id").is_err());
}
}