use std::{
fmt::Debug,
path::{Path, PathBuf},
str::FromStr,
};
use clap::{command, Parser};
use soroban_env_host::xdr::{
BumpFootprintExpirationOp, ContractCodeEntry, ContractDataEntry, ContractEntryBodyType,
Error as XdrError, ExtensionPoint, Hash, LedgerEntry, LedgerEntryChange, LedgerEntryData,
LedgerFootprint, LedgerKey, LedgerKeyContractData, Memo, MuxedAccount, Operation,
OperationBody, Preconditions, ReadXdr, ScAddress, ScSpecTypeDef, ScVal, SequenceNumber,
SorobanResources, SorobanTransactionData, Transaction, TransactionExt, TransactionMeta,
TransactionMetaV3, Uint256,
};
use stellar_strkey::DecodeError;
use crate::{
commands::config,
commands::contract::Durability,
rpc::{self, Client},
utils, wasm, Pwd,
};
#[derive(Parser, Debug, Clone)]
#[group(skip)]
pub struct Cmd {
#[arg(long = "id", required_unless_present = "wasm")]
contract_id: Option<String>,
#[arg(long = "key", conflicts_with = "key_xdr")]
key: Option<String>,
#[arg(long = "key-xdr", conflicts_with = "key")]
key_xdr: Option<String>,
#[arg(
long,
conflicts_with = "contract_id",
conflicts_with = "key",
conflicts_with = "key_xdr"
)]
wasm: Option<PathBuf>,
#[arg(long, value_enum, required = true)]
durability: Durability,
#[arg(long, required = true)]
ledgers_to_expire: u32,
#[command(flatten)]
config: config::Args,
#[command(flatten)]
pub fee: crate::fee::Args,
}
impl FromStr for Cmd {
type Err = clap::error::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use clap::{CommandFactory, FromArgMatches};
Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
}
}
impl Pwd for Cmd {
fn set_pwd(&mut self, pwd: &Path) {
self.config.set_pwd(pwd);
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("parsing key {key}: {error}")]
CannotParseKey {
key: String,
error: soroban_spec_tools::Error,
},
#[error("parsing XDR key {key}: {error}")]
CannotParseXdrKey { key: String, error: XdrError },
#[error("cannot parse contract ID {0}: {1}")]
CannotParseContractId(String, DecodeError),
#[error(transparent)]
Config(#[from] config::Error),
#[error("either `--key` or `--key-xdr` are required")]
KeyIsRequired,
#[error("xdr processing error: {0}")]
Xdr(#[from] XdrError),
#[error("Ledger entry not found")]
LedgerEntryNotFound,
#[error("missing operation result")]
MissingOperationResult,
#[error(transparent)]
Rpc(#[from] rpc::Error),
#[error(transparent)]
Wasm(#[from] wasm::Error),
}
impl Cmd {
#[allow(clippy::too_many_lines)]
pub async fn run(&self) -> Result<(), Error> {
let expiration_ledger_seq = if self.config.is_no_network() {
self.run_in_sandbox()?
} else {
self.run_against_rpc_server().await?
};
println!("New expiration ledger: {expiration_ledger_seq}");
Ok(())
}
async fn run_against_rpc_server(&self) -> Result<u32, Error> {
let network = self.config.get_network()?;
tracing::trace!(?network);
let needle = self.parse_key()?;
let network = &self.config.get_network()?;
let client = Client::new(&network.rpc_url)?;
let key = self.config.key_pair()?;
let public_strkey = stellar_strkey::ed25519::PublicKey(key.public.to_bytes()).to_string();
let account_details = client.get_account(&public_strkey).await?;
let sequence: i64 = account_details.seq_num.into();
let tx = Transaction {
source_account: MuxedAccount::Ed25519(Uint256(key.public.to_bytes())),
fee: self.fee.fee,
seq_num: SequenceNumber(sequence + 1),
cond: Preconditions::None,
memo: Memo::None,
operations: vec![Operation {
source_account: None,
body: OperationBody::BumpFootprintExpiration(BumpFootprintExpirationOp {
ext: ExtensionPoint::V0,
ledgers_to_expire: self.ledgers_to_expire,
}),
}]
.try_into()?,
ext: TransactionExt::V1(SorobanTransactionData {
ext: ExtensionPoint::V0,
resources: SorobanResources {
footprint: LedgerFootprint {
read_only: vec![needle].try_into()?,
read_write: vec![].try_into()?,
},
instructions: 0,
read_bytes: 0,
write_bytes: 0,
extended_meta_data_size_bytes: 0,
},
refundable_fee: 0,
}),
};
let (result, meta, events) = client
.prepare_and_send_transaction(&tx, &key, &network.network_passphrase, None)
.await?;
tracing::debug!(?result);
tracing::debug!(?meta);
if !events.is_empty() {
tracing::debug!(?events);
}
let TransactionMeta::V3(TransactionMetaV3 { operations, .. }) = meta else {
return Err(Error::LedgerEntryNotFound);
};
if operations.len() == 0 {
return Err(Error::LedgerEntryNotFound);
}
if operations[0].changes.len() != 2 {
return Err(Error::LedgerEntryNotFound);
}
match (&operations[0].changes[0], &operations[0].changes[1]) {
(
LedgerEntryChange::State(_),
LedgerEntryChange::Updated(LedgerEntry {
data:
LedgerEntryData::ContractData(ContractDataEntry {
expiration_ledger_seq,
..
})
| LedgerEntryData::ContractCode(ContractCodeEntry {
expiration_ledger_seq,
..
}),
..
}),
) => Ok(*expiration_ledger_seq),
_ => Err(Error::LedgerEntryNotFound),
}
}
fn run_in_sandbox(&self) -> Result<u32, Error> {
let needle = self.parse_key()?;
let mut state = self.config.get_state()?;
let mut expiration_ledger_seq = None;
state.ledger_entries = state
.ledger_entries
.iter()
.map(|(k, v)| {
let new_k = k.as_ref().clone();
let new_v = v.as_ref().clone();
(
Box::new(new_k.clone()),
Box::new(if needle == new_k {
let (new_v, new_expiration) = bump_entry(&new_v, self.ledgers_to_expire);
expiration_ledger_seq = Some(new_expiration);
new_v
} else {
new_v
}),
)
})
.collect::<Vec<_>>();
self.config.set_state(&mut state)?;
let Some(new_expiration_ledger_seq) = expiration_ledger_seq else {
return Err(Error::LedgerEntryNotFound);
};
Ok(new_expiration_ledger_seq)
}
fn contract_id(&self) -> Result<[u8; 32], Error> {
utils::contract_id_from_str(self.contract_id.as_ref().unwrap())
.map_err(|e| Error::CannotParseContractId(self.contract_id.clone().unwrap(), e))
}
fn parse_key(&self) -> Result<LedgerKey, Error> {
let key = if let Some(key) = &self.key {
soroban_spec_tools::from_string_primitive(key, &ScSpecTypeDef::Symbol).map_err(|e| {
Error::CannotParseKey {
key: key.clone(),
error: e,
}
})?
} else if let Some(key) = &self.key_xdr {
ScVal::from_xdr_base64(key).map_err(|e| Error::CannotParseXdrKey {
key: key.clone(),
error: e,
})?
} else if let Some(wasm) = &self.wasm {
return Ok(crate::wasm::Args { wasm: wasm.clone() }.try_into()?);
} else {
ScVal::LedgerKeyContractInstance
};
let contract_id = self.contract_id()?;
Ok(LedgerKey::ContractData(LedgerKeyContractData {
contract: ScAddress::Contract(Hash(contract_id)),
durability: self.durability.into(),
body_type: ContractEntryBodyType::DataEntry,
key,
}))
}
}
fn bump_entry(v: &LedgerEntry, ledgers_to_expire: u32) -> (LedgerEntry, u32) {
let mut new_v = v.clone();
let mut new_expiration_ledger_seq = 0;
if let LedgerEntryData::ContractData(ref mut data) = new_v.data {
data.expiration_ledger_seq += ledgers_to_expire;
new_expiration_ledger_seq = data.expiration_ledger_seq;
} else if let LedgerEntryData::ContractCode(ref mut code) = new_v.data {
code.expiration_ledger_seq += ledgers_to_expire;
new_expiration_ledger_seq = code.expiration_ledger_seq;
}
(new_v, new_expiration_ledger_seq)
}