use crate::models::DiscoveredKey;
use crate::types::Result;
#[derive(Debug, Clone)]
pub struct AlgorandConfig {
pub algod_url: String,
pub algod_token: String,
pub indexer_url: Option<String>,
pub indexer_token: Option<String>,
}
impl AlgorandConfig {
pub fn new(algod_url: &str, algod_token: &str) -> Self {
Self {
algod_url: algod_url.to_string(),
algod_token: algod_token.to_string(),
indexer_url: None,
indexer_token: None,
}
}
pub fn with_indexer(mut self, url: &str, token: &str) -> Self {
self.indexer_url = Some(url.to_string());
self.indexer_token = Some(token.to_string());
self
}
pub fn localnet() -> Self {
Self {
algod_url: "http://localhost:4001".to_string(),
algod_token: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
.to_string(),
indexer_url: Some("http://localhost:8980".to_string()),
indexer_token: Some(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
),
}
}
pub fn testnet() -> Self {
Self {
algod_url: "https://testnet-api.4160.nodely.dev".to_string(),
algod_token: String::new(),
indexer_url: Some("https://testnet-idx.4160.nodely.dev".to_string()),
indexer_token: Some(String::new()),
}
}
pub fn mainnet() -> Self {
Self {
algod_url: "https://mainnet-api.4160.nodely.dev".to_string(),
algod_token: String::new(),
indexer_url: Some("https://mainnet-idx.4160.nodely.dev".to_string()),
indexer_token: Some(String::new()),
}
}
}
#[derive(Debug, Clone)]
pub struct TransactionInfo {
pub txid: String,
pub confirmed_round: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct NoteTransaction {
pub txid: String,
pub sender: String,
pub receiver: String,
pub note: Vec<u8>,
pub confirmed_round: u64,
pub round_time: u64,
}
#[async_trait::async_trait]
pub trait AlgodClient: Send + Sync {
async fn get_suggested_params(&self) -> Result<SuggestedParams>;
async fn get_account_info(&self, address: &str) -> Result<AccountInfo>;
async fn submit_transaction(&self, signed_txn: &[u8]) -> Result<String>;
async fn wait_for_confirmation(&self, txid: &str, rounds: u32) -> Result<TransactionInfo>;
async fn get_current_round(&self) -> Result<u64>;
}
#[derive(Debug, Clone)]
pub struct SuggestedParams {
pub fee: u64,
pub min_fee: u64,
pub first_valid: u64,
pub last_valid: u64,
pub genesis_id: String,
pub genesis_hash: [u8; 32],
}
#[derive(Debug, Clone)]
pub struct AccountInfo {
pub address: String,
pub amount: u64,
pub min_balance: u64,
}
#[derive(Debug, Clone)]
pub struct PaginatedTransactions {
pub transactions: Vec<NoteTransaction>,
pub next_token: Option<String>,
}
pub const DEFAULT_DISCOVERY_PAGE_SIZE: u32 = 100;
#[async_trait::async_trait]
pub trait IndexerClient: Send + Sync {
async fn search_transactions(
&self,
address: &str,
after_round: Option<u64>,
limit: Option<u32>,
) -> Result<Vec<NoteTransaction>>;
async fn search_transactions_between(
&self,
address1: &str,
address2: &str,
after_round: Option<u64>,
limit: Option<u32>,
) -> Result<Vec<NoteTransaction>>;
async fn get_transaction(&self, txid: &str) -> Result<NoteTransaction>;
async fn wait_for_indexer(&self, txid: &str, timeout_secs: u32) -> Result<NoteTransaction>;
async fn search_transactions_paginated(
&self,
address: &str,
limit: Option<u32>,
next_token: Option<&str>,
) -> Result<PaginatedTransactions> {
let _ = next_token; let transactions = self.search_transactions(address, None, limit).await?;
Ok(PaginatedTransactions {
transactions,
next_token: None,
})
}
}
pub async fn discover_encryption_key(
indexer: &dyn IndexerClient,
address: &str,
) -> Result<Option<DiscoveredKey>> {
discover_encryption_key_paginated(indexer, address, DEFAULT_DISCOVERY_PAGE_SIZE, None).await
}
pub async fn discover_encryption_key_paginated(
indexer: &dyn IndexerClient,
address: &str,
page_size: u32,
max_pages: Option<u32>,
) -> Result<Option<DiscoveredKey>> {
let mut next_token: Option<String> = None;
let mut pages_searched: u32 = 0;
loop {
let result = indexer
.search_transactions_paginated(address, Some(page_size), next_token.as_deref())
.await?;
for tx in &result.transactions {
if tx.sender != address {
continue;
}
if tx.receiver != address {
continue;
}
if let Some(key) = parse_key_announcement(&tx.note, address) {
return Ok(Some(key));
}
}
pages_searched += 1;
match result.next_token {
Some(token) if !result.transactions.is_empty() => {
next_token = Some(token);
}
_ => break,
}
if let Some(max) = max_pages {
if pages_searched >= max {
break;
}
}
}
Ok(None)
}
fn decode_algorand_address(address: &str) -> Option<[u8; 32]> {
let decoded = data_encoding::BASE32_NOPAD
.decode(address.as_bytes())
.ok()?;
if decoded.len() != 36 {
return None;
}
let public_key = &decoded[..32];
let checksum = &decoded[32..36];
use sha2::Digest;
let hash = sha2::Sha512_256::digest(public_key);
if checksum != &hash[hash.len() - 4..] {
return None;
}
let mut ed25519_public_key = [0u8; 32];
ed25519_public_key.copy_from_slice(public_key);
Some(ed25519_public_key)
}
fn parse_key_announcement(note: &[u8], address: &str) -> Option<DiscoveredKey> {
if note.len() < 32 {
return None;
}
let mut public_key = [0u8; 32];
public_key.copy_from_slice(¬e[..32]);
let is_verified = if note.len() >= 96 {
let signature = ¬e[32..96];
match decode_algorand_address(address) {
Some(ed25519_key) => {
crate::signature::verify_encryption_key_bytes(&public_key, &ed25519_key, signature)
.unwrap_or(false)
}
None => false,
}
} else {
false
};
Some(DiscoveredKey {
public_key,
is_verified,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_localnet() {
let config = AlgorandConfig::localnet();
assert!(config.algod_url.contains("localhost"));
assert!(config.indexer_url.is_some());
}
#[test]
fn test_config_testnet() {
let config = AlgorandConfig::testnet();
assert!(config.algod_url.contains("testnet"));
}
#[test]
fn test_config_mainnet() {
let config = AlgorandConfig::mainnet();
assert!(config.algod_url.contains("mainnet"));
}
#[test]
fn test_config_with_indexer() {
let config = AlgorandConfig::new("http://localhost:4001", "token123")
.with_indexer("http://localhost:8980", "idx-token");
assert_eq!(config.algod_url, "http://localhost:4001");
assert_eq!(config.algod_token, "token123");
assert_eq!(
config.indexer_url,
Some("http://localhost:8980".to_string())
);
assert_eq!(config.indexer_token, Some("idx-token".to_string()));
}
#[test]
fn test_decode_algorand_address_valid() {
let pubkey = [0u8; 32];
use sha2::Digest;
let hash = sha2::Sha512_256::digest(pubkey);
let checksum = &hash[hash.len() - 4..];
let mut full = Vec::with_capacity(36);
full.extend_from_slice(&pubkey);
full.extend_from_slice(checksum);
let address = data_encoding::BASE32_NOPAD.encode(&full);
let decoded = decode_algorand_address(&address);
assert!(decoded.is_some());
assert_eq!(decoded.unwrap(), pubkey);
}
#[test]
fn test_decode_algorand_address_bad_checksum() {
let pubkey = [1u8; 32];
let bad_checksum = [0xFF, 0xFF, 0xFF, 0xFF];
let mut full = Vec::with_capacity(36);
full.extend_from_slice(&pubkey);
full.extend_from_slice(&bad_checksum);
let address = data_encoding::BASE32_NOPAD.encode(&full);
let result = decode_algorand_address(&address);
assert!(result.is_none(), "bad checksum should be rejected");
}
#[test]
fn test_decode_algorand_address_too_short() {
let result = decode_algorand_address("AAAA");
assert!(result.is_none());
}
#[test]
fn test_decode_algorand_address_invalid_base32() {
let result = decode_algorand_address("not-valid-base32!!!");
assert!(result.is_none());
}
#[test]
fn test_parse_key_announcement_too_short() {
let note = [0u8; 16]; let result = parse_key_announcement(¬e, "SOMEADDR");
assert!(result.is_none());
}
#[test]
fn test_parse_key_announcement_valid_unsigned() {
let mut note = [0u8; 32];
note[0] = 0x42; let result = parse_key_announcement(¬e, "SOMEADDR");
assert!(result.is_some());
let key = result.unwrap();
assert_eq!(key.public_key[0], 0x42);
assert!(!key.is_verified); }
#[test]
fn test_parse_key_announcement_with_bad_signature() {
let note = [0u8; 96];
let pubkey = [0u8; 32];
use sha2::Digest;
let hash = sha2::Sha512_256::digest(pubkey);
let checksum = &hash[hash.len() - 4..];
let mut full = Vec::with_capacity(36);
full.extend_from_slice(&pubkey);
full.extend_from_slice(checksum);
let address = data_encoding::BASE32_NOPAD.encode(&full);
let result = parse_key_announcement(¬e, &address);
assert!(result.is_some());
let key = result.unwrap();
assert!(!key.is_verified);
}
#[test]
fn test_transaction_info_debug() {
let info = TransactionInfo {
txid: "TXID123".to_string(),
confirmed_round: Some(1000),
};
let debug = format!("{:?}", info);
assert!(debug.contains("TXID123"));
}
#[test]
fn test_note_transaction_clone() {
let tx = NoteTransaction {
txid: "TX1".to_string(),
sender: "SENDER".to_string(),
receiver: "RECEIVER".to_string(),
note: vec![1, 2, 3],
confirmed_round: 100,
round_time: 1234567890,
};
let cloned = tx.clone();
assert_eq!(cloned.txid, tx.txid);
assert_eq!(cloned.note, tx.note);
}
#[test]
fn test_parse_key_announcement_with_valid_signature() {
use ed25519_dalek::{Signer, SigningKey};
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let ed25519_pubkey = signing_key.verifying_key().to_bytes();
use sha2::Digest;
let hash = sha2::Sha512_256::digest(ed25519_pubkey);
let checksum = &hash[hash.len() - 4..];
let mut addr_bytes = Vec::with_capacity(36);
addr_bytes.extend_from_slice(&ed25519_pubkey);
addr_bytes.extend_from_slice(checksum);
let address = data_encoding::BASE32_NOPAD.encode(&addr_bytes);
let encryption_key = [0xABu8; 32];
let signature = signing_key.sign(&encryption_key);
let mut note = Vec::with_capacity(96);
note.extend_from_slice(&encryption_key);
note.extend_from_slice(&signature.to_bytes());
let result = parse_key_announcement(¬e, &address);
assert!(result.is_some());
let key = result.unwrap();
assert_eq!(key.public_key, encryption_key);
assert!(key.is_verified, "valid signature should be verified");
}
#[test]
fn test_parse_key_announcement_wrong_signer() {
use ed25519_dalek::{Signer, SigningKey};
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let different_key = SigningKey::from_bytes(&[99u8; 32]);
let different_pubkey = different_key.verifying_key().to_bytes();
use sha2::Digest;
let hash = sha2::Sha512_256::digest(different_pubkey);
let checksum = &hash[hash.len() - 4..];
let mut addr_bytes = Vec::with_capacity(36);
addr_bytes.extend_from_slice(&different_pubkey);
addr_bytes.extend_from_slice(checksum);
let address = data_encoding::BASE32_NOPAD.encode(&addr_bytes);
let encryption_key = [0xABu8; 32];
let signature = signing_key.sign(&encryption_key);
let mut note = Vec::with_capacity(96);
note.extend_from_slice(&encryption_key);
note.extend_from_slice(&signature.to_bytes());
let result = parse_key_announcement(¬e, &address);
assert!(result.is_some());
let key = result.unwrap();
assert!(!key.is_verified, "wrong signer should not verify");
}
#[test]
fn test_parse_key_announcement_exactly_32_bytes() {
let note = [0x42u8; 32];
let result = parse_key_announcement(¬e, "SOMEADDR");
assert!(result.is_some());
assert!(!result.unwrap().is_verified);
}
#[test]
fn test_parse_key_announcement_between_32_and_96_bytes() {
let note = [0x42u8; 64];
let result = parse_key_announcement(¬e, "SOMEADDR");
assert!(result.is_some());
assert!(!result.unwrap().is_verified);
}
#[test]
fn test_parse_key_announcement_over_96_bytes() {
let note = vec![0x42u8; 128];
let result = parse_key_announcement(¬e, "SOMEADDR");
assert!(result.is_some());
assert!(!result.unwrap().is_verified);
}
#[test]
fn test_decode_algorand_address_various_keys() {
use sha2::Digest;
for i in 0u8..5 {
let pubkey = [i; 32];
let hash = sha2::Sha512_256::digest(pubkey);
let checksum = &hash[hash.len() - 4..];
let mut full = Vec::with_capacity(36);
full.extend_from_slice(&pubkey);
full.extend_from_slice(checksum);
let address = data_encoding::BASE32_NOPAD.encode(&full);
let decoded = decode_algorand_address(&address);
assert!(decoded.is_some(), "key {} should decode", i);
assert_eq!(decoded.unwrap(), pubkey);
}
}
struct MockDiscoveryIndexer {
transactions: Vec<NoteTransaction>,
}
#[async_trait::async_trait]
impl IndexerClient for MockDiscoveryIndexer {
async fn search_transactions(
&self,
address: &str,
_after_round: Option<u64>,
_limit: Option<u32>,
) -> crate::types::Result<Vec<NoteTransaction>> {
Ok(self
.transactions
.iter()
.filter(|tx| tx.sender == address || tx.receiver == address)
.cloned()
.collect())
}
async fn search_transactions_between(
&self,
_a1: &str,
_a2: &str,
_after_round: Option<u64>,
_limit: Option<u32>,
) -> crate::types::Result<Vec<NoteTransaction>> {
Ok(Vec::new())
}
async fn get_transaction(&self, _txid: &str) -> crate::types::Result<NoteTransaction> {
Err(crate::types::AlgoChatError::TransactionFailed(
"not found".to_string(),
))
}
async fn wait_for_indexer(
&self,
_txid: &str,
_timeout: u32,
) -> crate::types::Result<NoteTransaction> {
Err(crate::types::AlgoChatError::TransactionFailed(
"not found".to_string(),
))
}
}
#[tokio::test]
async fn test_discover_encryption_key_no_transactions() {
let indexer = MockDiscoveryIndexer {
transactions: Vec::new(),
};
let result = discover_encryption_key(&indexer, "SOMEADDR").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_discover_encryption_key_non_self_transfer() {
let indexer = MockDiscoveryIndexer {
transactions: vec![NoteTransaction {
txid: "TX1".to_string(),
sender: "ALICE".to_string(),
receiver: "BOB".to_string(), note: vec![0u8; 32],
confirmed_round: 100,
round_time: 1700000000,
}],
};
let result = discover_encryption_key(&indexer, "ALICE").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_discover_encryption_key_valid_self_transfer() {
let key = [0xABu8; 32];
let address = "MYADDR";
let indexer = MockDiscoveryIndexer {
transactions: vec![NoteTransaction {
txid: "TX1".to_string(),
sender: address.to_string(),
receiver: address.to_string(),
note: key.to_vec(),
confirmed_round: 100,
round_time: 1700000000,
}],
};
let result = discover_encryption_key(&indexer, address).await.unwrap();
assert!(result.is_some());
let discovered = result.unwrap();
assert_eq!(discovered.public_key, key);
assert!(!discovered.is_verified); }
#[tokio::test]
async fn test_discover_encryption_key_with_signed_announcement() {
use ed25519_dalek::{Signer, SigningKey};
use sha2::Digest;
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let ed25519_pubkey = signing_key.verifying_key().to_bytes();
let hash = sha2::Sha512_256::digest(ed25519_pubkey);
let checksum = &hash[hash.len() - 4..];
let mut addr_bytes = Vec::with_capacity(36);
addr_bytes.extend_from_slice(&ed25519_pubkey);
addr_bytes.extend_from_slice(checksum);
let address = data_encoding::BASE32_NOPAD.encode(&addr_bytes);
let encryption_key = [0xABu8; 32];
let signature = signing_key.sign(&encryption_key);
let mut note = Vec::with_capacity(96);
note.extend_from_slice(&encryption_key);
note.extend_from_slice(&signature.to_bytes());
let indexer = MockDiscoveryIndexer {
transactions: vec![NoteTransaction {
txid: "TX1".to_string(),
sender: address.clone(),
receiver: address.clone(),
note,
confirmed_round: 100,
round_time: 1700000000,
}],
};
let result = discover_encryption_key(&indexer, &address).await.unwrap();
assert!(result.is_some());
let discovered = result.unwrap();
assert_eq!(discovered.public_key, encryption_key);
assert!(discovered.is_verified);
}
#[tokio::test]
async fn test_discover_encryption_key_skips_short_notes() {
let address = "MYADDR";
let indexer = MockDiscoveryIndexer {
transactions: vec![NoteTransaction {
txid: "TX1".to_string(),
sender: address.to_string(),
receiver: address.to_string(),
note: vec![1, 2, 3], confirmed_round: 100,
round_time: 1700000000,
}],
};
let result = discover_encryption_key(&indexer, address).await.unwrap();
assert!(result.is_none());
}
#[test]
fn test_suggested_params_debug() {
let params = SuggestedParams {
fee: 1000,
min_fee: 1000,
first_valid: 100,
last_valid: 1100,
genesis_id: "testnet-v1.0".to_string(),
genesis_hash: [0u8; 32],
};
let debug = format!("{:?}", params);
assert!(debug.contains("testnet-v1.0"));
}
#[test]
fn test_account_info_debug() {
let info = AccountInfo {
address: "ADDR123".to_string(),
amount: 1_000_000,
min_balance: 100_000,
};
let debug = format!("{:?}", info);
assert!(debug.contains("ADDR123"));
assert!(debug.contains("1000000"));
}
use std::collections::HashMap;
use std::sync::Mutex;
struct MockPaginatedIndexer {
pages: HashMap<Option<String>, PaginatedTransactions>,
call_count: Mutex<u32>,
last_limit: Mutex<Option<u32>>,
}
impl MockPaginatedIndexer {
fn new(pages: Vec<PaginatedTransactions>) -> Self {
let mut map = HashMap::new();
if let Some(first) = pages.first() {
map.insert(None, first.clone());
}
for i in 0..pages.len().saturating_sub(1) {
if let Some(token) = &pages[i].next_token {
map.insert(Some(token.clone()), pages[i + 1].clone());
}
}
Self {
pages: map,
call_count: Mutex::new(0),
last_limit: Mutex::new(None),
}
}
fn call_count(&self) -> u32 {
*self.call_count.lock().unwrap()
}
fn last_limit(&self) -> Option<u32> {
*self.last_limit.lock().unwrap()
}
}
#[async_trait::async_trait]
impl IndexerClient for MockPaginatedIndexer {
async fn search_transactions(
&self,
_address: &str,
_after_round: Option<u64>,
_limit: Option<u32>,
) -> crate::types::Result<Vec<NoteTransaction>> {
Ok(Vec::new())
}
async fn search_transactions_between(
&self,
_a1: &str,
_a2: &str,
_after_round: Option<u64>,
_limit: Option<u32>,
) -> crate::types::Result<Vec<NoteTransaction>> {
Ok(Vec::new())
}
async fn get_transaction(&self, _txid: &str) -> crate::types::Result<NoteTransaction> {
Err(crate::types::AlgoChatError::TransactionFailed(
"not found".to_string(),
))
}
async fn wait_for_indexer(
&self,
_txid: &str,
_timeout: u32,
) -> crate::types::Result<NoteTransaction> {
Err(crate::types::AlgoChatError::TransactionFailed(
"not found".to_string(),
))
}
async fn search_transactions_paginated(
&self,
_address: &str,
limit: Option<u32>,
next_token: Option<&str>,
) -> crate::types::Result<PaginatedTransactions> {
*self.call_count.lock().unwrap() += 1;
*self.last_limit.lock().unwrap() = limit;
let key = next_token.map(|s| s.to_string());
match self.pages.get(&key) {
Some(page) => Ok(page.clone()),
None => Ok(PaginatedTransactions {
transactions: Vec::new(),
next_token: None,
}),
}
}
}
fn make_key_tx(address: &str, key: [u8; 32]) -> NoteTransaction {
NoteTransaction {
txid: "TX-KEY".to_string(),
sender: address.to_string(),
receiver: address.to_string(),
note: key.to_vec(),
confirmed_round: 100,
round_time: 1700000000,
}
}
fn make_non_key_tx(address: &str, txid: &str) -> NoteTransaction {
NoteTransaction {
txid: txid.to_string(),
sender: address.to_string(),
receiver: address.to_string(),
note: vec![1, 2, 3], confirmed_round: 100,
round_time: 1700000000,
}
}
#[tokio::test]
async fn test_paginated_finds_key_on_first_page() {
let address = "MYADDR";
let key = [0xABu8; 32];
let indexer = MockPaginatedIndexer::new(vec![PaginatedTransactions {
transactions: vec![make_key_tx(address, key)],
next_token: None,
}]);
let result = discover_encryption_key_paginated(&indexer, address, 50, None)
.await
.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().public_key, key);
assert_eq!(indexer.call_count(), 1);
}
#[tokio::test]
async fn test_paginated_finds_key_on_second_page() {
let address = "MYADDR";
let key = [0xCDu8; 32];
let indexer = MockPaginatedIndexer::new(vec![
PaginatedTransactions {
transactions: vec![make_non_key_tx(address, "TX0")],
next_token: Some("page2".to_string()),
},
PaginatedTransactions {
transactions: vec![make_key_tx(address, key)],
next_token: None,
},
]);
let result = discover_encryption_key_paginated(&indexer, address, 50, None)
.await
.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().public_key, key);
assert_eq!(indexer.call_count(), 2);
}
#[tokio::test]
async fn test_paginated_max_pages_limits_search() {
let address = "MYADDR";
let key = [0xEFu8; 32];
let indexer = MockPaginatedIndexer::new(vec![
PaginatedTransactions {
transactions: vec![make_non_key_tx(address, "TX0")],
next_token: Some("page2".to_string()),
},
PaginatedTransactions {
transactions: vec![make_key_tx(address, key)],
next_token: None,
},
]);
let result = discover_encryption_key_paginated(&indexer, address, 50, Some(1))
.await
.unwrap();
assert!(result.is_none());
assert_eq!(indexer.call_count(), 1);
}
#[tokio::test]
async fn test_paginated_exhaustive_search_all_pages() {
let address = "MYADDR";
let key = [0x42u8; 32];
let indexer = MockPaginatedIndexer::new(vec![
PaginatedTransactions {
transactions: vec![make_non_key_tx(address, "TX0")],
next_token: Some("p2".to_string()),
},
PaginatedTransactions {
transactions: vec![make_non_key_tx(address, "TX1")],
next_token: Some("p3".to_string()),
},
PaginatedTransactions {
transactions: vec![make_non_key_tx(address, "TX2")],
next_token: Some("p4".to_string()),
},
PaginatedTransactions {
transactions: vec![make_key_tx(address, key)],
next_token: None,
},
]);
let result = discover_encryption_key_paginated(&indexer, address, 50, None)
.await
.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().public_key, key);
assert_eq!(indexer.call_count(), 4);
}
#[tokio::test]
async fn test_paginated_empty_history_returns_none() {
let indexer = MockPaginatedIndexer::new(vec![PaginatedTransactions {
transactions: Vec::new(),
next_token: None,
}]);
let result = discover_encryption_key_paginated(&indexer, "NOONE", 50, None)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_paginated_page_size_forwarded() {
let indexer = MockPaginatedIndexer::new(vec![PaginatedTransactions {
transactions: Vec::new(),
next_token: None,
}]);
let _ = discover_encryption_key_paginated(&indexer, "ADDR", 25, None).await;
assert_eq!(indexer.last_limit(), Some(25));
}
#[tokio::test]
async fn test_paginated_skips_other_senders() {
let target = "TARGET";
let key = [0xBBu8; 32];
let other_tx = NoteTransaction {
txid: "TX0".to_string(),
sender: "OTHER".to_string(),
receiver: "OTHER".to_string(),
note: vec![0xFFu8; 32],
confirmed_round: 100,
round_time: 1700000000,
};
let indexer = MockPaginatedIndexer::new(vec![PaginatedTransactions {
transactions: vec![other_tx, make_key_tx(target, key)],
next_token: None,
}]);
let result = discover_encryption_key_paginated(&indexer, target, 50, None)
.await
.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().public_key, key);
}
#[test]
fn test_default_discovery_page_size() {
assert_eq!(DEFAULT_DISCOVERY_PAGE_SIZE, 100);
}
#[test]
fn test_paginated_transactions_debug() {
let pt = PaginatedTransactions {
transactions: Vec::new(),
next_token: Some("token123".to_string()),
};
let debug = format!("{:?}", pt);
assert!(debug.contains("token123"));
}
}