@@ -984,6 +984,8 @@ pub struct ContractConsensusTest<'a> {
984984 chain : ConsensusChain < ' a > ,
985985 /// Address of the contract deployer (the test faucet).
986986 contract_addr : StacksAddress ,
987+ /// Mapping of epoch → list of prerequisite contracts to deploy.
988+ setup_contracts_per_epoch : HashMap < StacksEpochId , Vec < SetupContract > > ,
987989 /// Mapping of epoch → list of `(contract_name, ClarityVersion)` deployed in that epoch.
988990 /// Multiple versions may exist per epoch (e.g., Clarity 1, 2, 3 in Epoch 3.0).
989991 contract_deploys_per_epoch : HashMap < StacksEpochId , Vec < ( String , ClarityVersion ) > > ,
@@ -1019,6 +1021,7 @@ impl ContractConsensusTest<'_> {
10191021 /// * `contract_code` - Clarity source code of the contract
10201022 /// * `function_name` - Contract function to test
10211023 /// * `function_args` - Arguments passed to `function_name` on every call
1024+ /// * `setup_contracts` - Contracts that must be deployed before epoch-specific logic runs
10221025 ///
10231026 /// # Panics
10241027 ///
@@ -1034,6 +1037,7 @@ impl ContractConsensusTest<'_> {
10341037 contract_code : & str ,
10351038 function_name : & str ,
10361039 function_args : & [ ClarityValue ] ,
1040+ setup_contracts : & [ SetupContract ] ,
10371041 ) -> Self {
10381042 assert ! (
10391043 !deploy_epochs. is_empty( ) ,
@@ -1051,15 +1055,48 @@ impl ContractConsensusTest<'_> {
10511055 HashMap :: new ( ) ;
10521056 let mut contract_calls_per_epoch: HashMap < StacksEpochId , Vec < String > > = HashMap :: new ( ) ;
10531057 let mut contract_names = vec ! [ ] ;
1058+ let mut setup_contracts_per_epoch: HashMap < StacksEpochId , Vec < SetupContract > > =
1059+ HashMap :: new ( ) ;
1060+
1061+ let mut epoch_candidates: BTreeSet < StacksEpochId > = deploy_epochs. iter ( ) . copied ( ) . collect ( ) ;
1062+ epoch_candidates. extend ( call_epochs. iter ( ) . copied ( ) ) ;
1063+ let default_setup_epoch = * epoch_candidates
1064+ . iter ( )
1065+ . next ( )
1066+ . expect ( "deploy_epochs guarantees at least one epoch" ) ;
1067+
1068+ for contract in setup_contracts {
1069+ // Deploy the setup contracts in the first epoch if not specified.
1070+ let deploy_epoch = contract. deploy_epoch . unwrap_or ( default_setup_epoch) ;
1071+ // Get the default Clarity version for the epoch of the contract if not specified.
1072+ let clarity_version = contract. clarity_version . or_else ( || {
1073+ if deploy_epoch < StacksEpochId :: Epoch21 {
1074+ None
1075+ } else {
1076+ Some ( ClarityVersion :: default_for_epoch ( deploy_epoch) )
1077+ }
1078+ } ) ;
1079+ let mut contract = contract. clone ( ) ;
1080+ contract. deploy_epoch = Some ( deploy_epoch) ;
1081+ contract. clarity_version = clarity_version;
1082+ setup_contracts_per_epoch
1083+ . entry ( deploy_epoch)
1084+ . or_default ( )
1085+ . push ( contract) ;
1086+ }
10541087
10551088 // Combine and sort unique epochs
1056- let all_epochs: BTreeSet < StacksEpochId > =
1057- deploy_epochs . iter ( ) . chain ( call_epochs ) . cloned ( ) . collect ( ) ;
1089+ let mut all_epochs: BTreeSet < StacksEpochId > = epoch_candidates ;
1090+ all_epochs . extend ( setup_contracts_per_epoch . keys ( ) . copied ( ) ) ;
10581091
10591092 // Precompute contract names and block counts
10601093 for epoch in & all_epochs {
10611094 let mut num_blocks = 0 ;
10621095
1096+ if let Some ( contracts) = setup_contracts_per_epoch. get ( epoch) {
1097+ num_blocks += contracts. len ( ) as u64 ;
1098+ }
1099+
10631100 if deploy_epochs. contains ( epoch) {
10641101 let clarity_versions = clarity_versions_for_epoch ( * epoch) ;
10651102 let epoch_name = format ! ( "Epoch{}" , epoch. to_string( ) . replace( '.' , "_" ) ) ;
@@ -1102,6 +1139,7 @@ impl ContractConsensusTest<'_> {
11021139 contract_code : contract_code. to_string ( ) ,
11031140 function_name : function_name. to_string ( ) ,
11041141 function_args : function_args. to_vec ( ) ,
1142+ setup_contracts_per_epoch,
11051143 all_epochs,
11061144 }
11071145 }
@@ -1134,6 +1172,30 @@ impl ContractConsensusTest<'_> {
11341172 result
11351173 }
11361174
1175+ /// Deploys prerequisite contracts scheduled for the given epoch.
1176+ fn deploy_setup_contracts ( & mut self , epoch : StacksEpochId ) -> Vec < ExpectedResult > {
1177+ let Some ( contracts) = self . setup_contracts_per_epoch . get ( & epoch) . cloned ( ) else {
1178+ return vec ! [ ] ;
1179+ } ;
1180+
1181+ let is_naka_block = epoch. uses_nakamoto_blocks ( ) ;
1182+ contracts
1183+ . into_iter ( )
1184+ . map ( |contract| {
1185+ self . chain . consume_pre_naka_prepare_phase ( ) ;
1186+ self . append_tx_block (
1187+ & TestTxSpec :: ContractDeploy {
1188+ sender : & FAUCET_PRIV_KEY ,
1189+ name : & contract. name ,
1190+ code : & contract. code ,
1191+ clarity_version : contract. clarity_version ,
1192+ } ,
1193+ is_naka_block,
1194+ )
1195+ } )
1196+ . collect ( )
1197+ }
1198+
11371199 /// Deploys all contract versions scheduled for the given epoch.
11381200 ///
11391201 /// For each Clarity version supported in the epoch:
@@ -1251,6 +1313,7 @@ impl ContractConsensusTest<'_> {
12511313 . test_chainstate
12521314 . advance_into_epoch ( & private_key, epoch) ;
12531315
1316+ results. extend ( self . deploy_setup_contracts ( epoch) ) ;
12541317 results. extend ( self . deploy_contracts ( epoch) ) ;
12551318 results. extend ( self . call_contracts ( epoch) ) ;
12561319 }
@@ -1464,8 +1527,9 @@ impl TestTxFactory {
14641527/// * `contract_code` — The Clarity source code for the contract.
14651528/// * `function_name` — The public function to call.
14661529/// * `function_args` — Function arguments, provided as a slice of [`ClarityValue`].
1467- /// * `deploy_epochs` — *(optional)* Epochs in which to deploy the contract. Defaults to all epochs ≥ 3 .0.
1530+ /// * `deploy_epochs` — *(optional)* Epochs in which to deploy the contract. Defaults to all epochs ≥ 2 .0.
14681531/// * `call_epochs` — *(optional)* Epochs in which to call the function. Defaults to [`EPOCHS_TO_TEST`].
1532+ /// * `setup_contracts` — *(optional)* Slice of [`SetupContract`] values to deploy once before the main contract logic.
14691533///
14701534/// # Example
14711535///
@@ -1474,9 +1538,15 @@ impl TestTxFactory {
14741538/// fn test_my_contract_call_consensus() {
14751539/// contract_call_consensus_test!(
14761540/// contract_name: "my-contract",
1477- /// contract_code: "(define-public (get-message) (ok \"hello\"))",
1541+ /// contract_code: "
1542+ /// (define-public (get-message)
1543+ /// (contract-call? .dependency.foo))",
14781544/// function_name: "get-message",
14791545/// function_args: &[],
1546+ /// setup_contracts: &[SetupContract::new(
1547+ /// "dependency",
1548+ /// "(define-public (foo) (ok \"hello\"))",
1549+ /// )],
14801550/// );
14811551/// }
14821552/// ```
@@ -1488,6 +1558,7 @@ macro_rules! contract_call_consensus_test {
14881558 function_args: $function_args: expr,
14891559 $( deploy_epochs: $deploy_epochs: expr, ) ?
14901560 $( call_epochs: $call_epochs: expr, ) ?
1561+ $( setup_contracts: $setup_contracts: expr, ) ?
14911562 ) => {
14921563 {
14931564 // Handle deploy_epochs parameter (default to all epochs >= 2.0 if not provided)
@@ -1497,6 +1568,8 @@ macro_rules! contract_call_consensus_test {
14971568 // Handle call_epochs parameter (default to EPOCHS_TO_TEST if not provided)
14981569 let call_epochs = $crate:: chainstate:: tests:: consensus:: EPOCHS_TO_TEST ;
14991570 $( let call_epochs = $call_epochs; ) ?
1571+ let setup_contracts: & [ $crate:: chainstate:: tests:: consensus:: SetupContract ] = & [ ] ;
1572+ $( let setup_contracts = $setup_contracts; ) ?
15001573 let contract_test = $crate:: chainstate:: tests:: consensus:: ContractConsensusTest :: new(
15011574 function_name!( ) ,
15021575 vec![ ] ,
@@ -1506,6 +1579,7 @@ macro_rules! contract_call_consensus_test {
15061579 $contract_code,
15071580 $function_name,
15081581 $function_args,
1582+ setup_contracts,
15091583 ) ;
15101584 let result = contract_test. run( ) ;
15111585 insta:: assert_ron_snapshot!( result) ;
@@ -1532,6 +1606,7 @@ pub(crate) use contract_call_consensus_test;
15321606/// * `contract_name` — Name of the contract being tested.
15331607/// * `contract_code` — The Clarity source code of the contract.
15341608/// * `deploy_epochs` — *(optional)* Epochs in which to deploy the contract. Defaults to [`EPOCHS_TO_TEST`].
1609+ /// * `setup_contracts` — *(optional)* Slice of [`SetupContract`] values to deploy before the main contract.
15351610///
15361611/// # Example
15371612///
@@ -1546,34 +1621,72 @@ pub(crate) use contract_call_consensus_test;
15461621/// }
15471622/// ```
15481623macro_rules! contract_deploy_consensus_test {
1549- // Handle the case where deploy_epochs is not provided
15501624 (
15511625 contract_name: $contract_name: expr,
15521626 contract_code: $contract_code: expr,
1627+ $( deploy_epochs: $deploy_epochs: expr, ) ?
1628+ $( setup_contracts: $setup_contracts: expr, ) ?
15531629 ) => {
1554- contract_deploy_consensus_test!(
1555- contract_name: $contract_name,
1556- contract_code: $contract_code,
1557- deploy_epochs: $crate:: chainstate:: tests:: consensus:: EPOCHS_TO_TEST ,
1558- ) ;
1559- } ;
1560- (
1561- contract_name: $contract_name: expr,
1562- contract_code: $contract_code: expr,
1563- deploy_epochs: $deploy_epochs: expr,
1564- ) => {
1565- $crate:: chainstate:: tests:: consensus:: contract_call_consensus_test!(
1566- contract_name: $contract_name,
1567- contract_code: $contract_code,
1568- function_name: "" , // No function calls, just deploys
1569- function_args: & [ ] , // No function calls, just deploys
1570- deploy_epochs: $deploy_epochs,
1571- call_epochs: & [ ] , // No function calls, just deploys
1572- ) ;
1630+ {
1631+ let deploy_epochs = $crate:: chainstate:: tests:: consensus:: EPOCHS_TO_TEST ;
1632+ $( let deploy_epochs = $deploy_epochs; ) ?
1633+ $crate:: chainstate:: tests:: consensus:: contract_call_consensus_test!(
1634+ contract_name: $contract_name,
1635+ contract_code: $contract_code,
1636+ function_name: "" , // No function calls, just deploys
1637+ function_args: & [ ] , // No function calls, just deploys
1638+ deploy_epochs: deploy_epochs,
1639+ call_epochs: & [ ] , // No function calls, just deploys
1640+ $( setup_contracts: $setup_contracts, ) ?
1641+ ) ;
1642+ }
15731643 } ;
15741644}
15751645pub ( crate ) use contract_deploy_consensus_test;
15761646
1647+ /// Contract deployment that must occur before `contract_call_consensus_test!` or `contract_deploy_consensus_test!` runs its own logic.
1648+ ///
1649+ /// These setups are useful when the primary contract references other contracts (traits, functions, etc.)
1650+ /// that need to exist ahead of time with deterministic names and versions.
1651+ #[ derive( Clone , Debug ) ]
1652+ pub struct SetupContract {
1653+ /// Contract name that should be deployed (no macro suffixes applied).
1654+ pub name : String ,
1655+ /// Source code for the supporting contract.
1656+ pub code : String ,
1657+ /// Optional Clarity version for this contract.
1658+ pub clarity_version : Option < ClarityVersion > ,
1659+ /// Optional epoch for this contract.
1660+ pub deploy_epoch : Option < StacksEpochId > ,
1661+ }
1662+
1663+ impl SetupContract {
1664+ /// Creates a new SetupContract with default deployment settings.
1665+ ///
1666+ /// By default, the contract will deploy in the first epoch used by the test and with the
1667+ /// default Clarity version for that epoch.
1668+ pub fn new ( name : impl Into < String > , code : impl Into < String > ) -> Self {
1669+ Self {
1670+ name : name. into ( ) ,
1671+ code : code. into ( ) ,
1672+ clarity_version : None ,
1673+ deploy_epoch : None ,
1674+ }
1675+ }
1676+
1677+ /// Override the epoch where this setup contract should deploy.
1678+ pub fn with_epoch ( mut self , epoch : StacksEpochId ) -> Self {
1679+ self . deploy_epoch = Some ( epoch) ;
1680+ self
1681+ }
1682+
1683+ /// Override the Clarity version used to deploy this setup contract.
1684+ pub fn with_clarity_version ( mut self , version : ClarityVersion ) -> Self {
1685+ self . clarity_version = Some ( version) ;
1686+ self
1687+ }
1688+ }
1689+
15771690// Just a namespace for utilities for writing consensus tests
15781691pub struct ConsensusUtils ;
15791692
0 commit comments