Skip to content

Commit b908b1b

Browse files
authored
feat(FI-1784): add environment presets to configure ICP rosetta api (#5982)
Adds an `environment` flag to support running rosetta in different modes: - `deprecated-testnet`: that's the default and is there for now for backward compatibility. - `production`: this is the equivalent of `--mainnet`. `--mainnet` is still supported for backward compatibility, but eventually should be removed. - `test`: which connects to the test ledgers. For maintainability, the parsing of the `Opts` has been moved to `Parsed` structs.
1 parent 6b03784 commit b908b1b

File tree

2 files changed

+182
-83
lines changed

2 files changed

+182
-83
lines changed

rs/rosetta-api/icp/src/main.rs

Lines changed: 169 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use clap::Parser;
22
use ic_crypto_utils_threshold_sig_der::{
33
parse_threshold_sig_key, parse_threshold_sig_key_from_der,
44
};
5+
use ic_nns_constants::{GOVERNANCE_CANISTER_ID, LEDGER_CANISTER_ID};
56
use ic_rosetta_api::request_handler::RosettaRequestHandler;
67
use ic_rosetta_api::rosetta_server::{RosettaApiServer, RosettaApiServerOpt};
78
use ic_rosetta_api::{ledger_client, DEFAULT_BLOCKCHAIN, DEFAULT_TOKEN_SYMBOL};
9+
use ic_types::crypto::threshold_sig::ThresholdSigPublicKey;
810
use ic_types::{CanisterId, PrincipalId};
911
use rosetta_core::metrics::RosettaMetrics;
1012
use std::{path::Path, path::PathBuf, str::FromStr, sync::Arc};
@@ -18,6 +20,20 @@ use tracing_subscriber::util::SubscriberInitExt;
1820
use tracing_subscriber::{Layer, Registry};
1921
use url::Url;
2022

23+
const TEST_LEDGER_CANISTER_ID: &str = "xafvr-biaaa-aaaai-aql5q-cai";
24+
const TEST_TOKEN_SYMBOL: &str = "TESTICP";
25+
26+
#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Default)]
27+
enum Environment {
28+
#[clap(help = "Production ledger canister on mainnet")]
29+
Production,
30+
#[clap(help = "Test ledger canister on mainnet")]
31+
Test,
32+
#[clap(name = "deprecated-testnet", help = "Testnet environment (deprecated)")]
33+
#[default]
34+
DeprecatedTestnet,
35+
}
36+
2137
#[derive(Debug, Parser)]
2238
#[clap(next_help_heading = "Server Configuration")]
2339
struct ServerConfig {
@@ -60,6 +76,48 @@ struct NetworkConfig {
6076
root_key: Option<PathBuf>,
6177
}
6278

79+
#[derive(Debug)]
80+
struct ParsedNetworkConfig {
81+
pub ic_url: Url,
82+
pub root_key: Option<ThresholdSigPublicKey>,
83+
}
84+
85+
impl ParsedNetworkConfig {
86+
fn from_config(config: NetworkConfig, environment: &Environment) -> Result<Self, String> {
87+
const DEPRECATED_TESTNET_URL: &str = "https://exchanges.testnet.dfinity.network";
88+
const MAINNET_URL: &str = "https://ic0.app";
89+
const MAINNET_ROOT_KEY: &str = r#"MIGCMB0GDSsGAQQBgtx8BQMBAgEGDCsGAQQBgtx8BQMCAQNhAIFMDm7HH6tYOwi9gTc8JVw8NxsuhIY8mKTx4It0I10U+12cDNVG2WhfkToMCyzFNBWDv0tDkuRn25bWW5u0y3FxEvhHLg1aTRRQX/10hLASkQkcX4e5iINGP5gJGguqrg=="#;
90+
91+
let url_str = match &config.ic_url {
92+
Some(url_str) => url_str.as_str(),
93+
None => match environment {
94+
Environment::DeprecatedTestnet => DEPRECATED_TESTNET_URL,
95+
Environment::Production | Environment::Test => MAINNET_URL,
96+
},
97+
};
98+
let ic_url = Url::parse(url_str).map_err(|e| format!("Unable to parse --ic-url: {}", e))?;
99+
100+
let root_key = match config.root_key {
101+
Some(root_key_path) => Some(
102+
parse_threshold_sig_key(root_key_path.as_path())
103+
.map_err(|e| format!("Unable to parse root key from file: {}", e))?,
104+
),
105+
None => {
106+
match environment {
107+
Environment::Production | Environment::Test => {
108+
// The mainnet root key
109+
let decoded = base64::decode(MAINNET_ROOT_KEY).unwrap();
110+
Some(parse_threshold_sig_key_from_der(&decoded).unwrap())
111+
}
112+
Environment::DeprecatedTestnet => None,
113+
}
114+
}
115+
};
116+
117+
Ok(Self { ic_url, root_key })
118+
}
119+
}
120+
63121
#[derive(Debug, Parser)]
64122
#[clap(next_help_heading = "Canister Configuration")]
65123
struct CanisterConfig {
@@ -73,9 +131,73 @@ struct CanisterConfig {
73131
governance_canister_id: Option<String>,
74132
}
75133

134+
#[derive(Debug)]
135+
struct ParsedCanisterConfig {
136+
pub ledger_canister_id: CanisterId,
137+
pub token_symbol: String,
138+
pub governance_canister_id: CanisterId,
139+
}
140+
141+
impl ParsedCanisterConfig {
142+
fn from_config(config: CanisterConfig, environment: &Environment) -> Result<Self, String> {
143+
// Apply environment preset defaults when no explicit value provided
144+
let ledger_canister_id = match config.ledger_canister_id {
145+
Some(explicit_value) => CanisterId::unchecked_from_principal(
146+
PrincipalId::from_str(&explicit_value).map_err(|e| {
147+
format!("Invalid ledger canister ID '{}': {}", explicit_value, e)
148+
})?,
149+
),
150+
None => match environment {
151+
Environment::Test => CanisterId::unchecked_from_principal(
152+
PrincipalId::from_str(TEST_LEDGER_CANISTER_ID).map_err(|e| {
153+
format!(
154+
"Invalid test ledger canister ID '{}': {}",
155+
TEST_LEDGER_CANISTER_ID, e
156+
)
157+
})?,
158+
),
159+
Environment::Production | Environment::DeprecatedTestnet => LEDGER_CANISTER_ID,
160+
},
161+
};
162+
163+
let token_symbol = match config.token_symbol {
164+
Some(explicit_value) => explicit_value,
165+
None => match environment {
166+
Environment::Test => TEST_TOKEN_SYMBOL.to_string(),
167+
Environment::Production | Environment::DeprecatedTestnet => {
168+
DEFAULT_TOKEN_SYMBOL.to_string()
169+
}
170+
},
171+
};
172+
173+
let governance_canister_id = match config.governance_canister_id {
174+
Some(explicit_value) => CanisterId::unchecked_from_principal(
175+
PrincipalId::from_str(&explicit_value).map_err(|e| {
176+
format!("Invalid governance canister ID '{}': {}", explicit_value, e)
177+
})?,
178+
),
179+
None => GOVERNANCE_CANISTER_ID,
180+
};
181+
182+
Ok(Self {
183+
ledger_canister_id,
184+
token_symbol,
185+
governance_canister_id,
186+
})
187+
}
188+
}
189+
76190
#[derive(Debug, Parser)]
77191
#[clap(version)]
78192
struct Opt {
193+
#[clap(
194+
short = 'e',
195+
long = "environment",
196+
default_value = "deprecated-testnet",
197+
help = "Environment preset that configures network and canister settings."
198+
)]
199+
environment: Environment,
200+
79201
#[clap(flatten)]
80202
server: ServerConfig,
81203

@@ -117,24 +239,6 @@ struct Opt {
117239
enable_rosetta_blocks: bool,
118240
}
119241

120-
impl Opt {
121-
fn default_url(&self) -> Url {
122-
let url = if self.mainnet {
123-
"https://ic0.app"
124-
} else {
125-
"https://exchanges.testnet.dfinity.network"
126-
};
127-
Url::parse(url).unwrap()
128-
}
129-
130-
fn ic_url(&self) -> Result<Url, String> {
131-
match self.network.ic_url.as_ref() {
132-
None => Ok(self.default_url()),
133-
Some(s) => Url::parse(s).map_err(|e| format!("Unable to parse --ic-url: {}", e)),
134-
}
135-
}
136-
}
137-
138242
fn init_logging(level: Level) -> std::io::Result<WorkerGuard> {
139243
std::fs::create_dir_all("log")?;
140244

@@ -186,6 +290,26 @@ async fn main() -> std::io::Result<()> {
186290
warn!("--log-config-file is deprecated and ignored")
187291
}
188292

293+
// Check for conflicting flags
294+
if opt.mainnet && opt.environment != Environment::DeprecatedTestnet {
295+
eprintln!("Cannot specify both --mainnet and --environment flags. Please use --environment production instead of --mainnet.");
296+
std::process::exit(1);
297+
}
298+
299+
// Handle mainnet flag by treating it as environment production
300+
let environment = if opt.mainnet {
301+
warn!("--mainnet flag is deprecated. Please use --environment production instead.");
302+
Environment::Production
303+
} else {
304+
opt.environment
305+
};
306+
307+
if environment == Environment::DeprecatedTestnet {
308+
warn!(
309+
"deprecated-testnet environment is deprecated. Please use --environment test instead."
310+
);
311+
}
312+
189313
let pkg_name = env!("CARGO_PKG_NAME");
190314
let pkg_version = env!("CARGO_PKG_VERSION");
191315
info!("Starting {}, pkg_version: {}", pkg_name, pkg_version);
@@ -196,66 +320,25 @@ async fn main() -> std::io::Result<()> {
196320
};
197321
info!("Listening on {}:{}", opt.server.address, listen_port);
198322
let addr = format!("{}:{}", opt.server.address, listen_port);
199-
let url = opt.ic_url().unwrap();
200-
info!("Internet Computer URL set to {}", url);
201-
202-
let (root_key, canister_id, governance_canister_id) = if opt.mainnet {
203-
let root_key = match opt.network.root_key {
204-
Some(root_key_path) => parse_threshold_sig_key(root_key_path.as_path())?,
205-
None => {
206-
// The mainnet root key
207-
let root_key_text = r#"MIGCMB0GDSsGAQQBgtx8BQMBAgEGDCsGAQQBgtx8BQMCAQNhAIFMDm7HH6tYOwi9gTc8JVw8NxsuhIY8mKTx4It0I10U+12cDNVG2WhfkToMCyzFNBWDv0tDkuRn25bWW5u0y3FxEvhHLg1aTRRQX/10hLASkQkcX4e5iINGP5gJGguqrg=="#;
208-
let decoded = base64::decode(root_key_text).unwrap();
209-
parse_threshold_sig_key_from_der(&decoded).unwrap()
210-
}
211-
};
212323

213-
let canister_id = match opt.canister.ledger_canister_id {
214-
Some(cid) => {
215-
CanisterId::unchecked_from_principal(PrincipalId::from_str(&cid[..]).unwrap())
216-
}
217-
None => ic_nns_constants::LEDGER_CANISTER_ID,
218-
};
324+
let network_config = ParsedNetworkConfig::from_config(opt.network, &environment)
325+
.unwrap_or_else(|e| {
326+
error!("Configuration error: {}", e);
327+
std::process::exit(1);
328+
});
329+
info!("Internet Computer URL set to {}", network_config.ic_url);
219330

220-
let governance_canister_id = match opt.canister.governance_canister_id {
221-
Some(cid) => {
222-
CanisterId::unchecked_from_principal(PrincipalId::from_str(&cid[..]).unwrap())
223-
}
224-
None => ic_nns_constants::GOVERNANCE_CANISTER_ID,
225-
};
226-
227-
(Some(root_key), canister_id, governance_canister_id)
228-
} else {
229-
let root_key = match opt.network.root_key {
230-
Some(root_key_path) => Some(parse_threshold_sig_key(root_key_path.as_path())?),
231-
None => {
232-
warn!("Data certificate will not be verified due to missing root key");
233-
None
234-
}
235-
};
236-
237-
let canister_id = match opt.canister.ledger_canister_id {
238-
Some(cid) => {
239-
CanisterId::unchecked_from_principal(PrincipalId::from_str(&cid[..]).unwrap())
240-
}
241-
None => ic_nns_constants::LEDGER_CANISTER_ID,
242-
};
243-
244-
let governance_canister_id = match opt.canister.governance_canister_id {
245-
Some(cid) => {
246-
CanisterId::unchecked_from_principal(PrincipalId::from_str(&cid[..]).unwrap())
247-
}
248-
None => ic_nns_constants::GOVERNANCE_CANISTER_ID,
249-
};
331+
if network_config.root_key.is_none() {
332+
warn!("Data certificate will not be verified due to missing root key");
333+
}
250334

251-
(root_key, canister_id, governance_canister_id)
252-
};
335+
let canister_config = ParsedCanisterConfig::from_config(opt.canister, &environment)
336+
.unwrap_or_else(|e| {
337+
error!("Configuration error: {}", e);
338+
std::process::exit(1);
339+
});
253340

254-
let token_symbol = opt
255-
.canister
256-
.token_symbol
257-
.unwrap_or_else(|| DEFAULT_TOKEN_SYMBOL.to_string());
258-
info!("Token symbol set to {}", token_symbol);
341+
info!("Token symbol set to {}", canister_config.token_symbol);
259342

260343
let store_location: Option<&Path> = match opt.storage.store_type.as_ref() {
261344
"sqlite" => Some(&opt.storage.location),
@@ -272,7 +355,6 @@ async fn main() -> std::io::Result<()> {
272355
let Opt {
273356
offline,
274357
exit_on_sync,
275-
mainnet,
276358
not_whitelisted,
277359
expose_metrics,
278360
blockchain,
@@ -289,29 +371,33 @@ async fn main() -> std::io::Result<()> {
289371
enable_rosetta_blocks = opt.enable_rosetta_blocks;
290372
}
291373

374+
// Determine effective mainnet setting based on the environment
375+
let effective_mainnet =
376+
environment == Environment::Production || environment == Environment::Test;
377+
292378
let client = ledger_client::LedgerClient::new(
293-
url,
294-
canister_id,
295-
token_symbol,
296-
governance_canister_id,
379+
network_config.ic_url,
380+
canister_config.ledger_canister_id,
381+
canister_config.token_symbol,
382+
canister_config.governance_canister_id,
297383
store_location,
298384
opt.storage.max_blocks,
299385
offline,
300-
root_key,
386+
network_config.root_key,
301387
enable_rosetta_blocks,
302388
opt.storage.optimize_indexes,
303389
)
304390
.await
305391
.map_err(|e| {
306-
let msg = if mainnet && !not_whitelisted && e.is_internal_error_403() {
392+
let msg = if effective_mainnet && !not_whitelisted && e.is_internal_error_403() {
307393
", You may not be whitelisted; please try running the Rosetta server again with the '--not_whitelisted' flag"
308394
} else {""};
309395
(e, msg)
310396
})
311397
.unwrap_or_else(|(e, is_403)| panic!("Failed to initialize ledger client{}: {:?}", is_403, e));
312398

313399
let ledger = Arc::new(client);
314-
let canister_id_str = canister_id.to_string();
400+
let canister_id_str = canister_config.ledger_canister_id.to_string();
315401
let rosetta_metrics = RosettaMetrics::new("ICP".to_string(), canister_id_str);
316402
let req_handler = RosettaRequestHandler::new(blockchain, ledger.clone(), rosetta_metrics);
317403

@@ -335,7 +421,7 @@ async fn main() -> std::io::Result<()> {
335421
serv.run(RosettaApiServerOpt {
336422
exit_on_sync,
337423
offline,
338-
mainnet,
424+
mainnet: effective_mainnet,
339425
not_whitelisted,
340426
})
341427
.await

rs/rosetta-api/icp/tests/integration_tests/tests/tests.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder};
2222
use rosetta_core::objects::ObjectMap;
2323
use serde::Deserialize;
2424
use std::path::PathBuf;
25+
use std::process::Command;
2526
use std::thread::sleep;
2627
use std::time::{Duration, SystemTime};
2728
use tempfile::TempDir;
@@ -1324,3 +1325,15 @@ async fn test_network_status_single_genesis_transaction() {
13241325
genesis_block.block_identifier
13251326
);
13261327
}
1328+
1329+
#[test]
1330+
fn test_mainnet_and_env_flag_set_returns_error() {
1331+
let output = Command::new(get_rosetta_path())
1332+
.args(["--environment", "test", "--mainnet"])
1333+
.output()
1334+
.expect("Failed to execute binary");
1335+
1336+
assert!(!output.status.success());
1337+
let stderr = String::from_utf8_lossy(&output.stderr);
1338+
assert!(stderr.contains("Cannot specify both --mainnet and --environment flags"));
1339+
}

0 commit comments

Comments
 (0)