@@ -10,6 +10,7 @@ use crate::{
1010 urls,
1111 wallet:: { self , prompt_to_use_wallet} ,
1212 } ,
13+ output:: { invalid_input_error, network_error, prompt_required_error} ,
1314} ;
1415use anyhow:: { Result , anyhow} ;
1516use clap:: Args ;
@@ -19,6 +20,7 @@ use pop_chains::{
1920 encode_call_data, find_callable_by_name, find_pallet_by_name, raw_value_to_string,
2021 render_storage_key_values, sign_and_submit_extrinsic, supported_actions, type_to_param,
2122} ;
23+ use pop_common:: create_signer;
2224use scale_info:: PortableRegistry ;
2325use scale_value:: { Composite , Value , ValueDef } ;
2426use serde:: Serialize ;
@@ -148,6 +150,23 @@ pub struct CallChainCommand {
148150 metadata : bool ,
149151}
150152
153+ /// Structured output for `pop --json call chain`.
154+ #[ derive( Debug , Serialize ) ]
155+ pub ( crate ) struct CallChainOutput {
156+ pallet : String ,
157+ function : String ,
158+ call_data : String ,
159+ result : CallChainResult ,
160+ }
161+
162+ /// Result details for a chain call.
163+ #[ derive( Debug , Serialize ) ]
164+ #[ serde( rename_all = "snake_case" , tag = "kind" ) ]
165+ pub ( crate ) enum CallChainResult {
166+ DryRun { return_value : String } ,
167+ Submitted { tx_hash : String , block_hash : Option < String > , events : Vec < String > } ,
168+ }
169+
151170impl CallChainCommand {
152171 /// Executes the command.
153172 pub ( crate ) async fn execute ( mut self ) -> Result < ( ) > {
@@ -316,6 +335,126 @@ impl CallChainCommand {
316335 Ok ( ( ) )
317336 }
318337
338+ /// Executes `call chain` in JSON mode and returns structured output.
339+ pub ( crate ) async fn execute_json ( self ) -> Result < CallChainOutput > {
340+ if self . metadata {
341+ return Err ( invalid_input_error (
342+ "`pop --json call chain` does not support `--metadata`" ,
343+ ) ) ;
344+ }
345+ if self . use_wallet {
346+ return Err ( invalid_input_error (
347+ "`pop --json call chain` does not support `--use-wallet`; provide `--suri`" ,
348+ ) ) ;
349+ }
350+ if self . call_data . is_some ( ) {
351+ return Err ( invalid_input_error (
352+ "`pop --json call chain` does not support `--call`; provide --pallet/--function/--args" ,
353+ ) ) ;
354+ }
355+
356+ let mut missing = Vec :: new ( ) ;
357+ if self . url . is_none ( ) {
358+ missing. push ( "--url" ) ;
359+ }
360+ if self . pallet . is_none ( ) {
361+ missing. push ( "--pallet" ) ;
362+ }
363+ if self . function . is_none ( ) {
364+ missing. push ( "--function" ) ;
365+ }
366+ if self . suri . is_none ( ) {
367+ missing. push ( "--suri" ) ;
368+ }
369+ if !missing. is_empty ( ) {
370+ return Err ( prompt_required_error ( format ! (
371+ "Missing required flags for `pop --json call chain`: {}" ,
372+ missing. join( ", " )
373+ ) ) ) ;
374+ }
375+
376+ let mut json_cli = crate :: cli:: JsonCli ;
377+ let chain = chain:: configure (
378+ "Select a chain (type to filter)" ,
379+ "Which chain would you like to interact with?" ,
380+ urls:: LOCAL ,
381+ & self . url ,
382+ |_| true ,
383+ & mut json_cli,
384+ )
385+ . await
386+ . map_err ( map_chain_network_error) ?;
387+
388+ let pallet_name = self . pallet . as_ref ( ) . expect ( "checked above; qed" ) ;
389+ let function_name = self . function . as_ref ( ) . expect ( "checked above; qed" ) ;
390+ let call_item = find_callable_by_name ( & chain. pallets , pallet_name, function_name)
391+ . map_err ( |e| invalid_input_error ( e. to_string ( ) ) ) ?;
392+ let function = call_item. as_function ( ) . ok_or_else ( || {
393+ invalid_input_error (
394+ "`pop --json call chain` currently supports dispatchable functions only" ,
395+ )
396+ } ) ?;
397+
398+ let expanded_args = self . expand_file_arguments ( ) ?;
399+ if expanded_args. len ( ) < function. params . len ( ) {
400+ return Err ( prompt_required_error ( format ! (
401+ "Missing required `--args` values for `{}`: expected {}, got {}" ,
402+ function. name,
403+ function. params. len( ) ,
404+ expanded_args. len( )
405+ ) ) ) ;
406+ }
407+ if expanded_args. len ( ) > function. params . len ( ) {
408+ return Err ( invalid_input_error ( format ! (
409+ "Expected {} arguments for `{}`, but received {}." ,
410+ function. params. len( ) ,
411+ function. name,
412+ expanded_args. len( )
413+ ) ) ) ;
414+ }
415+
416+ let call = Call {
417+ function : call_item. clone ( ) ,
418+ args : expanded_args,
419+ suri : self . suri . clone ( ) ,
420+ use_wallet : false ,
421+ skip_confirm : true ,
422+ execute : self . execute ,
423+ sudo : self . sudo ,
424+ } ;
425+ let xt = call
426+ . prepare_extrinsic ( & chain. client , & mut json_cli)
427+ . map_err ( |e| invalid_input_error ( e. to_string ( ) ) ) ?;
428+ let call_data =
429+ encode_call_data ( & chain. client , & xt) . map_err ( |e| invalid_input_error ( e. to_string ( ) ) ) ?;
430+ let suri = self . suri . expect ( "checked above; qed" ) ;
431+
432+ let result = if self . execute {
433+ let submit_output = sign_and_submit_extrinsic ( & chain. client , & chain. url , xt, & suri)
434+ . await
435+ . map_err ( map_chain_submit_error) ?;
436+ let ( tx_hash, events) = parse_chain_submit_output ( & submit_output) ;
437+ CallChainResult :: Submitted { tx_hash, block_hash : None , events }
438+ } else {
439+ let signer = create_signer ( & suri) . map_err ( |e| invalid_input_error ( e. to_string ( ) ) ) ?;
440+ let tx = chain
441+ . client
442+ . tx ( )
443+ . create_signed ( & xt, & signer, Default :: default ( ) )
444+ . await
445+ . map_err ( map_chain_network_error) ?;
446+ let validation = tx. validate ( ) . await . map_err ( map_chain_network_error) ?;
447+ CallChainResult :: DryRun { return_value : format ! ( "{validation:?}" ) }
448+ } ;
449+
450+ Ok ( CallChainOutput {
451+ pallet : function. pallet . clone ( ) ,
452+ function : function. name . clone ( ) ,
453+ call_data,
454+ result,
455+ } )
456+ }
457+
319458 // Configure the call based on command line arguments/call UI.
320459 fn configure_call ( & mut self , chain : & Chain , cli : & mut impl Cli ) -> Result < Call > {
321460 loop {
@@ -1018,6 +1157,32 @@ fn parse_pallet_name(name: &str) -> Result<String, String> {
10181157 }
10191158}
10201159
1160+ fn map_chain_network_error ( err : impl std:: fmt:: Display ) -> anyhow:: Error {
1161+ network_error ( err. to_string ( ) )
1162+ }
1163+
1164+ /// Maps extrinsic submission errors to `INTERNAL`. By this point, the RPC connection is
1165+ /// already established (metadata was fetched). Failures here are typically runtime
1166+ /// validation or dispatch errors, not transport issues.
1167+ fn map_chain_submit_error ( err : impl std:: fmt:: Display ) -> anyhow:: Error {
1168+ anyhow:: anyhow!( "{err}" )
1169+ }
1170+
1171+ fn parse_chain_submit_output ( output : & str ) -> ( String , Vec < String > ) {
1172+ let mut lines = output. lines ( ) ;
1173+ let tx_hash = lines
1174+ . next ( )
1175+ . and_then ( |line| line. split_once ( "hash:" ) . map ( |( _, hash) | hash. trim ( ) . to_string ( ) ) )
1176+ . filter ( |hash| !hash. is_empty ( ) )
1177+ . unwrap_or_else ( || "unknown" . to_string ( ) ) ;
1178+ let events = lines
1179+ . map ( str:: trim)
1180+ . filter ( |line| !line. is_empty ( ) )
1181+ . map ( ToOwned :: to_owned)
1182+ . collect ( ) ;
1183+ ( tx_hash, events)
1184+ }
1185+
10211186#[ cfg( test) ]
10221187mod tests {
10231188 use super :: * ;
@@ -1809,4 +1974,20 @@ mod tests {
18091974 cli. verify ( ) ?;
18101975 Ok ( ( ) )
18111976 }
1977+
1978+ #[ test]
1979+ fn parse_chain_submit_output_works ( ) {
1980+ let output =
1981+ "Extrinsic Submitted with hash: 0x1234\n \n System.ExtrinsicSuccess\n Balances.Transfer" ;
1982+ let ( tx_hash, events) = parse_chain_submit_output ( output) ;
1983+ assert_eq ! ( tx_hash, "0x1234" ) ;
1984+ assert_eq ! ( events, vec![ "System.ExtrinsicSuccess" , "Balances.Transfer" ] ) ;
1985+ }
1986+
1987+ #[ tokio:: test]
1988+ async fn execute_json_requires_required_flags ( ) {
1989+ let cmd = CallChainCommand :: default ( ) ;
1990+ let err = cmd. execute_json ( ) . await . expect_err ( "expected prompt required error" ) ;
1991+ assert ! ( err. downcast_ref:: <crate :: output:: PromptRequiredError >( ) . is_some( ) ) ;
1992+ }
18121993}
0 commit comments