Skip to content

Commit e510597

Browse files
kariyclaude
andcommitted
feat(rpc): add synchronous Katana RPC API for transaction submission
Add katana_addInvokeTransaction, katana_addDeclareTransaction, and katana_addDeployAccountTransaction RPC methods that submit a transaction and wait for the receipt before returning. Also fix missing `vrf` feature in katana-node's `cartridge` feature flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 62600ea commit e510597

File tree

9 files changed

+299
-1
lines changed

9 files changed

+299
-1
lines changed

crates/node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ cartridge = [
6060
"katana-rpc-api/cartridge",
6161
"katana-rpc-server/cartridge",
6262
"paymaster",
63+
"vrf",
6364
]
6465
explorer = ["katana-rpc-server/explorer"]
6566
grpc = ["dep:katana-grpc"]

crates/node/src/config/rpc.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub const DEFAULT_RPC_MAX_CALL_GAS: u64 = 1_000_000_000;
3232
pub enum RpcModuleKind {
3333
Starknet,
3434
Dev,
35+
Katana,
3536
#[cfg(feature = "cartridge")]
3637
Cartridge,
3738
#[cfg(feature = "tee")]
@@ -103,6 +104,7 @@ impl RpcModulesList {
103104
pub fn all() -> Self {
104105
Self(HashSet::from([
105106
RpcModuleKind::Starknet,
107+
RpcModuleKind::Katana,
106108
RpcModuleKind::Dev,
107109
#[cfg(feature = "cartridge")]
108110
RpcModuleKind::Cartridge,

crates/node/src/full/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use katana_metrics::{MetricsServer, MetricsServerHandle, Report};
1717
use katana_pipeline::{Pipeline, PipelineHandle};
1818
use katana_pool::ordering::TipOrdering;
1919
use katana_provider::DbProviderFactory;
20+
use katana_rpc_api::katana::KatanaApiServer;
2021
use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer};
2122
use katana_rpc_server::cors::Cors;
2223
use katana_rpc_server::starknet::{StarknetApi, StarknetApiConfig};
@@ -195,6 +196,7 @@ impl Node {
195196
rpc_modules.merge(StarknetApiServer::into_rpc(starknet_api.clone()))?;
196197
rpc_modules.merge(StarknetWriteApiServer::into_rpc(starknet_api.clone()))?;
197198
rpc_modules.merge(StarknetTraceApiServer::into_rpc(starknet_api.clone()))?;
199+
rpc_modules.merge(KatanaApiServer::into_rpc(starknet_api.clone()))?;
198200
}
199201

200202
#[allow(unused_mut)]

crates/node/src/lib.rs

100644100755
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use katana_provider::{
3939
#[cfg(feature = "cartridge")]
4040
use katana_rpc_api::cartridge::CartridgeApiServer;
4141
use katana_rpc_api::dev::DevApiServer;
42+
use katana_rpc_api::katana::KatanaApiServer;
4243
#[cfg(feature = "paymaster")]
4344
use katana_rpc_api::paymaster::PaymasterApiServer;
4445
use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer};
@@ -318,6 +319,10 @@ where
318319
rpc_modules.merge(StarknetTraceApiServer::into_rpc(starknet_api.clone()))?;
319320
}
320321

322+
if config.rpc.apis.contains(&RpcModuleKind::Starknet) {
323+
rpc_modules.merge(KatanaApiServer::into_rpc(starknet_api.clone()))?;
324+
}
325+
321326
if config.rpc.apis.contains(&RpcModuleKind::Dev) {
322327
let api = DevApi::new(backend.clone(), block_producer.clone());
323328
rpc_modules.merge(DevApiServer::into_rpc(api))?;

crates/rpc/rpc-api/src/katana.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use jsonrpsee::core::RpcResult;
2+
use jsonrpsee::proc_macros::rpc;
3+
use katana_rpc_types::broadcasted::{
4+
BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx,
5+
};
6+
use katana_rpc_types::receipt::TxReceiptWithBlockInfo;
7+
8+
/// Katana-specific JSON-RPC methods.
9+
#[cfg_attr(not(feature = "client"), rpc(server, namespace = "katana"))]
10+
#[cfg_attr(feature = "client", rpc(client, server, namespace = "katana"))]
11+
pub trait KatanaApi {
12+
/// Submit a new invoke transaction and wait until the receipt is available.
13+
#[method(name = "addInvokeTransaction")]
14+
async fn add_invoke_transaction(
15+
&self,
16+
invoke_transaction: BroadcastedInvokeTx,
17+
) -> RpcResult<TxReceiptWithBlockInfo>;
18+
19+
/// Submit a new declare transaction and wait until the receipt is available.
20+
#[method(name = "addDeclareTransaction")]
21+
async fn add_declare_transaction(
22+
&self,
23+
declare_transaction: BroadcastedDeclareTx,
24+
) -> RpcResult<TxReceiptWithBlockInfo>;
25+
26+
/// Submit a new deploy account transaction and wait until the receipt is available.
27+
#[method(name = "addDeployAccountTransaction")]
28+
async fn add_deploy_account_transaction(
29+
&self,
30+
deploy_account_transaction: BroadcastedDeployAccountTx,
31+
) -> RpcResult<TxReceiptWithBlockInfo>;
32+
}

crates/rpc/rpc-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
pub mod dev;
44
pub mod error;
5+
pub mod katana;
56
pub mod starknet;
67
pub mod starknet_ext;
78

crates/rpc/rpc-client/src/starknet.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,45 @@ impl Client {
281281
.map_err(Into::into)
282282
}
283283

284+
/// Submit a new transaction and wait until the receipt is available.
285+
pub async fn katana_add_invoke_transaction(
286+
&self,
287+
invoke_transaction: BroadcastedInvokeTx,
288+
) -> Result<TxReceiptWithBlockInfo> {
289+
katana_rpc_api::katana::KatanaApiClient::add_invoke_transaction(
290+
&self.client,
291+
invoke_transaction,
292+
)
293+
.await
294+
.map_err(Into::into)
295+
}
296+
297+
/// Submit a new class declaration transaction and wait until the receipt is available.
298+
pub async fn katana_add_declare_transaction(
299+
&self,
300+
declare_transaction: BroadcastedDeclareTx,
301+
) -> Result<TxReceiptWithBlockInfo> {
302+
katana_rpc_api::katana::KatanaApiClient::add_declare_transaction(
303+
&self.client,
304+
declare_transaction,
305+
)
306+
.await
307+
.map_err(Into::into)
308+
}
309+
310+
/// Submit a new deploy account transaction and wait until the receipt is available.
311+
pub async fn katana_add_deploy_account_transaction(
312+
&self,
313+
deploy_account_transaction: BroadcastedDeployAccountTx,
314+
) -> Result<TxReceiptWithBlockInfo> {
315+
katana_rpc_api::katana::KatanaApiClient::add_deploy_account_transaction(
316+
&self.client,
317+
deploy_account_transaction,
318+
)
319+
.await
320+
.map_err(Into::into)
321+
}
322+
284323
////////////////////////////////////////////////////////////////////////////
285324
// Trace API methods
286325
////////////////////////////////////////////////////////////////////////////

crates/rpc/rpc-server/src/starknet/write.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1+
use std::time::Duration;
2+
13
use jsonrpsee::core::{async_trait, RpcResult};
24
use katana_pool::TransactionPool;
3-
use katana_provider::ProviderFactory;
5+
use katana_primitives::transaction::TxHash;
6+
use katana_provider::{ProviderFactory, ProviderRO};
47
use katana_rpc_api::error::starknet::StarknetApiError;
8+
use katana_rpc_api::katana::KatanaApiServer;
59
use katana_rpc_api::starknet::StarknetWriteApiServer;
610
use katana_rpc_types::broadcasted::{
711
AddDeclareTransactionResponse, AddDeployAccountTransactionResponse,
812
AddInvokeTransactionResponse, BroadcastedDeclareTx, BroadcastedDeployAccountTx,
913
BroadcastedInvokeTx,
1014
};
15+
use katana_rpc_types::receipt::TxReceiptWithBlockInfo;
1116
use katana_rpc_types::{BroadcastedTx, BroadcastedTxWithChainId};
1217

1318
use super::StarknetApi;
1419
use crate::starknet::pending::PendingBlockProvider;
1520

21+
const TX_RECEIPT_POLL_INTERVAL: Duration = Duration::from_millis(100);
22+
1623
impl<Pool, PoolTx, Pending, PF> StarknetApi<Pool, Pending, PF>
1724
where
1825
Pool: TransactionPool<Transaction = PoolTx> + Send + Sync + 'static,
@@ -79,6 +86,30 @@ where
7986
}
8087
}
8188

89+
impl<Pool, PoolTx, Pending, PF> StarknetApi<Pool, Pending, PF>
90+
where
91+
Pool: TransactionPool<Transaction = PoolTx> + Send + Sync + 'static,
92+
PoolTx: From<BroadcastedTxWithChainId>,
93+
Pending: PendingBlockProvider,
94+
PF: ProviderFactory,
95+
<PF as ProviderFactory>::Provider: ProviderRO,
96+
{
97+
pub(super) async fn wait_for_tx_receipt(
98+
&self,
99+
transaction_hash: TxHash,
100+
) -> Result<TxReceiptWithBlockInfo, StarknetApiError> {
101+
loop {
102+
match self.receipt(transaction_hash).await {
103+
Ok(receipt) => return Ok(receipt),
104+
Err(StarknetApiError::TxnHashNotFound) => {
105+
tokio::time::sleep(TX_RECEIPT_POLL_INTERVAL).await;
106+
}
107+
Err(error) => return Err(error),
108+
}
109+
}
110+
}
111+
}
112+
82113
#[async_trait]
83114
impl<Pool, PoolTx, Pending, PF> StarknetWriteApiServer for StarknetApi<Pool, Pending, PF>
84115
where
@@ -108,3 +139,37 @@ where
108139
Ok(self.add_deploy_account_tx(deploy_account_transaction).await?)
109140
}
110141
}
142+
143+
#[async_trait]
144+
impl<Pool, PoolTx, Pending, PF> KatanaApiServer for StarknetApi<Pool, Pending, PF>
145+
where
146+
Pool: TransactionPool<Transaction = PoolTx> + Send + Sync + 'static,
147+
PoolTx: From<BroadcastedTxWithChainId>,
148+
Pending: PendingBlockProvider,
149+
PF: ProviderFactory,
150+
<PF as ProviderFactory>::Provider: ProviderRO,
151+
{
152+
async fn add_invoke_transaction(
153+
&self,
154+
invoke_transaction: BroadcastedInvokeTx,
155+
) -> RpcResult<TxReceiptWithBlockInfo> {
156+
let response = self.add_invoke_tx(invoke_transaction).await?;
157+
Ok(self.wait_for_tx_receipt(response.transaction_hash).await?)
158+
}
159+
160+
async fn add_declare_transaction(
161+
&self,
162+
declare_transaction: BroadcastedDeclareTx,
163+
) -> RpcResult<TxReceiptWithBlockInfo> {
164+
let response = self.add_declare_tx(declare_transaction).await?;
165+
Ok(self.wait_for_tx_receipt(response.transaction_hash).await?)
166+
}
167+
168+
async fn add_deploy_account_transaction(
169+
&self,
170+
deploy_account_transaction: BroadcastedDeployAccountTx,
171+
) -> RpcResult<TxReceiptWithBlockInfo> {
172+
let response = self.add_deploy_account_tx(deploy_account_transaction).await?;
173+
Ok(self.wait_for_tx_receipt(response.transaction_hash).await?)
174+
}
175+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use std::path::PathBuf;
2+
3+
use assert_matches::assert_matches;
4+
use katana_genesis::constant::DEFAULT_STRK_FEE_TOKEN_ADDRESS;
5+
use katana_primitives::block::BlockIdOrTag;
6+
use katana_primitives::{felt, Felt};
7+
use katana_rpc_api::katana::KatanaApiClient;
8+
use katana_rpc_types::broadcasted::{
9+
BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx,
10+
};
11+
use katana_rpc_types::receipt::{RpcDeployAccountTxReceipt, RpcTxReceipt};
12+
use katana_utils::node::test_config;
13+
use katana_utils::TestNode;
14+
use starknet::accounts::{
15+
Account, AccountFactory, ConnectedAccount, OpenZeppelinAccountFactory as OZAccountFactory,
16+
};
17+
use starknet::signers::{LocalWallet, SigningKey};
18+
19+
mod common;
20+
21+
use common::{Erc20Contract, Uint256};
22+
23+
fn convert_broadcasted_tx<T, U>(tx: T) -> U
24+
where
25+
T: serde::Serialize,
26+
U: serde::de::DeserializeOwned,
27+
{
28+
let value = serde_json::to_value(tx).expect("failed to serialize tx");
29+
serde_json::from_value(value).expect("failed to deserialize tx")
30+
}
31+
32+
#[tokio::test]
33+
async fn katana_add_transactions_return_receipts() {
34+
let mut config = test_config();
35+
config.dev.fee = false;
36+
config.dev.account_validation = false;
37+
38+
let sequencer = TestNode::new_with_config(config).await;
39+
let rpc_client = sequencer.rpc_http_client();
40+
let provider = sequencer.starknet_rpc_client();
41+
let account = sequencer.account();
42+
43+
// -----------------------------------------------------------------------
44+
// katana_addInvokeTransaction
45+
46+
let erc20 = Erc20Contract::new(DEFAULT_STRK_FEE_TOKEN_ADDRESS.into(), &account);
47+
let recipient = Felt::ONE;
48+
let amount = Uint256 { low: Felt::ONE, high: Felt::ZERO };
49+
50+
let fee = erc20.transfer(&recipient, &amount).estimate_fee().await.unwrap();
51+
let nonce = account.get_nonce().await.unwrap();
52+
53+
let prepared_invoke = erc20
54+
.transfer(&recipient, &amount)
55+
.nonce(nonce)
56+
.l1_gas(fee.l1_gas_consumed)
57+
.l1_gas_price(fee.l1_gas_price)
58+
.l2_gas(fee.l2_gas_consumed)
59+
.l2_gas_price(fee.l2_gas_price)
60+
.l1_data_gas(fee.l1_data_gas_consumed)
61+
.l1_data_gas_price(fee.l1_data_gas_price)
62+
.tip(0)
63+
.prepared()
64+
.unwrap();
65+
66+
let invoke_tx: BroadcastedInvokeTx =
67+
convert_broadcasted_tx(prepared_invoke.get_invoke_request(false, false).await.unwrap());
68+
69+
let invoke_receipt =
70+
KatanaApiClient::add_invoke_transaction(&rpc_client, invoke_tx).await.unwrap();
71+
assert_matches!(invoke_receipt.receipt, RpcTxReceipt::Invoke(_));
72+
73+
// -----------------------------------------------------------------------
74+
// katana_addDeclareTransaction
75+
76+
let path: PathBuf = PathBuf::from("tests/test_data/cairo1_contract.json");
77+
let (contract, compiled_class_hash) =
78+
common::prepare_contract_declaration_params(&path).unwrap();
79+
80+
let fee = account
81+
.declare_v3(contract.clone().into(), compiled_class_hash)
82+
.estimate_fee()
83+
.await
84+
.unwrap();
85+
let nonce = account.get_nonce().await.unwrap();
86+
87+
let prepared_declare = account
88+
.declare_v3(contract.into(), compiled_class_hash)
89+
.nonce(nonce)
90+
.l1_gas(fee.l1_gas_consumed)
91+
.l1_gas_price(fee.l1_gas_price)
92+
.l2_gas(fee.l2_gas_consumed)
93+
.l2_gas_price(fee.l2_gas_price)
94+
.l1_data_gas(fee.l1_data_gas_consumed)
95+
.l1_data_gas_price(fee.l1_data_gas_price)
96+
.tip(0)
97+
.prepared()
98+
.unwrap();
99+
100+
let declare_tx: BroadcastedDeclareTx =
101+
convert_broadcasted_tx(prepared_declare.get_declare_request(false, false).await.unwrap());
102+
103+
let declare_receipt =
104+
KatanaApiClient::add_declare_transaction(&rpc_client, declare_tx).await.unwrap();
105+
assert_matches!(declare_receipt.receipt, RpcTxReceipt::Declare(_));
106+
107+
// -----------------------------------------------------------------------
108+
// katana_addDeployAccountTransaction
109+
110+
let chain_id = provider.chain_id().await.unwrap();
111+
let signer = LocalWallet::from(SigningKey::from_random());
112+
let class_hash = provider
113+
.get_class_hash_at(BlockIdOrTag::PreConfirmed, account.address().into())
114+
.await
115+
.unwrap();
116+
let salt = felt!("0x123");
117+
118+
let factory =
119+
OZAccountFactory::new(class_hash, chain_id, &signer, account.provider()).await.unwrap();
120+
121+
let deploy_account_tx = factory.deploy_v3(salt);
122+
let deployed_address = deploy_account_tx.address();
123+
124+
let fee = deploy_account_tx.estimate_fee().await.unwrap();
125+
let nonce = deploy_account_tx.fetch_nonce().await.unwrap();
126+
127+
let prepared_deploy = factory
128+
.deploy_v3(salt)
129+
.nonce(nonce)
130+
.l1_gas(fee.l1_gas_consumed)
131+
.l1_gas_price(fee.l1_gas_price)
132+
.l2_gas(fee.l2_gas_consumed)
133+
.l2_gas_price(fee.l2_gas_price)
134+
.l1_data_gas(fee.l1_data_gas_consumed)
135+
.l1_data_gas_price(fee.l1_data_gas_price)
136+
.tip(0)
137+
.prepared()
138+
.unwrap();
139+
140+
let deploy_tx: BroadcastedDeployAccountTx =
141+
convert_broadcasted_tx(prepared_deploy.get_deploy_request(false, false).await.unwrap());
142+
143+
let deploy_receipt =
144+
KatanaApiClient::add_deploy_account_transaction(&rpc_client, deploy_tx).await.unwrap();
145+
146+
assert_matches!(
147+
deploy_receipt.receipt,
148+
RpcTxReceipt::DeployAccount(RpcDeployAccountTxReceipt { contract_address, .. })
149+
=> assert_eq!(contract_address, deployed_address)
150+
);
151+
}

0 commit comments

Comments
 (0)