Skip to content

Commit c9e7d4a

Browse files
committed
feat: add --postcondition-mode to contract publish + bugfix anchor mode usage, #6194
1 parent 42faabe commit c9e7d4a

File tree

1 file changed

+289
-18
lines changed

1 file changed

+289
-18
lines changed

stackslib/src/blockstack_cli.rs

Lines changed: 289 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ use blockstack_lib::burnchains::Address;
3434
use blockstack_lib::chainstate::stacks::{
3535
StacksBlock, StacksBlockHeader, StacksMicroblock, StacksPrivateKey, StacksPublicKey,
3636
StacksTransaction, StacksTransactionSigner, TokenTransferMemo, TransactionAnchorMode,
37-
TransactionAuth, TransactionContractCall, TransactionPayload, TransactionSmartContract,
38-
TransactionSpendingCondition, TransactionVersion, C32_ADDRESS_VERSION_MAINNET_SINGLESIG,
39-
C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
37+
TransactionAuth, TransactionContractCall, TransactionPayload, TransactionPostConditionMode,
38+
TransactionSmartContract, TransactionSpendingCondition, TransactionVersion,
39+
C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
4040
};
4141
use blockstack_lib::clarity_cli::vm_execute;
4242
use blockstack_lib::core::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET};
@@ -90,6 +90,10 @@ is that the miner chooses, but you can decide which with the following options:
9090
9191
--microblock-only indicates to mine this transaction only in a microblock
9292
--block-only indicates to mine this transaction only in a block
93+
94+
The use of post-conditions in the contract can be controlled with the following option:
95+
96+
--postcondition-mode indicates the post-condition mode for the contract. Allowed values: [`allow`, `deny`]. Default: `deny`.
9397
";
9498

9599
const CALL_USAGE: &str = "blockstack-cli (options) contract-call [origin-secret-key-hex] [fee-rate] [nonce] [contract-publisher-address] [contract-name] [function-name] [args...]
@@ -340,15 +344,21 @@ fn parse_anchor_mode(
340344
for i in 0..num_args {
341345
if args[i] == "--microblock-only" {
342346
if idx > 0 {
343-
return Err(CliError::Message(format!("USAGE:\n {}", usage,)));
347+
return Err(CliError::Message(format!(
348+
"Multiple anchor mode detected.\n\nUSAGE:\n{}",
349+
usage,
350+
)));
344351
}
345352

346353
offchain_only = true;
347354
idx = i;
348355
}
349356
if args[i] == "--block-only" {
350357
if idx > 0 {
351-
return Err(CliError::Message(format!("USAGE:\n {}", usage,)));
358+
return Err(CliError::Message(format!(
359+
"Multiple anchor mode detected.\n\nUSAGE:\n{}",
360+
usage,
361+
)));
352362
}
353363

354364
onchain_only = true;
@@ -367,6 +377,49 @@ fn parse_anchor_mode(
367377
}
368378
}
369379

380+
fn parse_postcondition_mode(
381+
args: &mut Vec<String>,
382+
usage: &str,
383+
) -> Result<TransactionPostConditionMode, CliError> {
384+
let mut i = 0;
385+
let mut value = None;
386+
while i < args.len() {
387+
if args[i] == "--postcondition-mode" {
388+
if value.is_some() {
389+
return Err(CliError::Message(format!(
390+
"Duplicated `--postcondition-mode`.\n\nUSAGE:\n{}",
391+
usage
392+
)));
393+
}
394+
if i + 1 >= args.len() {
395+
return Err(CliError::Message(format!(
396+
"Missing value for `--postcondition-mode`.\n\nUSAGE:\n{}",
397+
usage
398+
)));
399+
}
400+
value = Some(args.remove(i + 1));
401+
args.remove(i);
402+
continue; // do not increment i since elements shifted
403+
}
404+
i += 1;
405+
}
406+
407+
let mode = match value {
408+
Some(mode_str) => match mode_str.as_ref() {
409+
"allow" => TransactionPostConditionMode::Allow,
410+
"deny" => TransactionPostConditionMode::Deny,
411+
_ => {
412+
return Err(CliError::Message(format!(
413+
"Invalid value for `--postcondition-mode`.\n\nUSAGE:\n{}",
414+
usage,
415+
)))
416+
}
417+
},
418+
None => TransactionPostConditionMode::Deny,
419+
};
420+
Ok(mode)
421+
}
422+
370423
fn handle_contract_publish(
371424
args_slice: &[String],
372425
version: TransactionVersion,
@@ -375,15 +428,16 @@ fn handle_contract_publish(
375428
let mut args = args_slice.to_vec();
376429

377430
if !args.is_empty() && args[0] == "-h" {
378-
return Err(CliError::Message(format!("USAGE:\n {}", PUBLISH_USAGE)));
431+
return Err(CliError::Message(format!("USAGE:\n{}", PUBLISH_USAGE)));
379432
}
380-
if args.len() != 5 {
433+
if args.len() < 5 {
381434
return Err(CliError::Message(format!(
382-
"Incorrect argument count supplied \n\nUSAGE:\n {}",
435+
"Incorrect argument count supplied \n\nUSAGE:\n{}",
383436
PUBLISH_USAGE
384437
)));
385438
}
386439
let anchor_mode = parse_anchor_mode(&mut args, PUBLISH_USAGE)?;
440+
let postcond_mode = parse_postcondition_mode(&mut args, PUBLISH_USAGE)?;
387441
let sk_publisher = &args[0];
388442
let tx_fee = args[1].parse()?;
389443
let nonce = args[2].parse()?;
@@ -410,6 +464,7 @@ fn handle_contract_publish(
410464
tx_fee,
411465
);
412466
unsigned_tx.anchor_mode = anchor_mode;
467+
unsigned_tx.post_condition_mode = postcond_mode;
413468

414469
let mut unsigned_tx_bytes = vec![];
415470
unsigned_tx
@@ -896,10 +951,26 @@ fn main_handler(mut argv: Vec<String>) -> Result<String, CliError> {
896951

897952
#[cfg(test)]
898953
mod test {
954+
use std::panic;
955+
956+
use blockstack_lib::chainstate::stacks::TransactionPostCondition;
899957
use stacks_common::util::cargo_workspace;
900958

901959
use super::*;
902960

961+
mod utils {
962+
use super::*;
963+
pub fn tx_deserialize(hex_str: &str) -> StacksTransaction {
964+
let tx_str = hex_bytes(&hex_str).expect("Failed to get hex byte from tx str!");
965+
let mut cursor = io::Cursor::new(&tx_str);
966+
StacksTransaction::consensus_deserialize(&mut cursor).expect("Failed deserialize tx!")
967+
}
968+
969+
pub fn file_read(file_path: &str) -> String {
970+
fs::read_to_string(file_path).expect("Failed to read file contents")
971+
}
972+
}
973+
903974
#[test]
904975
fn generate_should_work() {
905976
assert!(main_handler(vec!["generate-sk".into(), "--testnet".into()]).is_ok());
@@ -912,20 +983,54 @@ mod test {
912983
}
913984

914985
#[test]
915-
fn simple_publish() {
986+
fn test_contract_publish_ok_with_mandatory_params() {
987+
let contract_path = cargo_workspace("sample/contracts/tokens.clar")
988+
.display()
989+
.to_string();
916990
let publish_args = [
917991
"publish",
918992
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
919993
"1",
920994
"0",
921995
"foo-contract",
922-
&cargo_workspace("sample/contracts/tokens.clar")
923-
.display()
924-
.to_string(),
996+
&contract_path,
925997
];
926998

927-
assert!(main_handler(to_string_vec(&publish_args)).is_ok());
999+
let result = main_handler(to_string_vec(&publish_args));
1000+
assert!(result.is_ok());
1001+
1002+
let serial_tx = result.unwrap();
1003+
let deser_tx = utils::tx_deserialize(&serial_tx);
1004+
1005+
assert_eq!(TransactionVersion::Mainnet, deser_tx.version);
1006+
assert_eq!(CHAIN_ID_MAINNET, deser_tx.chain_id);
1007+
assert!(matches!(deser_tx.auth, TransactionAuth::Standard(..)));
1008+
assert_eq!(1, deser_tx.get_tx_fee());
1009+
assert_eq!(0, deser_tx.get_origin_nonce());
1010+
assert_eq!(TransactionAnchorMode::Any, deser_tx.anchor_mode);
1011+
assert_eq!(
1012+
TransactionPostConditionMode::Deny,
1013+
deser_tx.post_condition_mode
1014+
);
1015+
assert_eq!(
1016+
Vec::<TransactionPostCondition>::new(),
1017+
deser_tx.post_conditions
1018+
);
1019+
1020+
let (contract, clarity) = match deser_tx.payload {
1021+
TransactionPayload::SmartContract(a, b) => (a, b),
1022+
_ => panic!("Should not happen!"),
1023+
};
1024+
assert_eq!("foo-contract", contract.name.as_str());
1025+
assert_eq!(
1026+
utils::file_read(&contract_path),
1027+
contract.code_body.to_string()
1028+
);
1029+
assert_eq!(None, clarity);
1030+
}
9281031

1032+
#[test]
1033+
fn test_contract_publish_fails_on_unexistent_file() {
9291034
let publish_args = [
9301035
"publish",
9311036
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
@@ -937,11 +1042,177 @@ mod test {
9371042
.to_string(),
9381043
];
9391044

940-
assert!(format!(
941-
"{}",
942-
main_handler(to_string_vec(&publish_args)).unwrap_err()
943-
)
944-
.contains("IO error"));
1045+
let result = main_handler(to_string_vec(&publish_args));
1046+
assert!(result.is_err());
1047+
1048+
let err_msg = result.unwrap_err().to_string();
1049+
assert!(err_msg.starts_with("IO error reading CLI input:"));
1050+
}
1051+
1052+
#[test]
1053+
fn test_contract_publish_ok_with_anchor_mode() {
1054+
let contract_path = cargo_workspace("sample/contracts/tokens.clar")
1055+
.display()
1056+
.to_string();
1057+
1058+
let mut publish_args = [
1059+
"publish",
1060+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1061+
"1",
1062+
"0",
1063+
"foo-contract",
1064+
&contract_path,
1065+
"--microblock-only",
1066+
];
1067+
1068+
// Scenario OK with anchor mode = `offchain`
1069+
let result = main_handler(to_string_vec(&publish_args));
1070+
assert!(result.is_ok());
1071+
1072+
let serial_tx = result.unwrap();
1073+
let deser_tx = utils::tx_deserialize(&serial_tx);
1074+
assert_eq!(TransactionAnchorMode::OffChainOnly, deser_tx.anchor_mode);
1075+
1076+
// Scenario OK with anchor mode = `onchain`
1077+
publish_args[6] = "--block-only";
1078+
let result = main_handler(to_string_vec(&publish_args));
1079+
assert!(result.is_ok());
1080+
1081+
let serial_tx = result.unwrap();
1082+
let deser_tx = utils::tx_deserialize(&serial_tx);
1083+
assert_eq!(TransactionAnchorMode::OnChainOnly, deser_tx.anchor_mode);
1084+
}
1085+
1086+
#[test]
1087+
fn test_contract_publish_fails_with_anchor_mode() {
1088+
let contract_path = cargo_workspace("sample/contracts/tokens.clar")
1089+
.display()
1090+
.to_string();
1091+
1092+
// Scenario FAIL using both anchor modes
1093+
let publish_args = [
1094+
"publish",
1095+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1096+
"1",
1097+
"0",
1098+
"foo-contract",
1099+
&contract_path,
1100+
"--microblock-only",
1101+
"--block-only",
1102+
];
1103+
1104+
let result = main_handler(to_string_vec(&publish_args));
1105+
assert!(result.is_err());
1106+
1107+
let exp_err_msg = format!(
1108+
"{}\n\nUSAGE:\n{}",
1109+
"Multiple anchor mode detected.", PUBLISH_USAGE
1110+
);
1111+
assert_eq!(exp_err_msg, result.unwrap_err().to_string());
1112+
}
1113+
1114+
#[test]
1115+
fn test_contract_publish_ok_with_postcond_mode() {
1116+
let contract_path = cargo_workspace("sample/contracts/tokens.clar")
1117+
.display()
1118+
.to_string();
1119+
1120+
let mut publish_args = [
1121+
"publish",
1122+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1123+
"1",
1124+
"0",
1125+
"foo-contract",
1126+
&contract_path,
1127+
"--postcondition-mode",
1128+
"allow",
1129+
];
1130+
1131+
// Scenario OK with post-condition = `allow`
1132+
let result = main_handler(to_string_vec(&publish_args));
1133+
assert!(result.is_ok());
1134+
1135+
let serial_tx = result.unwrap();
1136+
let deser_tx = utils::tx_deserialize(&serial_tx);
1137+
assert_eq!(
1138+
TransactionPostConditionMode::Allow,
1139+
deser_tx.post_condition_mode
1140+
);
1141+
1142+
// Scenario OK with post-condition = `deny`
1143+
publish_args[7] = "deny";
1144+
let result = main_handler(to_string_vec(&publish_args));
1145+
assert!(result.is_ok());
1146+
}
1147+
1148+
#[test]
1149+
fn test_contract_publish_fails_with_postcond_mode() {
1150+
let contract_path = cargo_workspace("sample/contracts/tokens.clar")
1151+
.display()
1152+
.to_string();
1153+
1154+
// Scenario FAIL with invalid post-condition
1155+
let publish_args = [
1156+
"publish",
1157+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1158+
"1",
1159+
"0",
1160+
"foo-contract",
1161+
&contract_path,
1162+
"--postcondition-mode",
1163+
"invalid",
1164+
];
1165+
1166+
let result = main_handler(to_string_vec(&publish_args));
1167+
assert!(result.is_err());
1168+
1169+
let exp_err_msg = format!(
1170+
"{}\n\nUSAGE:\n{}",
1171+
"Invalid value for `--postcondition-mode`.", PUBLISH_USAGE
1172+
);
1173+
assert_eq!(exp_err_msg, result.unwrap_err().to_string());
1174+
1175+
// Scenario FAIL with missing post-condition value
1176+
let publish_args = [
1177+
"publish",
1178+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1179+
"1",
1180+
"0",
1181+
"foo-contract",
1182+
&contract_path,
1183+
"--postcondition-mode",
1184+
];
1185+
1186+
let result = main_handler(to_string_vec(&publish_args));
1187+
assert!(result.is_err());
1188+
1189+
let exp_err_msg = format!(
1190+
"{}\n\nUSAGE:\n{}",
1191+
"Missing value for `--postcondition-mode`.", PUBLISH_USAGE
1192+
);
1193+
assert_eq!(exp_err_msg, result.unwrap_err().to_string());
1194+
1195+
// Scenario FAIL with duplicated post-condition
1196+
let publish_args = [
1197+
"publish",
1198+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1199+
"1",
1200+
"0",
1201+
"foo-contract",
1202+
&contract_path,
1203+
"--postcondition-mode",
1204+
"allow",
1205+
"--postcondition-mode",
1206+
];
1207+
1208+
let result = main_handler(to_string_vec(&publish_args));
1209+
assert!(result.is_err());
1210+
1211+
let exp_err_msg = format!(
1212+
"{}\n\nUSAGE:\n{}",
1213+
"Duplicated `--postcondition-mode`.", PUBLISH_USAGE
1214+
);
1215+
assert_eq!(exp_err_msg, result.unwrap_err().to_string());
9451216
}
9461217

9471218
#[test]

0 commit comments

Comments
 (0)