Skip to content

Commit fb195c9

Browse files
fix: detect held transfers in wallet transfer (TIP-1028)
1 parent ab4607f commit fb195c9

3 files changed

Lines changed: 534 additions & 9 deletions

File tree

crates/tempo-common/src/payment/session/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ pub use close::{
4444
pub use tx::{
4545
build_open_calls, build_tip1034_open_calls, build_tip1034_top_up_calls, build_top_up_calls,
4646
resolve_and_sign_tx, resolve_and_sign_tx_with_fee_payer,
47-
resolve_and_sign_tx_with_fee_payer_info, submit_tempo_tx, SignedTempoTx,
47+
resolve_and_sign_tx_with_fee_payer_info, submit_tempo_tx, submit_tempo_tx_and_wait,
48+
ConfirmedTempoTx, SignedTempoTx, TempoTxReceipt,
4849
};

crates/tempo-common/src/payment/session/tx.rs

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
//! All transactions use expiring nonces (nonceKey=MAX, nonce=0) so no
55
//! on-chain nonce fetch is needed.
66
7-
use std::num::NonZeroU64;
7+
use std::{num::NonZeroU64, time::Duration};
88

99
use alloy::{
10+
network::Network,
1011
primitives::{Address, Bytes, TxKind, B256, U256},
11-
providers::Provider,
12+
providers::{PendingTransactionError, Provider},
1213
sol,
1314
sol_types::SolCall,
1415
};
@@ -349,6 +350,122 @@ pub async fn submit_tempo_tx(
349350
}
350351
}
351352

353+
/// On-chain receipt type for the Tempo network.
354+
pub type TempoTxReceipt = <mpp::client::TempoNetwork as Network>::ReceiptResponse;
355+
356+
/// A broadcast Tempo transaction together with its mined receipt.
357+
pub struct ConfirmedTempoTx {
358+
pub tx_hash: String,
359+
pub receipt: TempoTxReceipt,
360+
}
361+
362+
/// Maximum time to wait for a transaction receipt before reporting unknown finality.
363+
const RECEIPT_TIMEOUT: Duration = Duration::from_secs(30);
364+
365+
/// Submit a Tempo type-0x76 transaction and wait for its mined receipt.
366+
///
367+
/// Unlike [`submit_tempo_tx`], which returns as soon as the transaction is
368+
/// broadcast, this waits for inclusion so callers can inspect the receipt
369+
/// (status and logs). Uses the same stored-key-authorization retry path.
370+
///
371+
/// # Errors
372+
///
373+
/// Returns an error when signing, broadcast, or receipt polling fails (including
374+
/// a timeout, in which case the error carries the broadcast tx hash).
375+
pub async fn submit_tempo_tx_and_wait(
376+
provider: &alloy::providers::RootProvider<mpp::client::TempoNetwork>,
377+
wallet: &Signer,
378+
chain_id: u64,
379+
fee_token: Address,
380+
from: Address,
381+
calls: Vec<tempo_primitives::transaction::Call>,
382+
) -> ChannelResult<ConfirmedTempoTx> {
383+
let tx_bytes =
384+
resolve_and_sign_tx(provider, wallet, chain_id, fee_token, from, calls.clone()).await?;
385+
386+
let pending = match provider.send_raw_transaction(&tx_bytes).await {
387+
Ok(pending) => pending,
388+
Err(original) if wallet.has_stored_key_authorization() => {
389+
let provisioning_signer =
390+
wallet
391+
.with_key_authorization()
392+
.ok_or_else(|| KeyError::SigningOperation {
393+
operation: "key provisioning",
394+
reason: "stored key authorization could not be applied to signing mode"
395+
.to_string(),
396+
})?;
397+
let retry_bytes = resolve_and_sign_tx(
398+
provider,
399+
&provisioning_signer,
400+
chain_id,
401+
fee_token,
402+
from,
403+
calls,
404+
)
405+
.await?;
406+
provider
407+
.send_raw_transaction(&retry_bytes)
408+
.await
409+
.map_err(|source| {
410+
classify_tx_error(&source)
411+
.or_else(|| classify_tx_error(&original))
412+
.unwrap_or_else(|| {
413+
NetworkError::RpcSource {
414+
operation: "broadcast transaction",
415+
source: Box::new(original),
416+
}
417+
.into()
418+
})
419+
})?
420+
}
421+
Err(source) => {
422+
return Err(classify_tx_error(&source).unwrap_or_else(|| {
423+
NetworkError::RpcSource {
424+
operation: "broadcast transaction",
425+
source: Box::new(source),
426+
}
427+
.into()
428+
}))
429+
}
430+
};
431+
432+
let tx_hash = format!("{:#x}", pending.tx_hash());
433+
434+
let receipt = pending
435+
.with_timeout(Some(RECEIPT_TIMEOUT))
436+
.get_receipt()
437+
.await
438+
.map_err(|source| NetworkError::RpcSource {
439+
operation: "await transaction receipt",
440+
source: Box::new(TxReceiptError {
441+
tx_hash: tx_hash.clone(),
442+
source,
443+
}),
444+
})?;
445+
446+
Ok(ConfirmedTempoTx { tx_hash, receipt })
447+
}
448+
449+
/// Wraps a receipt-polling failure with the broadcast tx hash so users can
450+
/// follow up on a transaction whose finality is unknown.
451+
#[derive(Debug)]
452+
struct TxReceiptError {
453+
tx_hash: String,
454+
source: PendingTransactionError,
455+
}
456+
457+
impl std::fmt::Display for TxReceiptError {
458+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459+
write!(f, "{} (tx {})", self.source, self.tx_hash)
460+
}
461+
}
462+
463+
impl std::error::Error for TxReceiptError {
464+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
465+
Some(&self.source)
466+
}
467+
}
468+
352469
// ==================== Transaction Construction ====================
353470

354471
/// Build the escrow open calls: approve + open.

0 commit comments

Comments
 (0)