Skip to content

Commit 5a10187

Browse files
authored
feat(pop-cli): add --json support for call chain and contract (#993)
* feat(pop-cli): add --json support for call chain and contract * fix(pop-cli): finalize json call contract feedback handling
1 parent 0f2e0c2 commit 5a10187

File tree

6 files changed

+536
-22
lines changed

6 files changed

+536
-22
lines changed

crates/pop-cli/src/commands/call/chain.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};
1415
use anyhow::{Result, anyhow};
1516
use 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;
2224
use scale_info::PortableRegistry;
2325
use scale_value::{Composite, Value, ValueDef};
2426
use 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+
151170
impl 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)]
10221187
mod 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\nSystem.ExtrinsicSuccess\nBalances.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

Comments
 (0)