use std::sync::LazyLock;
#[cfg(any(feature = "client", feature = "facilitator"))]
use r402::proto::Base64Bytes;
pub use r402::scheme::ExactScheme;
use serde::{Deserialize, Serialize};
#[cfg(feature = "facilitator")]
use solana_commitment_config::CommitmentConfig;
#[cfg(any(feature = "client", feature = "facilitator"))]
use solana_message::compiled_instruction::CompiledInstruction;
use solana_pubkey::{Pubkey, pubkey};
#[cfg(any(feature = "client", feature = "facilitator"))]
use solana_signature::Signature;
#[cfg(any(feature = "client", feature = "facilitator"))]
use solana_signer::Signer;
#[cfg(any(feature = "client", feature = "facilitator"))]
use solana_transaction::versioned::VersionedTransaction;
use crate::chain::Address;
#[cfg(feature = "facilitator")]
use crate::chain::{SolanaChainProviderError, SolanaChainProviderLike};
#[cfg(any(feature = "client", feature = "facilitator"))]
use crate::exact::{SolanaExactError, TransactionSignError, TransactionToB64Error};
#[allow(clippy::expect_used, reason = "hardcoded constant is infallible")]
pub static PHANTOM_LIGHTHOUSE_PROGRAM: LazyLock<Pubkey> = LazyLock::new(|| {
"L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95"
.parse()
.expect("Invalid Lighthouse program ID")
});
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExactSolanaPayload {
pub transaction: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SupportedPaymentKindExtra {
pub fee_payer: Address,
}
pub const ATA_PROGRAM_PUBKEY: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
#[derive(Debug)]
#[cfg(any(feature = "client", feature = "facilitator"))]
pub struct InstructionInt {
index: usize,
instruction: CompiledInstruction,
account_keys: Vec<Pubkey>,
}
#[derive(Debug)]
#[cfg(any(feature = "client", feature = "facilitator"))]
pub struct TransactionInt {
inner: VersionedTransaction,
}
#[cfg(any(feature = "client", feature = "facilitator"))]
impl TransactionInt {
#[must_use]
pub const fn new(transaction: VersionedTransaction) -> Self {
Self { inner: transaction }
}
#[must_use]
pub const fn inner(&self) -> &VersionedTransaction {
&self.inner
}
pub fn instruction(&self, index: usize) -> Result<InstructionInt, SolanaExactError> {
let instruction = self
.inner
.message
.instructions()
.get(index)
.cloned()
.ok_or(SolanaExactError::NoInstructionAtIndex(index))?;
let account_keys = self.inner.message.static_account_keys().to_vec();
Ok(InstructionInt {
index,
instruction,
account_keys,
})
}
#[must_use]
pub fn is_fully_signed(&self) -> bool {
let num_required = self.inner.message.header().num_required_signatures;
if self.inner.signatures.len() < num_required as usize {
return false;
}
let default = Signature::default();
for signature in &self.inner.signatures {
if default.eq(signature) {
return false;
}
}
true
}
#[cfg(feature = "facilitator")]
pub fn sign<P: SolanaChainProviderLike>(
self,
provider: &P,
) -> Result<Self, SolanaChainProviderError> {
let tx = provider.sign(self.inner)?;
Ok(Self { inner: tx })
}
pub fn sign_with_keypair<S: Signer>(self, signer: &S) -> Result<Self, TransactionSignError> {
let mut tx = self.inner;
let msg_bytes = tx.message.serialize();
let signature = signer
.try_sign_message(msg_bytes.as_slice())
.map_err(|e| TransactionSignError(format!("{e}")))?;
let num_required = tx.message.header().num_required_signatures as usize;
let static_keys = tx.message.static_account_keys();
#[allow(
clippy::indexing_slicing,
reason = "num_required <= static_keys.len() by Solana message invariant"
)]
let pos = static_keys[..num_required]
.iter()
.position(|k| *k == signer.pubkey())
.ok_or_else(|| {
TransactionSignError("Signer not found in required signers".to_owned())
})?;
if tx.signatures.len() < num_required {
tx.signatures.resize(num_required, Signature::default());
}
#[allow(
clippy::indexing_slicing,
reason = "pos < num_required, resize ensures len >= num_required"
)]
{
tx.signatures[pos] = signature;
}
Ok(Self { inner: tx })
}
#[cfg(feature = "facilitator")]
#[allow(
clippy::needless_pass_by_value,
reason = "CommitmentConfig is a small Copy type"
)]
pub async fn send_and_confirm<P: SolanaChainProviderLike>(
&self,
provider: &P,
commitment_config: CommitmentConfig,
) -> Result<Signature, SolanaChainProviderError> {
provider
.send_and_confirm(&self.inner, commitment_config)
.await
}
pub fn as_base64(&self) -> Result<String, TransactionToB64Error> {
let bytes =
bincode::serialize(&self.inner).map_err(|e| TransactionToB64Error(format!("{e}")))?;
let base64_bytes = Base64Bytes::encode(bytes);
let string =
String::from_utf8(base64_bytes.0).map_err(|e| TransactionToB64Error(format!("{e}")))?;
Ok(string)
}
}
#[cfg(any(feature = "client", feature = "facilitator"))]
impl InstructionInt {
#[must_use]
pub const fn has_data(&self) -> bool {
!self.instruction.data.is_empty()
}
#[must_use]
pub const fn has_accounts(&self) -> bool {
!self.instruction.accounts.is_empty()
}
#[must_use]
pub const fn data_slice(&self) -> &[u8] {
self.instruction.data.as_slice()
}
pub const fn assert_not_empty(&self) -> Result<(), SolanaExactError> {
if !self.has_data() || !self.has_accounts() {
return Err(SolanaExactError::EmptyInstructionAtIndex(self.index));
}
Ok(())
}
#[must_use]
pub fn program_id(&self) -> Pubkey {
*self.instruction.program_id(self.account_keys.as_slice())
}
pub fn account(&self, index: u8) -> Result<Pubkey, SolanaExactError> {
let account_index = self
.instruction
.accounts
.get(index as usize)
.copied()
.ok_or(SolanaExactError::NoAccountAtIndex(index))?;
let pubkey = self
.account_keys
.get(account_index as usize)
.copied()
.ok_or(SolanaExactError::NoAccountAtIndex(index))?;
Ok(pubkey)
}
}
pub mod v2 {
use r402::proto::U64String;
use r402::proto::v2 as proto_v2;
use super::{ExactScheme, ExactSolanaPayload, SupportedPaymentKindExtra};
use crate::chain::Address;
pub type VerifyRequest = proto_v2::VerifyRequest<PaymentPayload, PaymentRequirements>;
pub type SettleRequest = VerifyRequest;
pub type PaymentPayload = proto_v2::PaymentPayload<PaymentRequirements, ExactSolanaPayload>;
pub type PaymentRequirements =
proto_v2::PaymentRequirements<ExactScheme, U64String, Address, SupportedPaymentKindExtra>;
}