1use alloy::{
3 network::EthereumWallet, primitives::Address, providers::ProviderBuilder, signers::local::PrivateKeySigner,
4 transports::http::reqwest::Url,
5};
6use clap::Parser;
7use eyre::{Context, Result};
8use newton_prover_chainio::{
9 policy_client::get_policy_address_for_client,
10 version::{
11 get_policy_data_factory_for_policy_data, get_policy_data_factory_version, get_policy_factory_for_policy,
12 get_policy_factory_version,
13 },
14};
15use newton_prover_core::{
16 config::rpc::RpcProviderConfig,
17 newton_policy::NewtonPolicy,
18 newton_policy_client::NewtonPolicyClient,
19 newton_policy_data::NewtonPolicyData,
20 newton_policy_data_factory::NewtonPolicyDataFactory,
21 newton_policy_factory::NewtonPolicyFactory,
22 version::{is_compatible, MIN_COMPATIBLE_VERSION, PROTOCOL_VERSION},
23};
24use serde::Serialize;
25use std::str::FromStr;
26use tracing::{debug, error, info, warn};
27
28#[derive(Parser, Debug)]
29pub enum VersionCommand {
30 CheckCompatibility {
32 #[arg(long)]
34 policy_client: Address,
35
36 #[arg(long)]
38 chain_id: u64,
39
40 #[arg(long)]
42 rpc_url: Option<String>,
43 },
44
45 Migrate {
47 #[arg(long)]
49 policy_client: Address,
50
51 #[arg(long, env = "PRIVATE_KEY")]
53 private_key: String,
54
55 #[arg(long)]
57 chain_id: u64,
58
59 #[arg(long)]
61 rpc_url: Option<String>,
62
63 #[arg(long)]
65 skip_check: bool,
66
67 #[arg(long)]
69 dry_run: bool,
70 },
71
72 Info,
74}
75
76#[derive(Serialize, Debug)]
77pub struct CompatibilityReport {
78 pub protocol_version: String,
79 pub policy_client: Address,
80 pub policy_address: Address,
81 pub policy_factory_version: String,
82 pub policy_compatible: bool,
83 pub policy_data_reports: Vec<PolicyDataReport>,
84 pub all_compatible: bool,
85 pub migration_required: bool,
86}
87
88#[derive(Serialize, Debug)]
89pub struct PolicyDataReport {
90 pub address: Address,
91 pub factory_version: String,
92 pub compatible: bool,
93}
94
95impl VersionCommand {
96 pub async fn run(self) -> Result<()> {
97 match self {
98 VersionCommand::CheckCompatibility {
99 policy_client,
100 chain_id,
101 rpc_url,
102 } => check_compatibility(policy_client, chain_id, rpc_url).await,
103 VersionCommand::Migrate {
104 policy_client,
105 private_key,
106 chain_id,
107 rpc_url,
108 skip_check,
109 dry_run,
110 } => migrate_policy(policy_client, private_key, chain_id, rpc_url, skip_check, dry_run).await,
111 VersionCommand::Info => {
112 print_version_info();
113 Ok(())
114 }
115 }
116 }
117}
118
119async fn check_compatibility(policy_client: Address, chain_id: u64, rpc_url: Option<String>) -> Result<()> {
120 let rpc_url = rpc_url.unwrap_or_else(|| {
121 RpcProviderConfig::load(chain_id)
122 .expect("Failed to load RPC config")
123 .http
124 });
125
126 info!("Checking version compatibility for policy client: {}", policy_client);
127 info!("Protocol version: {}", PROTOCOL_VERSION);
128
129 let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
131 info!("Policy address: {}", policy_address);
132
133 let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
135 let policy_version = get_policy_factory_version(policy_factory, &rpc_url).await?;
136 info!("Policy factory version: {}", policy_version);
137
138 let policy_compatible = is_compatible(&policy_version, MIN_COMPATIBLE_VERSION)?;
140 if policy_compatible {
141 info!("✓ Policy version is compatible");
142 } else {
143 warn!(
144 "✗ Policy version is INCOMPATIBLE (minimum required: v{})",
145 MIN_COMPATIBLE_VERSION
146 );
147 }
148
149 info!("Checking policy data...");
151
152 let provider = ProviderBuilder::new().connect_http(rpc_url.parse()?);
154 let policy_contract = NewtonPolicy::new(policy_address, provider);
155
156 let policy_data_addresses = policy_contract
157 .getPolicyData()
158 .call()
159 .await
160 .context("Failed to get policy data addresses")?;
161
162 info!("Found {} policy data contracts", policy_data_addresses.len());
163
164 let mut policy_data_reports = Vec::new();
165 let mut all_compatible = policy_compatible;
166
167 for policy_data_addr in policy_data_addresses {
168 debug!("Checking policy data at {}...", policy_data_addr);
169
170 match check_policy_data_compatibility(policy_data_addr, &rpc_url).await {
171 Ok((version, compatible)) => {
172 debug!("Version: {}, Compatible: {}", version, compatible);
173 policy_data_reports.push(PolicyDataReport {
174 address: policy_data_addr,
175 factory_version: version,
176 compatible,
177 });
178 all_compatible = all_compatible && compatible;
179 }
180 Err(e) => {
181 error!("Error checking compatibility: {}", e);
182 policy_data_reports.push(PolicyDataReport {
184 address: policy_data_addr,
185 factory_version: "unknown".to_string(),
186 compatible: false,
187 });
188 all_compatible = false;
189 }
190 }
191 }
192
193 let report = CompatibilityReport {
194 protocol_version: PROTOCOL_VERSION.to_string(),
195 policy_client,
196 policy_address,
197 policy_factory_version: policy_version.clone(),
198 policy_compatible,
199 policy_data_reports,
200 all_compatible,
201 migration_required: !all_compatible,
202 };
203
204 info!("=== Compatibility Report ===");
205 info!("{}", serde_json::to_string_pretty(&report)?);
206
207 if report.migration_required {
208 warn!("Migration required!");
209 info!("To migrate your policy to the latest version:");
210 info!(" newton-cli policy migrate --policy-client {} \\", policy_client);
211 info!(" --private-key $YOUR_PRIVATE_KEY \\");
212 info!(" --chain-id {}", chain_id);
213 info!("Guide: https://docs.newton.xyz/versioning/migration");
214 std::process::exit(1);
215 } else {
216 info!("✓ All versions are compatible. No migration needed.");
217 Ok(())
218 }
219}
220
221fn print_version_info() {
222 info!("Newton Protocol Version Information");
223 info!("====================================");
224 info!("Protocol Version: {}", PROTOCOL_VERSION);
225 info!("Minimum Compatible Version: {}", MIN_COMPATIBLE_VERSION);
226 info!("Version Compatibility:");
227 info!(" - Major versions must match exactly");
228 info!(" - Minor version must be >= minimum");
229 info!(" - Patch version is ignored (backward-compatible bug fixes)");
230 info!("For more information, see: https://docs.newton.xyz/versioning");
231}
232
233async fn check_policy_data_compatibility(policy_data_addr: Address, rpc_url: &str) -> Result<(String, bool)> {
235 let factory = get_policy_data_factory_for_policy_data(policy_data_addr, rpc_url).await?;
236 let version = get_policy_data_factory_version(factory, rpc_url).await?;
237 let compatible = is_compatible(&version, MIN_COMPATIBLE_VERSION)?;
238 Ok((version, compatible))
239}
240
241async fn get_policy_info(policy_address: Address, rpc_url: &str) -> Result<PolicyInfo> {
243 let provider = ProviderBuilder::new().connect_http(rpc_url.parse()?);
244 let policy_contract = NewtonPolicy::new(policy_address, provider);
245
246 let policy_cid = policy_contract
247 .getPolicyCid()
248 .call()
249 .await
250 .context("Failed to get policy CID")?;
251
252 let schema_cid = policy_contract
253 .getSchemaCid()
254 .call()
255 .await
256 .context("Failed to get schema CID")?;
257
258 let entrypoint = policy_contract
259 .getEntrypoint()
260 .call()
261 .await
262 .context("Failed to get entrypoint")?;
263
264 let metadata_cid = policy_contract
265 .getMetadataCid()
266 .call()
267 .await
268 .context("Failed to get metadata CID")?;
269
270 let policy_data = policy_contract
271 .getPolicyData()
272 .call()
273 .await
274 .context("Failed to get policy data addresses")?;
275
276 Ok(PolicyInfo {
277 policy_cid,
278 schema_cid,
279 entrypoint,
280 metadata_cid,
281 policy_data,
282 })
283}
284
285#[derive(Debug, Clone)]
286struct PolicyInfo {
287 policy_cid: String,
288 schema_cid: String,
289 entrypoint: String,
290 metadata_cid: String,
291 policy_data: Vec<Address>,
292}
293
294async fn migrate_policy(
296 policy_client: Address,
297 private_key: String,
298 chain_id: u64,
299 rpc_url: Option<String>,
300 skip_check: bool,
301 dry_run: bool,
302) -> Result<()> {
303 let rpc_url = rpc_url.unwrap_or_else(|| {
304 RpcProviderConfig::load(chain_id)
305 .expect("Failed to load RPC config")
306 .http
307 });
308
309 info!("=== Policy Migration Tool ===");
310 info!("Policy Client: {}", policy_client);
311 info!("Chain ID: {}", chain_id);
312 info!("Protocol Version: {}", PROTOCOL_VERSION);
313
314 if !skip_check {
316 info!("Step 1: Checking current compatibility...");
317
318 let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
319 let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
320 let current_version = get_policy_factory_version(policy_factory, &rpc_url).await?;
321
322 info!("Current policy version: {}", current_version);
323 info!("Minimum required version: {}", MIN_COMPATIBLE_VERSION);
324
325 let is_compatible_now = is_compatible(¤t_version, MIN_COMPATIBLE_VERSION)?;
326
327 if is_compatible_now {
328 info!("✓ Policy is already compatible!");
329 info!("No migration needed.");
330 return Ok(());
331 }
332
333 warn!("✗ Policy is incompatible and needs migration");
334 }
335
336 info!("Step 2: Reading current policy configuration...");
338
339 let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
341 info!("Current policy address: {}", policy_address);
342
343 let policy_info = get_policy_info(policy_address, &rpc_url).await?;
345 info!("- Policy CID: {}", policy_info.policy_cid);
346 info!("- Schema CID: {}", policy_info.schema_cid);
347 info!("- Entrypoint: {}", policy_info.entrypoint);
348 info!("- Metadata CID: {}", policy_info.metadata_cid);
349 info!("- Policy Data count: {}", policy_info.policy_data.len());
350
351 info!("Step 3: Deploying new policy data (if needed) and new policy with latest factory version...");
353
354 let mut new_policy_data_addresses = Vec::new();
356 let mut incompatible_policy_data = Vec::new();
357
358 for (i, policy_data_addr) in policy_info.policy_data.iter().enumerate() {
359 debug!("Checking policy data {} at {}...", i + 1, policy_data_addr);
360
361 match check_policy_data_compatibility(*policy_data_addr, &rpc_url).await {
362 Ok((version, compatible)) => {
363 debug!("Version: {}, Compatible: {}", version, compatible);
364 if !compatible {
365 incompatible_policy_data.push(*policy_data_addr);
366 } else {
367 new_policy_data_addresses.push(*policy_data_addr);
369 }
370 }
371 Err(e) => {
372 error!("Error checking compatibility: {}", e);
373 incompatible_policy_data.push(*policy_data_addr);
374 }
375 }
376 }
377
378 if !incompatible_policy_data.is_empty() {
379 info!(
380 "Step 3a: Migrating {} incompatible policy data contracts...",
381 incompatible_policy_data.len()
382 );
383
384 if dry_run {
385 warn!(
386 "DRY RUN MODE - Would migrate {} incompatible policy data contracts:",
387 incompatible_policy_data.len()
388 );
389 for addr in &incompatible_policy_data {
390 info!("- {}", addr);
391 }
392 for _ in &incompatible_policy_data {
394 new_policy_data_addresses.push(Address::ZERO);
395 }
396 } else {
397 let private_key_str = private_key.strip_prefix("0x").unwrap_or(&private_key);
399 let signer = PrivateKeySigner::from_str(private_key_str).context("Failed to parse private key")?;
400 let signer_address = signer.address();
401 let wallet = EthereumWallet::from(signer);
402
403 let url = Url::parse(&rpc_url).context("Invalid RPC URL")?;
405 let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
406
407 for (i, policy_data_addr) in incompatible_policy_data.iter().enumerate() {
408 info!(
409 "Migrating policy data {} of {}: {}",
410 i + 1,
411 incompatible_policy_data.len(),
412 policy_data_addr
413 );
414
415 let policy_data_factory = get_policy_data_factory_for_policy_data(*policy_data_addr, &rpc_url).await?;
417 info!("Using policy data factory at: {}", policy_data_factory);
418
419 let policy_data_contract = NewtonPolicyData::new(*policy_data_addr, provider.clone());
421 let wasm_cid = policy_data_contract
422 .getWasmCid()
423 .call()
424 .await
425 .context("Failed to get WASM CID")?;
426 let secrets_schema_cid = policy_data_contract
427 .getSecretsSchemaCid()
428 .call()
429 .await
430 .context("Failed to get secrets schema")?;
431 let expire_after = policy_data_contract
432 .getExpireAfter()
433 .call()
434 .await
435 .context("Failed to get expireAfter")?;
436 let metadata_cid = policy_data_contract
437 .getMetadataCid()
438 .call()
439 .await
440 .context("Failed to get metadata CID")?;
441
442 info!("- WASM CID: {}", wasm_cid);
443 info!("- Secrets Schema CID: {}", secrets_schema_cid);
444 info!("- Expire After: {}", expire_after);
445 info!("- Metadata CID: {}", metadata_cid);
446
447 let factory_contract = NewtonPolicyDataFactory::new(policy_data_factory, provider.clone());
449
450 info!("Submitting deployPolicyData transaction...");
451 let tx = factory_contract
452 .deployPolicyData(wasm_cid, secrets_schema_cid, expire_after, metadata_cid, signer_address)
453 .send()
454 .await
455 .context("Failed to send deployPolicyData transaction")?;
456
457 info!("Transaction sent: {:?}", tx.tx_hash());
458 info!("Waiting for transaction confirmation...");
459
460 let receipt = tx.get_receipt().await.context("Failed to get transaction receipt")?;
461
462 let logs = receipt.inner.logs();
464 let policy_data_deployed_event = logs
465 .iter()
466 .find_map(|log| log.log_decode::<NewtonPolicyDataFactory::PolicyDataDeployed>().ok())
467 .ok_or_else(|| eyre::eyre!("PolicyDataDeployed event not found in transaction logs"))?;
468
469 let new_policy_data = policy_data_deployed_event.data().policyData;
470 info!("✓ New policy data deployed at: {}", new_policy_data);
471 info!(
472 "Factory version: {}",
473 policy_data_deployed_event.data().implementationVersion
474 );
475
476 new_policy_data_addresses.push(new_policy_data);
477 }
478
479 info!("✓ All incompatible policy data contracts migrated successfully");
480 }
481 } else {
482 info!("✓ All policy data contracts are compatible, no migration needed");
483 }
484
485 info!("Step 3b: Deploying new policy with latest factory version...");
487
488 let new_policy_address = if dry_run {
489 info!("DRY RUN MODE - Would deploy new policy with:");
490 info!("- Factory version: {} (latest)", PROTOCOL_VERSION);
491 info!("- Entrypoint: {}", policy_info.entrypoint);
492 info!("- Policy CID: {}", policy_info.policy_cid);
493 info!("- Schema CID: {}", policy_info.schema_cid);
494 info!("- Policy Data: {} contracts", new_policy_data_addresses.len());
495 info!("- Metadata CID: {}", policy_info.metadata_cid);
496 info!("Note: This would call NewtonPolicyFactory.deployPolicy() with the above parameters");
497 Address::ZERO } else {
499 info!("Deploying new policy using latest factory...");
500
501 let private_key_str = private_key.strip_prefix("0x").unwrap_or(&private_key);
503 let signer = PrivateKeySigner::from_str(private_key_str).context("Failed to parse private key")?;
504 let signer_address = signer.address();
505 let wallet = EthereumWallet::from(signer);
506
507 let url = Url::parse(&rpc_url).context("Invalid RPC URL")?;
509 let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
510
511 let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
513 info!("Using policy factory at: {}", policy_factory);
514
515 let factory_contract = NewtonPolicyFactory::new(policy_factory, provider.clone());
516
517 info!("Submitting deployPolicy transaction...");
519 let tx = factory_contract
520 .deployPolicy(
521 policy_info.entrypoint.clone(),
522 policy_info.policy_cid.clone(),
523 policy_info.schema_cid.clone(),
524 new_policy_data_addresses.clone(),
525 policy_info.metadata_cid.clone(),
526 signer_address,
527 )
528 .send()
529 .await
530 .context("Failed to send deployPolicy transaction")?;
531
532 info!("Transaction sent: {:?}", tx.tx_hash());
533 info!("Waiting for transaction confirmation...");
534
535 let receipt = tx.get_receipt().await.context("Failed to get transaction receipt")?;
536
537 let logs = receipt.inner.logs();
539 let policy_deployed_event = logs
540 .iter()
541 .find_map(|log| log.log_decode::<NewtonPolicyFactory::PolicyDeployed>().ok())
542 .ok_or_else(|| eyre::eyre!("PolicyDeployed event not found in transaction logs"))?;
543
544 let new_policy = policy_deployed_event.data().policy;
545 info!("✓ New policy deployed at: {}", new_policy);
546 info!(
547 "Factory version: {}",
548 policy_deployed_event.data().implementationVersion
549 );
550
551 new_policy
552 };
553
554 info!("Step 4: Updating policy client to use new policy...");
556
557 if dry_run {
558 info!(
559 "DRY RUN MODE - Would call setPolicyAddress({}) on policy client {}",
560 new_policy_address, policy_client
561 );
562 info!("Note: This calls the owner-only setPolicyAddress() function on the existing policy client");
563 info!("No redeployment needed - the policy client address stays the same");
564 } else {
565 let private_key_str = private_key.strip_prefix("0x").unwrap_or(&private_key);
567 let signer = PrivateKeySigner::from_str(private_key_str).context("Failed to parse private key")?;
568 let wallet = EthereumWallet::from(signer);
569 let url = Url::parse(&rpc_url).context("Invalid RPC URL")?;
570 let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
571
572 let policy_client_contract = NewtonPolicyClient::new(policy_client, provider);
573
574 info!(
575 "Calling setPolicyAddress({}) on policy client {}...",
576 new_policy_address, policy_client
577 );
578
579 let tx = policy_client_contract
580 .setPolicyAddress(new_policy_address)
581 .send()
582 .await
583 .context("Failed to send setPolicyAddress transaction. Ensure the signer is the policy client owner.")?;
584
585 info!("Transaction sent: {:?}", tx.tx_hash());
586 info!("Waiting for confirmation...");
587
588 let receipt = tx
589 .get_receipt()
590 .await
591 .context("Failed to get setPolicyAddress receipt")?;
592
593 if !receipt.status() {
594 return Err(eyre::eyre!(
595 "setPolicyAddress transaction reverted (tx: {:?}). Possible causes:\n\
596 - Signer is not the policy client owner\n\
597 - New policy factory version is incompatible with TaskManager minimum",
598 receipt.transaction_hash
599 ));
600 }
601
602 info!(
603 "✓ Policy client {} now points to new policy {}",
604 policy_client, new_policy_address
605 );
606 }
607
608 info!("Step 5: Verifying migration...");
610
611 if dry_run {
612 info!("DRY RUN MODE - Would verify:");
613 info!("✓ New policy is deployed and accessible");
614 info!("✓ Policy client correctly references new policy");
615 info!("✓ New policy version is compatible with protocol v{}", PROTOCOL_VERSION);
616 info!("✓ All policy data is migrated or compatible");
617 info!("Verification would include:");
618 info!("1. Calling getPolicyAddress() on policy client");
619 info!("2. Checking factory version of new policy");
620 info!("3. Verifying policy configuration matches original");
621 info!("4. Testing policy functionality with a test transaction");
622 } else {
623 info!("Verifying migration...");
624
625 let current_policy = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
627 if current_policy != new_policy_address {
628 return Err(eyre::eyre!(
629 "Verification failed: Policy client points to {}, expected {}",
630 current_policy,
631 new_policy_address
632 ));
633 }
634 info!("✓ Policy client correctly references new policy");
635
636 let new_policy_factory = get_policy_factory_for_policy(new_policy_address, &rpc_url).await?;
638 let new_policy_version = get_policy_factory_version(new_policy_factory, &rpc_url).await?;
639
640 if !is_compatible(&new_policy_version, MIN_COMPATIBLE_VERSION)? {
641 return Err(eyre::eyre!(
642 "Verification failed: New policy version {} is not compatible with minimum required {}",
643 new_policy_version,
644 MIN_COMPATIBLE_VERSION
645 ));
646 }
647 info!("✓ New policy version {} is compatible", new_policy_version);
648
649 let new_policy_info = get_policy_info(new_policy_address, &rpc_url).await?;
651 if new_policy_info.policy_cid != policy_info.policy_cid {
652 warn!(
653 "Policy CID mismatch: old={}, new={}",
654 policy_info.policy_cid, new_policy_info.policy_cid
655 );
656 }
657 if new_policy_info.schema_cid != policy_info.schema_cid {
658 warn!(
659 "Schema CID mismatch: old={}, new={}",
660 policy_info.schema_cid, new_policy_info.schema_cid
661 );
662 }
663 if new_policy_info.entrypoint != policy_info.entrypoint {
664 warn!(
665 "Entrypoint mismatch: old={}, new={}",
666 policy_info.entrypoint, new_policy_info.entrypoint
667 );
668 }
669 if new_policy_info.metadata_cid != policy_info.metadata_cid {
670 warn!(
671 "Metadata CID mismatch: old={}, new={}",
672 policy_info.metadata_cid, new_policy_info.metadata_cid
673 );
674 }
675 info!("✓ Policy configuration verified");
676
677 if new_policy_info.policy_data.len() != new_policy_data_addresses.len() {
679 return Err(eyre::eyre!(
680 "Verification failed: Policy data count mismatch. Expected {}, got {}",
681 new_policy_data_addresses.len(),
682 new_policy_info.policy_data.len()
683 ));
684 }
685
686 for (i, addr) in new_policy_info.policy_data.iter().enumerate() {
687 if let Ok(factory) = get_policy_data_factory_for_policy_data(*addr, &rpc_url).await {
688 if let Ok(version) = get_policy_data_factory_version(factory, &rpc_url).await {
689 if !is_compatible(&version, MIN_COMPATIBLE_VERSION)? {
690 return Err(eyre::eyre!(
691 "Verification failed: Policy data {} version {} is not compatible",
692 i + 1,
693 version
694 ));
695 }
696 }
697 }
698 }
699 info!("✓ All policy data verified as compatible");
700
701 info!("✓ Migration verified successfully!");
702 }
703
704 info!("=== Migration Summary ===");
705
706 if dry_run {
707 info!("✓ Dry run completed successfully");
708 info!("The following would be performed:");
709 info!("1. Deploy new policy at factory version {}", PROTOCOL_VERSION);
710 if !incompatible_policy_data.is_empty() {
711 info!(
712 "2. Migrate {} incompatible policy data contracts",
713 incompatible_policy_data.len()
714 );
715 } else {
716 info!("2. Reuse all existing policy data (all compatible)");
717 }
718 info!("3. Update policy client {} to reference new policy", policy_client);
719 info!("4. Verify migration success");
720 info!("");
721 info!("Remove --dry-run to execute the actual migration");
722 } else {
723 info!("✓ Migration completed successfully!");
724 info!("Summary:");
725 info!("- Old policy: {}", policy_address);
726 info!("- New policy: {}", new_policy_address);
727 info!("- Policy client: {}", policy_client);
728 if !incompatible_policy_data.is_empty() {
729 info!("- Migrated {} policy data contracts", incompatible_policy_data.len());
730 }
731 }
732
733 Ok(())
734}