Skip to content

Commit b27c96e

Browse files
committed
feat: implement console commands for accessing contract state data
Add three new REPL commands to inspect 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
1 parent 506044a commit b27c96e

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed

components/clarinet-cli/tests/console.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,105 @@ 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", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
85+
&[
86+
"::get_constant counter MISSING_CONSTANT",
87+
],
88+
);
89+
90+
assert!(!output.is_empty());
91+
let output_text = output.join(" ");
92+
93+
// Should contain error message for missing constant
94+
assert!(output_text.contains("not found") || output_text.contains("MISSING_CONSTANT"));
95+
assert!(output_text.contains("Constant:") && output_text.contains("MISSING_CONSTANT"));
96+
assert!(output_text.contains("contract:") && output_text.contains("counter"));
97+
}
98+
99+
#[test]
100+
fn test_get_constant_command_found() {
101+
let output = run_console_command(
102+
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
103+
&[
104+
"::get_constant counter MAX_COUNT",
105+
],
106+
);
107+
108+
assert!(!output.is_empty());
109+
let output_text = output.join(" ");
110+
111+
// Should contain the constant value
112+
assert!(output_text.contains("Contract:"));
113+
assert!(output_text.contains("counter"));
114+
assert!(output_text.contains("Constant:"));
115+
assert!(output_text.contains("MAX_COUNT"));
116+
assert!(output_text.contains("Value:"));
117+
assert!(output_text.contains("u100"));
118+
}
119+
120+
#[test]
121+
fn test_get_data_var_command() {
122+
let output = run_console_command(
123+
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
124+
&[
125+
"::get_data_var counter count",
126+
],
127+
);
128+
129+
assert!(!output.is_empty());
130+
let output_text = output.join(" ");
131+
132+
// Should contain the data variable value
133+
assert!(output_text.contains("Contract:"));
134+
assert!(output_text.contains("counter"));
135+
assert!(output_text.contains("Data var:"));
136+
assert!(output_text.contains("count"));
137+
assert!(output_text.contains("Value:"));
138+
assert!(output_text.contains("u0")); // Initial value should be u0
139+
}
140+
141+
#[test]
142+
fn test_get_map_val_command_not_found() {
143+
let output = run_console_command(
144+
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
145+
&[
146+
"::get_map_val counter test-map u1",
147+
],
148+
);
149+
150+
assert!(!output.is_empty());
151+
let output_text = output.join(" ");
152+
153+
// Should contain not found message for empty map
154+
assert!(output_text.contains("Map entry not found") || output_text.contains("not found"));
155+
assert!(output_text.contains("test-map"));
156+
assert!(output_text.contains("u1"));
157+
}
158+
159+
#[test]
160+
fn test_get_map_val_command_found() {
161+
let output = run_console_command(
162+
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
163+
&[
164+
"(contract-call? .counter set-map-entry u1 u42)",
165+
"::get_map_val counter test-map u1",
166+
],
167+
);
168+
169+
assert!(!output.is_empty());
170+
let output_text = output.join(" ");
171+
172+
// Should contain the map value
173+
assert!(output_text.contains("Contract:"));
174+
assert!(output_text.contains("counter"));
175+
assert!(output_text.contains("Map:"));
176+
assert!(output_text.contains("test-map"));
177+
assert!(output_text.contains("Key:"));
178+
assert!(output_text.contains("u1"));
179+
assert!(output_text.contains("Value:"));
180+
assert!(output_text.contains("some u42"));
181+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
(define-data-var count uint u0)
22

3+
(define-constant MAX_COUNT u100)
4+
5+
(define-map test-map uint uint)
6+
37
(define-public (increment)
48
(ok (var-set count (+ (var-get count) u1)))
59
)
10+
11+
(define-public (set-map-entry (key uint) (value uint))
12+
(ok (map-set test-map key value))
13+
)

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

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ impl Session {
242242
cmd if cmd.starts_with("::set_epoch") => self.set_epoch(cmd),
243243
cmd if cmd.starts_with("::encode") => self.encode(cmd),
244244
cmd if cmd.starts_with("::decode") => self.decode(cmd),
245+
cmd if cmd.starts_with("::get_constant") => self.get_constant(cmd),
246+
cmd if cmd.starts_with("::get_data_var") => self.get_data_var(cmd),
247+
cmd if cmd.starts_with("::get_map_val") => self.get_map_val(cmd),
245248

246249
_ => "Invalid command. Try `::help`".yellow().to_string(),
247250
}
@@ -868,6 +871,18 @@ impl Session {
868871
"{}",
869872
"::decode <bytes>\t\t\tDecode a Clarity Value bytes representation".yellow()
870873
));
874+
output.push(format!(
875+
"{}",
876+
"::get_constant <contract> <constant>\tGet constant value from a contract".yellow()
877+
));
878+
output.push(format!(
879+
"{}",
880+
"::get_data_var <contract> <var>\t\tGet data variable value from a contract".yellow()
881+
));
882+
output.push(format!(
883+
"{}",
884+
"::get_map_val <contract> <map> <key>\tGet map value from a contract".yellow()
885+
));
871886

872887
output.join("\n")
873888
}
@@ -1101,6 +1116,226 @@ impl Session {
11011116
format!("{}", value_to_string(&value).green())
11021117
}
11031118

1119+
pub fn get_constant(&mut self, cmd: &str) -> String {
1120+
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();
1121+
1122+
if args.len() != 2 {
1123+
return format!("{}", "Usage: ::get_constant <contract> <constant>".red());
1124+
}
1125+
1126+
let contract_name = args[0];
1127+
let constant_name = args[1];
1128+
1129+
let default_deployer = match self.settings.initial_deployer.as_ref() {
1130+
Some(account) => account.address.clone(),
1131+
None => self.get_tx_sender(),
1132+
};
1133+
1134+
let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
1135+
Ok(id) => id,
1136+
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
1137+
};
1138+
1139+
let contract = match self.contracts.get(&contract_id) {
1140+
Some(contract) => contract,
1141+
None => return format!("{} {}", "Contract not found:".red(), contract_id),
1142+
};
1143+
1144+
// Search for constant in the contract's AST
1145+
for function_definition in &contract.ast.expressions {
1146+
if let Some(expr) = function_definition.match_list() {
1147+
if expr.len() >= 3 {
1148+
if let Some(name_expr) = expr.get(1) {
1149+
if let Some(name) = name_expr.match_atom() {
1150+
if name.as_str() == constant_name && expr.len() >= 3 {
1151+
let expr_str = format!("{}", expr[2]);
1152+
return format!("{} {}\n{} {}\n{} {}",
1153+
"Contract:".yellow(),
1154+
contract_id.to_string().green(),
1155+
"Constant:".yellow(),
1156+
name.green(),
1157+
"Value:".yellow(),
1158+
expr_str.green()
1159+
);
1160+
}
1161+
}
1162+
}
1163+
}
1164+
}
1165+
}
1166+
1167+
format!("{} {} {} {}",
1168+
"Constant:".red(),
1169+
constant_name.red(),
1170+
"not found in contract:".red(),
1171+
contract_id.to_string().red()
1172+
)
1173+
}
1174+
1175+
pub fn get_data_var(&mut self, cmd: &str) -> String {
1176+
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();
1177+
1178+
if args.len() != 2 {
1179+
return format!("{}", "Usage: ::get_data_var <contract> <var>".red());
1180+
}
1181+
1182+
let contract_name = args[0];
1183+
let var_name = args[1];
1184+
1185+
let default_deployer = match self.settings.initial_deployer.as_ref() {
1186+
Some(account) => account.address.clone(),
1187+
None => self.get_tx_sender(),
1188+
};
1189+
1190+
let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
1191+
Ok(id) => id,
1192+
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
1193+
};
1194+
1195+
match self.interpreter.get_data_var(&contract_id, var_name) {
1196+
Some(value_hex) => {
1197+
// Convert hex string back to Clarity Value for display
1198+
let value_bytes = match decode_hex(&value_hex) {
1199+
Ok(bytes) => bytes,
1200+
Err(e) => return format!("{} {} {} {}",
1201+
"Failed to decode value:".red(),
1202+
e.to_string().red(),
1203+
"from contract:".red(),
1204+
contract_id.to_string().red()
1205+
)
1206+
};
1207+
1208+
let value = match Value::consensus_deserialize(&mut &value_bytes[..]) {
1209+
Ok(value) => value,
1210+
Err(e) => return format!("{} {} {} {}",
1211+
"Failed to deserialize value:".red(),
1212+
e.to_string().red(),
1213+
"from contract:".red(),
1214+
contract_id.to_string().red()
1215+
)
1216+
};
1217+
1218+
format!("{} {}\n{} {}\n{} {}",
1219+
"Contract:".yellow(),
1220+
contract_id.to_string().green(),
1221+
"Data var:".yellow(),
1222+
var_name.green(),
1223+
"Value:".yellow(),
1224+
value_to_string(&value).green()
1225+
)
1226+
}
1227+
None => {
1228+
format!("{} {} {} {}",
1229+
"Data var:".red(),
1230+
var_name.red(),
1231+
"not found in contract:".red(),
1232+
contract_id.to_string().red()
1233+
)
1234+
}
1235+
}
1236+
}
1237+
1238+
pub fn get_map_val(&mut self, cmd: &str) -> String {
1239+
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();
1240+
1241+
if args.len() < 3 {
1242+
return format!("{}", "Usage: ::get_map_val <contract> <map> <key>".red());
1243+
}
1244+
1245+
let contract_name = args[0];
1246+
let map_name = args[1];
1247+
let key_expr = args[2..].join(" ");
1248+
1249+
let default_deployer = match self.settings.initial_deployer.as_ref() {
1250+
Some(account) => account.address.clone(),
1251+
None => self.get_tx_sender(),
1252+
};
1253+
1254+
let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
1255+
Ok(id) => id,
1256+
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
1257+
};
1258+
1259+
// Parse the key expression to get a Clarity Value
1260+
let key_value = match self.eval_clarity_string(&key_expr) {
1261+
value => value,
1262+
};
1263+
1264+
// Extract the actual Value from the SymbolicExpression
1265+
let key_value = match key_value.match_atom_value() {
1266+
Some(value) => value.clone(),
1267+
None => {
1268+
// For complex expressions, we need to evaluate them differently
1269+
let result = match self.eval_with_hooks(format!("({})", key_expr), None, false) {
1270+
Ok(result) => {
1271+
match &result.result {
1272+
EvaluationResult::Snippet(snippet_result) => snippet_result.result.clone(),
1273+
_ => return format!("{} {} {} {}",
1274+
"Unable to evaluate key expression:".red(),
1275+
key_expr.red(),
1276+
"for contract:".red(),
1277+
contract_id.to_string().red()
1278+
)
1279+
}
1280+
}
1281+
Err(_) => return format!("{} {} {} {}",
1282+
"Unable to evaluate key expression:".red(),
1283+
key_expr.red(),
1284+
"for contract:".red(),
1285+
contract_id.to_string().red()
1286+
)
1287+
};
1288+
result
1289+
}
1290+
};
1291+
1292+
match self.interpreter.get_map_entry(&contract_id, map_name, &key_value) {
1293+
Some(value_hex) => {
1294+
// Convert hex string back to Clarity Value for display
1295+
let value_bytes = match decode_hex(&value_hex) {
1296+
Ok(bytes) => bytes,
1297+
Err(e) => return format!("{} {} {} {}",
1298+
"Failed to decode value:".red(),
1299+
e.to_string().red(),
1300+
"from contract:".red(),
1301+
contract_id.to_string().red()
1302+
)
1303+
};
1304+
1305+
let value = match Value::consensus_deserialize(&mut &value_bytes[..]) {
1306+
Ok(value) => value,
1307+
Err(e) => return format!("{} {} {} {}",
1308+
"Failed to deserialize value:".red(),
1309+
e.to_string().red(),
1310+
"from contract:".red(),
1311+
contract_id.to_string().red()
1312+
)
1313+
};
1314+
1315+
format!("{} {}\n{} {}\n{} {}\n{} {}",
1316+
"Contract:".yellow(),
1317+
contract_id.to_string().green(),
1318+
"Map:".yellow(),
1319+
map_name.green(),
1320+
"Key:".yellow(),
1321+
key_expr.green(),
1322+
"Value:".yellow(),
1323+
value_to_string(&value).green()
1324+
)
1325+
}
1326+
None => {
1327+
format!("{} {} {} {} {} {}",
1328+
"Map entry not found for key:".red(),
1329+
key_expr.red(),
1330+
"in map:".red(),
1331+
map_name.red(),
1332+
"of contract:".red(),
1333+
contract_id.to_string().red()
1334+
)
1335+
}
1336+
}
1337+
}
1338+
11041339
#[cfg(not(target_arch = "wasm32"))]
11051340
pub fn get_costs(&mut self, output: &mut Vec<String>, cmd: &str) {
11061341
let Some((_, expr)) = cmd.split_once(' ') else {

0 commit comments

Comments
 (0)