Skip to content

Commit 200ee25

Browse files
Add DevnetProvider (#3760)
<!-- Reference any GitHub issues resolved by this PR --> ## Introduced changes Towards #3603 **Stack**: - #3762 - #3761 - #3760 ⬅ ## Checklist <!-- Make sure all of these are complete --> - [x] Linked relevant issue - [ ] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [ ] Added changes to `CHANGELOG.md` --------- Co-authored-by: Artur Michalek <[email protected]> Co-authored-by: Artur Michałek <[email protected]>
1 parent e7e4001 commit 200ee25

File tree

6 files changed

+193
-7
lines changed

6 files changed

+193
-7
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use crate::AccountData;
2+
use ::serde::{Deserialize, Serialize, de::DeserializeOwned};
3+
use anyhow::{Context, Error, ensure};
4+
use reqwest::Client;
5+
use serde_json::json;
6+
use starknet_types_core::felt::Felt;
7+
use url::Url;
8+
9+
/// A Devnet-RPC client.
10+
#[derive(Debug, Clone)]
11+
pub struct DevnetProvider {
12+
client: Client,
13+
url: Url,
14+
}
15+
16+
/// All Devnet-RPC methods as listed in the official docs.
17+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
18+
pub enum DevnetProviderMethod {
19+
#[serde(rename = "devnet_getConfig")]
20+
GetConfig,
21+
22+
#[serde(rename = "devnet_getPredeployedAccounts")]
23+
GetPredeployedAccounts,
24+
}
25+
26+
impl DevnetProvider {
27+
#[must_use]
28+
pub fn new(url: &str) -> Self {
29+
let url = Url::parse(url).expect("Invalid URL");
30+
Self {
31+
client: Client::new(),
32+
url,
33+
}
34+
}
35+
}
36+
37+
impl DevnetProvider {
38+
async fn send_request<P, R>(&self, method: DevnetProviderMethod, params: P) -> anyhow::Result<R>
39+
where
40+
P: Serialize + Send + Sync,
41+
R: DeserializeOwned,
42+
{
43+
let res = self
44+
.client
45+
.post(self.url.clone())
46+
.header("Content-Type", "application/json")
47+
.json(&json!({
48+
"jsonrpc": "2.0",
49+
"method": method,
50+
"params": params,
51+
"id": 1,
52+
}))
53+
.send()
54+
.await
55+
.context("Failed to send request")?
56+
.json::<serde_json::Value>()
57+
.await
58+
.context("Failed to parse response")?;
59+
60+
if let Some(error) = res.get("error") {
61+
Err(anyhow::anyhow!(error.to_string()))
62+
} else if let Some(result) = res.get("result") {
63+
serde_json::from_value(result.clone()).map_err(anyhow::Error::from)
64+
} else {
65+
panic!("Malformed RPC response: {res}")
66+
}
67+
}
68+
69+
/// Fetches the current Devnet configuration.
70+
pub async fn get_config(&self) -> Result<Config, Error> {
71+
self.send_request(DevnetProviderMethod::GetConfig, json!({}))
72+
.await
73+
}
74+
75+
/// Fetches the list of predeployed accounts in Devnet.
76+
pub async fn get_predeployed_accounts(&self) -> Result<Vec<PredeployedAccount>, Error> {
77+
self.send_request(DevnetProviderMethod::GetPredeployedAccounts, json!({}))
78+
.await
79+
}
80+
81+
/// Ensures the Devnet instance is alive.
82+
pub async fn ensure_alive(&self) -> Result<(), Error> {
83+
let is_alive = self
84+
.client
85+
.get(format!(
86+
"{}/is_alive",
87+
self.url.to_string().replace("/rpc", "")
88+
))
89+
.send()
90+
.await
91+
.map(|res| res.status().is_success())
92+
.unwrap_or(false);
93+
94+
ensure!(
95+
is_alive,
96+
"Node at {} is not responding to the Devnet health check (GET `/is_alive`). It may not be a Devnet instance or it may be down.",
97+
self.url
98+
);
99+
Ok(())
100+
}
101+
}
102+
103+
#[derive(Debug, Serialize, Deserialize)]
104+
pub struct Config {
105+
pub seed: u32,
106+
pub account_contract_class_hash: Felt,
107+
pub total_accounts: u8,
108+
}
109+
110+
#[derive(Debug, Serialize, Deserialize)]
111+
pub struct PredeployedAccount {
112+
pub address: Felt,
113+
pub private_key: Felt,
114+
pub public_key: Felt,
115+
}
116+
117+
impl From<&PredeployedAccount> for AccountData {
118+
fn from(predeployed_account: &PredeployedAccount) -> Self {
119+
Self {
120+
address: Some(predeployed_account.address),
121+
private_key: predeployed_account.private_key,
122+
public_key: predeployed_account.public_key,
123+
class_hash: None,
124+
salt: None,
125+
deployed: None,
126+
legacy: None,
127+
account_type: None,
128+
}
129+
}
130+
}

crates/sncast/src/helpers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod command;
55
pub mod config;
66
pub mod configuration;
77
pub mod constants;
8+
pub mod devnet_provider;
89
pub mod fee;
910
pub mod interactive;
1011
pub mod output_format;

crates/sncast/tests/helpers/constants.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ pub const SEPOLIA_RPC_URL: &str = "http://188.34.188.184:7070/rpc/v0_9";
88

99
pub const URL: &str = "http://127.0.0.1:5055/rpc";
1010
pub const NETWORK: &str = "testnet";
11-
pub const SEED: u32 = 1_053_545_548;
11+
pub const DEVNET_SEED: u32 = 1_053_545_548;
12+
pub const DEVNET_ACCOUNTS_NUMBER: u8 = 20;
1213

1314
// Block number used by devnet to fork the Sepolia testnet network in the tests
14-
pub const FORK_BLOCK_NUMBER: u32 = 721_720;
15+
pub const DEVNET_FORK_BLOCK_NUMBER: u32 = 721_720;
1516

1617
pub const CONTRACTS_DIR: &str = "tests/data/contracts";
1718
pub const SCRIPTS_DIR: &str = "tests/data/scripts";

crates/sncast/tests/helpers/devnet.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::helpers::constants::{FORK_BLOCK_NUMBER, SEED, SEPOLIA_RPC_URL, URL};
1+
use crate::helpers::constants::{
2+
DEVNET_ACCOUNTS_NUMBER, DEVNET_FORK_BLOCK_NUMBER, DEVNET_SEED, SEPOLIA_RPC_URL, URL,
3+
};
24
use crate::helpers::fixtures::{
35
deploy_braavos_account, deploy_cairo_0_account, deploy_keystore_account,
46
deploy_latest_oz_account, deploy_ready_account,
@@ -39,17 +41,17 @@ fn start_devnet() {
3941
"--port",
4042
&port,
4143
"--seed",
42-
&SEED.to_string(),
44+
&DEVNET_SEED.to_string(),
4345
"--state-archive-capacity",
4446
"full",
4547
"--fork-network",
4648
SEPOLIA_RPC_URL,
4749
"--fork-block",
48-
&FORK_BLOCK_NUMBER.to_string(),
50+
&DEVNET_FORK_BLOCK_NUMBER.to_string(),
4951
"--initial-balance",
5052
"9999999999999999999999999999999",
5153
"--accounts",
52-
"20",
54+
&DEVNET_ACCOUNTS_NUMBER.to_string(),
5355
])
5456
.stdout(Stdio::null())
5557
.spawn()
@@ -80,7 +82,7 @@ fn start_devnet() {
8082
#[dtor]
8183
fn stop_devnet() {
8284
let port = Url::parse(URL).unwrap().port().unwrap_or(80).to_string();
83-
let pattern = format!("starknet-devnet.*{port}.*{SEED}");
85+
let pattern = format!("starknet-devnet.*{port}.*{DEVNET_SEED}");
8486

8587
Command::new("pkill")
8688
.args(["-f", &pattern])
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::helpers::constants::{DEVNET_ACCOUNTS_NUMBER, DEVNET_SEED, SEPOLIA_RPC_URL, URL};
2+
use num_traits::ToPrimitive;
3+
use sncast::helpers::{constants::OZ_CLASS_HASH, devnet_provider::DevnetProvider};
4+
5+
#[tokio::test]
6+
async fn test_get_config() {
7+
let devnet_provider = DevnetProvider::new(URL);
8+
let config = devnet_provider
9+
.get_config()
10+
.await
11+
.expect("Failed to get config");
12+
13+
assert!(config.account_contract_class_hash == OZ_CLASS_HASH);
14+
assert!(config.seed == DEVNET_SEED);
15+
assert!(config.total_accounts == DEVNET_ACCOUNTS_NUMBER);
16+
}
17+
18+
#[tokio::test]
19+
async fn test_get_predeployed_accounts() {
20+
let devnet_provider = DevnetProvider::new(URL);
21+
let predeployed_accounts = devnet_provider
22+
.get_predeployed_accounts()
23+
.await
24+
.expect("Failed to get predeployed accounts");
25+
26+
assert!(predeployed_accounts.len().to_u8().unwrap() == DEVNET_ACCOUNTS_NUMBER);
27+
}
28+
29+
#[tokio::test]
30+
async fn test_is_alive_happy_case() {
31+
let devnet_provider = DevnetProvider::new(URL);
32+
devnet_provider
33+
.ensure_alive()
34+
.await
35+
.expect("Failed to ensure the devnet is alive");
36+
}
37+
38+
#[tokio::test]
39+
async fn test_is_alive_fails_on_sepolia_node() {
40+
let devnet_provider = DevnetProvider::new(SEPOLIA_RPC_URL);
41+
let res = devnet_provider.ensure_alive().await;
42+
assert!(res.is_err(), "Expected an error");
43+
44+
let err = res.unwrap_err().to_string();
45+
assert!(
46+
err == format!(
47+
"Node at {SEPOLIA_RPC_URL} is not responding to the Devnet health check (GET `/is_alive`). It may not be a Devnet instance or it may be down."
48+
),
49+
"Unexpected error message: {err}"
50+
);
51+
}

crates/sncast/tests/helpers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod constants;
22
pub mod devnet;
3+
pub mod devnet_provider;
34
pub mod env;
45
pub mod fee;
56
pub mod fixtures;

0 commit comments

Comments
 (0)