Skip to main content

soroban_cli/commands/tx/
xdr.rs

1use crate::xdr::{
2    Limits, Operation, ReadXdr, Transaction, TransactionEnvelope, TransactionV1Envelope,
3};
4use std::ffi::OsString;
5use std::fs::File;
6use std::io::{stdin, Read};
7use std::io::{Cursor, IsTerminal};
8use std::path::Path;
9use stellar_xdr::curr::Limited;
10
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13    #[error("failed to decode XDR: {0}")]
14    XDRDecode(#[from] stellar_xdr::curr::Error),
15    #[error(transparent)]
16    Io(#[from] std::io::Error),
17    #[error("only transaction v1 is supported")]
18    OnlyTransactionV1Supported,
19    #[error("too many operations, limited to 100 operations in a transaction")]
20    TooManyOperations,
21    #[error("no transaction provided")]
22    NoStdin,
23}
24
25pub fn tx_envelope_from_input(input: &Option<OsString>) -> Result<TransactionEnvelope, Error> {
26    let read: &mut dyn Read = if let Some(input) = input {
27        let exist = Path::new(input).try_exists();
28        if let Ok(true) = exist {
29            &mut File::open(input)?
30        } else {
31            &mut Cursor::new(input.clone().into_encoded_bytes())
32        }
33    } else {
34        if stdin().is_terminal() {
35            return Err(Error::NoStdin);
36        }
37        &mut stdin()
38    };
39
40    let mut lim = Limited::new(SkipWhitespace::new(read), Limits::none());
41    Ok(TransactionEnvelope::read_xdr_base64_to_end(&mut lim)?)
42}
43
44// TODO: use SkipWhitespace from rs-stellar-xdr once it's updated to 23.0
45pub struct SkipWhitespace<R: Read> {
46    pub inner: R,
47}
48
49impl<R: Read> SkipWhitespace<R> {
50    pub fn new(inner: R) -> Self {
51        SkipWhitespace { inner }
52    }
53}
54
55impl<R: Read> Read for SkipWhitespace<R> {
56    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
57        loop {
58            let n = self.inner.read(buf)?;
59            if n == 0 {
60                return Ok(0);
61            }
62
63            let mut written = 0;
64            for read in 0..n {
65                if !buf[read].is_ascii_whitespace() {
66                    buf[written] = buf[read];
67                    written += 1;
68                }
69            }
70
71            if written > 0 {
72                return Ok(written);
73            }
74        }
75    }
76}
77
78pub fn unwrap_envelope_v1(tx_env: TransactionEnvelope) -> Result<Transaction, Error> {
79    let TransactionEnvelope::Tx(TransactionV1Envelope { tx, .. }) = tx_env else {
80        return Err(Error::OnlyTransactionV1Supported);
81    };
82    Ok(tx)
83}
84
85pub fn add_op(tx_env: TransactionEnvelope, op: Operation) -> Result<TransactionEnvelope, Error> {
86    let mut tx = unwrap_envelope_v1(tx_env)?;
87    let mut ops = tx.operations.to_vec();
88    ops.push(op);
89    tx.operations = ops.try_into().map_err(|_| Error::TooManyOperations)?;
90    Ok(tx.into())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::io::Cursor;
97
98    struct ChunkedReader {
99        chunks: Vec<Vec<u8>>,
100        pos: usize,
101    }
102
103    impl ChunkedReader {
104        fn new(chunks: Vec<&[u8]>) -> Self {
105            Self {
106                chunks: chunks.iter().map(|c| c.to_vec()).collect(),
107                pos: 0,
108            }
109        }
110    }
111
112    impl Read for ChunkedReader {
113        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
114            if self.pos >= self.chunks.len() {
115                return Ok(0);
116            }
117            let chunk = &self.chunks[self.pos];
118            let n = chunk.len().min(buf.len());
119            buf[..n].copy_from_slice(&chunk[..n]);
120            self.pos += 1;
121            Ok(n)
122        }
123    }
124
125    #[test]
126    fn skip_whitespace_preserves_content() {
127        let input = Cursor::new(b"helloworld");
128        let mut reader = SkipWhitespace::new(input);
129        let mut result = String::new();
130        reader.read_to_string(&mut result).unwrap();
131        assert_eq!(result, "helloworld");
132    }
133
134    #[test]
135    fn skip_whitespace_strips_all_whitespace_types() {
136        let input = Cursor::new(b"hello \t\n\r world");
137        let mut reader = SkipWhitespace::new(input);
138        let mut result = String::new();
139        reader.read_to_string(&mut result).unwrap();
140        assert_eq!(result, "helloworld");
141    }
142
143    #[test]
144    fn skip_whitespace_handles_only_whitespace() {
145        let input = Cursor::new(b"\n \t \r\n");
146        let mut reader = SkipWhitespace::new(input);
147        let mut result = String::new();
148        reader.read_to_string(&mut result).unwrap();
149        assert_eq!(result, "");
150    }
151
152    #[test]
153    fn skip_whitespace_handles_empty_input() {
154        let input = Cursor::new(b"");
155        let mut reader = SkipWhitespace::new(input);
156        let mut result = String::new();
157        reader.read_to_string(&mut result).unwrap();
158        assert_eq!(result, "");
159    }
160
161    #[test]
162    fn skip_whitespace_loops_past_whitespace_only_chunks() {
163        // Exercises the loop iterating more than once: first chunk is all
164        // whitespace, second chunk has content. A Cursor would satisfy both
165        // reads in one shot and would never trigger the loop.
166        let reader = ChunkedReader::new(vec![b"\n\n", b"hello", b""]);
167        let mut skipper = SkipWhitespace::new(reader);
168        let mut result = String::new();
169        skipper.read_to_string(&mut result).unwrap();
170        assert_eq!(result, "hello");
171    }
172
173    #[test]
174    fn skip_whitespace_handles_leading_trailing_whitespace() {
175        let input = Cursor::new(b"\n\nhello\n\n");
176        let mut reader = SkipWhitespace::new(input);
177        let mut result = String::new();
178        reader.read_to_string(&mut result).unwrap();
179        assert_eq!(result, "hello");
180    }
181}