use std::future::Future;
use std::time::{Duration, Instant};
#[cfg(feature = "bridge-solana")]
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::time::sleep;
#[cfg(feature = "bridge-solana")]
use zera_proto::zera_guardian::solana_payload;
use zera_proto::zera_guardian::{
payload_response, zera_payload, NetworkType, PayloadRequest, PayloadResponse, SolanaPayload,
ZeraPayload,
};
use crate::error::{Result, ZeraError};
use crate::grpc::{create_guardian_client, GuardianClient, UnaryTransport};
#[cfg(feature = "bridge-solana")]
use crate::smart_contracts::use_cases::bridge::guardian::types::{
SubmitVaaToSolanaOperation, SubmitVaaToSolanaOptions, SubmitVaaToSolanaResult,
};
use crate::smart_contracts::use_cases::bridge::guardian::types::{
SubmitVaaToZeraOperation, SubmitVaaToZeraOptions, SubmitVaaToZeraResult, VaaRetryOptions,
};
#[cfg(feature = "bridge-solana")]
use crate::smart_contracts::use_cases::bridge::solana::{
build_mint_wrapped_existing_transaction, build_mint_wrapped_transaction,
build_release_sol_transaction, build_release_spl_transaction, GuardianSignature, SolanaRpc,
};
use crate::smart_contracts::use_cases::bridge::zera::{
create_sol_and_send, mint_sol_and_send, release_zera_and_send, CreateSolOptions,
MintSolOptions, ReleaseZeraOptions,
};
use crate::types::RpcConfig;
#[cfg(feature = "bridge-solana")]
use solana_sdk::hash::Hash;
#[cfg(feature = "bridge-solana")]
use solana_sdk::signature::{Keypair, Signer};
#[cfg(feature = "bridge-solana")]
use solana_sdk::transaction::Transaction;
#[cfg(feature = "bridge-solana")]
const GUARDIAN_NATIVE_SOL_MINT: &str = "So11111111111111111111111111111111111111111";
pub async fn fetch_solana_vaa(
txn_hash: &str,
guardian_config: RpcConfig,
retry: VaaRetryOptions,
) -> Result<SolanaPayload> {
let client = create_guardian_client(guardian_config)?;
fetch_solana_vaa_with_client(txn_hash, &client, retry).await
}
pub async fn fetch_zera_vaa(
tx_signature: &str,
guardian_config: RpcConfig,
retry: VaaRetryOptions,
) -> Result<ZeraPayload> {
let client = create_guardian_client(guardian_config)?;
fetch_zera_vaa_with_client(tx_signature, &client, retry).await
}
pub async fn submit_vaa_to_zera(
tx_signature: &str,
guardian_config: RpcConfig,
public_key_base58_identifier: &str,
private_key_base58: &str,
options: SubmitVaaToZeraOptions,
) -> Result<SubmitVaaToZeraResult> {
let client = create_guardian_client(guardian_config)?;
submit_vaa_to_zera_with_client(
tx_signature,
&client,
public_key_base58_identifier,
private_key_base58,
options,
)
.await
}
#[cfg(feature = "bridge-solana")]
pub async fn submit_vaa_to_solana<R>(
txn_hash: &str,
guardian_config: RpcConfig,
payer: &Keypair,
solana_rpc: &R,
options: SubmitVaaToSolanaOptions,
) -> Result<SubmitVaaToSolanaResult>
where
R: SolanaRpc,
{
let client = create_guardian_client(guardian_config)?;
submit_vaa_to_solana_with_client(txn_hash, &client, payer, solana_rpc, options).await
}
async fn fetch_solana_vaa_with_client<T>(
txn_hash: &str,
client: &GuardianClient<T>,
retry: VaaRetryOptions,
) -> Result<SolanaPayload>
where
T: UnaryTransport,
{
if txn_hash.is_empty() {
return Err(ZeraError::Validation("txnHash is required".to_string()));
}
fetch_with_retry(
|| async {
let response = client
.get_payload_request(&PayloadRequest {
payload_id: txn_hash.to_string(),
network_type: NetworkType::Zera as i32,
})
.await?;
let mut payload = expect_solana_payload(response)?;
let (signatures, public_keys) =
deduplicate_signature_pairs(&payload.signatures, &payload.public_keys)?;
payload.signatures = signatures;
payload.public_keys = public_keys;
Ok(payload)
},
&retry,
)
.await
}
async fn fetch_zera_vaa_with_client<T>(
tx_signature: &str,
client: &GuardianClient<T>,
retry: VaaRetryOptions,
) -> Result<ZeraPayload>
where
T: UnaryTransport,
{
if tx_signature.is_empty() {
return Err(ZeraError::Validation("txSignature is required".to_string()));
}
fetch_with_retry(
|| async {
let response = client
.get_payload_request(&PayloadRequest {
payload_id: tx_signature.to_string(),
network_type: NetworkType::Solana as i32,
})
.await?;
let mut payload = expect_zera_payload(response)?;
let (signatures, public_keys) =
deduplicate_signature_pairs(&payload.signatures, &payload.public_keys)?;
payload.signatures = signatures;
payload.public_keys = public_keys;
Ok(payload)
},
&retry,
)
.await
}
async fn submit_vaa_to_zera_with_client<T>(
tx_signature: &str,
client: &GuardianClient<T>,
public_key_base58_identifier: &str,
private_key_base58: &str,
options: SubmitVaaToZeraOptions,
) -> Result<SubmitVaaToZeraResult>
where
T: UnaryTransport,
{
let payload = fetch_zera_vaa_with_client(tx_signature, client, options.retry.clone()).await?;
let (txn_hash, operation) = match payload.payload.as_ref() {
Some(zera_payload::Payload::ReleasePayload(release_payload)) => (
release_zera_and_send(
&release_payload.zera_wallet_address,
public_key_base58_identifier,
private_key_base58,
ReleaseZeraOptions {
grpc_config: Some(options.zera_config.clone()),
gas_fee_in_usd: options.gas_fee_in_usd,
fee_id: options.fee_id.clone(),
fee_amount_usd: options.fee_amount_usd.clone(),
payload: Some(payload.clone()),
..Default::default()
},
)
.await?,
SubmitVaaToZeraOperation::Release,
),
Some(zera_payload::Payload::MintPayload(mint_payload)) => (
mint_sol_and_send(
&mint_payload.zera_wallet_address,
public_key_base58_identifier,
private_key_base58,
MintSolOptions {
grpc_config: Some(options.zera_config.clone()),
gas_fee_in_usd: options.gas_fee_in_usd,
fee_id: options.fee_id.clone(),
fee_amount_usd: options.fee_amount_usd.clone(),
payload: Some(payload.clone()),
..Default::default()
},
)
.await?,
SubmitVaaToZeraOperation::Mint,
),
Some(zera_payload::Payload::ContractPayload(contract_payload)) => (
create_sol_and_send(
&contract_payload.zera_wallet_address,
public_key_base58_identifier,
private_key_base58,
CreateSolOptions {
grpc_config: Some(options.zera_config.clone()),
gas_fee_in_usd: options.gas_fee_in_usd,
fee_id: options.fee_id.clone(),
fee_amount_usd: options.fee_amount_usd.clone(),
payload: Some(payload.clone()),
..Default::default()
},
)
.await?,
SubmitVaaToZeraOperation::Mint,
),
None => {
return Err(ZeraError::Unsupported(
"Unsupported payload type: none".to_string(),
));
}
};
Ok(SubmitVaaToZeraResult {
txn_hash,
operation,
payload,
})
}
#[cfg(feature = "bridge-solana")]
async fn submit_vaa_to_solana_with_client<T, R>(
txn_hash: &str,
client: &GuardianClient<T>,
payer: &Keypair,
solana_rpc: &R,
options: SubmitVaaToSolanaOptions,
) -> Result<SubmitVaaToSolanaResult>
where
T: UnaryTransport,
R: SolanaRpc,
{
let payload = fetch_solana_vaa_with_client(txn_hash, client, options.retry.clone()).await?;
let signatures = guardian_signatures_from_payload(&payload)?;
let timestamp = payload_timestamp_seconds(&payload);
let (signature, operation) = match payload.payload.as_ref() {
Some(solana_payload::Payload::ReleasePayload(release_payload)) => {
let is_native_sol = release_payload.solana_mint_address.is_empty()
|| release_payload.solana_mint_address == GUARDIAN_NATIVE_SOL_MINT;
if is_native_sol {
let result = build_release_sol_transaction(
crate::smart_contracts::use_cases::bridge::solana::ReleaseSolOptions {
amount: release_payload.amount,
recipient: release_payload.solana_wallet_address.clone(),
txn_id: release_payload.txn_hash.clone(),
timestamp,
signatures,
expected_hash: payload.signed_hash.clone(),
usd_amount: release_payload.usd_amount,
},
&payer.pubkey(),
Some(solana_rpc),
)
.await?;
let verify_signature = submit_signed_transaction(
result.verify_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
false,
)
.await?;
let signature = submit_signed_transaction(
result.release_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
true,
)
.await
.map_err(|error| annotate_post_verify_error(error, &verify_signature))?;
(signature, SubmitVaaToSolanaOperation::ReleaseSol)
} else {
let result = build_release_spl_transaction(
crate::smart_contracts::use_cases::bridge::solana::ReleaseSplOptions {
amount: release_payload.amount,
recipient: release_payload.solana_wallet_address.clone(),
mint: release_payload.solana_mint_address.clone(),
txn_id: release_payload.txn_hash.clone(),
timestamp,
signatures,
expected_hash: payload.signed_hash.clone(),
usd_price_nano: release_payload.usd_amount,
liquidity_usd_nano: release_payload.liquidity_usd,
tier: parse_payload_u8(release_payload.tier, "releasePayload.tier")?,
},
&payer.pubkey(),
Some(solana_rpc),
)
.await?;
let verify_signature = submit_signed_transaction(
result.verify_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
false,
)
.await?;
let signature = submit_signed_transaction(
result.release_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
true,
)
.await
.map_err(|error| annotate_post_verify_error(error, &verify_signature))?;
(signature, SubmitVaaToSolanaOperation::ReleaseSpl)
}
}
Some(solana_payload::Payload::MintPayload(mint_payload)) => {
let result = build_mint_wrapped_existing_transaction(
crate::smart_contracts::use_cases::bridge::solana::MintWrappedExistingOptions {
amount: mint_payload.amount,
recipient: mint_payload.solana_wallet_address.clone(),
contract_id: mint_payload.zera_contract_id.clone(),
txn_id: mint_payload.txn_hash.clone(),
timestamp,
signatures,
expected_hash: payload.signed_hash.clone(),
usd_price_nano: mint_payload.usd_amount,
liquidity_usd_nano: mint_payload.liquidity_usd,
tier: parse_payload_u8(mint_payload.tier, "mintPayload.tier")?,
},
&payer.pubkey(),
Some(solana_rpc),
)
.await?;
let verify_signature = submit_signed_transaction(
result.verify_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
false,
)
.await?;
let signature = submit_signed_transaction(
result.mint_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
true,
)
.await
.map_err(|error| annotate_post_verify_error(error, &verify_signature))?;
(signature, SubmitVaaToSolanaOperation::MintWrapped)
}
Some(solana_payload::Payload::ContractPayload(contract_payload)) => {
let result = build_mint_wrapped_transaction(
crate::smart_contracts::use_cases::bridge::solana::MintWrappedOptions {
amount: contract_payload.amount,
recipient: contract_payload.solana_wallet_address.clone(),
contract_id: contract_payload.zera_contract_id.clone(),
decimals: parse_contract_decimals(&contract_payload.decimals)?,
name: contract_payload.name.clone(),
symbol: contract_payload.symbol.clone(),
uri: contract_payload.uri.clone(),
txn_id: contract_payload.txn_hash.clone(),
timestamp,
signatures,
expected_hash: payload.signed_hash.clone(),
usd_price_nano: contract_payload.usd_amount,
liquidity_usd_nano: contract_payload.liquidity_usd,
tier: parse_payload_u8(contract_payload.tier, "contractPayload.tier")?,
},
&payer.pubkey(),
Some(solana_rpc),
)
.await?;
let verify_signature = submit_signed_transaction(
result.verify_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
false,
)
.await?;
let signature = submit_signed_transaction(
result.mint_transaction,
payer,
solana_rpc,
options.skip_confirmation,
options.skip_preflight,
true,
)
.await
.map_err(|error| annotate_post_verify_error(error, &verify_signature))?;
(signature, SubmitVaaToSolanaOperation::MintWrapped)
}
Some(payload_case) => {
return Err(ZeraError::Unsupported(format!(
"Unsupported payload type: {}",
solana_payload_case_name(payload_case)
)));
}
None => {
return Err(ZeraError::Unsupported(
"Unsupported payload type: none".to_string(),
));
}
};
Ok(SubmitVaaToSolanaResult {
signature,
operation,
payload,
})
}
fn deduplicate_signature_pairs(
signatures: &[String],
public_keys: &[String],
) -> Result<(Vec<String>, Vec<String>)> {
if signatures.len() != public_keys.len() {
return Err(ZeraError::Validation(format!(
"VAA integrity check failed: signatures count ({}) does not match public keys count ({})",
signatures.len(),
public_keys.len()
)));
}
if signatures.is_empty() {
return Err(ZeraError::Validation(
"VAA integrity check failed: no signatures present".to_string(),
));
}
let mut seen = std::collections::HashSet::new();
let mut deduplicated_signatures = Vec::with_capacity(signatures.len());
let mut deduplicated_public_keys = Vec::with_capacity(public_keys.len());
for (signature, public_key) in signatures.iter().zip(public_keys) {
if seen.insert(signature.clone()) {
deduplicated_signatures.push(signature.clone());
deduplicated_public_keys.push(public_key.clone());
}
}
Ok((deduplicated_signatures, deduplicated_public_keys))
}
#[cfg(feature = "bridge-solana")]
fn guardian_signatures_from_payload(payload: &SolanaPayload) -> Result<Vec<GuardianSignature>> {
if payload.signatures.len() != payload.public_keys.len() {
return Err(ZeraError::Validation(format!(
"VAA integrity check failed: signatures count ({}) does not match public keys count ({})",
payload.signatures.len(),
payload.public_keys.len()
)));
}
Ok(payload
.signatures
.iter()
.zip(payload.public_keys.iter())
.map(|(signature, public_key)| GuardianSignature {
signature: signature.clone(),
public_key: public_key.clone(),
})
.collect())
}
#[cfg(feature = "bridge-solana")]
fn payload_timestamp_seconds(payload: &SolanaPayload) -> u64 {
payload
.timestamp
.as_ref()
.and_then(|timestamp| u64::try_from(timestamp.seconds).ok())
.unwrap_or_else(current_unix_timestamp)
}
#[cfg(feature = "bridge-solana")]
fn current_unix_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(feature = "bridge-solana")]
fn parse_payload_u8(value: i32, field_name: &str) -> Result<u8> {
u8::try_from(value).map_err(|_| {
ZeraError::Validation(format!(
"{field_name} must fit into an unsigned 8-bit integer"
))
})
}
#[cfg(feature = "bridge-solana")]
fn parse_contract_decimals(value: &str) -> Result<u8> {
value.parse::<u8>().map_err(|error| {
ZeraError::Validation(format!(
"contractPayload.decimals must parse as u8: {error}"
))
})
}
#[cfg(feature = "bridge-solana")]
fn solana_payload_case_name(payload: &solana_payload::Payload) -> &'static str {
match payload {
solana_payload::Payload::ContractPayload(_) => "contractPayload",
solana_payload::Payload::MintPayload(_) => "mintPayload",
solana_payload::Payload::ReleasePayload(_) => "releasePayload",
solana_payload::Payload::PausePayload(_) => "pausePayload",
solana_payload::Payload::UpgradeBridgePayload(_) => "upgradeBridgePayload",
solana_payload::Payload::UpdateGuardianKeysPayload(_) => "updateGuardianKeysPayload",
solana_payload::Payload::RegisterPayload(_) => "registerPayload",
}
}
#[cfg(feature = "bridge-solana")]
fn annotate_post_verify_error(error: ZeraError, verify_signature: &str) -> ZeraError {
let prefix = format!("phase 2 submission failed after verify transaction {verify_signature}: ");
match error {
ZeraError::NotImplemented(message) => ZeraError::NotImplemented(message),
ZeraError::InvalidConfig(message) => ZeraError::InvalidConfig(format!("{prefix}{message}")),
ZeraError::InvalidInput(message) => ZeraError::InvalidInput(format!("{prefix}{message}")),
ZeraError::Validation(message) => ZeraError::Validation(format!("{prefix}{message}")),
ZeraError::Rpc(message) => ZeraError::Rpc(format!("{prefix}{message}")),
ZeraError::Transport(message) => ZeraError::Transport(format!("{prefix}{message}")),
ZeraError::Serialization(message) => ZeraError::Serialization(format!("{prefix}{message}")),
ZeraError::Crypto(message) => ZeraError::Crypto(format!("{prefix}{message}")),
ZeraError::Unsupported(message) => ZeraError::Unsupported(format!("{prefix}{message}")),
other => other,
}
}
#[cfg(feature = "bridge-solana")]
async fn submit_signed_transaction<R>(
mut transaction: Transaction,
payer: &Keypair,
solana_rpc: &R,
skip_confirmation: bool,
skip_preflight: bool,
refresh_blockhash: bool,
) -> Result<String>
where
R: SolanaRpc,
{
let recent_blockhash =
if refresh_blockhash || transaction.message.recent_blockhash == Hash::default() {
solana_rpc.latest_blockhash().await?
} else {
transaction.message.recent_blockhash
};
transaction
.try_sign(&[payer], recent_blockhash)
.map_err(|error| ZeraError::Crypto(error.to_string()))?;
if skip_confirmation {
solana_rpc
.send_transaction(&transaction, skip_preflight)
.await
} else {
solana_rpc
.send_and_confirm_transaction(&transaction, skip_preflight)
.await
}
}
async fn fetch_with_retry<T, F, Fut>(fetcher: F, options: &VaaRetryOptions) -> Result<T>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T>>,
{
if !options.retry {
return fetcher().await;
}
let started_at = Instant::now();
let mut delay_ms = options.initial_delay_ms;
loop {
match fetcher().await {
Ok(value) => return Ok(value),
Err(error) => {
let elapsed_ms = started_at.elapsed().as_millis() as u64;
if elapsed_ms.saturating_add(delay_ms) > options.max_elapsed_ms {
return Err(error);
}
sleep(Duration::from_millis(delay_ms)).await;
let remaining_ms = options
.max_elapsed_ms
.saturating_sub(started_at.elapsed().as_millis() as u64);
delay_ms = delay_ms.saturating_mul(2).min(remaining_ms);
}
}
}
}
fn expect_solana_payload(response: PayloadResponse) -> Result<SolanaPayload> {
match response.payload {
Some(payload_response::Payload::SolanaPayload(payload)) => Ok(payload),
Some(payload_response::Payload::ZeraPayload(_)) => Err(ZeraError::Validation(
"Expected Solana payload, got: zeraPayload".to_string(),
)),
None => Err(ZeraError::Validation(
"Expected Solana payload, got: none".to_string(),
)),
}
}
fn expect_zera_payload(response: PayloadResponse) -> Result<ZeraPayload> {
match response.payload {
Some(payload_response::Payload::ZeraPayload(payload)) => Ok(payload),
Some(payload_response::Payload::SolanaPayload(_)) => Err(ZeraError::Validation(
"Expected ZERA payload, got: solanaPayload".to_string(),
)),
None => Err(ZeraError::Validation(
"Expected ZERA payload, got: none".to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use prost::Message;
use zera_proto::zera_guardian::{
payload_response, solana_payload, zera_payload, PayloadResponse, SolanaMintPayload,
SolanaPayload, ZeraMintPayload, ZeraPayload,
};
use super::*;
use crate::grpc::TransportResponse;
#[derive(Clone, Default)]
struct QueueTransport {
responses: Arc<Mutex<VecDeque<TransportResponse>>>,
}
impl QueueTransport {
fn push<M: Message>(&self, message: &M) {
self.responses
.lock()
.expect("responses")
.push_back(grpc_ok(message));
}
}
#[async_trait]
impl UnaryTransport for QueueTransport {
async fn unary_bytes(
&self,
_path: &str,
_framed_request: Vec<u8>,
) -> Result<TransportResponse> {
Ok(self
.responses
.lock()
.expect("responses")
.pop_front()
.expect("queued response"))
}
}
#[derive(Clone)]
struct FlakyTransport {
attempts: Arc<AtomicUsize>,
success_response: TransportResponse,
}
#[async_trait]
impl UnaryTransport for FlakyTransport {
async fn unary_bytes(
&self,
_path: &str,
_framed_request: Vec<u8>,
) -> Result<TransportResponse> {
if self.attempts.fetch_add(1, Ordering::SeqCst) == 0 {
Err(ZeraError::Rpc("guardian miss".to_string()))
} else {
Ok(self.success_response.clone())
}
}
}
fn grpc_ok<M: Message>(message: &M) -> 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 zera_payload_with_duplicates() -> PayloadResponse {
PayloadResponse {
payload: Some(payload_response::Payload::ZeraPayload(ZeraPayload {
payload: Some(zera_payload::Payload::MintPayload(ZeraMintPayload {
solana_mint_address: "So11111111111111111111111111111111111111112".to_string(),
amount: "123".to_string(),
zera_wallet_address: "zera-wallet".to_string(),
tx_signature: "sig".to_string(),
usd_price: "7".to_string(),
})),
signed_hash: "signed-hash".to_string(),
signatures: vec!["dup".to_string(), "dup".to_string(), "unique".to_string()],
public_keys: vec!["pk-1".to_string(), "pk-2".to_string(), "pk-3".to_string()],
})),
}
}
fn solana_payload_response() -> PayloadResponse {
PayloadResponse {
payload: Some(payload_response::Payload::SolanaPayload(SolanaPayload {
payload: Some(solana_payload::Payload::MintPayload(SolanaMintPayload {
zera_contract_id: "$ZRA+0000".to_string(),
solana_wallet_address: "wallet".to_string(),
amount: 55,
txn_hash: "txn-hash".to_string(),
usd_amount: 44,
liquidity_usd: 33,
tier: 2,
})),
signed_hash: "signed-hash".to_string(),
signatures: vec!["sig-1".to_string()],
public_keys: vec!["pk-1".to_string()],
timestamp: None,
})),
}
}
#[tokio::test]
async fn fetch_zera_vaa_deduplicates_signatures_and_preserves_first_seen_order() {
let transport = QueueTransport::default();
transport.push(&zera_payload_with_duplicates());
let client = GuardianClient::with_transport(transport);
let payload = fetch_zera_vaa_with_client("solana-sig", &client, VaaRetryOptions::default())
.await
.expect("fetch zera vaa");
assert_eq!(payload.signatures, vec!["dup", "unique"]);
assert_eq!(payload.public_keys, vec!["pk-1", "pk-3"]);
}
#[tokio::test]
async fn fetch_zera_vaa_rejects_mismatched_signature_and_public_key_counts() {
let transport = QueueTransport::default();
transport.push(&PayloadResponse {
payload: Some(payload_response::Payload::ZeraPayload(ZeraPayload {
payload: Some(zera_payload::Payload::MintPayload(ZeraMintPayload {
solana_mint_address: "mint".to_string(),
amount: "1".to_string(),
zera_wallet_address: "zera-wallet".to_string(),
tx_signature: "sig".to_string(),
usd_price: "2".to_string(),
})),
signed_hash: "signed-hash".to_string(),
signatures: vec!["sig-1".to_string(), "sig-2".to_string()],
public_keys: vec!["pk-1".to_string()],
})),
});
let client = GuardianClient::with_transport(transport);
let error = fetch_zera_vaa_with_client("solana-sig", &client, VaaRetryOptions::default())
.await
.unwrap_err();
assert!(error
.to_string()
.contains("signatures count (2) does not match public keys count (1)"));
}
#[tokio::test]
async fn fetch_zera_vaa_rejects_empty_signature_sets() {
let transport = QueueTransport::default();
transport.push(&PayloadResponse {
payload: Some(payload_response::Payload::ZeraPayload(ZeraPayload {
payload: Some(zera_payload::Payload::MintPayload(ZeraMintPayload {
solana_mint_address: "mint".to_string(),
amount: "1".to_string(),
zera_wallet_address: "zera-wallet".to_string(),
tx_signature: "sig".to_string(),
usd_price: "2".to_string(),
})),
signed_hash: "signed-hash".to_string(),
signatures: vec![],
public_keys: vec![],
})),
});
let client = GuardianClient::with_transport(transport);
let error = fetch_zera_vaa_with_client("solana-sig", &client, VaaRetryOptions::default())
.await
.unwrap_err();
assert!(error.to_string().contains("no signatures present"));
}
#[tokio::test]
async fn fetch_solana_vaa_retries_and_returns_successful_attempt() {
let attempts = Arc::new(AtomicUsize::new(0));
let transport = FlakyTransport {
attempts: attempts.clone(),
success_response: grpc_ok(&solana_payload_response()),
};
let client = GuardianClient::with_transport(transport);
let payload = fetch_solana_vaa_with_client(
"zera-hash",
&client,
VaaRetryOptions {
retry: true,
max_elapsed_ms: 50,
initial_delay_ms: 1,
},
)
.await
.expect("fetch solana vaa");
assert_eq!(payload.signatures, vec!["sig-1"]);
assert_eq!(attempts.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn fetch_zera_vaa_reports_payload_type_mismatch_using_js_case_names() {
let transport = QueueTransport::default();
transport.push(&solana_payload_response());
let client = GuardianClient::with_transport(transport);
let error = fetch_zera_vaa_with_client("solana-sig", &client, VaaRetryOptions::default())
.await
.unwrap_err();
assert!(error
.to_string()
.contains("Expected ZERA payload, got: solanaPayload"));
}
}