use std::fmt::Debug;
use std::future::Future;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use r402_core::chain::{ChainId, ChainProvider};
use stellar_rpc_client::{GetTransactionResponse, SimulateTransactionResponse};
use stellar_xdr::{
FeeBumpTransaction, FeeBumpTransactionExt, FeeBumpTransactionInnerTx, Hash, SequenceNumber,
TransactionEnvelope, TransactionExt, TransactionV1Envelope,
};
use super::rpc::{StellarJsonRpc, StellarRpc, StellarRpcError};
use super::signer::{StellarSigner, StellarSignerError};
use super::types::StellarChainReference;
use super::xdr::{
StellarXdrError, encode_transaction_envelope, inner_transaction, muxed_account_from_str,
};
use crate::{BASE_FEE_STROOPS, DEFAULT_MAX_TRANSACTION_FEE_STROOPS};
#[derive(Debug, thiserror::Error)]
pub enum StellarFacilitatorError {
#[error(transparent)]
Signer(#[from] StellarSignerError),
#[error(transparent)]
Rpc(#[from] StellarRpcError),
#[error(transparent)]
Xdr(#[from] StellarXdrError),
#[error("no stellar facilitator signer configured")]
NoSigner,
#[error("stellar transaction fee does not fit in u32")]
FeeOverflow,
}
#[derive(Debug)]
pub struct StellarChainProvider {
chain: StellarChainReference,
signers: Vec<StellarSigner>,
fee_bump: Option<StellarSigner>,
rpc: StellarJsonRpc,
max_transaction_fee_stroops: u32,
next_signer: AtomicUsize,
}
impl Clone for StellarChainProvider {
fn clone(&self) -> Self {
Self {
chain: self.chain,
signers: self.signers.clone(),
fee_bump: self.fee_bump.clone(),
rpc: self.rpc.clone(),
max_transaction_fee_stroops: self.max_transaction_fee_stroops,
next_signer: AtomicUsize::new(self.next_signer.load(Ordering::Relaxed)),
}
}
}
impl StellarChainProvider {
pub fn new(
chain: StellarChainReference,
signers: Vec<StellarSigner>,
rpc_url: Option<&str>,
) -> Result<Self, StellarFacilitatorError> {
if signers.is_empty() {
return Err(StellarFacilitatorError::NoSigner);
}
Ok(Self {
chain,
signers,
fee_bump: None,
rpc: StellarJsonRpc::connect(chain, rpc_url)?,
max_transaction_fee_stroops: DEFAULT_MAX_TRANSACTION_FEE_STROOPS,
next_signer: AtomicUsize::new(0),
})
}
#[must_use]
pub fn with_horizon_url(mut self, url: impl Into<String>) -> Self {
self.rpc = self.rpc.with_horizon_url(url);
self
}
#[must_use]
pub const fn with_max_transaction_fee_stroops(mut self, fee: u32) -> Self {
self.max_transaction_fee_stroops = fee;
self
}
#[must_use]
pub fn with_fee_bump_signer(mut self, signer: StellarSigner) -> Self {
self.fee_bump = Some(signer);
self
}
#[must_use]
pub const fn chain_reference(&self) -> StellarChainReference {
self.chain
}
#[must_use]
pub const fn max_transaction_fee_stroops(&self) -> u32 {
self.max_transaction_fee_stroops
}
#[must_use]
pub const fn rpc(&self) -> &StellarJsonRpc {
&self.rpc
}
#[must_use]
pub fn facilitator_addresses(&self) -> Vec<String> {
let mut addrs: Vec<String> = self
.signers
.iter()
.map(|s| s.address().to_owned())
.collect();
if let Some(bump) = &self.fee_bump
&& !addrs.iter().any(|a| a == bump.address())
{
addrs.push(bump.address().to_owned());
}
addrs
}
#[must_use]
pub fn select_signer(&self) -> Option<&StellarSigner> {
if self.signers.is_empty() {
return None;
}
let index = self.next_signer.fetch_add(1, Ordering::Relaxed) % self.signers.len();
self.signers.get(index)
}
pub async fn settle_envelope(
&self,
client_envelope: &TransactionEnvelope,
sim: &SimulateTransactionResponse,
max_timeout_seconds: u64,
now_unix: u64,
) -> Result<GetTransactionResponse, StellarFacilitatorError> {
let signer = self
.select_signer()
.ok_or(StellarFacilitatorError::NoSigner)?;
let account = self.rpc.get_account(signer.address()).await?;
let seq = SequenceNumber(account.seq_num.0.saturating_add(1));
let soroban = sim
.transaction_data()
.map_err(|e| StellarFacilitatorError::Rpc(StellarRpcError::Request(e.to_string())))?;
let resource_fee = u64::try_from(soroban.resource_fee.max(0)).unwrap_or(0);
let fee = u32::try_from(u64::from(BASE_FEE_STROOPS).saturating_add(resource_fee))
.map_err(|_| StellarFacilitatorError::FeeOverflow)?;
let client_tx = inner_transaction(client_envelope)?;
let rebuilt = TransactionEnvelope::Tx(TransactionV1Envelope {
tx: stellar_xdr::Transaction {
source_account: muxed_account_from_str(signer.address())?,
fee,
seq_num: seq,
cond: timeout_preconditions(max_timeout_seconds, now_unix),
memo: client_tx.memo.clone(),
operations: client_tx.operations.clone(),
ext: TransactionExt::V1(soroban),
},
signatures: Vec::new()
.try_into()
.map_err(|e: stellar_xdr::Error| StellarXdrError::Xdr(e.to_string()))?,
});
let mut rebuilt = rebuilt;
signer.sign_envelope(&mut rebuilt, self.chain.passphrase())?;
let to_submit = if let Some(bump) = &self.fee_bump {
wrap_fee_bump(&rebuilt, bump, self.chain.passphrase())?
} else {
rebuilt
};
let hash = self.rpc.send_transaction(&to_submit).await?;
let timeout = Duration::from_secs(max_timeout_seconds.max(1));
Ok(self.rpc.poll_transaction(&hash, timeout).await?)
}
}
const fn timeout_preconditions(
max_timeout_seconds: u64,
now_unix: u64,
) -> stellar_xdr::Preconditions {
stellar_xdr::Preconditions::Time(stellar_xdr::TimeBounds {
min_time: stellar_xdr::TimePoint(0),
max_time: stellar_xdr::TimePoint(now_unix.saturating_add(max_timeout_seconds)),
})
}
fn wrap_fee_bump(
inner: &TransactionEnvelope,
bump: &StellarSigner,
passphrase: &str,
) -> Result<TransactionEnvelope, StellarFacilitatorError> {
let TransactionEnvelope::Tx(inner_v1) = inner else {
return Err(StellarFacilitatorError::Xdr(StellarXdrError::Shape(
"fee-bump inner transaction must be a v1 envelope".to_owned(),
)));
};
let inner_fee = i64::from(inner_v1.tx.fee);
let total_fee = inner_fee.saturating_add(i64::from(BASE_FEE_STROOPS));
let mut envelope = TransactionEnvelope::from(FeeBumpTransaction {
fee_source: muxed_account_from_str(bump.address())?,
fee: total_fee,
inner_tx: FeeBumpTransactionInnerTx::Tx(inner_v1.clone()),
ext: FeeBumpTransactionExt::V0,
});
bump.sign_envelope(&mut envelope, passphrase)?;
Ok(envelope)
}
impl ChainProvider for StellarChainProvider {
fn signer_addresses(&self) -> Vec<String> {
self.facilitator_addresses()
}
fn chain_id(&self) -> ChainId {
self.chain.into()
}
}
impl StellarRpc for StellarChainProvider {
fn latest_ledger(&self) -> impl Future<Output = Result<u32, StellarRpcError>> + Send {
self.rpc.latest_ledger()
}
fn get_account(
&self,
address: &str,
) -> impl Future<Output = Result<stellar_xdr::AccountEntry, StellarRpcError>> + Send {
self.rpc.get_account(address)
}
fn simulate_transaction(
&self,
tx: &TransactionEnvelope,
) -> impl Future<Output = Result<SimulateTransactionResponse, StellarRpcError>> + Send {
self.rpc.simulate_transaction(tx)
}
fn send_transaction(
&self,
tx: &TransactionEnvelope,
) -> impl Future<Output = Result<Hash, StellarRpcError>> + Send {
self.rpc.send_transaction(tx)
}
fn get_transaction(
&self,
hash: &Hash,
) -> impl Future<Output = Result<GetTransactionResponse, StellarRpcError>> + Send {
self.rpc.get_transaction(hash)
}
fn poll_transaction(
&self,
hash: &Hash,
timeout: Duration,
) -> impl Future<Output = Result<GetTransactionResponse, StellarRpcError>> + Send {
self.rpc.poll_transaction(hash, timeout)
}
fn estimated_ledger_seconds(&self) -> impl Future<Output = u64> + Send {
self.rpc.estimated_ledger_seconds()
}
}
pub fn envelope_base64(envelope: &TransactionEnvelope) -> Result<String, StellarXdrError> {
encode_transaction_envelope(envelope)
}