Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1f1756f
wip
kariy Feb 19, 2026
b4cb1eb
wip
kariy Feb 19, 2026
1ef4231
wip
kariy Feb 19, 2026
5e05c31
wip
kariy Feb 20, 2026
e5f8d73
wip
kariy Feb 20, 2026
f7391a0
wip
kariy Feb 20, 2026
9118f6e
wip
kariy Feb 21, 2026
7935667
wip
kariy Feb 21, 2026
1c7b1a3
wip
kariy Feb 21, 2026
5217e3d
wip
kariy Feb 21, 2026
fc439e7
wip
kariy Feb 23, 2026
e35b307
wip
kariy Feb 23, 2026
8e116cc
wip
kariy Feb 23, 2026
0ce6151
wip
kariy Feb 23, 2026
46a5636
wip
kariy Feb 23, 2026
ded3296
wip
kariy Feb 23, 2026
6df8734
wip
kariy Feb 23, 2026
14ebc36
add integration tests for ControllerDeployment middleware
kariy Feb 23, 2026
b505daf
wip
kariy Feb 23, 2026
cb4b134
fmt
kariy Feb 23, 2026
8145b26
wip
kariy Feb 23, 2026
bb2f363
wip
kariy Feb 24, 2026
cb07222
wip
kariy Feb 24, 2026
b700aea
Add custom Call serde for outside execution with renamed fields
kariy Feb 24, 2026
4eeb580
wip
kariy Feb 24, 2026
118ae02
wip
kariy Feb 24, 2026
5525164
wip
kariy Feb 24, 2026
8fca616
Merge branch 'main' into refactor/paymaster-v2
kariy Feb 25, 2026
c9838b1
rpc: extract controller deployment middleware context
kariy Feb 25, 2026
6a38439
rpc: remove unused controller deployment paymaster plumbing
kariy Feb 25, 2026
e24a554
rpc: refactor controller deployment middleware flow
kariy Feb 25, 2026
5b162f8
rpc-server: fix clone bounds and txpool trait imports
kariy Feb 26, 2026
712e173
rpc-server tests: fix pool trait imports and clean unused import
kariy Feb 26, 2026
6d60b64
Merge remote-tracking branch 'origin/main' into refactor/paymaster-v2
kariy Feb 26, 2026
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
392 changes: 268 additions & 124 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions bin/katana/src/cli/rpc/starknet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::{Args, Subcommand};
use katana_primitives::block::{BlockHash, BlockIdOrTag, BlockNumber, ConfirmedBlockIdOrTag};
use katana_primitives::class::ClassHash;
use katana_primitives::contract::StorageKey;
use katana_primitives::execution::{EntryPointSelector, FunctionCall};
use katana_primitives::execution::{Call, EntryPointSelector};
use katana_primitives::transaction::TxHash;
use katana_primitives::{ContractAddress, Felt};
use katana_rpc_types::event::{EventFilter, EventFilterWithPage, ResultPageRequest};
Expand Down Expand Up @@ -398,8 +398,7 @@ impl StarknetCommands {
let entry_point_selector = args.selector;
let calldata = args.calldata;

let function_call =
FunctionCall { contract_address, entry_point_selector, calldata };
let function_call = Call { contract_address, entry_point_selector, calldata };

let block_id = args.block.0;
let result = client.call(function_call, block_id).await?;
Expand Down
3 changes: 2 additions & 1 deletion crates/cartridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ build = "build.rs"
katana-contracts.workspace = true
katana-genesis.workspace = true
katana-primitives.workspace = true
katana-rpc-types = { path = "../rpc/rpc-types" }
katana-rpc-types = { workspace = true }

anyhow.workspace = true
ark-ff = "0.4.2"
cainome-cairo-serde.workspace = true
lazy_static.workspace = true
reqwest.workspace = true
serde.workspace = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ pub enum Error {

/// Client for interacting with the Cartridge service.
#[derive(Debug, Clone)]
pub struct Client {
pub struct CartridgeApiClient {
url: Url,
client: reqwest::Client,
}

impl Client {
impl CartridgeApiClient {
/// Creates a new [`CartridgeApiClient`] with the given URL.
pub fn new(url: Url) -> Self {
Self { url, client: reqwest::Client::new() }
Expand Down
15 changes: 7 additions & 8 deletions crates/cartridge/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

pub mod client;
pub mod api;
pub mod vrf;

pub use client::Client;
pub use vrf::{
bootstrap_vrf, get_vrf_account, resolve_executable, wait_for_http_ok, InfoResponse,
RequestContext, SignedOutsideExecution, VrfAccountCredentials, VrfBootstrap,
VrfBootstrapConfig, VrfBootstrapResult, VrfClient, VrfClientError, VrfOutsideExecution,
VrfService, VrfServiceConfig, VrfServiceProcess, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT,
VRF_HARDCODED_SECRET_KEY, VRF_SERVER_PORT,
pub use api::CartridgeApiClient;
pub use vrf::server::{
bootstrap_vrf, get_vrf_account, resolve_executable, wait_for_http_ok, VrfAccountCredentials,
VrfBootstrap, VrfBootstrapConfig, VrfBootstrapResult, VrfServer, VrfServerConfig,
VrfServiceProcess, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT, VRF_HARDCODED_SECRET_KEY,
VRF_SERVER_PORT,
};

#[rustfmt::skip]
Expand Down
26 changes: 24 additions & 2 deletions crates/cartridge/src/vrf/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use katana_primitives::Felt;
use cainome_cairo_serde::CairoSerde;
use katana_primitives::execution::Call;
use katana_primitives::{ContractAddress, Felt};
use katana_rpc_types::outside_execution::{OutsideExecutionV2, OutsideExecutionV3};
use serde::{Deserialize, Serialize};
use starknet::macros::selector;
use url::Url;

#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
Expand All @@ -25,11 +28,30 @@ pub enum VrfOutsideExecution {
/// A signed outside execution request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedOutsideExecution {
pub address: Felt,
pub address: ContractAddress,
pub outside_execution: VrfOutsideExecution,
pub signature: Vec<Felt>,
}

impl From<SignedOutsideExecution> for Call {
fn from(value: SignedOutsideExecution) -> Self {
let (entry_point_selector, mut calldata) = match &value.outside_execution {
VrfOutsideExecution::V2(v) => {
let calldata = OutsideExecutionV2::cairo_serialize(v);
(selector!("execute_from_outside_v2"), calldata)
}
VrfOutsideExecution::V3(v) => {
let calldata = OutsideExecutionV3::cairo_serialize(v);
(selector!("execute_from_outside_v3"), calldata)
}
};

calldata.extend(value.signature);

Call { contract_address: value.address, entry_point_selector, calldata }
}
}

/// Response from GET /info endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InfoResponse {
Expand Down
179 changes: 2 additions & 177 deletions crates/cartridge/src/vrf/mod.rs
Original file line number Diff line number Diff line change
@@ -1,181 +1,6 @@
//! VRF (Verifiable Random Function) support for Cartridge.
//!
//! This module provides:
//! - VRF client for communicating with the VRF server
//! - Bootstrap logic for deploying VRF contracts
//! - Sidecar process management
//! Cartridge VRF (Verifiable Random Function) service.

mod bootstrap;
mod client;
pub mod server;

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration, Instant};
use std::{env, io};

pub use bootstrap::{
bootstrap_vrf, get_vrf_account, VrfAccountCredentials, VrfBootstrap, VrfBootstrapConfig,
VrfBootstrapResult, BOOTSTRAP_TIMEOUT, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT,
VRF_HARDCODED_SECRET_KEY,
};
pub use client::*;
use katana_primitives::{ContractAddress, Felt};
use tokio::process::{Child, Command};
use tokio::time::sleep;
use tracing::{debug, info, warn};
use url::Url;

const LOG_TARGET: &str = "katana::cartridge::vrf::sidecar";

pub const VRF_SERVER_PORT: u16 = 3000;
const DEFAULT_VRF_SERVICE_PATH: &str = "vrf-server";
pub const SIDECAR_TIMEOUT: Duration = Duration::from_secs(10);

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("bootstrap_result not set - call bootstrap() or bootstrap_result()")]
BootstrapResultNotSet,
#[error("sidecar binary not found at {0}")]
BinaryNotFound(PathBuf),
#[error("sidecar binary '{0}' not found in PATH")]
BinaryNotInPath(PathBuf),
#[error("PATH environment variable is not set")]
PathNotSet,
#[error("failed to spawn VRF sidecar")]
Spawn(#[source] io::Error),
#[error("VRF sidecar did not become ready before timeout")]
SidecarTimeout,
#[error("bootstrap failed")]
Bootstrap(#[source] anyhow::Error),
}

pub type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug, Clone)]
pub struct VrfServiceConfig {
pub vrf_account_address: ContractAddress,
pub vrf_private_key: Felt,
pub secret_key: u64,
}

#[derive(Debug, Clone)]
pub struct VrfService {
config: VrfServiceConfig,
path: PathBuf,
}

impl VrfService {
pub fn new(config: VrfServiceConfig) -> Self {
Self { config, path: PathBuf::from(DEFAULT_VRF_SERVICE_PATH) }
}

/// Sets the path to the vrf service program.
///
/// If no path is set, the default executable name [`DEFAULT_VRF_SERVICE_PATH`] will be used.
pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
self.path = path.into();
self
}

pub async fn start(self) -> Result<VrfServiceProcess> {
let bin = resolve_executable(&self.path)?;

let mut command = Command::new(bin);
command
.arg("--port")
.arg(VRF_SERVER_PORT.to_string())
.arg("--account-address")
.arg(self.config.vrf_account_address.to_hex_string())
.arg("--account-private-key")
.arg(self.config.vrf_private_key.to_hex_string())
.arg("--secret-key")
.arg(self.config.secret_key.to_string())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.kill_on_drop(true);

let process = command.spawn().map_err(Error::Spawn)?;
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), VRF_SERVER_PORT);

let url = Url::parse(&format!("http://{addr}")).expect("valid url");
let client = VrfClient::new(url);
wait_for_http_ok(&client, "vrf info", SIDECAR_TIMEOUT).await?;

info!(%addr, "VRF service started.");

Ok(VrfServiceProcess { process, addr, inner: self })
}
}

/// A running VRF sidecar process.
#[derive(Debug)]
pub struct VrfServiceProcess {
process: Child,
inner: VrfService,
addr: SocketAddr,
}

impl VrfServiceProcess {
/// Get the address of the VRF service.
pub fn addr(&self) -> &SocketAddr {
&self.addr
}

pub fn process(&mut self) -> &mut Child {
&mut self.process
}

pub fn config(&self) -> &VrfServiceConfig {
&self.inner.config
}

pub async fn shutdown(&mut self) -> io::Result<()> {
self.process.kill().await
}
}

/// Resolve an executable path, searching in PATH if necessary.
pub fn resolve_executable(path: &Path) -> Result<PathBuf> {
if path.components().count() > 1 {
return if path.is_file() {
Ok(path.to_path_buf())
} else {
Err(Error::BinaryNotFound(path.to_path_buf()))
};
}

let path_var = env::var_os("PATH").ok_or(Error::PathNotSet)?;
for dir in env::split_paths(&path_var) {
let candidate = dir.join(path);
if candidate.is_file() {
return Ok(candidate);
}
}

Err(Error::BinaryNotInPath(path.to_path_buf()))
}

/// Wait for the VRF sidecar to become ready by polling its `/info` endpoint.
pub async fn wait_for_http_ok(client: &VrfClient, name: &str, timeout: Duration) -> Result<()> {
let start = Instant::now();

loop {
match client.info().await {
Ok(_) => {
info!(target: LOG_TARGET, %name, "sidecar ready");
return Ok(());
}
Err(err) => {
debug!(target: LOG_TARGET, %name, error = %err, "waiting for sidecar");
}
}

if start.elapsed() > timeout {
warn!(target: LOG_TARGET, %name, "sidecar did not become ready in time");
return Err(Error::SidecarTimeout);
}

sleep(Duration::from_millis(200)).await;
}
}
Loading
Loading