11use std:: net:: SocketAddr ;
2+ use std:: path:: Path ;
3+ use std:: process:: Command ;
24use std:: sync:: Arc ;
35
46use katana_chain_spec:: { dev, ChainSpec } ;
@@ -23,6 +25,23 @@ use starknet::providers::{JsonRpcClient, Url};
2325pub use starknet:: providers:: { Provider , ProviderError } ;
2426use starknet:: signers:: { LocalWallet , SigningKey } ;
2527
28+ /// Errors that can occur when populating a test node with contracts.
29+ #[ derive( Debug , thiserror:: Error ) ]
30+ pub enum PopulateError {
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+
2645pub type ForkTestNode = TestNode < ForkProviderFactory > ;
2746
2847#[ derive( Debug ) ]
@@ -124,6 +143,117 @@ where
124143 let client = self . rpc_http_client ( ) ;
125144 katana_rpc_client:: starknet:: Client :: new_with_client ( client)
126145 }
146+
147+ /// Populates the node with contracts by cloning the dojo repository,
148+ /// building contracts with `scarb`, and deploying them with `sozo migrate`.
149+ ///
150+ /// This method requires `git`, `asdf`, and `sozo` to be available in PATH.
151+ /// The scarb version is managed by asdf using the `.tool-versions` file
152+ /// in the dojo repository.
153+ pub async fn populate ( & self ) -> Result < ( ) , PopulateError > {
154+ let rpc_url = format ! ( "http://{}" , self . rpc_addr( ) ) ;
155+
156+ let ( address, account) = self
157+ . backend ( )
158+ . chain_spec
159+ . genesis ( )
160+ . accounts ( )
161+ . next ( )
162+ . expect ( "must have at least one genesis account" ) ;
163+ let private_key = account. private_key ( ) . ok_or ( PopulateError :: MissingPrivateKey ) ?;
164+
165+ let address_hex = address. to_string ( ) ;
166+ let private_key_hex = format ! ( "{private_key:#x}" ) ;
167+
168+ tokio:: task:: spawn_blocking ( move || {
169+ let temp_dir = tempfile:: tempdir ( ) ?;
170+
171+ // Clone dojo repository at v1.7.0
172+ run_git_clone ( temp_dir. path ( ) ) ?;
173+
174+ let project_dir = temp_dir. path ( ) . join ( "dojo/examples/spawn-and-move" ) ;
175+
176+ // Build contracts using asdf to ensure correct scarb version
177+ run_scarb_build ( & project_dir) ?;
178+
179+ // Deploy contracts to the katana node
180+ run_sozo_migrate ( & project_dir, & rpc_url, & address_hex, & private_key_hex) ?;
181+
182+ Ok ( ( ) )
183+ } )
184+ . await ?
185+ }
186+ }
187+
188+ fn run_git_clone ( temp_dir : & Path ) -> Result < ( ) , PopulateError > {
189+ let output = Command :: new ( "git" )
190+ . args ( [ "clone" , "--depth" , "1" , "--branch" , "v1.7.0" , "https://github.com/dojoengine/dojo" ] )
191+ . current_dir ( temp_dir)
192+ . output ( )
193+ . map_err ( |e| PopulateError :: GitClone ( e. to_string ( ) ) ) ?;
194+
195+ if !output. status . success ( ) {
196+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
197+ return Err ( PopulateError :: GitClone ( stderr. to_string ( ) ) ) ;
198+ }
199+ Ok ( ( ) )
200+ }
201+
202+ fn run_scarb_build ( project_dir : & Path ) -> Result < ( ) , PopulateError > {
203+ let output = Command :: new ( "asdf" )
204+ . args ( [ "exec" , "scarb" , "build" ] )
205+ . current_dir ( project_dir)
206+ . output ( )
207+ . map_err ( |e| PopulateError :: ScarbBuild ( e. to_string ( ) ) ) ?;
208+
209+ if !output. status . success ( ) {
210+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
211+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
212+ let combined = format ! ( "{stdout}\n {stderr}" ) ;
213+
214+ let lines: Vec < & str > = combined. lines ( ) . collect ( ) ;
215+ let last_50: String =
216+ lines. iter ( ) . rev ( ) . take ( 50 ) . rev ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) . join ( "\n " ) ;
217+
218+ return Err ( PopulateError :: ScarbBuild ( last_50) ) ;
219+ }
220+ Ok ( ( ) )
221+ }
222+
223+ fn run_sozo_migrate (
224+ project_dir : & Path ,
225+ rpc_url : & str ,
226+ address : & str ,
227+ private_key : & str ,
228+ ) -> Result < ( ) , PopulateError > {
229+ let output = Command :: new ( "sozo" )
230+ . args ( [
231+ "migrate" ,
232+ "--rpc-url" ,
233+ rpc_url,
234+ "--account-address" ,
235+ address,
236+ "--private-key" ,
237+ private_key,
238+ ] )
239+ . current_dir ( project_dir)
240+ . output ( )
241+ . map_err ( |e| PopulateError :: SozoMigrate ( e. to_string ( ) ) ) ?;
242+
243+ if !output. status . success ( ) {
244+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
245+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
246+ let combined = format ! ( "{stdout}\n {stderr}" ) ;
247+
248+ let lines: Vec < & str > = combined. lines ( ) . collect ( ) ;
249+ let last_50: String =
250+ lines. iter ( ) . rev ( ) . take ( 50 ) . rev ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) . join ( "\n " ) ;
251+
252+ eprintln ! ( "sozo migrate failed. Last 50 lines of output:\n {last_50}" ) ;
253+
254+ return Err ( PopulateError :: SozoMigrate ( last_50) ) ;
255+ }
256+ Ok ( ( ) )
127257}
128258
129259pub fn test_config ( ) -> Config {
0 commit comments