Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions cmd/soroban-cli/src/commands/tx/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,22 @@ impl Args {
return Ok(TxnEnvelopeResult::TxnEnvelope(Box::new(tx.into())));
}

let txn_resp = client
.send_transaction_polling(&self.config.sign(tx, args.quiet).await?)
.await?;
let signed_tx = self.config.sign(tx, args.quiet).await?;
let txn_resp = match client.send_transaction_polling(&signed_tx).await {
Ok(res) => res,
Err(e) => {
// Preserve the signed envelope on a failed send so it can be
// resubmitted without re-signing (#2609).
if !args.no_cache {
crate::tx::save_failed_send(
&signed_tx,
&network,
&crate::print::Print::new(args.quiet),
);
}
return Err(e.into());
}
};

if !args.no_cache {
data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?;
Expand Down
51 changes: 51 additions & 0 deletions cmd/soroban-cli/src/config/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ impl std::fmt::Display for DatedAction {
.as_ref()
.map_or_else(|| "SUCCESS".to_string(), |_| "ERROR".to_string()),
Action::Send { response } => response.status.clone(),
Action::SendFailed { .. } => "FAILED".to_string(),
};
write!(
f,
Expand Down Expand Up @@ -164,13 +165,21 @@ pub enum Action {
Send {
response: GetTransactionResponseRaw,
},
/// A signed transaction whose submission failed. Preserved so it can be
/// resubmitted (e.g. piped back into `stellar tx send`) without
/// re-signing (#2609).
SendFailed {
/// Base64 XDR of the signed transaction envelope.
envelope_xdr: String,
},
}

impl Action {
pub fn type_str(&self) -> String {
match self {
Action::Simulate { .. } => "Simulate",
Action::Send { .. } => "Send ",
Action::SendFailed { .. } => "SendFail",
}
.to_string()
}
Expand Down Expand Up @@ -233,6 +242,48 @@ mod test {
});
}

#[test]
#[serial]
fn test_send_failed_round_trips_and_renders_as_failed() {
let t = assert_fs::TempDir::new().unwrap();
with_env_set("STELLAR_DATA_HOME", t.path(), || {
let rpc_uri = Url::from_str("http://localhost:8000").unwrap();
let envelope = "AAAAAgAAAABiQCwo7WLMLL5nQ+q8XW4dGSXHbqhUFTv2FSNQ".to_string();

let id = write(
Action::SendFailed {
envelope_xdr: envelope.clone(),
},
&rpc_uri,
)
.unwrap();

let (action, _) = read(&id).unwrap();
match action {
Action::SendFailed { envelope_xdr } => assert_eq!(
envelope_xdr, envelope,
"the saved envelope must round-trip unchanged"
),
_ => panic!("expected Action::SendFailed"),
}

let rendered = list_actions()
.unwrap()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
assert!(
rendered.contains("SendFail"),
"ls should name the action type: {rendered}"
);
assert!(
rendered.contains("FAILED"),
"ls should render a FAILED status: {rendered}"
);
});
}

#[test]
#[serial]
fn actionlog_write_redacts_rpc_url_password_on_disk() {
Expand Down
49 changes: 46 additions & 3 deletions cmd/soroban-cli/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use crate::{
signer::{self, Signer},
utils::transaction_env_hash,
xdr::{
self, FeeBumpTransaction, FeeBumpTransactionExt, FeeBumpTransactionInnerTx, Transaction,
TransactionEnvelope,
self, FeeBumpTransaction, FeeBumpTransactionExt, FeeBumpTransactionInnerTx, Limits,
Transaction, TransactionEnvelope, WriteXdr,
},
};
use soroban_rpc::GetTransactionResponse;
Expand All @@ -17,6 +17,38 @@ pub mod builder;
/// 10,000,000 stroops in 1 XLM
pub const ONE_XLM: i64 = 10_000_000;

/// Preserve a signed envelope whose submission failed in the action log, and
/// tell the user how to resubmit it without re-signing (#2609).
///
/// Best-effort on purpose: a failure to save must not mask the original RPC
/// error, so it is only logged at debug level.
pub(crate) fn save_failed_send(
signed_tx: &TransactionEnvelope,
network: &network::Network,
print: &print::Print,
) {
let saved = network
.rpc_uri()
.map_err(|e| e.to_string())
.and_then(|uri| {
signed_tx
.to_xdr_base64(Limits::none())
.map_err(|e| e.to_string())
.and_then(|envelope_xdr| {
data::write(data::Action::SendFailed { envelope_xdr }, &uri)
.map_err(|e| e.to_string())
})
});
match saved {
Ok(id) => print.warnln(format!(
"The transaction failed to send, but the signed envelope was saved to the \
action log and can be resubmitted without re-signing:\n \
stellar cache actionlog read --id {id} | jq -r .action.send_failed.envelope_xdr | stellar tx send"
)),
Err(e) => tracing::debug!("failed to save the signed envelope to the action log: {e}"),
}
}

/// Simulates, signs, and sends a transaction to the network.
///
/// This function handles a couple common tasks related to sending transactions:
Expand Down Expand Up @@ -106,7 +138,18 @@ where
print.globeln("Sending transaction…");

// returns an error if the transaction fails
let res = client.send_transaction_polling(&signed_tx).await?;
let res = match client.send_transaction_polling(&signed_tx).await {
Ok(res) => res,
Err(e) => {
// A failed send would otherwise lose the signed (and possibly
// fee-bumped) envelope; preserve it in the action log so it can
// be resubmitted without collecting signatures again (#2609).
if !no_cache {
save_failed_send(&signed_tx, &network, &print);
}
return Err(e.into());
}
};

print.checkln("Transaction submitted successfully!");

Expand Down