use eyre::{Result, eyre};
use solana_sdk::{
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer,
transaction::Transaction,
};
use std::str::FromStr;
use crate::commands::config::config_pb::Chain;
use crate::solana::derive_user_balance_pda;
pub fn resolve_program_and_instance(chain: &Chain) -> Result<(Pubkey, Pubkey)> {
if chain.factory_address.is_empty() {
return Err(eyre!(
"chain {} has no factory_address (the Solana program id)",
chain.network
));
}
let program_id = Pubkey::from_str(&chain.factory_address).map_err(|e| {
eyre!(
"invalid Solana program id '{}': {}",
chain.factory_address,
e
)
})?;
let instance_str = chain
.trade_contract
.as_ref()
.map(|tc| tc.address.clone())
.filter(|s| !s.is_empty())
.ok_or_else(|| {
eyre!(
"Solana chain '{}' has no trade_contract.address (instance PDA) configured",
chain.network
)
})?;
let instance = Pubkey::from_str(&instance_str)
.map_err(|e| eyre!("invalid Solana instance address '{}': {}", instance_str, e))?;
Ok((program_id, instance))
}
pub async fn submit_user_signed(
rpc_url: &str,
user_keypair: &Keypair,
ix: Instruction,
) -> Result<String> {
submit_user_signed_multi(rpc_url, user_keypair, &[ix]).await
}
pub async fn submit_user_signed_multi(
rpc_url: &str,
user_keypair: &Keypair,
ixs: &[Instruction],
) -> Result<String> {
use solana_client::nonblocking::rpc_client::RpcClient;
let client = RpcClient::new(rpc_url.to_string());
let blockhash = client
.get_latest_blockhash()
.await
.map_err(|e| eyre!("get_latest_blockhash: {}", e))?;
let tx = Transaction::new_signed_with_payer(
ixs,
Some(&user_keypair.pubkey()),
&[user_keypair],
blockhash,
);
let sig = client
.send_and_confirm_transaction(&tx)
.await
.map_err(|e| {
let message = e.to_string();
match crate::solana::explain_midrib_error(&message) {
Some(hint) => eyre!("send_and_confirm_transaction: {message}\n -> {hint}"),
None => eyre!("send_and_confirm_transaction: {message}"),
}
})?;
Ok(sig.to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InstanceAuthorities {
pub signer: Pubkey,
pub operator_admin: Pubkey,
}
pub async fn fetch_instance_authorities(
rpc_url: &str,
instance: &Pubkey,
) -> Result<InstanceAuthorities> {
use solana_client::nonblocking::rpc_client::RpcClient;
let client = RpcClient::new(rpc_url.to_string());
let acc = client
.get_account(instance)
.await
.map_err(|e| eyre!("get_account (TradingInstance {}): {}", instance, e))?;
const SIGNER_OFFSET: usize = 8 + 32;
const OPERATOR_ADMIN_OFFSET: usize = 8 + 32 + 32 + 32 + 2 + 32 + 2;
if acc.data.len() < OPERATOR_ADMIN_OFFSET + 32 {
return Err(eyre!(
"TradingInstance account {} too small: {} bytes",
instance,
acc.data.len()
));
}
let read = |offset: usize| -> Result<Pubkey> {
let bytes: [u8; 32] = acc.data[offset..offset + 32]
.try_into()
.map_err(|_| eyre!("TradingInstance account data layout error at {}", offset))?;
Ok(Pubkey::new_from_array(bytes))
};
Ok(InstanceAuthorities {
signer: read(SIGNER_OFFSET)?,
operator_admin: read(OPERATOR_ADMIN_OFFSET)?,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WithdrawEpochState {
pub epoch: u64,
pub withdrawn: u64,
pub cap: u64,
}
pub async fn fetch_withdraw_epoch(
rpc_url: &str,
instance: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> Result<Option<WithdrawEpochState>> {
use solana_client::nonblocking::rpc_client::RpcClient;
let client = RpcClient::new(rpc_url.to_string());
let (pda, _) = crate::solana::derive_withdraw_epoch_pda(instance, mint, program_id);
let response = client
.get_account_with_commitment(&pda, client.commitment())
.await
.map_err(|e| eyre!("get_account (WithdrawEpoch PDA): {}", e))?;
let Some(acc) = response.value else {
return Ok(None);
};
const EPOCH_OFFSET: usize = 8 + 32 + 32;
const WITHDRAWN_OFFSET: usize = EPOCH_OFFSET + 8;
const CAP_OFFSET: usize = WITHDRAWN_OFFSET + 8;
if acc.data.len() < CAP_OFFSET + 8 {
return Err(eyre!(
"WithdrawEpoch account too small: {} bytes",
acc.data.len()
));
}
let read_u64 = |offset: usize| -> Result<u64> {
let bytes: [u8; 8] = acc.data[offset..offset + 8]
.try_into()
.map_err(|_| eyre!("WithdrawEpoch account data layout error at {}", offset))?;
Ok(u64::from_le_bytes(bytes))
};
Ok(Some(WithdrawEpochState {
epoch: read_u64(EPOCH_OFFSET)?,
withdrawn: read_u64(WITHDRAWN_OFFSET)?,
cap: read_u64(CAP_OFFSET)?,
}))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SettleEpochState {
pub epoch: u64,
pub settled: u128,
pub cap: u64,
}
pub async fn fetch_settle_epoch(
rpc_url: &str,
instance: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> Result<Option<SettleEpochState>> {
use solana_client::nonblocking::rpc_client::RpcClient;
let client = RpcClient::new(rpc_url.to_string());
let (pda, _) = crate::solana::derive_settle_epoch_pda(instance, mint, program_id);
let response = client
.get_account_with_commitment(&pda, client.commitment())
.await
.map_err(|e| eyre!("get_account (SettleEpoch PDA): {}", e))?;
let Some(acc) = response.value else {
return Ok(None);
};
decode_settle_epoch(&acc.data).map(Some)
}
fn decode_settle_epoch(data: &[u8]) -> Result<SettleEpochState> {
const EPOCH_OFFSET: usize = 8 + 32 + 32;
const SETTLED_OFFSET: usize = EPOCH_OFFSET + 8;
const CAP_OFFSET: usize = SETTLED_OFFSET + 16;
if data.len() < CAP_OFFSET + 8 {
return Err(eyre!("SettleEpoch account too small: {} bytes", data.len()));
}
let bytes = |offset: usize, len: usize| &data[offset..offset + len];
Ok(SettleEpochState {
epoch: u64::from_le_bytes(bytes(EPOCH_OFFSET, 8).try_into()?),
settled: u128::from_le_bytes(bytes(SETTLED_OFFSET, 16).try_into()?),
cap: u64::from_le_bytes(bytes(CAP_OFFSET, 8).try_into()?),
})
}
pub async fn fetch_user_balance(
rpc_url: &str,
instance: &Pubkey,
user: &Pubkey,
mint: &Pubkey,
program_id: &Pubkey,
) -> Result<(u64, u64)> {
use solana_client::nonblocking::rpc_client::RpcClient;
let client = RpcClient::new(rpc_url.to_string());
let (pda, _) = derive_user_balance_pda(instance, user, mint, program_id);
let response = client
.get_account_with_commitment(&pda, client.commitment())
.await
.map_err(|e| eyre!("get_account (UserBalance PDA): {}", e))?;
let Some(acc) = response.value else {
return Ok((0, 0));
};
const DEPOSITED_OFFSET: usize = 8 + 32 + 32 + 32;
const LOCKED_OFFSET: usize = DEPOSITED_OFFSET + 8;
if acc.data.len() < LOCKED_OFFSET + 8 {
return Err(eyre!(
"UserBalance account too small: {} bytes",
acc.data.len()
));
}
let deposited_bytes: [u8; 8] = acc.data[DEPOSITED_OFFSET..DEPOSITED_OFFSET + 8]
.try_into()
.map_err(|_| eyre!("UserBalance account data layout error (deposited)"))?;
let locked_bytes: [u8; 8] = acc.data[LOCKED_OFFSET..LOCKED_OFFSET + 8]
.try_into()
.map_err(|_| eyre!("UserBalance account data layout error (locked)"))?;
Ok((
u64::from_le_bytes(deposited_bytes),
u64::from_le_bytes(locked_bytes),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_settle_epoch_reads_each_field_at_its_offset() {
let mut data = vec![0xAAu8; 8 + 32 + 32];
data.extend_from_slice(&7u64.to_le_bytes());
data.extend_from_slice(&(u64::MAX as u128 + 5).to_le_bytes());
data.extend_from_slice(&1_234u64.to_le_bytes());
data.push(254);
assert_eq!(data.len(), 105);
assert_eq!(
decode_settle_epoch(&data).unwrap(),
SettleEpochState {
epoch: 7,
settled: u64::MAX as u128 + 5,
cap: 1_234,
}
);
assert!(decode_settle_epoch(&data[..100]).is_err());
}
}