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
75 changes: 44 additions & 31 deletions src/protocol/methods/tempo/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ fn parse_b256_hex(s: &str) -> Option<B256> {
#[derive(Clone)]
pub struct ChargeMethod<P> {
provider: Arc<P>,
fee_payer_signer: Option<Arc<alloy::signers::local::PrivateKeySigner>>,
fee_payer_signer: Option<Arc<dyn alloy::signers::Signer + Send + Sync>>,
store: Option<Arc<dyn Store>>,
}

Expand Down Expand Up @@ -136,7 +136,13 @@ where
///
/// When set, requests with `feePayer: true` will be accepted and
/// broadcast. Without a fee payer signer, such requests are rejected.
pub fn with_fee_payer(mut self, signer: alloy::signers::local::PrivateKeySigner) -> Self {
///
/// Accepts any type implementing alloy's [`Signer`](alloy::signers::Signer)
/// trait — local private keys, KMS-backed signers, hardware wallets, etc.
pub fn with_fee_payer(
mut self,
signer: impl alloy::signers::Signer + Send + Sync + 'static,
) -> Self {
self.fee_payer_signer = Some(Arc::new(signer));
self
}
Expand Down Expand Up @@ -559,7 +565,8 @@ where
)
})?;

self.cosign_fee_payer_transaction(&tx_bytes, fee_payer_signer, currency)?
self.cosign_fee_payer_transaction(&tx_bytes, &**fee_payer_signer, currency)
.await?
} else {
tx_bytes.to_vec()
};
Expand Down Expand Up @@ -638,16 +645,15 @@ where
/// Accepts a `0x78` fee payer envelope, recovers the sender via
/// ecrecover, validates fee-payer invariants, then co-signs and
/// returns a complete `0x76` transaction ready for broadcast.
fn cosign_fee_payer_transaction(
async fn cosign_fee_payer_transaction(
&self,
tx_bytes: &[u8],
fee_payer_signer: &alloy::signers::local::PrivateKeySigner,
fee_payer_signer: &(dyn alloy::signers::Signer + Send + Sync),
fee_token: Address,
) -> Result<Vec<u8>, VerificationError> {
use super::fee_payer_envelope::{FeePayerEnvelope78, TEMPO_FEE_PAYER_ENVELOPE_TYPE_ID};
use alloy::consensus::transaction::SignerRecoverable;
use alloy::eips::Encodable2718;
use alloy::signers::SignerSync;
use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;

if tx_bytes.is_empty() {
Expand Down Expand Up @@ -724,7 +730,8 @@ where
// Compute the fee payer signature hash and co-sign
let fp_hash = tx.fee_payer_signature_hash(sender);
let fp_sig = fee_payer_signer
.sign_hash_sync(&fp_hash)
.sign_hash(&fp_hash)
.await
.map_err(|e| VerificationError::new(format!("Failed to co-sign transaction: {e}")))?;

tx.fee_payer_signature = Some(fp_sig);
Expand Down Expand Up @@ -1042,8 +1049,8 @@ mod tests {

/// Round-trip: sign 0x78 envelope → cosign_fee_payer_transaction
/// succeeds and produces a valid co-signed 0x76 transaction.
#[test]
fn test_fee_payer_round_trip_0x78_envelope() {
#[tokio::test]
async fn test_fee_payer_round_trip_0x78_envelope() {
use super::super::{FeePayerEnvelope78, TEMPO_FEE_PAYER_ENVELOPE_TYPE_ID};
use alloy::eips::Decodable2718;
use alloy::signers::SignerSync;
Expand Down Expand Up @@ -1073,11 +1080,13 @@ mod tests {

let result = method.cosign_fee_payer_transaction(
&encoded,
method.fee_payer_signer.as_ref().unwrap(),
&**method.fee_payer_signer.as_ref().unwrap(),
fee_token,
);

let co_signed = result.expect("cosign should succeed for valid 0x78 envelope");
let co_signed = result
.await
.expect("cosign should succeed for valid 0x78 envelope");

// Result should be a valid 0x76 transaction
assert_eq!(
Expand All @@ -1099,8 +1108,8 @@ mod tests {
}

/// cosign_fee_payer_transaction rejects txs with wrong nonce_key.
#[test]
fn test_cosign_rejects_wrong_nonce_key() {
#[tokio::test]
async fn test_cosign_rejects_wrong_nonce_key() {
let client_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_payer_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_token: Address = "0x20c0000000000000000000000000000000000000"
Expand All @@ -1120,20 +1129,20 @@ mod tests {

let result = method.cosign_fee_payer_transaction(
&encoded,
method.fee_payer_signer.as_ref().unwrap(),
&**method.fee_payer_signer.as_ref().unwrap(),
fee_token,
);

let err = result.expect_err("should reject wrong nonce_key");
let err = result.await.expect_err("should reject wrong nonce_key");
assert!(
err.to_string().contains("expiring nonce key"),
"error should mention expiring nonce key, got: {err}"
);
}

/// cosign_fee_payer_transaction rejects txs without valid_before.
#[test]
fn test_cosign_rejects_missing_valid_before() {
#[tokio::test]
async fn test_cosign_rejects_missing_valid_before() {
let client_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_payer_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_token: Address = "0x20c0000000000000000000000000000000000000"
Expand All @@ -1153,20 +1162,22 @@ mod tests {

let result = method.cosign_fee_payer_transaction(
&encoded,
method.fee_payer_signer.as_ref().unwrap(),
&**method.fee_payer_signer.as_ref().unwrap(),
fee_token,
);

let err = result.expect_err("should reject missing valid_before");
let err = result
.await
.expect_err("should reject missing valid_before");
assert!(
err.to_string().contains("must include valid_before"),
"error should mention valid_before, got: {err}"
);
}

/// cosign_fee_payer_transaction rejects txs with expired valid_before.
#[test]
fn test_cosign_rejects_expired_valid_before() {
#[tokio::test]
async fn test_cosign_rejects_expired_valid_before() {
let client_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_payer_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_token: Address = "0x20c0000000000000000000000000000000000000"
Expand All @@ -1193,20 +1204,22 @@ mod tests {

let result = method.cosign_fee_payer_transaction(
&encoded,
method.fee_payer_signer.as_ref().unwrap(),
&**method.fee_payer_signer.as_ref().unwrap(),
fee_token,
);

let err = result.expect_err("should reject expired valid_before");
let err = result
.await
.expect_err("should reject expired valid_before");
assert!(
err.to_string().contains("expired"),
"error should mention expiration, got: {err}"
);
}

/// cosign_fee_payer_transaction rejects empty input.
#[test]
fn test_cosign_rejects_empty_input() {
#[tokio::test]
async fn test_cosign_rejects_empty_input() {
let fee_payer_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_token: Address = "0x20c0000000000000000000000000000000000000"
.parse()
Expand All @@ -1220,20 +1233,20 @@ mod tests {

let result = method.cosign_fee_payer_transaction(
&[],
method.fee_payer_signer.as_ref().unwrap(),
&**method.fee_payer_signer.as_ref().unwrap(),
fee_token,
);

let err = result.expect_err("should reject empty input");
let err = result.await.expect_err("should reject empty input");
assert!(
err.to_string().contains("Empty transaction bytes"),
"error should mention empty, got: {err}"
);
}

/// cosign_fee_payer_transaction rejects non-0x78 type byte.
#[test]
fn test_cosign_rejects_wrong_type_byte() {
#[tokio::test]
async fn test_cosign_rejects_wrong_type_byte() {
let fee_payer_signer = alloy::signers::local::PrivateKeySigner::random();
let fee_token: Address = "0x20c0000000000000000000000000000000000000"
.parse()
Expand All @@ -1247,11 +1260,11 @@ mod tests {

let result = method.cosign_fee_payer_transaction(
&[0x79, 0xc0], // wrong type byte
method.fee_payer_signer.as_ref().unwrap(),
&**method.fee_payer_signer.as_ref().unwrap(),
fee_token,
);

let err = result.expect_err("should reject wrong type");
let err = result.await.expect_err("should reject wrong type");
assert!(
err.to_string()
.contains("Expected fee payer envelope (0x78)"),
Expand Down
15 changes: 11 additions & 4 deletions src/protocol/methods/tempo/session_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ pub struct SessionMethod<P> {
store: Arc<dyn ChannelStore>,
config: SessionMethodConfig,
/// Optional signer for submitting on-chain close transactions.
close_signer: Option<Arc<alloy::signers::local::PrivateKeySigner>>,
///
/// Accepts any EVM signer (local key, KMS, hardware wallet, etc.).
close_signer: Option<Arc<dyn alloy::signers::Signer + Send + Sync>>,
}

impl<P> SessionMethod<P> {
Expand Down Expand Up @@ -254,7 +256,13 @@ where
}

/// Set the signer used for submitting on-chain close transactions.
pub fn with_close_signer(mut self, signer: alloy::signers::local::PrivateKeySigner) -> Self {
///
/// Accepts any type implementing alloy's [`Signer`](alloy::signers::Signer)
/// trait — local private keys, KMS-backed signers, hardware wallets, etc.
pub fn with_close_signer(
mut self,
signer: impl alloy::signers::Signer + Send + Sync + 'static,
) -> Self {
self.close_signer = Some(Arc::new(signer));
self
}
Expand Down Expand Up @@ -705,7 +713,6 @@ where
let close_tx_hash = if let Some(ref signer) = self.close_signer {
use alloy::eips::Encodable2718;
use alloy::primitives::Bytes;
use alloy::signers::SignerSync;
use alloy::sol_types::SolCall;
use tempo_primitives::transaction::Call;
use tempo_primitives::TempoTransaction;
Expand Down Expand Up @@ -749,7 +756,7 @@ where
};

let sig_hash = tempo_tx.signature_hash();
let signature = signer.sign_hash_sync(&sig_hash).map_err(|e| {
let signature = signer.sign_hash(&sig_hash).await.map_err(|e| {
VerificationError::network_error(format!("failed to sign close tx: {}", e))
})?;
let signed_tx = tempo_tx.into_signed(signature.into());
Expand Down
12 changes: 9 additions & 3 deletions src/server/tempo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub struct TempoBuilder {
pub(crate) decimals: u32,
pub(crate) fee_payer: bool,
pub(crate) chain_id: Option<u64>,
pub(crate) fee_payer_signer: Option<alloy::signers::local::PrivateKeySigner>,
pub(crate) fee_payer_signer: Option<Box<dyn alloy::signers::Signer + Send + Sync>>,
}

impl TempoBuilder {
Expand Down Expand Up @@ -96,8 +96,14 @@ impl TempoBuilder {
/// When clients send transactions with `feePayer: true`, the server
/// uses this signer to co-sign and sponsor the transaction gas fees.
/// The signer's account must have sufficient balance for gas.
pub fn fee_payer_signer(mut self, signer: alloy::signers::local::PrivateKeySigner) -> Self {
self.fee_payer_signer = Some(signer);
///
/// Accepts any type implementing alloy's [`Signer`](alloy::signers::Signer)
/// trait — local private keys, KMS-backed signers, hardware wallets, etc.
pub fn fee_payer_signer(
mut self,
signer: impl alloy::signers::Signer + Send + Sync + 'static,
) -> Self {
self.fee_payer_signer = Some(Box::new(signer));
self
}
}
Expand Down