11use std:: {
2- io:: { BufRead , Write } ,
2+ fs:: create_dir_all,
3+ io:: { BufRead , Read } ,
34 path:: PathBuf ,
45 process:: { Child , Command , Stdio } ,
56 sync:: atomic:: { AtomicU32 , Ordering } ,
67 time:: Duration ,
78} ;
89
9- use sc_chain_spec:: { ChainSpecBuilder , ChainType } ;
10- use serde_json:: { Value , json} ;
10+ use serde_json:: { Value as JsonValue , json} ;
1111use sp_core:: crypto:: Ss58Codec ;
1212use sp_runtime:: AccountId32 ;
1313use std:: fs;
@@ -22,7 +22,7 @@ use alloy::{
2222 } ,
2323} ;
2424
25- use crate :: { Node , chainspec :: ChainSpec } ;
25+ use crate :: Node ;
2626use revive_dt_config:: Arguments ;
2727use revive_dt_node_interaction:: {
2828 EthereumNode , trace:: trace_transaction, transaction:: execute_transaction,
@@ -37,23 +37,80 @@ pub struct KitchensinkNode {
3737 eth_proxy_binary : PathBuf ,
3838 rpc_url : String ,
3939 wallet : EthereumWallet ,
40+ base_directory : PathBuf ,
4041 process_substrate : Option < Child > ,
4142 process_proxy : Option < Child > ,
4243}
4344
4445impl KitchensinkNode {
46+ const BASE_DIRECTORY : & str = "kitchensink" ;
4547 const SUBSTRATE_READY_MARKER : & str = "Running JSON-RPC server" ;
4648 const ETH_PROXY_READY_MARKER : & str = "Running JSON-RPC server" ;
49+ const CHAIN_SPEC_JSON_FILE : & str = "template_chainspec.json" ;
4750 const BASE_SUBSTRATE_RPC_PORT : u16 = 9944 ;
4851 const BASE_PROXY_RPC_PORT : u16 = 8545 ;
4952
50- fn spawn_process ( & mut self , genesis : String ) -> anyhow:: Result < ( ) > {
53+ fn init ( & mut self , genesis_path : & str ) -> anyhow:: Result < & mut Self > {
54+ create_dir_all ( & self . base_directory ) ?;
55+
56+ let template_chainspec_path = self . base_directory . join ( Self :: CHAIN_SPEC_JSON_FILE ) ;
57+
58+ let status = Command :: new ( & self . substrate_binary )
59+ . arg ( "export-chain-spec" )
60+ . arg ( "--chain" )
61+ . arg ( "dev" )
62+ . arg ( "--output" )
63+ . arg ( & template_chainspec_path)
64+ . status ( ) ?;
65+
66+ if !status. success ( ) {
67+ anyhow:: bail!( "substrate-node export-chain-spec failed" ) ;
68+ }
69+
70+ let mut file = std:: fs:: File :: open ( & template_chainspec_path) ?;
71+ let mut content = String :: new ( ) ;
72+ file. read_to_string ( & mut content) ?;
73+ let mut chainspec_json: JsonValue = serde_json:: from_str ( & content) ?;
74+
75+ let existing_chainspec_balances =
76+ chainspec_json[ "genesis" ] [ "runtimeGenesis" ] [ "patch" ] [ "balances" ] [ "balances" ]
77+ . as_array ( )
78+ . cloned ( )
79+ . unwrap_or_default ( ) ;
80+
81+ let mut merged_balances: Vec < ( String , u128 ) > = existing_chainspec_balances
82+ . into_iter ( )
83+ . filter_map ( |val| {
84+ if let Some ( arr) = val. as_array ( ) {
85+ if arr. len ( ) == 2 {
86+ let account = arr[ 0 ] . as_str ( ) ?. to_string ( ) ;
87+ let balance = arr[ 1 ] . as_f64 ( ) ? as u128 ;
88+ return Some ( ( account, balance) ) ;
89+ }
90+ }
91+ None
92+ } )
93+ . collect ( ) ;
94+ let mut eth_balances = self . extract_balance_from_genesis_file ( genesis_path) ?;
95+ merged_balances. append ( & mut eth_balances) ;
96+
97+ chainspec_json[ "genesis" ] [ "runtimeGenesis" ] [ "patch" ] [ "balances" ] [ "balances" ] =
98+ json ! ( merged_balances) ;
99+
100+ serde_json:: to_writer_pretty (
101+ std:: fs:: File :: create ( & template_chainspec_path) ?,
102+ & chainspec_json,
103+ ) ?;
104+ Ok ( self )
105+ }
106+
107+ fn spawn_process ( & mut self ) -> anyhow:: Result < ( ) > {
51108 let substrate_rpc_port = Self :: BASE_SUBSTRATE_RPC_PORT + self . id as u16 ;
52109 let proxy_rpc_port = Self :: BASE_PROXY_RPC_PORT + self . id as u16 ;
53110
54111 self . rpc_url = format ! ( "http://127.0.0.1:{proxy_rpc_port}" ) ;
55112
56- let chainspec_path = self . generate_chainspec_file ( & genesis ) ? ;
113+ let chainspec_path = self . base_directory . join ( Self :: CHAIN_SPEC_JSON_FILE ) ;
57114
58115 // Start Substrate node
59116 let mut substrate_process = Command :: new ( & self . substrate_binary )
@@ -103,141 +160,12 @@ impl KitchensinkNode {
103160 Ok ( ( ) )
104161 }
105162
106- fn generate_chainspec_file ( & self , genesis_path : & str ) -> anyhow:: Result < PathBuf > {
107- let mut merged_balances: Vec < ( String , u128 ) > = vec ! [
108- (
109- "5Ff3iXP75ruzroPWRP2FYBHWnmGGBSb63857BgnzCoXNxfPo" . to_string( ) ,
110- 1_000_000_000_000_000_000_000 ,
111- ) ,
112- (
113- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy" . to_string( ) ,
114- 1_000_000_000_000_000_000_000 ,
115- ) ,
116- ] ;
117- let mut eth_balances = self . extract_balance_from_genesis_file ( genesis_path) ?;
118- merged_balances. append ( & mut eth_balances) ;
119-
120- let chainspec: ChainSpec = serde_json:: from_str ( include_str ! ( "default_chainspec.json" ) ) ?;
121-
122- let wasm_binary = hex:: decode (
123- chainspec
124- . genesis
125- . runtime_genesis
126- . code
127- . trim_start_matches ( "0x" ) ,
128- ) ?;
129-
130- let builder: ChainSpecBuilder < ( ) , ( ) > = ChainSpecBuilder :: new ( & wasm_binary, ( ) )
131- . with_name ( "Custom Testnet" )
132- . with_id ( "custom_testnet" )
133- . with_chain_type ( ChainType :: Development )
134- . with_genesis_config_patch ( json ! ( {
135- "balances" : { "balances" : merged_balances } ,
136-
137- "staking" : {
138- "invulnerables" : [
139- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy"
140- ] ,
141- "minimumValidatorCount" : 1 ,
142- "slashRewardFraction" : 100000000 ,
143- "stakers" : [
144- [
145- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy" ,
146- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy" ,
147- 1000000000000000000u128 ,
148- "Validator"
149- ]
150- ] ,
151- "validatorCount" : 1
152- } ,
153-
154- "session" : {
155- "keys" : [
156- [
157- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy" ,
158- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy" ,
159- {
160- "authority_discovery" : "5EZaeQ8djPcq9pheJUhgerXQZt9YaHnMJpiHMRhwQeinqUW8" ,
161- "babe" : "5EZaeQ8djPcq9pheJUhgerXQZt9YaHnMJpiHMRhwQeinqUW8" ,
162- "beefy" : "KWAeFsyPkaVyGuWbZN9uDANzndXXtR5N5haFCTQZ9Ym2U3wzu" ,
163- "grandpa" : "5Fb9ayurnxnaXj56CjmyQLBiadfRCqUbL2VWNbbe1nZU6wiC" ,
164- "im_online" : "5EZaeQ8djPcq9pheJUhgerXQZt9YaHnMJpiHMRhwQeinqUW8" ,
165- "mixnet" : "5EZaeQ8djPcq9pheJUhgerXQZt9YaHnMJpiHMRhwQeinqUW8"
166- }
167- ]
168- ]
169- } ,
170-
171- "sudo" : {
172- "key" : "5Ff3iXP75ruzroPWRP2FYBHWnmGGBSb63857BgnzCoXNxfPo"
173- } ,
174-
175- "elections" : {
176- "members" : [
177- [
178- "5Ff3iXP75ruzroPWRP2FYBHWnmGGBSb63857BgnzCoXNxfPo" ,
179- 1000000000000000000u128
180- ] ,
181- [
182- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy" ,
183- 1000000000000000000u128
184- ]
185- ]
186- } ,
187-
188- "revive" : {
189- "mappedAccounts" : [
190- "5Ff3iXP75ruzroPWRP2FYBHWnmGGBSb63857BgnzCoXNxfPo" ,
191- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy"
192- ]
193- } ,
194-
195- "technicalCommittee" : {
196- "members" : [
197- "5Ff3iXP75ruzroPWRP2FYBHWnmGGBSb63857BgnzCoXNxfPo" ,
198- "5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy"
199- ]
200- } ,
201-
202- "nominationPools" : {
203- "minCreateBond" : 1000000000000000u128 ,
204- "minJoinBond" : 100000000000000u128
205- } ,
206-
207- "society" : {
208- "pot" : 0
209- } ,
210-
211- "babe" : {
212- "epochConfig" : {
213- "allowed_slots" : "PrimaryAndSecondaryPlainSlots" ,
214- "c" : [ 1 , 4 ]
215- }
216- }
217- } ) )
218- . with_code ( & wasm_binary. to_vec ( ) ) ;
219-
220- let chainspec = builder. build ( ) ;
221- let json_spec = chainspec
222- . as_json ( false )
223- . map_err ( |e| anyhow:: anyhow!( "Failed to generate JSON: {}" , e) ) ?;
224-
225- let temp_path =
226- std:: env:: temp_dir ( ) . join ( format ! ( "chainspec-{}.json" , uuid:: Uuid :: new_v4( ) ) ) ;
227-
228- let mut temp_file = std:: fs:: File :: create ( & temp_path) ?;
229- temp_file. write_all ( json_spec. as_bytes ( ) ) ?;
230- temp_file. flush ( ) ?;
231-
232- Ok ( temp_path)
233- }
234-
235163 fn extract_balance_from_genesis_file (
236164 & self ,
237165 genesis_path : & str ,
238166 ) -> anyhow:: Result < Vec < ( String , u128 ) > > {
239167 let genesis_str = fs:: read_to_string ( genesis_path) ?;
240- let genesis_json: Value = serde_json:: from_str ( & genesis_str) ?;
168+ let genesis_json: JsonValue = serde_json:: from_str ( & genesis_str) ?;
241169 let alloc = genesis_json
242170 . get ( "alloc" )
243171 . and_then ( |a| a. as_object ( ) )
@@ -354,14 +282,17 @@ impl EthereumNode for KitchensinkNode {
354282
355283impl Node for KitchensinkNode {
356284 fn new ( config : & Arguments ) -> Self {
285+ let kitchensink_directory = config. directory ( ) . join ( Self :: BASE_DIRECTORY ) ;
357286 let id = NODE_COUNT . fetch_add ( 1 , Ordering :: SeqCst ) ;
287+ let base_directory = kitchensink_directory. join ( id. to_string ( ) ) ;
358288
359289 Self {
360290 id,
361291 substrate_binary : config. kitchensink . clone ( ) ,
362292 eth_proxy_binary : config. eth_proxy . clone ( ) ,
363293 rpc_url : String :: new ( ) ,
364294 wallet : config. wallet ( ) ,
295+ base_directory,
365296 process_substrate : None ,
366297 process_proxy : None ,
367298 }
@@ -382,7 +313,7 @@ impl Node for KitchensinkNode {
382313 }
383314
384315 fn spawn ( & mut self , genesis : String ) -> anyhow:: Result < ( ) > {
385- self . spawn_process ( genesis)
316+ self . init ( & genesis) ? . spawn_process ( )
386317 }
387318
388319 fn state_diff ( & self , transaction : TransactionReceipt ) -> anyhow:: Result < DiffMode > {
@@ -444,7 +375,7 @@ mod tests {
444375 }
445376
446377 #[ test]
447- fn test_generate_chainspec ( ) {
378+ fn test_init_generates_chainspec_with_balances ( ) {
448379 // Setup: Write a minimal Geth-style genesis.json
449380 let test_genesis_path = std:: env:: temp_dir ( ) . join ( "test_genesis_file.json" ) ;
450381 let genesis_content = r#"
@@ -460,30 +391,37 @@ mod tests {
460391 }
461392 "# ;
462393 fs:: write ( & test_genesis_path, genesis_content) . expect ( "Failed to write test genesis" ) ;
394+ let mut dummy_node = KitchensinkNode :: new ( & test_config ( ) . 0 ) ;
463395
464- let dummy_node = KitchensinkNode :: new ( & Arguments :: default ( ) ) ;
396+ // Call `init()`
397+ dummy_node
398+ . init ( test_genesis_path. to_str ( ) . unwrap ( ) )
399+ . expect ( "init failed" ) ;
465400
466- let result = dummy_node. generate_chainspec_file ( test_genesis_path. to_str ( ) . unwrap ( ) ) ;
401+ // Check that the patched chainspec file was generated
402+ let final_chainspec_path = dummy_node
403+ . base_directory
404+ . join ( KitchensinkNode :: CHAIN_SPEC_JSON_FILE ) ;
405+ assert ! ( final_chainspec_path. exists( ) , "Chainspec file should exist" ) ;
467406
468- match result {
469- Ok ( path) => {
470- println ! ( "Chainspec file generated at: {:?}" , path) ;
471- let contents = fs:: read_to_string ( & path) . expect ( "Failed to read chainspec" ) ;
407+ let contents = fs:: read_to_string ( & final_chainspec_path) . expect ( "Failed to read chainspec" ) ;
472408
473- let first_eth_addr = dummy_node
474- . eth_to_substrate_address ( "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" )
475- . unwrap ( ) ;
476- let second_eth_addr = dummy_node
477- . eth_to_substrate_address ( "Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2" )
478- . unwrap ( ) ;
409+ // Validate that the Substrate addresses derived from the Ethereum addresses are in the file
410+ let first_eth_addr = dummy_node
411+ . eth_to_substrate_address ( "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" )
412+ . unwrap ( ) ;
413+ let second_eth_addr = dummy_node
414+ . eth_to_substrate_address ( "Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2" )
415+ . unwrap ( ) ;
479416
480- assert ! ( contents. contains( & first_eth_addr) ) ;
481- assert ! ( contents. contains( & second_eth_addr) ) ;
482- }
483- Err ( e) => {
484- panic ! ( "Failed to generate chainspec: {:?}" , e) ;
485- }
486- }
417+ assert ! (
418+ contents. contains( & first_eth_addr) ,
419+ "Chainspec should contain Substrate address for first Ethereum account"
420+ ) ;
421+ assert ! (
422+ contents. contains( & second_eth_addr) ,
423+ "Chainspec should contain Substrate address for second Ethereum account"
424+ ) ;
487425 }
488426
489427 #[ test]
0 commit comments