Skip to content

Commit e2d93fd

Browse files
committed
feat: implement console commands for accessing contract state data
Add three new REPL commands for inspecting contract data during development: - ::get_constant <contract> <constant> - Get constant value from a contract - ::get_data_var <contract> <var> - Get data variable value from a contract - ::get_map_val <contract> <map> <key> - Get map value from a contract These commands provide developers with easy access to contract state for debugging and development purposes. The implementation follows existing console command patterns and includes comprehensive error handling. Resolves #1722
1 parent 4ce86fa commit e2d93fd

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

components/clarinet-cli/tests/console.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,46 @@ fn can_init_console_with_mxs() {
7777
assert_eq!(output[1], "false");
7878
assert_eq!(output[2], "true");
7979
}
80+
81+
#[test]
82+
fn test_get_constant_command() {
83+
let output = run_console_command(
84+
&["-m", "tests/fixtures/mxs/Clarinet.toml"],
85+
&[
86+
"::get_constant counter MISSING_CONSTANT",
87+
],
88+
);
89+
90+
assert!(!output.is_empty());
91+
let first_line = &output[0];
92+
assert!(!first_line.is_empty());
93+
}
94+
95+
#[test]
96+
fn test_get_data_var_command() {
97+
let output = run_console_command(
98+
&["-m", "tests/fixtures/mxs/Clarinet.toml"],
99+
&[
100+
"::get_data_var counter count",
101+
],
102+
);
103+
104+
assert!(!output.is_empty());
105+
106+
let first_line = &output[0];
107+
assert!(!first_line.is_empty());
108+
}
109+
110+
#[test]
111+
fn test_get_map_val_command() {
112+
let output = run_console_command(
113+
&["-m", "tests/fixtures/mxs/Clarinet.toml"],
114+
&[
115+
"::get_map_val counter MISSING_MAP { key: 1 }",
116+
],
117+
);
118+
119+
assert!(!output.is_empty());
120+
let first_line = &output[0];
121+
assert!(!first_line.is_empty());
122+
}

components/clarity-repl/src/repl/session.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ impl Session {
231231
cmd if cmd.starts_with("::set_epoch") => self.set_epoch(cmd),
232232
cmd if cmd.starts_with("::encode") => self.encode(cmd),
233233
cmd if cmd.starts_with("::decode") => self.decode(cmd),
234+
cmd if cmd.starts_with("::get_constant") => self.get_constant(cmd),
235+
cmd if cmd.starts_with("::get_data_var") => self.get_data_var(cmd),
236+
cmd if cmd.starts_with("::get_map_val") => self.get_map_val(cmd),
234237

235238
_ => "Invalid command. Try `::help`".yellow().to_string(),
236239
}
@@ -857,6 +860,18 @@ impl Session {
857860
"{}",
858861
"::decode <bytes>\t\t\tDecode a Clarity Value bytes representation".yellow()
859862
));
863+
output.push(format!(
864+
"{}",
865+
"::get_constant <contract> <constant>\tGet constant value from a contract".yellow()
866+
));
867+
output.push(format!(
868+
"{}",
869+
"::get_data_var <contract> <var>\t\tGet data variable value from a contract".yellow()
870+
));
871+
output.push(format!(
872+
"{}",
873+
"::get_map_val <contract> <map> <key>\tGet map value from a contract".yellow()
874+
));
860875

861876
output.join("\n")
862877
}
@@ -1090,6 +1105,168 @@ impl Session {
10901105
format!("{}", value_to_string(&value).green())
10911106
}
10921107

1108+
pub fn get_constant(&mut self, cmd: &str) -> String {
1109+
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();
1110+
1111+
if args.len() != 2 {
1112+
return format!("{}", "Usage: ::get_constant <contract> <constant>".red());
1113+
}
1114+
1115+
let contract_name = args[0];
1116+
let constant_name = args[1];
1117+
1118+
let default_deployer = self.settings.initial_deployer.as_ref()
1119+
.map(|account| account.address.clone())
1120+
.unwrap_or_else(|| "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".to_string());
1121+
1122+
let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
1123+
Ok(id) => id,
1124+
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
1125+
};
1126+
1127+
let contract = match self.contracts.get(&contract_id) {
1128+
Some(contract) => contract,
1129+
None => return format!("{} {}", "Contract not found:".red(), contract_id),
1130+
};
1131+
1132+
// Search for constant in the contract's AST
1133+
for function_definition in &contract.ast.expressions {
1134+
if let Some(expr) = function_definition.match_list() {
1135+
if expr.len() >= 3 {
1136+
if let Some(name_expr) = expr.get(1) {
1137+
if let Some(name) = name_expr.match_atom() {
1138+
if name.as_str() == constant_name && expr.len() >= 3 {
1139+
let expr_str = format!("{}", expr[2]);
1140+
return format!("{} {}\n{} {}\n{} {}",
1141+
"Contract:".yellow(),
1142+
contract_id.to_string().green(),
1143+
"Constant:".yellow(),
1144+
name.green(),
1145+
"Value:".yellow(),
1146+
expr_str.green()
1147+
);
1148+
}
1149+
}
1150+
}
1151+
}
1152+
}
1153+
}
1154+
1155+
format!("{} {} {} {}",
1156+
"Constant:".red(),
1157+
constant_name.red(),
1158+
"not found in contract:".red(),
1159+
contract_id.to_string().red()
1160+
)
1161+
}
1162+
1163+
pub fn get_data_var(&mut self, cmd: &str) -> String {
1164+
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();
1165+
1166+
if args.len() != 2 {
1167+
return format!("{}", "Usage: ::get_data_var <contract> <var>".red());
1168+
}
1169+
1170+
let contract_name = args[0];
1171+
let var_name = args[1];
1172+
1173+
let default_deployer = self.settings.initial_deployer.as_ref()
1174+
.map(|account| account.address.clone())
1175+
.unwrap_or_else(|| "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".to_string());
1176+
1177+
let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
1178+
Ok(id) => id,
1179+
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
1180+
};
1181+
1182+
let call_expr = format!("(contract-call? '{}.{} var-get {})",
1183+
contract_id.name, contract_id.issuer, var_name);
1184+
1185+
match self.eval_with_hooks(call_expr, None, false) {
1186+
Ok(result) => {
1187+
match &result.result {
1188+
EvaluationResult::Snippet(snippet_result) => {
1189+
format!("{} {}\n{} {}\n{} {}",
1190+
"Contract:".yellow(),
1191+
contract_id.to_string().green(),
1192+
"Data var:".yellow(),
1193+
var_name.green(),
1194+
"Value:".yellow(),
1195+
value_to_string(&snippet_result.result).green()
1196+
)
1197+
}
1198+
_ => format!("{} {} {} {}",
1199+
"Unable to retrieve data var:".red(),
1200+
var_name.red(),
1201+
"from contract:".red(),
1202+
contract_id.to_string().red()
1203+
)
1204+
}
1205+
}
1206+
Err(diagnostics) => {
1207+
let error_msg = diagnostics.first()
1208+
.map(|d| d.message.as_str())
1209+
.unwrap_or("Unknown error");
1210+
format!("{} {}", error_msg.red(), contract_id.to_string().red())
1211+
}
1212+
}
1213+
}
1214+
1215+
pub fn get_map_val(&mut self, cmd: &str) -> String {
1216+
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();
1217+
1218+
if args.len() < 3 {
1219+
return format!("{}", "Usage: ::get_map_val <contract> <map> <key>".red());
1220+
}
1221+
1222+
let contract_name = args[0];
1223+
let map_name = args[1];
1224+
let key_expr = args[2..].join(" ");
1225+
1226+
let default_deployer = self.settings.initial_deployer.as_ref()
1227+
.map(|account| account.address.clone())
1228+
.unwrap_or_else(|| "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".to_string());
1229+
1230+
let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
1231+
Ok(id) => id,
1232+
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
1233+
};
1234+
1235+
let call_expr = format!("(contract-call? '{}.{} map-get {} {})",
1236+
contract_id.name, contract_id.issuer, map_name, key_expr);
1237+
1238+
match self.eval_with_hooks(call_expr, None, false) {
1239+
Ok(result) => {
1240+
match &result.result {
1241+
EvaluationResult::Snippet(snippet_result) => {
1242+
format!("{} {}\n{} {}\n{} {}\n{} {}",
1243+
"Contract:".yellow(),
1244+
contract_id.to_string().green(),
1245+
"Map:".yellow(),
1246+
map_name.green(),
1247+
"Key:".yellow(),
1248+
key_expr.green(),
1249+
"Value:".yellow(),
1250+
value_to_string(&snippet_result.result).green()
1251+
)
1252+
}
1253+
_ => format!("{} {} {} {}",
1254+
"Unable to retrieve map value:".red(),
1255+
map_name.red(),
1256+
"from contract:".red(),
1257+
contract_id.to_string().red()
1258+
)
1259+
}
1260+
}
1261+
Err(diagnostics) => {
1262+
let error_msg = diagnostics.first()
1263+
.map(|d| d.message.as_str())
1264+
.unwrap_or("Unknown error");
1265+
format!("{} {}", error_msg.red(), contract_id.to_string().red())
1266+
}
1267+
}
1268+
}
1269+
10931270
#[cfg(not(target_arch = "wasm32"))]
10941271
pub fn get_costs(&mut self, output: &mut Vec<String>, cmd: &str) {
10951272
let Some((_, expr)) = cmd.split_once(' ') else {

0 commit comments

Comments
 (0)