Skip to content

Commit b632d20

Browse files
kariyclaude
andauthored
feat(utils): add migrate methods to TestNode (#412)
Adds `migrate_spawn_and_move()` and `migrate_simple()` methods to `TestNode` for bootstrapping the blockchain's state. --------- Co-authored-by: Ammar Arif <kariy@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3661d53 commit b632d20

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/utils/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ async-trait.workspace = true
2525
futures.workspace = true
2626
rand.workspace = true
2727
starknet.workspace = true
28+
tempfile.workspace = true
2829
thiserror.workspace = true
2930
tokio = { workspace = true, features = [ "macros", "signal", "time" ], default-features = false }
3031

crates/utils/src/node.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use std::net::SocketAddr;
2+
use std::path::Path;
3+
use std::process::Command;
24
use std::sync::Arc;
35

46
use katana_chain_spec::{dev, ChainSpec};
@@ -23,6 +25,23 @@ use starknet::providers::{JsonRpcClient, Url};
2325
pub use starknet::providers::{Provider, ProviderError};
2426
use starknet::signers::{LocalWallet, SigningKey};
2527

28+
/// Errors that can occur when migrating contracts to a test node.
29+
#[derive(Debug, thiserror::Error)]
30+
pub enum MigrateError {
31+
#[error("Failed to create temp directory: {0}")]
32+
TempDir(#[from] std::io::Error),
33+
#[error("Git clone failed: {0}")]
34+
GitClone(String),
35+
#[error("Scarb build failed: {0}")]
36+
ScarbBuild(String),
37+
#[error("Sozo migrate failed: {0}")]
38+
SozoMigrate(String),
39+
#[error("Missing genesis account private key")]
40+
MissingPrivateKey,
41+
#[error("Spawn blocking task failed: {0}")]
42+
SpawnBlocking(#[from] tokio::task::JoinError),
43+
}
44+
2645
pub type ForkTestNode = TestNode<ForkProviderFactory>;
2746

2847
#[derive(Debug)]
@@ -124,6 +143,138 @@ where
124143
let client = self.rpc_http_client();
125144
katana_rpc_client::starknet::Client::new_with_client(client)
126145
}
146+
147+
/// Migrates the `spawn-and-move` example contracts from the dojo repository.
148+
///
149+
/// This method requires `git`, `asdf`, and `sozo` to be available in PATH.
150+
/// The scarb version is managed by asdf using the `.tool-versions` file
151+
/// in the dojo repository.
152+
pub async fn migrate_spawn_and_move(&self) -> Result<(), MigrateError> {
153+
self.migrate_example("spawn-and-move").await
154+
}
155+
156+
/// Migrates the `simple` example contracts from the dojo repository.
157+
///
158+
/// This method requires `git`, `asdf`, and `sozo` to be available in PATH.
159+
/// The scarb version is managed by asdf using the `.tool-versions` file
160+
/// in the dojo repository.
161+
pub async fn migrate_simple(&self) -> Result<(), MigrateError> {
162+
self.migrate_example("simple").await
163+
}
164+
165+
/// Migrates contracts from a dojo example project.
166+
///
167+
/// Clones the dojo repository, builds contracts with `scarb`, and deploys
168+
/// them with `sozo migrate`.
169+
///
170+
/// This method requires `git`, `asdf`, and `sozo` to be available in PATH.
171+
/// The scarb version is managed by asdf using the `.tool-versions` file
172+
/// in the dojo repository.
173+
async fn migrate_example(&self, example: &str) -> Result<(), MigrateError> {
174+
let rpc_url = format!("http://{}", self.rpc_addr());
175+
176+
let (address, account) = self
177+
.backend()
178+
.chain_spec
179+
.genesis()
180+
.accounts()
181+
.next()
182+
.expect("must have at least one genesis account");
183+
let private_key = account.private_key().ok_or(MigrateError::MissingPrivateKey)?;
184+
185+
let address_hex = address.to_string();
186+
let private_key_hex = format!("{private_key:#x}");
187+
let example_path = format!("dojo/examples/{example}");
188+
189+
tokio::task::spawn_blocking(move || {
190+
let temp_dir = tempfile::tempdir()?;
191+
192+
// Clone dojo repository at v1.7.0
193+
run_git_clone(temp_dir.path())?;
194+
195+
let project_dir = temp_dir.path().join(&example_path);
196+
197+
// Build contracts using asdf to ensure correct scarb version
198+
run_scarb_build(&project_dir)?;
199+
200+
// Deploy contracts to the katana node
201+
run_sozo_migrate(&project_dir, &rpc_url, &address_hex, &private_key_hex)?;
202+
203+
Ok(())
204+
})
205+
.await?
206+
}
207+
}
208+
209+
fn run_git_clone(temp_dir: &Path) -> Result<(), MigrateError> {
210+
let output = Command::new("git")
211+
.args(["clone", "--depth", "1", "--branch", "v1.7.0", "https://github.com/dojoengine/dojo"])
212+
.current_dir(temp_dir)
213+
.output()
214+
.map_err(|e| MigrateError::GitClone(e.to_string()))?;
215+
216+
if !output.status.success() {
217+
let stderr = String::from_utf8_lossy(&output.stderr);
218+
return Err(MigrateError::GitClone(stderr.to_string()));
219+
}
220+
Ok(())
221+
}
222+
223+
fn run_scarb_build(project_dir: &Path) -> Result<(), MigrateError> {
224+
let output = Command::new("asdf")
225+
.args(["exec", "scarb", "build"])
226+
.current_dir(project_dir)
227+
.output()
228+
.map_err(|e| MigrateError::ScarbBuild(e.to_string()))?;
229+
230+
if !output.status.success() {
231+
let stdout = String::from_utf8_lossy(&output.stdout);
232+
let stderr = String::from_utf8_lossy(&output.stderr);
233+
let combined = format!("{stdout}\n{stderr}");
234+
235+
let lines: Vec<&str> = combined.lines().collect();
236+
let last_50: String =
237+
lines.iter().rev().take(50).rev().cloned().collect::<Vec<_>>().join("\n");
238+
239+
return Err(MigrateError::ScarbBuild(last_50));
240+
}
241+
Ok(())
242+
}
243+
244+
fn run_sozo_migrate(
245+
project_dir: &Path,
246+
rpc_url: &str,
247+
address: &str,
248+
private_key: &str,
249+
) -> Result<(), MigrateError> {
250+
let output = Command::new("sozo")
251+
.args([
252+
"migrate",
253+
"--rpc-url",
254+
rpc_url,
255+
"--account-address",
256+
address,
257+
"--private-key",
258+
private_key,
259+
])
260+
.current_dir(project_dir)
261+
.output()
262+
.map_err(|e| MigrateError::SozoMigrate(e.to_string()))?;
263+
264+
if !output.status.success() {
265+
let stdout = String::from_utf8_lossy(&output.stdout);
266+
let stderr = String::from_utf8_lossy(&output.stderr);
267+
let combined = format!("{stdout}\n{stderr}");
268+
269+
let lines: Vec<&str> = combined.lines().collect();
270+
let last_50: String =
271+
lines.iter().rev().take(50).rev().cloned().collect::<Vec<_>>().join("\n");
272+
273+
eprintln!("sozo migrate failed. Last 50 lines of output:\n{last_50}");
274+
275+
return Err(MigrateError::SozoMigrate(last_50));
276+
}
277+
Ok(())
127278
}
128279

129280
pub fn test_config() -> Config {

0 commit comments

Comments
 (0)