Skip to content

Commit b6781f9

Browse files
authored
feat: cartridge paymaster support (#22)
1 parent fb5df46 commit b6781f9

File tree

32 files changed

+956
-928
lines changed

32 files changed

+956
-928
lines changed

Cargo.lock

Lines changed: 121 additions & 479 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ debug = true
5555
inherits = "release"
5656

5757
[workspace.dependencies]
58-
cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.4.12", features = [ "abigen-rs" ] }
59-
cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.4.12" }
58+
cainome = { version = "0.5.0", features = [ "abigen-rs" ] }
59+
cainome-cairo-serde = { version = "0.1.0" }
6060
dojo-utils = { git = "https://github.com/dojoengine/dojo", tag = "v1.2.2" }
6161

6262
# metrics
@@ -104,6 +104,9 @@ cairo-lang-utils = "2.11.2"
104104
# only under the `test_utils` feature. So we expose through this feature.
105105
cairo-vm = { version = "1.0.2", features = [ "test_utils" ] }
106106

107+
# Controller PR revision until merged.
108+
# https://github.com/cartridge-gg/controller/pull/1454
109+
account_sdk = { git = "https://github.com/cartridge-gg/controller", rev = "dbbe0353d64de743739d425f8aab91ca3ac0e16f" }
107110
anyhow = "1.0.89"
108111
arbitrary = { version = "1.3.2", features = [ "derive" ] }
109112
assert_fs = "1.1"

bin/katana/src/cli/init/deployment.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ pub async fn check_program_info(
288288
let (program_info_res, facts_registry_res) =
289289
tokio::join!(appchain.get_program_info().call(), appchain.get_facts_registry().call());
290290

291-
let actual_program_info = program_info_res?;
292-
let facts_registry = facts_registry_res?;
291+
let actual_program_info = program_info_res.map_err(|e| ContractInitError::Other(anyhow!(e)))?;
292+
let facts_registry = facts_registry_res.map_err(|e| ContractInitError::Other(anyhow!(e)))?;
293293

294294
if actual_program_info.layout_bridge_program_hash != LAYOUT_BRIDGE_PROGRAM_HASH {
295295
return Err(ContractInitError::InvalidLayoutBridgeProgramHash {

crates/chain-spec/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,3 @@ katana-provider = { workspace = true, features = [ "test-utils" ] }
2626
rstest.workspace = true
2727
similar-asserts.workspace = true
2828
tempfile.workspace = true
29-
30-
[features]
31-
controller = [ "katana-primitives/controller" ]

crates/chain-spec/src/dev.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,6 @@ mod tests {
269269
use katana_primitives::genesis::allocation::{
270270
GenesisAccount, GenesisAccountAlloc, GenesisContractAlloc,
271271
};
272-
#[cfg(feature = "controller")]
273-
use katana_primitives::genesis::constant::{CONTROLLER_ACCOUNT_CLASS, CONTROLLER_CLASS_HASH};
274272
use katana_primitives::genesis::constant::{
275273
DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_HASH,
276274
DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT, DEFAULT_ACCOUNT_COMPILED_CLASS_HASH,
@@ -290,8 +288,6 @@ mod tests {
290288
(DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_LEGACY_UDC_CLASS.clone().into()),
291289
(DEFAULT_LEGACY_ERC20_CLASS_HASH, DEFAULT_LEGACY_ERC20_CLASS.clone().into()),
292290
(DEFAULT_ACCOUNT_CLASS_HASH, DEFAULT_ACCOUNT_CLASS.clone().into()),
293-
#[cfg(feature = "controller")]
294-
(CONTROLLER_CLASS_HASH, CONTROLLER_ACCOUNT_CLASS.clone().into()),
295291
]);
296292

297293
let allocations = [
@@ -383,11 +379,7 @@ mod tests {
383379

384380
similar_asserts::assert_eq!(actual_block, expected_block);
385381

386-
if cfg!(feature = "controller") {
387-
assert!(actual_state_updates.classes.len() == 4);
388-
} else {
389-
assert!(actual_state_updates.classes.len() == 3);
390-
}
382+
assert!(actual_state_updates.classes.len() == 3);
391383

392384
assert_eq!(
393385
actual_state_updates
@@ -460,21 +452,6 @@ mod tests {
460452
"The default oz account contract sierra class should be declared"
461453
);
462454

463-
#[cfg(feature = "controller")]
464-
{
465-
assert_eq!(
466-
actual_state_updates.state_updates.declared_classes.get(&CONTROLLER_CLASS_HASH),
467-
Some(&CONTROLLER_ACCOUNT_CLASS.clone().compile().unwrap().class_hash().unwrap()),
468-
"The controller account class should be declared"
469-
);
470-
471-
assert_eq!(
472-
actual_state_updates.classes.get(&CONTROLLER_CLASS_HASH),
473-
Some(&*CONTROLLER_ACCOUNT_CLASS),
474-
"The controller account contract sierra class should be declared"
475-
);
476-
}
477-
478455
// check that all contract allocations exist in the state updates
479456

480457
assert_eq!(

crates/cli/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ assert_matches.workspace = true
3333
starknet.workspace = true
3434

3535
[features]
36-
default = [ "server", "slot" ]
36+
cartridge = [
37+
"dep:katana-slot-controller",
38+
"katana-node/cartridge",
39+
"katana-rpc/cartridge",
40+
]
41+
default = [ "cartridge", "server", "slot" ]
3742
server = [ ]
38-
slot = [ "dep:katana-slot-controller", "katana-chain-spec/controller" ]
43+
slot = [ ]

crates/cli/src/args.rs

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use katana_node::config::dev::{DevConfig, FixedL1GasPriceConfig};
1717
use katana_node::config::execution::ExecutionConfig;
1818
use katana_node::config::fork::ForkingConfig;
1919
use katana_node::config::metrics::MetricsConfig;
20+
use katana_node::config::paymaster::PaymasterConfig;
2021
use katana_node::config::rpc::RpcConfig;
2122
#[cfg(feature = "server")]
2223
use katana_node::config::rpc::{RpcModuleKind, RpcModulesList};
@@ -117,6 +118,10 @@ pub struct NodeArgs {
117118

118119
#[command(flatten)]
119120
pub explorer: ExplorerOptions,
121+
122+
#[cfg(feature = "cartridge")]
123+
#[command(flatten)]
124+
pub cartridge: CartridgeOptions,
120125
}
121126

122127
impl NodeArgs {
@@ -167,6 +172,25 @@ impl NodeArgs {
167172
// the messagign config will eventually be removed slowly.
168173
let messaging = if cs_messaging.is_some() { cs_messaging } else { self.messaging.clone() };
169174

175+
#[cfg(feature = "cartridge")]
176+
{
177+
let paymaster = self.cartridge_config();
178+
179+
Ok(Config {
180+
db,
181+
dev,
182+
rpc,
183+
chain,
184+
metrics,
185+
forking,
186+
execution,
187+
messaging,
188+
paymaster,
189+
sequencing,
190+
})
191+
}
192+
193+
#[cfg(not(feature = "cartridge"))]
170194
Ok(Config { metrics, db, dev, rpc, chain, execution, sequencing, messaging, forking })
171195
}
172196

@@ -181,7 +205,8 @@ impl NodeArgs {
181205
fn rpc_config(&self) -> Result<RpcConfig> {
182206
#[cfg(feature = "server")]
183207
{
184-
let modules = if let Some(modules) = &self.server.http_modules {
208+
#[allow(unused_mut)]
209+
let mut modules = if let Some(modules) = &self.server.http_modules {
185210
// TODO: This check should be handled in the `katana-node` level. Right now if you
186211
// instantiate katana programmatically, you can still add the dev module without
187212
// enabling dev mode.
@@ -204,6 +229,14 @@ impl NodeArgs {
204229
modules
205230
};
206231

232+
// The cartridge rpc must be enabled if the paymaster is enabled.
233+
// We put it here so that even when the individual api are explicitly specified
234+
// (ie `--rpc.api`) we guarantee that the cartridge rpc is enabled.
235+
#[cfg(feature = "cartridge")]
236+
if self.cartridge.paymaster {
237+
modules.add(RpcModuleKind::Cartridge);
238+
}
239+
207240
let cors_origins = self.server.http_cors_origins.clone();
208241

209242
Ok(RpcConfig {
@@ -248,17 +281,18 @@ impl NodeArgs {
248281
chain_spec.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS;
249282
}
250283

251-
// generate dev accounts
284+
// Generate dev accounts.
285+
// If `cartridge` is enabled, the first account will be the paymaster.
252286
let accounts = DevAllocationsGenerator::new(self.development.total_accounts)
253287
.with_seed(parse_seed(&self.development.seed))
254288
.with_balance(U256::from(DEFAULT_PREFUNDED_ACCOUNT_BALANCE))
255289
.generate();
256290

257291
chain_spec.genesis.extend_allocations(accounts.into_iter().map(|(k, v)| (k, v.into())));
258292

259-
#[cfg(feature = "slot")]
260-
if self.slot.controller {
261-
katana_slot_controller::add_controller_account(&mut chain_spec.genesis)?;
293+
#[cfg(feature = "cartridge")]
294+
if self.cartridge.controllers || self.cartridge.paymaster {
295+
katana_slot_controller::add_controller_classes(&mut chain_spec.genesis);
262296
}
263297

264298
Ok((Arc::new(ChainSpec::Dev(chain_spec)), None))
@@ -328,6 +362,15 @@ impl NodeArgs {
328362
None
329363
}
330364

365+
#[cfg(feature = "cartridge")]
366+
fn cartridge_config(&self) -> Option<PaymasterConfig> {
367+
if self.cartridge.paymaster {
368+
Some(PaymasterConfig { cartridge_api_url: self.cartridge.api.clone() })
369+
} else {
370+
None
371+
}
372+
}
373+
331374
/// Parse the node config from the command line arguments and the config file,
332375
/// and merge them together prioritizing the command line arguments.
333376
pub fn with_config_file(mut self) -> Result<Self> {
@@ -389,6 +432,11 @@ impl NodeArgs {
389432
}
390433
}
391434

435+
#[cfg(feature = "cartridge")]
436+
{
437+
self.cartridge.merge(config.cartridge.as_ref());
438+
}
439+
392440
Ok(self)
393441
}
394442
}
@@ -676,4 +724,39 @@ chain_id.Named = "Mainnet"
676724

677725
assert!(config.rpc.apis.contains(&RpcModuleKind::Dev));
678726
}
727+
728+
#[cfg(feature = "cartridge")]
729+
#[test]
730+
fn cartridge_paymaster() {
731+
let args = NodeArgs::parse_from(["katana", "--cartridge.paymaster"]);
732+
let config = args.config().unwrap();
733+
734+
// Verify cartridge module is automatically enabled
735+
assert!(config.rpc.apis.contains(&RpcModuleKind::Cartridge));
736+
737+
// Test with paymaster explicitly specified in RPC modules
738+
let args =
739+
NodeArgs::parse_from(["katana", "--cartridge.paymaster", "--http.api", "starknet"]);
740+
let config = args.config().unwrap();
741+
742+
// Verify cartridge module is still enabled even when not in explicit RPC list
743+
assert!(config.rpc.apis.contains(&RpcModuleKind::Cartridge));
744+
assert!(config.rpc.apis.contains(&RpcModuleKind::Starknet));
745+
746+
// Verify that all the Controller classes are added to the genesis
747+
for (_, class) in katana_slot_controller::CONTROLLERS.iter() {
748+
assert!(config.chain.genesis().classes.get(&class.hash).is_some());
749+
}
750+
751+
// Test without paymaster enabled
752+
let args = NodeArgs::parse_from(["katana"]);
753+
let config = args.config().unwrap();
754+
755+
// Verify cartridge module is not enabled by default
756+
assert!(!config.rpc.apis.contains(&RpcModuleKind::Cartridge));
757+
758+
for (_, class) in katana_slot_controller::CONTROLLERS.iter() {
759+
assert!(config.chain.genesis().classes.get(&class.hash).is_none());
760+
}
761+
}
679762
}

crates/cli/src/file.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub struct NodeArgsConfig {
2525
pub server: Option<ServerOptions>,
2626
#[cfg(feature = "server")]
2727
pub metrics: Option<MetricsOptions>,
28+
#[cfg(feature = "cartridge")]
29+
pub cartridge: Option<CartridgeOptions>,
2830
}
2931

3032
impl NodeArgsConfig {
@@ -71,6 +73,15 @@ impl TryFrom<NodeArgs> for NodeArgsConfig {
7173
if args.metrics == MetricsOptions::default() { None } else { Some(args.metrics) };
7274
}
7375

76+
#[cfg(feature = "cartridge")]
77+
{
78+
node_config.cartridge = if args.cartridge == CartridgeOptions::default() {
79+
None
80+
} else {
81+
Some(args.cartridge)
82+
};
83+
}
84+
7485
Ok(node_config)
7586
}
7687
}

crates/cli/src/options.rs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ pub struct LoggingOptions {
369369
#[arg(default_value_t = LogFormat::Full)]
370370
pub log_format: LogFormat,
371371
}
372-
#[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq)]
372+
#[derive(Debug, Args, Default, Clone, Serialize, Deserialize, PartialEq)]
373373
#[command(next_help_heading = "Gas Price Oracle Options")]
374374
pub struct GasPriceOracleOptions {
375375
/// The L1 ETH gas price. (denominated in wei)
@@ -397,17 +397,6 @@ pub struct GasPriceOracleOptions {
397397
pub l1_strk_data_gas_price: Option<NonZeroU128>,
398398
}
399399

400-
impl Default for GasPriceOracleOptions {
401-
fn default() -> Self {
402-
Self {
403-
l1_eth_gas_price: None,
404-
l1_strk_gas_price: None,
405-
l1_eth_data_gas_price: None,
406-
l1_strk_data_gas_price: None,
407-
}
408-
}
409-
}
410-
411400
#[cfg(feature = "slot")]
412401
#[derive(Debug, Args, Clone, Serialize, Deserialize, Default, PartialEq)]
413402
#[command(next_help_heading = "Slot options")]
@@ -417,6 +406,63 @@ pub struct SlotOptions {
417406
pub controller: bool,
418407
}
419408

409+
#[cfg(feature = "cartridge")]
410+
#[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq)]
411+
#[command(next_help_heading = "Cartridge options")]
412+
pub struct CartridgeOptions {
413+
/// Declare all versions of the Controller class at genesis. This is implictly enabled if
414+
/// `--cartridge.paymaster` is provided.
415+
#[arg(long = "cartridge.controllers")]
416+
pub controllers: bool,
417+
418+
/// Whether to use the Cartridge paymaster.
419+
/// This has the cost to call the Cartridge API to check
420+
/// if a controller account exists on each estimate fee call.
421+
///
422+
/// Mostly used for local development using controller, and must be
423+
/// disabled for slot deployments.
424+
#[arg(long = "cartridge.paymaster")]
425+
#[arg(default_value_t = false)]
426+
#[serde(default)]
427+
pub paymaster: bool,
428+
429+
/// The root URL for the Cartridge API.
430+
///
431+
/// This is used to fetch the calldata for the constructor of the given controller
432+
/// address (at the moment). Must be configurable for local development
433+
/// with local cartridge API.
434+
#[arg(long = "cartridge.api", requires = "paymaster")]
435+
#[arg(default_value = "https://api.cartridge.gg")]
436+
#[serde(default = "default_api_url")]
437+
pub api: Url,
438+
}
439+
440+
#[cfg(feature = "cartridge")]
441+
impl CartridgeOptions {
442+
pub fn merge(&mut self, other: Option<&Self>) {
443+
if let Some(other) = other {
444+
if self.paymaster == default_paymaster() {
445+
self.paymaster = other.paymaster;
446+
}
447+
448+
if self.api == default_api_url() {
449+
self.api = other.api.clone();
450+
}
451+
}
452+
}
453+
}
454+
455+
#[cfg(feature = "cartridge")]
456+
impl Default for CartridgeOptions {
457+
fn default() -> Self {
458+
CartridgeOptions {
459+
controllers: false,
460+
paymaster: default_paymaster(),
461+
api: default_api_url(),
462+
}
463+
}
464+
}
465+
420466
#[derive(Debug, Default, Args, Clone, Serialize, Deserialize, PartialEq)]
421467
#[command(next_help_heading = "Explorer options")]
422468
pub struct ExplorerOptions {
@@ -519,3 +565,11 @@ where
519565
None => serializer.serialize_none(),
520566
}
521567
}
568+
569+
fn default_paymaster() -> bool {
570+
false
571+
}
572+
573+
fn default_api_url() -> Url {
574+
Url::parse("https://api.cartridge.gg").expect("qed; invalid url")
575+
}

0 commit comments

Comments
 (0)