Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci-tempo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
QUANTUM_FOUNDRY_BASE_COMMIT: "f1abb2ca347187bb6dea8c3881ca44ce50aab1e7"
QUANTUM_HARNESS_COMMIT: "8f3612c60f9fa66ea3a09eab99a2e0802f373673"

jobs:
sanity-check:
Expand Down
7 changes: 6 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3526,6 +3526,9 @@ impl EthApi<FoundryNetwork> {
FoundryTxEnvelope::Deposit(_) => self.backend.ensure_op_deposits_active(),
FoundryTxEnvelope::Legacy(_) => Ok(()),
FoundryTxEnvelope::Tempo(_) => self.backend.ensure_tempo_active(),
FoundryTxEnvelope::Quantum(_) => Err(BlockchainError::Message(
"quantum transactions are not supported by Anvil".to_string(),
)),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/anvil/src/eth/backend/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl ReceiptBuilder for FoundryReceiptBuilder {
unreachable!("deposit receipts are built in commit_transaction")
}
FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt),
FoundryTxType::Quantum => FoundryReceiptEnvelope::Quantum(receipt),
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/anvil/src/eth/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ impl Signer<foundry_primitives::FoundryNetwork> for DevSigner {
let sig = signer.sign_transaction_sync(&mut t)?;
FoundryTxEnvelope::Tempo(t.into_signed(sig.into()))
}
FoundryTypedTx::Quantum(_) => {
return Err(BlockchainError::Message(
"Quantum transactions must be pre-signed and are not supported by Anvil's dev signer"
.to_string(),
))
}
};
Ok(envelope)
}
Expand All @@ -158,5 +164,6 @@ pub fn build_impersonated(typed_tx: FoundryTypedTx) -> FoundryTxEnvelope {
let tempo_sig: TempoSignature = signature.into();
FoundryTxEnvelope::Tempo(tx.into_signed(tempo_sig))
}
FoundryTypedTx::Quantum(tx) => FoundryTxEnvelope::Quantum(tx),
}
}
2 changes: 1 addition & 1 deletion crates/cast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ foundry-compilers.workspace = true
foundry-config.workspace = true
foundry-debugger.workspace = true
foundry-evm.workspace = true
foundry-primitives.workspace = true
foundry-wallets.workspace = true
forge-fmt.workspace = true

Expand Down Expand Up @@ -55,7 +56,6 @@ alloy-sol-types.workspace = true
alloy-transport.workspace = true
alloy-ens = { workspace = true, features = ["provider"] }
alloy-eips.workspace = true
ml-dsa = "0.1.0-rc.8"
tempo-alloy.workspace = true
tempo-contracts.workspace = true
tempo-primitives.workspace = true
Expand Down
18 changes: 18 additions & 0 deletions crates/cast/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use foundry_common::{
},
shell, stdin,
};
use foundry_primitives::QuantumNetwork;
use op_alloy_network::Optimism;
use std::time::Instant;
use tempo_alloy::TempoNetwork;
Expand Down Expand Up @@ -368,6 +369,13 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
.block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
.await?
}
Some(NetworkVariant::Quantum) => {
let provider =
ProviderBuilder::<QuantumNetwork>::from_config(&config)?.build()?;
Cast::new(&provider)
.block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
.await?
}
// Ethereum (default) or no --raw flag
_ => {
let provider =
Expand Down Expand Up @@ -585,6 +593,13 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
.transaction(tx_hash, from, nonce, field, is_raw, to_request)
.await?
}
Some(NetworkVariant::Quantum) => {
let provider =
ProviderBuilder::<QuantumNetwork>::from_config(&config)?.build()?;
Cast::new(&provider)
.transaction(tx_hash, from, nonce, field, is_raw, to_request)
.await?
}
// Ethereum (default) or no --raw flag
_ => {
let provider = utils::get_provider(&config)?;
Expand Down Expand Up @@ -799,6 +814,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
Some(NetworkVariant::Tempo) => {
SimpleCast::decode_raw_transaction::<TempoNetwork>(&tx)?
}
Some(NetworkVariant::Quantum) => {
SimpleCast::decode_raw_transaction::<QuantumNetwork>(&tx)?
}
_ => SimpleCast::decode_raw_transaction::<Ethereum>(&tx)?,
};
sh_println!("{}", serde_json::to_string_pretty(&decoded_tx)?)?;
Expand Down
3 changes: 3 additions & 0 deletions crates/cast/src/cmd/da_estimate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ impl DAEstimateArgs {
NetworkVariant::Tempo => Err(eyre::eyre!(
"DA estimation is not supported for Tempo: EIP-4844 blob transactions are not available on this network"
)),
NetworkVariant::Quantum => Err(eyre::eyre!(
"DA estimation is not supported for Quantum: the first-class QuantumNetwork adapter does not implement this path yet"
)),
}
}
}
Expand Down
121 changes: 73 additions & 48 deletions crates/cast/src/cmd/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ use std::{path::PathBuf, str::FromStr, time::Duration};

use alloy_consensus::{SignableTransaction, Signed};
use alloy_ens::NameOrAddress;
use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
use alloy_primitives::Address;
use alloy_network::{Ethereum, EthereumWallet, Network};
use alloy_primitives::{Address, hex};
use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
use alloy_signer::{Signature, Signer};
use clap::Parser;
use eyre::{Result, eyre};
use foundry_cli::{opts::TransactionOpts, utils::LoadConfig};
use foundry_common::{
FoundryTransactionBuilder,
FoundryTransactionBuilder, QUANTUM_BOOTSTRAP_SELECTOR, QUANTUM_KEYVAULT_ADDRESS,
derive_primary_pubkey, parse_seed_file, sign_quantum_transaction_request,
fmt::{UIfmt, UIfmtReceiptExt},
provider::ProviderBuilder,
};
use foundry_primitives::QuantumNetwork;
use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
use tempo_alloy::TempoNetwork;

use crate::{
cmd::tip20::iso4217_warning_message,
quantum::{QuantumWriteContractV1, build_phase0_payload, parse_seed_file},
tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
};
use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
Expand Down Expand Up @@ -109,38 +110,57 @@ impl SendTxArgs {
}

async fn run_quantum(self) -> Result<()> {
let Self { to, mut sig, args, data, send_tx, command, unlocked, force: _, tx, path } =
self;
let Self {
to,
mut sig,
args,
data,
send_tx,
command,
unlocked,
force: _,
mut tx,
path,
} = self;

if unlocked {
return Err(eyre!("the Phase 0 Quantum seam does not support --unlocked"));
return Err(eyre!("the Quantum adapter path does not support --unlocked"));
}
if send_tx.browser.browser {
return Err(eyre!("the Phase 0 Quantum seam does not support browser signing"));
return Err(eyre!("the Quantum adapter path does not support browser signing"));
}
if tx.tempo.is_tempo() {
return Err(eyre!("Quantum and Tempo options cannot be combined"));
}
if command.is_some() {
return Err(eyre!("the Phase 0 Quantum seam only supports cast send-style call flows"));
return Err(eyre!("the Quantum adapter path only supports cast send-style call flows"));
}
if path.is_some() {
return Err(eyre!("the Phase 0 Quantum seam does not support blob data"));
return Err(eyre!("the Quantum adapter path does not support blob data"));
}
if let Some(data) = data {
sig = Some(data);
}

let sender = tx.quantum.sender.ok_or_else(|| {
eyre!("--quantum.sender is required for the Phase 0 Quantum seam")
})?;
let seed_path = tx.quantum.primary_seed_file.as_ref().ok_or_else(|| {
eyre!("--quantum.primary-seed-file is required for the Phase 0 Quantum seam")
})?;
let sender = tx
.quantum
.sender
.ok_or_else(|| eyre!("--quantum.sender is required for Quantum writes"))?;
validate_quantum_sender(send_tx.eth.wallet.from, sender)?;
let seed_path =
tx.quantum.primary_seed_file.as_ref().ok_or_else(|| {
eyre!("--quantum.primary-seed-file is required for Quantum writes")
})?;
let primary_seed = parse_seed_file(seed_path)?;

if quantum_send_requests_bootstrap(to.as_ref(), sig.as_deref())
&& tx.quantum.init_primary_pubkey.is_none()
{
tx.quantum.init_primary_pubkey = Some(derive_primary_pubkey(primary_seed));
}

let config = send_tx.eth.load_config()?;
let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
let provider = ProviderBuilder::<QuantumNetwork>::from_config(&config)?.build()?;

if let Some(interval) = send_tx.poll_interval {
provider.client().set_poll_interval(Duration::from_secs(interval));
Expand All @@ -154,37 +174,7 @@ impl SendTxArgs {
.await?;

let (tx_request, _) = builder.build(sender).await?;
let kind = TransactionBuilder::kind(&tx_request)
.ok_or_else(|| eyre!("Quantum Phase 0 requires an explicit call destination"))?;
let max_fee_per_gas = TransactionBuilder::max_fee_per_gas(&tx_request)
.ok_or_else(|| eyre!("failed to resolve max fee per gas for Quantum transaction"))?;
let max_priority_fee_per_gas = TransactionBuilder::max_priority_fee_per_gas(&tx_request)
.ok_or_else(|| {
eyre!("failed to resolve max priority fee per gas for Quantum transaction")
})?;
let gas_limit = TransactionBuilder::gas_limit(&tx_request)
.ok_or_else(|| eyre!("failed to resolve gas limit for Quantum transaction"))?;
let nonce = TransactionBuilder::nonce(&tx_request)
.ok_or_else(|| eyre!("failed to resolve nonce for Quantum transaction"))?;
let chain_id = TransactionBuilder::chain_id(&tx_request)
.ok_or_else(|| eyre!("failed to resolve chain ID for Quantum transaction"))?;

let payload = build_phase0_payload(QuantumWriteContractV1 {
sender,
key_id: tx.quantum.resolved_key_id(),
nonce,
chain_id,
max_priority_fee_per_gas,
max_fee_per_gas,
gas_limit,
kind,
value: TransactionBuilder::value(&tx_request).unwrap_or_default(),
input: TransactionBuilder::input(&tx_request).cloned().unwrap_or_default(),
access_list: TransactionBuilder::access_list(&tx_request)
.cloned()
.unwrap_or_default(),
primary_seed,
})?;
let payload = sign_quantum_transaction_request(tx_request, primary_seed)?;

let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
let cast = CastTxSender::new(&provider);
Expand Down Expand Up @@ -418,6 +408,41 @@ impl SendTxArgs {
}
}

fn validate_quantum_sender(cli_from: Option<Address>, quantum_sender: Address) -> Result<()> {
if let Some(from) = cli_from
&& from != quantum_sender
{
eyre::bail!(
"--from must match --quantum.sender when using the Quantum adapter path"
)
}

Ok(())
}

fn quantum_send_requests_bootstrap(to: Option<&NameOrAddress>, input: Option<&str>) -> bool {
quantum_destination_is_keyvault(to) && quantum_input_is_bootstrap(input)
}

fn quantum_destination_is_keyvault(to: Option<&NameOrAddress>) -> bool {
match to {
Some(NameOrAddress::Address(addr)) => *addr == QUANTUM_KEYVAULT_ADDRESS,
Some(NameOrAddress::Name(name)) => {
Address::from_str(name).ok() == Some(QUANTUM_KEYVAULT_ADDRESS)
}
None => false,
}
}

fn quantum_input_is_bootstrap(input: Option<&str>) -> bool {
let Some(input) = input else { return false };
input.starts_with("bootstrapKey(")
|| input
.trim()
.trim_start_matches("0x")
.starts_with(&hex::encode(QUANTUM_BOOTSTRAP_SELECTOR))
}

pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
provider: P,
tx: N::TransactionRequest,
Expand Down
Loading
Loading