soroban_cli/commands/contract/
read.rs1use std::{
2 fmt::Debug,
3 io::{self, stdout},
4};
5
6use crate::xdr::{
7 ContractDataEntry, Error as XdrError, LedgerEntryData, LedgerKey, LedgerKeyContractData,
8 Limits, ScVal, WriteXdr,
9};
10use clap::{Parser, ValueEnum};
11
12use crate::utils::XDR_DEPTH_LIMIT;
13use crate::{
14 config::{self, locator},
15 key,
16 rpc::{self, FullLedgerEntries, FullLedgerEntry},
17};
18
19#[derive(Parser, Debug, Clone)]
20#[group(skip)]
21pub struct Cmd {
22 #[arg(long, value_enum, default_value("string"))]
24 pub output: Output,
25 #[command(flatten)]
26 pub key: key::Args,
27 #[command(flatten)]
28 config: config::ArgsLocatorAndNetwork,
29}
30
31#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
32pub enum Output {
33 String,
35 Json,
37 Xdr,
39}
40
41#[derive(thiserror::Error, Debug)]
42pub enum Error {
43 #[error("parsing key {key}: {error}")]
44 CannotParseKey {
45 key: String,
46 error: soroban_spec_tools::Error,
47 },
48 #[error("parsing XDR key {key}: {error}")]
49 CannotParseXdrKey { key: String, error: XdrError },
50 #[error("cannot parse contract ID {contract_id}: {error}")]
51 CannotParseContractId {
52 contract_id: String,
53 error: stellar_strkey::DecodeError,
54 },
55 #[error("cannot print result {result:?}: {error}")]
56 CannotPrintResult {
57 result: ScVal,
58 error: soroban_spec_tools::Error,
59 },
60 #[error("cannot print result {result:?}: {error}")]
61 CannotPrintJsonResult {
62 result: ScVal,
63 error: serde_json::Error,
64 },
65 #[error("cannot print as csv: {error}")]
66 CannotPrintAsCsv { error: csv::Error },
67 #[error("cannot print: {error}")]
68 CannotPrintFlush { error: io::Error },
69 #[error(transparent)]
70 Config(#[from] config::Error),
71 #[error("either `--key` or `--key-xdr` are required when querying a network")]
72 KeyIsRequired,
73 #[error(transparent)]
74 Rpc(#[from] rpc::Error),
75 #[error(transparent)]
76 Xdr(#[from] XdrError),
77 #[error("no matching contract data entries were found for the specified contract id")]
78 NoContractDataEntryFoundForContractID,
79 #[error(transparent)]
80 Key(#[from] key::Error),
81 #[error("Only contract data and code keys are allowed")]
82 OnlyDataAllowed,
83 #[error(transparent)]
84 Locator(#[from] locator::Error),
85 #[error(transparent)]
86 Network(#[from] config::network::Error),
87}
88
89impl Cmd {
90 pub async fn run(&self) -> Result<(), Error> {
91 let entries = self
92 .execute(&config::Args {
93 locator: self.config.locator.clone(),
94 network: self.config.network.clone(),
95 source_account: config::UnresolvedMuxedAccount::default(),
96 sign_with: config::sign_with::Args::default(),
97 fee: None,
98 inclusion_fee: None,
99 })
100 .await?;
101 self.output_entries(&entries)
102 }
103
104 pub async fn execute(&self, config: &config::Args) -> Result<FullLedgerEntries, Error> {
105 let network = config.get_network()?;
106 tracing::trace!(?network);
107 let client = network.rpc_client()?;
108 let keys = self.key.parse_keys(&config.locator, &network)?;
109 Ok(client.get_full_ledger_entries(&keys).await?)
110 }
111
112 fn output_entries(&self, entries: &FullLedgerEntries) -> Result<(), Error> {
113 if entries.entries.is_empty() {
114 return Err(Error::NoContractDataEntryFoundForContractID);
115 }
116 tracing::trace!("{entries:#?}");
117 let mut out = csv::Writer::from_writer(stdout());
118 for entry in &entries.entries {
119 out.write_record(Self::entry_record(self.output, entry)?)
120 .map_err(|e| Error::CannotPrintAsCsv { error: e })?;
121 }
122 out.flush()
123 .map_err(|e| Error::CannotPrintFlush { error: e })?;
124 Ok(())
125 }
126
127 fn entry_record(output: Output, entry: &FullLedgerEntry) -> Result<[String; 4], Error> {
128 let FullLedgerEntry {
129 key,
130 val,
131 live_until_ledger_seq,
132 last_modified_ledger,
133 } = entry;
134 let (
135 LedgerKey::ContractData(LedgerKeyContractData { key, .. }),
136 LedgerEntryData::ContractData(ContractDataEntry { val, .. }),
137 ) = &(key, val)
138 else {
139 return Err(Error::OnlyDataAllowed);
140 };
141 let output = match output {
142 Output::String => [
146 soroban_spec_tools::sanitize(&soroban_spec_tools::to_string(key).map_err(|e| {
147 Error::CannotPrintResult {
148 result: key.clone(),
149 error: e,
150 }
151 })?),
152 soroban_spec_tools::sanitize(&soroban_spec_tools::to_string(val).map_err(|e| {
153 Error::CannotPrintResult {
154 result: val.clone(),
155 error: e,
156 }
157 })?),
158 last_modified_ledger.to_string(),
159 live_until_ledger_seq.unwrap_or_default().to_string(),
160 ],
161 Output::Json => [
162 serde_json::to_string_pretty(&key).map_err(|error| {
163 Error::CannotPrintJsonResult {
164 result: key.clone(),
165 error,
166 }
167 })?,
168 serde_json::to_string_pretty(&val).map_err(|error| {
169 Error::CannotPrintJsonResult {
170 result: val.clone(),
171 error,
172 }
173 })?,
174 serde_json::to_string_pretty(&last_modified_ledger).map_err(|error| {
175 Error::CannotPrintJsonResult {
176 result: val.clone(),
177 error,
178 }
179 })?,
180 serde_json::to_string_pretty(&live_until_ledger_seq.unwrap_or_default()).map_err(
181 |error| Error::CannotPrintJsonResult {
182 result: val.clone(),
183 error,
184 },
185 )?,
186 ],
187 Output::Xdr => [
188 key.to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?,
189 val.to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?,
190 last_modified_ledger.to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?,
191 live_until_ledger_seq
192 .unwrap_or_default()
193 .to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?,
194 ],
195 };
196 Ok(output)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::xdr::{
204 ContractDataDurability, ContractId, ExtensionPoint, Hash, ScAddress, ScSymbol, StringM,
205 };
206
207 fn symbol(bytes: &[u8]) -> ScVal {
208 let s: StringM<32> = bytes.to_vec().try_into().unwrap();
209 ScVal::Symbol(ScSymbol(s))
210 }
211
212 fn contract_data_entry(key: ScVal, val: ScVal) -> FullLedgerEntry {
213 let contract = ScAddress::Contract(ContractId(Hash([0u8; 32])));
214 FullLedgerEntry {
215 key: LedgerKey::ContractData(LedgerKeyContractData {
216 contract: contract.clone(),
217 key,
218 durability: ContractDataDurability::Persistent,
219 }),
220 val: LedgerEntryData::ContractData(ContractDataEntry {
221 ext: ExtensionPoint::V0,
222 contract,
223 key: ScVal::Void,
224 durability: ContractDataDurability::Persistent,
225 val,
226 }),
227 last_modified_ledger: 2026,
228 live_until_ledger_seq: Some(3_000_000),
229 }
230 }
231
232 #[test]
236 fn string_output_strips_control_bytes_from_symbol() {
237 let attack = b"\x1b[2J\x1b[Hpwned";
238 let entry = contract_data_entry(symbol(attack), symbol(attack));
239
240 let record = Cmd::entry_record(Output::String, &entry).unwrap();
241
242 assert!(
243 !record[0].as_bytes().contains(&0x1b),
244 "key field leaked an ESC byte: {:?}",
245 record[0]
246 );
247 assert!(
248 !record[1].as_bytes().contains(&0x1b),
249 "val field leaked an ESC byte: {:?}",
250 record[1]
251 );
252 assert!(record[0].contains("pwned"));
254 }
255}