Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

* [FEATURE] Distinct `ApplyTransactionAfterSubmitFailed` error variant for the case where `submit_proven_transaction` succeeds but the local store update fails. The error carries the pending `TransactionStoreUpdate` so callers can persist it and re-apply later via `apply_transaction_update` without resubmitting (which the node would reject if the original is still in the mempool or has been finalized in a block, since the account and network state have already been mutated by the accepted copy) ([#2059](https://github.com/0xMiden/miden-client/pull/2059)).
* [FEATURE][web] Serialize all async `WebClient` JS methods — both the explicit wrappers and every async call that falls through `createClientProxy` to the underlying WASM client (e.g. `getAccount`, `importAccountById`, `getAccountStorage`) — via an internal `_serializeWasmCall` chain. Prevents `"recursive use of an object detected"` panics when an unwrapped read/write races the auto-sync timer or any explicitly-wrapped method. Expose `waitForIdle()` on `MidenClient` so callers can drain in-flight work before mutating non-WASM state ([#2057](https://github.com/0xMiden/miden-client/pull/2057)).
* [FEATURE][web] Split `@miden-sdk/miden-sdk` into eager and lazy entry points. The default entry (`import from "@miden-sdk/miden-sdk"`) now awaits WASM at module top level via a small shim (`js/eager.js`) — consumers don't need `await MidenClient.ready()` / `isReady` before constructing wasm-bindgen types. The lazy entry (`import from "@miden-sdk/miden-sdk/lazy"`) preserves the previous behavior and is required for Capacitor WKWebView hosts (the custom-scheme handler hangs on TLA) and Next.js SSR. Verified empirically against the Miden Wallet's iOS E2E suite on devnet. `@miden-sdk/react` imports from `/lazy` internally and manages readiness via `isReady`.
* [FEATURE][web] Expose `lastAuthError()` on `MidenClient` for typed sign-callback failure recovery — preserves the raw thrown value from the JS signCallback so consumers can distinguish locked/rejected/IO-error failure modes ([#2058](https://github.com/0xMiden/miden-client/pull/2058)).
Expand Down
31 changes: 31 additions & 0 deletions crates/rust-client/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
Expand Down Expand Up @@ -165,6 +166,20 @@ pub enum ClientError {
#[source]
source: RpcError,
},
#[error(
"transaction {} was accepted into the node's mempool at block {} but the local store \
update failed. The pending store update is attached and can be re-applied later via \
`apply_transaction_update`. Resubmitting the same transaction will be rejected if the \
original is still in the mempool or has been finalized in a block, because the \
account (and network) state has already been mutated by the accepted copy.",
pending_update.executed_transaction().id(),
pending_update.submission_height()
)]
Comment on lines +169 to +177

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid giving out too much internal details, but also they should be more correct: really, the nullifiers are not the only problem here. In fact, there may be no nullifiers involved in the transaction at all.

Rather, we should say that if the original transaction was accepted by the mempool and has not expired (and/or was finalized in a block), a duplicate submission would fail because the state of the account (and/or the network's state) was already mutated.

ApplyTransactionAfterSubmitFailed {
pending_update: Box<crate::transaction::TransactionStoreUpdate>,
#[source]
source: Box<ClientError>,
},
}

// CONVERSIONS
Expand Down Expand Up @@ -228,6 +243,22 @@ impl From<&ClientError> for Option<ErrorHint> {
or provide the seed when importing.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::ApplyTransactionAfterSubmitFailed { pending_update, .. } => {
let tx_id = pending_update.executed_transaction().id();
let submission_height = pending_update.submission_height();
Some(ErrorHint {
message: format!(
"Transaction {tx_id} was accepted into the node's mempool at block \
{submission_height} but the local store update failed. The pending \
update is attached to this error as `pending_update`; you can re-apply \
it later via `Client::apply_transaction_update`. Do NOT resubmit the \
same transaction: if the original is still in the mempool or has been \
finalized in a block, the account (and network) state has already been \
mutated by the accepted copy, so the node will reject the retry."
),
docs_url: Some(TROUBLESHOOTING_DOC),
})
},
_ => None,
}
}
Expand Down
22 changes: 21 additions & 1 deletion crates/rust-client/src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,27 @@ where
let submission_height =
self.submit_proven_transaction(proven_transaction, &tx_result).await?;

self.apply_transaction(&tx_result, submission_height).await?;
// The transaction has been accepted by the node; the local store update
// is a separate step that can fail independently. On failure, return a
// distinct error carrying the pending update so the caller can decide
// how to recover (re-apply later via `apply_transaction_update`,
// persist for the next session, etc.).
//
// The update is boxed so it does not inflate the enclosing future
// across await points (triggers clippy::large_futures).
let tx_update =
Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);

if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
info!(
"apply_transaction_update failed for submitted tx {tx_id}; returning \
ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
);
return Err(ClientError::ApplyTransactionAfterSubmitFailed {
pending_update: tx_update,
source: Box::new(apply_err),
});
}

Ok(tx_id)
}
Expand Down
Loading