@@ -34,9 +34,9 @@ use blockstack_lib::burnchains::Address;
34
34
use blockstack_lib:: chainstate:: stacks:: {
35
35
StacksBlock , StacksBlockHeader , StacksMicroblock , StacksPrivateKey , StacksPublicKey ,
36
36
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 ,
40
40
} ;
41
41
use blockstack_lib:: clarity_cli:: vm_execute;
42
42
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:
90
90
91
91
--microblock-only indicates to mine this transaction only in a microblock
92
92
--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`.
93
97
" ;
94
98
95
99
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(
340
344
for i in 0 ..num_args {
341
345
if args[ i] == "--microblock-only" {
342
346
if idx > 0 {
343
- return Err ( CliError :: Message ( format ! ( "USAGE:\n {}" , usage, ) ) ) ;
347
+ return Err ( CliError :: Message ( format ! (
348
+ "Multiple anchor mode detected.\n \n USAGE:\n {}" ,
349
+ usage,
350
+ ) ) ) ;
344
351
}
345
352
346
353
offchain_only = true ;
347
354
idx = i;
348
355
}
349
356
if args[ i] == "--block-only" {
350
357
if idx > 0 {
351
- return Err ( CliError :: Message ( format ! ( "USAGE:\n {}" , usage, ) ) ) ;
358
+ return Err ( CliError :: Message ( format ! (
359
+ "Multiple anchor mode detected.\n \n USAGE:\n {}" ,
360
+ usage,
361
+ ) ) ) ;
352
362
}
353
363
354
364
onchain_only = true ;
@@ -367,6 +377,49 @@ fn parse_anchor_mode(
367
377
}
368
378
}
369
379
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 \n USAGE:\n {}" ,
391
+ usage
392
+ ) ) ) ;
393
+ }
394
+ if i + 1 >= args. len ( ) {
395
+ return Err ( CliError :: Message ( format ! (
396
+ "Missing value for `--postcondition-mode`.\n \n USAGE:\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 \n USAGE:\n {}" ,
414
+ usage,
415
+ ) ) )
416
+ }
417
+ } ,
418
+ None => TransactionPostConditionMode :: Deny ,
419
+ } ;
420
+ Ok ( mode)
421
+ }
422
+
370
423
fn handle_contract_publish (
371
424
args_slice : & [ String ] ,
372
425
version : TransactionVersion ,
@@ -375,15 +428,16 @@ fn handle_contract_publish(
375
428
let mut args = args_slice. to_vec ( ) ;
376
429
377
430
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 ) ) ) ;
379
432
}
380
- if args. len ( ) != 5 {
433
+ if args. len ( ) < 5 {
381
434
return Err ( CliError :: Message ( format ! (
382
- "Incorrect argument count supplied \n \n USAGE:\n {}" ,
435
+ "Incorrect argument count supplied \n \n USAGE:\n {}" ,
383
436
PUBLISH_USAGE
384
437
) ) ) ;
385
438
}
386
439
let anchor_mode = parse_anchor_mode ( & mut args, PUBLISH_USAGE ) ?;
440
+ let postcond_mode = parse_postcondition_mode ( & mut args, PUBLISH_USAGE ) ?;
387
441
let sk_publisher = & args[ 0 ] ;
388
442
let tx_fee = args[ 1 ] . parse ( ) ?;
389
443
let nonce = args[ 2 ] . parse ( ) ?;
@@ -410,6 +464,7 @@ fn handle_contract_publish(
410
464
tx_fee,
411
465
) ;
412
466
unsigned_tx. anchor_mode = anchor_mode;
467
+ unsigned_tx. post_condition_mode = postcond_mode;
413
468
414
469
let mut unsigned_tx_bytes = vec ! [ ] ;
415
470
unsigned_tx
@@ -896,10 +951,26 @@ fn main_handler(mut argv: Vec<String>) -> Result<String, CliError> {
896
951
897
952
#[ cfg( test) ]
898
953
mod test {
954
+ use std:: panic;
955
+
956
+ use blockstack_lib:: chainstate:: stacks:: TransactionPostCondition ;
899
957
use stacks_common:: util:: cargo_workspace;
900
958
901
959
use super :: * ;
902
960
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
+
903
974
#[ test]
904
975
fn generate_should_work ( ) {
905
976
assert ! ( main_handler( vec![ "generate-sk" . into( ) , "--testnet" . into( ) ] ) . is_ok( ) ) ;
@@ -912,20 +983,54 @@ mod test {
912
983
}
913
984
914
985
#[ 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 ( ) ;
916
990
let publish_args = [
917
991
"publish" ,
918
992
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3" ,
919
993
"1" ,
920
994
"0" ,
921
995
"foo-contract" ,
922
- & cargo_workspace ( "sample/contracts/tokens.clar" )
923
- . display ( )
924
- . to_string ( ) ,
996
+ & contract_path,
925
997
] ;
926
998
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
+ }
928
1031
1032
+ #[ test]
1033
+ fn test_contract_publish_fails_on_unexistent_file ( ) {
929
1034
let publish_args = [
930
1035
"publish" ,
931
1036
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3" ,
@@ -937,11 +1042,177 @@ mod test {
937
1042
. to_string ( ) ,
938
1043
] ;
939
1044
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 \n USAGE:\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 \n USAGE:\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 \n USAGE:\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 \n USAGE:\n {}" ,
1213
+ "Duplicated `--postcondition-mode`." , PUBLISH_USAGE
1214
+ ) ;
1215
+ assert_eq ! ( exp_err_msg, result. unwrap_err( ) . to_string( ) ) ;
945
1216
}
946
1217
947
1218
#[ test]
0 commit comments