@stacks/rendezvous 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Rendezvous `rv` is a Clarity fuzzer designed to cut through your smart contract'
8
8
 
9
9
  ### Prerequisites
10
10
 
11
- - **Node.js**: Supported versions include 18, 20, and 22. Other versions may work, but they are untested.
11
+ - **Node.js**: Supported versions include 20, 22, and 23. Other versions may work, but they are untested.
12
12
 
13
13
  ### Inspiration
14
14
 
@@ -53,7 +53,6 @@ npx rv <path-to-clarinet-project> <contract-name> <type>
53
53
  **Options:**
54
54
 
55
55
  - `--seed` – The seed to use for the replay functionality.
56
- - `--path` – The path to use for the replay functionality.
57
56
  - `--runs` – The number of test iterations to use for exercising the contracts.
58
57
  (default: `100`)
59
58
  - `--bail` – Stop after the first failure.
package/dist/app.js CHANGED
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  return (mod && mod.__esModule) ? mod : { "default": mod };
14
14
  };
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.tryParseRemoteDataSettings = exports.getManifestFileName = exports.invalidRemoteDataWarningMessage = exports.noRemoteData = void 0;
16
+ exports.tryParseRemoteDataSettings = exports.getManifestFileName = exports.noRemoteData = void 0;
17
17
  exports.main = main;
18
18
  const path_1 = require("path");
19
19
  const events_1 = require("events");
@@ -25,6 +25,7 @@ const citizen_1 = require("./citizen");
25
25
  const package_json_1 = require("./package.json");
26
26
  const ansicolor_1 = require("ansicolor");
27
27
  const fs_1 = require("fs");
28
+ const util_1 = require("util");
28
29
  const dialer_1 = require("./dialer");
29
30
  const logger = (log, logLevel = "log") => {
30
31
  console[logLevel](log);
@@ -35,16 +36,7 @@ const logger = (log, logLevel = "log") => {
35
36
  */
36
37
  exports.noRemoteData = {
37
38
  enabled: false,
38
- api_url: "",
39
- initial_height: 1,
40
39
  };
41
- exports.invalidRemoteDataWarningMessage = `\nRemote data settings existing in Clarinet.toml, but remote data feature will not be used! To use remote data, please make sure the following fields are set:
42
-
43
- - enabled = true
44
- - api_url = <stacks-api-url>
45
- - initial_height = <stacks-block-height-to-fork-from>
46
-
47
- under the "repl.remote_data" section in the Clarinet.toml file.`;
48
40
  /**
49
41
  * Gets the manifest file name for a Clarinet project.
50
42
  * If a custom manifest exists (`Clarinet-<contract-name>.toml`), it is used.
@@ -64,17 +56,14 @@ exports.getManifestFileName = getManifestFileName;
64
56
  const tryParseRemoteDataSettings = (manifestPath, radio) => {
65
57
  var _a, _b;
66
58
  const clarinetToml = toml_1.default.parse((0, fs_1.readFileSync)((0, path_1.resolve)(manifestPath), "utf-8"));
67
- const remoteDataUserSettings = (_b = (_a = clarinetToml.repl) === null || _a === void 0 ? void 0 : _a["remote_data"]) !== null && _b !== void 0 ? _b : undefined;
68
- const invalidRemoteDataSetup = !(remoteDataUserSettings === null || remoteDataUserSettings === void 0 ? void 0 : remoteDataUserSettings["api_url"]) ||
69
- !(remoteDataUserSettings === null || remoteDataUserSettings === void 0 ? void 0 : remoteDataUserSettings["enabled"]) ||
70
- !(remoteDataUserSettings === null || remoteDataUserSettings === void 0 ? void 0 : remoteDataUserSettings["initial_height"]);
71
- if (remoteDataUserSettings !== undefined && invalidRemoteDataSetup) {
72
- radio.emit("logMessage", (0, ansicolor_1.yellow)(exports.invalidRemoteDataWarningMessage));
73
- }
74
- else if (remoteDataUserSettings) {
59
+ const remoteDataUserSettings = (_b = (_a = clarinetToml.repl) === null || _a === void 0 ? void 0 : _a.remote_data) !== null && _b !== void 0 ? _b : undefined;
60
+ if (remoteDataUserSettings && (remoteDataUserSettings === null || remoteDataUserSettings === void 0 ? void 0 : remoteDataUserSettings.enabled) === true) {
75
61
  radio.emit("logMessage", (0, ansicolor_1.yellow)("\nUsing remote data. Setting up the environment can take up to a minute..."));
76
62
  }
77
- if (!remoteDataUserSettings || invalidRemoteDataSetup) {
63
+ // If no remote data settings are provided, we still need to return an object
64
+ // with the `enabled` property set to `false`. That is what simnet expects
65
+ // at least in order to initialize an empty simnet session.
66
+ if (!remoteDataUserSettings) {
78
67
  return exports.noRemoteData;
79
68
  }
80
69
  return remoteDataUserSettings;
@@ -83,7 +72,7 @@ exports.tryParseRemoteDataSettings = tryParseRemoteDataSettings;
83
72
  const helpMessage = `
84
73
  rv v${package_json_1.version}
85
74
 
86
- Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--path=<path>] [--runs=<runs>] [--dial=<path-to-dialers-file>] [--help]
75
+ Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--runs=<runs>] [--dial=<path-to-dialers-file>] [--help]
87
76
 
88
77
  Positional arguments:
89
78
  path-to-clarinet-project - The path to the Clarinet project.
@@ -92,45 +81,61 @@ const helpMessage = `
92
81
 
93
82
  Options:
94
83
  --seed - The seed to use for the replay functionality.
95
- --path - The path to use for the replay functionality.
96
84
  --runs - The runs to use for iterating over the tests. Default: 100.
97
85
  --bail - Stop after the first failure.
98
86
  --dial – The path to a JavaScript file containing custom pre- and post-execution functions (dialers).
99
87
  --help - Show the help message.
100
88
  `;
101
- const parseBooleanOption = (argName) => process.argv.slice(4).includes(`--${argName}`);
102
- const parseOption = (argName) => {
103
- var _a;
104
- return (_a = process.argv
105
- .find((arg, idx) => idx >= 4 && arg.toLowerCase().startsWith(`--${argName}`))) === null || _a === void 0 ? void 0 : _a.split("=")[1];
106
- };
107
89
  function main() {
108
90
  return __awaiter(this, void 0, void 0, function* () {
109
- var _a;
110
91
  const radio = new events_1.EventEmitter();
111
92
  radio.on("logMessage", (log) => logger(log));
112
93
  radio.on("logFailure", (log) => logger((0, ansicolor_1.red)(log), "error"));
113
- const args = process.argv;
114
- if (args.includes("--help")) {
94
+ const { positionals: positionalArgs, values: options } = (0, util_1.parseArgs)({
95
+ allowPositionals: true,
96
+ args: process.argv.slice(2),
97
+ options: {
98
+ seed: { type: "string" },
99
+ runs: { type: "string" },
100
+ dial: { type: "string" },
101
+ bail: { type: "boolean" },
102
+ help: { type: "boolean", short: "h" },
103
+ },
104
+ });
105
+ const [manifestDir, sutContractName, type] = positionalArgs;
106
+ const runConfig = {
107
+ /** The relative path to the Clarinet project. */
108
+ manifestDir: manifestDir,
109
+ /** The target contract name. */
110
+ sutContractName: sutContractName,
111
+ /** The type of testing to be executed. Valid values: test, invariant. */
112
+ type: type === null || type === void 0 ? void 0 : type.toLowerCase(),
113
+ /** The seed to use for the replay functionality. */
114
+ seed: options.seed ? parseInt(options.seed, 10) : undefined,
115
+ /** The number of runs to use. */
116
+ runs: options.runs ? parseInt(options.runs, 10) : undefined,
117
+ /** Whether to bail on the first failure. */
118
+ bail: options.bail || false,
119
+ /** The path to the dialer file. */
120
+ dial: options.dial || undefined,
121
+ /** Whether to show the help message. */
122
+ help: options.help || false,
123
+ };
124
+ if (runConfig.help) {
115
125
  radio.emit("logMessage", helpMessage);
116
126
  return;
117
127
  }
118
- /** The relative path to the Clarinet project. */
119
- const manifestDir = args[2];
120
- if (!manifestDir || manifestDir.startsWith("--")) {
128
+ if (!runConfig.manifestDir) {
121
129
  radio.emit("logMessage", (0, ansicolor_1.red)("\nNo path to Clarinet project provided. Supply it immediately or face the relentless scrutiny of your contract's vulnerabilities."));
122
130
  radio.emit("logMessage", helpMessage);
123
131
  return;
124
132
  }
125
- /** The target contract name. */
126
- const sutContractName = args[3];
127
- if (!sutContractName || sutContractName.startsWith("--")) {
133
+ if (!runConfig.sutContractName) {
128
134
  radio.emit("logMessage", (0, ansicolor_1.red)("\nNo target contract name provided. Please provide the contract name to be fuzzed."));
129
135
  radio.emit("logMessage", helpMessage);
130
136
  return;
131
137
  }
132
- const type = (_a = args[4]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
133
- if (!type || type.startsWith("--") || !["test", "invariant"].includes(type)) {
138
+ if (!runConfig.type || !["test", "invariant"].includes(runConfig.type)) {
134
139
  radio.emit("logMessage", (0, ansicolor_1.red)("\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant."));
135
140
  radio.emit("logMessage", helpMessage);
136
141
  return;
@@ -139,59 +144,49 @@ function main() {
139
144
  * The relative path to the manifest file, either `Clarinet.toml` or
140
145
  * `Clarinet-<contract-name>.toml`. If the latter exists, it is used.
141
146
  */
142
- const manifestPath = (0, path_1.join)(manifestDir, (0, exports.getManifestFileName)(manifestDir, sutContractName));
147
+ const manifestPath = (0, path_1.join)(runConfig.manifestDir, (0, exports.getManifestFileName)(runConfig.manifestDir, runConfig.sutContractName));
143
148
  radio.emit("logMessage", `Using manifest path: ${manifestPath}`);
144
- radio.emit("logMessage", `Target contract: ${sutContractName}`);
145
- const seed = parseInt(parseOption("seed"), 10) || undefined;
146
- if (seed !== undefined) {
147
- radio.emit("logMessage", `Using seed: ${seed}`);
149
+ radio.emit("logMessage", `Target contract: ${runConfig.sutContractName}`);
150
+ if (runConfig.seed !== undefined) {
151
+ radio.emit("logMessage", `Using seed: ${runConfig.seed}`);
148
152
  }
149
- const path = parseOption("path") || undefined;
150
- if (path !== undefined) {
151
- radio.emit("logMessage", `Using path: ${path}`);
153
+ if (runConfig.runs !== undefined) {
154
+ radio.emit("logMessage", `Using runs: ${runConfig.runs}`);
152
155
  }
153
- const runs = parseInt(parseOption("runs"), 10) || undefined;
154
- if (runs !== undefined) {
155
- radio.emit("logMessage", `Using runs: ${runs}`);
156
- }
157
- const bail = parseBooleanOption("bail");
158
- if (bail) {
156
+ if (runConfig.bail) {
159
157
  radio.emit("logMessage", `Bailing on first failure.`);
160
158
  }
161
- /**
162
- * The path to the dialer file. The dialer file allows the user to register
163
- * custom pre and post-execution JavaScript functions to be executed before
164
- * and after the public function calls during invariant testing.
165
- */
166
- const dialPath = parseOption("dial") || undefined;
167
- if (dialPath !== undefined) {
168
- radio.emit("logMessage", `Using dial path: ${dialPath}`);
159
+ if (runConfig.dial !== undefined) {
160
+ radio.emit("logMessage", `Using dial path: ${runConfig.dial}`);
169
161
  }
170
162
  /**
171
163
  * The dialer registry, which is used to keep track of all the custom dialers
172
164
  * registered by the user using the `--dial` flag.
173
165
  */
174
- const dialerRegistry = dialPath !== undefined ? new dialer_1.DialerRegistry(dialPath) : undefined;
166
+ const dialerRegistry = runConfig.dial !== undefined
167
+ ? new dialer_1.DialerRegistry(runConfig.dial)
168
+ : undefined;
175
169
  if (dialerRegistry !== undefined) {
176
170
  dialerRegistry.registerDialers();
177
171
  }
178
172
  const remoteDataSettings = (0, exports.tryParseRemoteDataSettings)(manifestPath, radio);
179
- const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(manifestDir, manifestPath, remoteDataSettings, sutContractName);
173
+ const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, remoteDataSettings, runConfig.sutContractName);
180
174
  /**
181
175
  * The list of contract IDs for the SUT contract names, as per the simnet.
182
176
  */
183
- const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => (0, shared_1.getContractNameFromContractId)(deployedContract) === sutContractName);
177
+ const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => (0, shared_1.getContractNameFromContractId)(deployedContract) ===
178
+ runConfig.sutContractName);
184
179
  const rendezvousAllFunctions = (0, shared_1.getFunctionsFromContractInterfaces)(new Map(Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet)).filter(([contractId]) => rendezvousList.includes(contractId))));
185
180
  // Select the testing routine based on `type`.
186
181
  // If "invariant", call `checkInvariants` to verify contract invariants.
187
182
  // If "test", call `checkProperties` for property-based testing.
188
- switch (type) {
183
+ switch (runConfig.type) {
189
184
  case "invariant": {
190
- yield (0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, dialerRegistry, radio);
185
+ yield (0, invariant_1.checkInvariants)(simnet, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, dialerRegistry, radio);
191
186
  break;
192
187
  }
193
188
  case "test": {
194
- (0, property_1.checkProperties)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, radio);
189
+ (0, property_1.checkProperties)(simnet, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, radio);
195
190
  break;
196
191
  }
197
192
  }
package/dist/citizen.js CHANGED
@@ -50,7 +50,9 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSetting
50
50
  const balanceHex = simnet.runSnippet(`(stx-get-balance '${address})`);
51
51
  return [address, (0, transactions_1.cvToValue)((0, transactions_1.hexToCV)(balanceHex))];
52
52
  }));
53
- const sbtcBalancesMap = (0, exports.getSbtcBalancesFromSimnet)(simnet);
53
+ // If the sbtc-token contract is included in the deployment plan, we need to
54
+ // restore the sBTC balances. Otherwise, use an empty map.
55
+ const sbtcBalancesMap = (0, exports.getSbtcBalancesFromSimnet)(simnet, deploymentPlan, remoteDataSettings);
54
56
  yield simnet.initEmptySession(remoteDataSettings);
55
57
  simnetAddresses.forEach((address) => {
56
58
  simnet.mintSTX(address, stxBalancesMap.get(address));
@@ -323,30 +325,59 @@ function scheduleRendezvous(targetContractSource, tests) {
323
325
  (ok (map-set context function-name {called: called})))`;
324
326
  return `${targetContractSource}\n\n${context}\n\n${tests}`;
325
327
  }
328
+ /**
329
+ * Checks if a contract can be found in the deployment plan.
330
+ * @param deploymentPlan The parsed deployment plan.
331
+ * @param contractAddress The address of the contract.
332
+ * @param contractName The name of the contract.
333
+ * @returns True if the contract can be found in the deployment plan, false
334
+ * otherwise.
335
+ */
336
+ const isContractInDeploymentPlan = (deploymentPlan, contractAddress, contractName) => {
337
+ return deploymentPlan.plan.batches.some((batch) => batch.transactions.some((transaction) => {
338
+ var _a, _b;
339
+ return ((_a = transaction["emulated-contract-publish"]) === null || _a === void 0 ? void 0 : _a["contract-name"]) ===
340
+ contractName &&
341
+ ((_b = transaction["emulated-contract-publish"]) === null || _b === void 0 ? void 0 : _b["emulated-sender"]) ===
342
+ contractAddress;
343
+ }));
344
+ };
326
345
  /**
327
346
  * Maps the simnet accounts to their sBTC balances. The function tries to call
328
347
  * the `get-balance` function of the `sbtc-token` contract for each address. If
329
348
  * the call fails, it returns a balance of 0 for that address. The call fails
330
349
  * if the user is not working with sBTC.
331
350
  * @param simnet The simnet instance.
351
+ * @param deploymentPlan The parsed deployment plan.
352
+ * @param remoteDataSettings The remote data settings.
332
353
  * @returns A map of addresses to their sBTC balances.
333
354
  */
334
- const getSbtcBalancesFromSimnet = (simnet) => new Map([...simnet.getAccounts().values()].map((address) => {
335
- try {
336
- const { result: getBalanceResult } = simnet.callReadOnlyFn("SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token", "get-balance", [transactions_1.Cl.principal(address)], address);
337
- // If the previous read-only call works, the user is working with
338
- // sBTC. This means we can proceed with restoring sBTC balances.
339
- const sbtcBalanceJSON = (0, transactions_1.cvToJSON)(getBalanceResult);
340
- // The `get-balance` function returns a response containing the uint
341
- // balance of the address. In the JSON representation, the balance is
342
- // represented as a string. We need to parse it to an integer.
343
- const sbtcBalance = parseInt(sbtcBalanceJSON.value.value, 10);
344
- return [address, sbtcBalance];
345
- }
346
- catch (e) {
347
- return [address, 0];
355
+ const getSbtcBalancesFromSimnet = (simnet, deploymentPlan, remoteDataSettings) => {
356
+ // If the user is not using remote data and the deployment plan does not
357
+ // contain the `sbtc-token` contract, return a map with 0 balances for all
358
+ // addresses. When remote data is enabled, the sbtc-token contract will not
359
+ // necessarily be present in the deployment plan.
360
+ if (!remoteDataSettings.enabled &&
361
+ !isContractInDeploymentPlan(deploymentPlan, "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4", "sbtc-token")) {
362
+ return new Map([...simnet.getAccounts().values()].map((address) => [address, 0]));
348
363
  }
349
- }));
364
+ return new Map([...simnet.getAccounts().values()].map((address) => {
365
+ try {
366
+ const { result: getBalanceResult } = simnet.callReadOnlyFn("SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token", "get-balance", [transactions_1.Cl.principal(address)], address);
367
+ // If the previous read-only call works, the user is working with
368
+ // sBTC. This means we can proceed with restoring sBTC balances.
369
+ const sbtcBalanceJSON = (0, transactions_1.cvToJSON)(getBalanceResult);
370
+ // The `get-balance` function returns a response containing the uint
371
+ // balance of the address. In the JSON representation, the balance is
372
+ // represented as a string. We need to parse it to an integer.
373
+ const sbtcBalance = parseInt(sbtcBalanceJSON.value.value, 10);
374
+ return [address, sbtcBalance];
375
+ }
376
+ catch (e) {
377
+ return [address, 0];
378
+ }
379
+ }));
380
+ };
350
381
  exports.getSbtcBalancesFromSimnet = getSbtcBalancesFromSimnet;
351
382
  /**
352
383
  * Utility function that restores the test wallets' initial sBTC balances in
package/dist/invariant.js CHANGED
@@ -29,7 +29,6 @@ const dialer_1 = require("./dialer");
29
29
  * @param rendezvousAllFunctions The map of all function interfaces for each
30
30
  * target contract.
31
31
  * @param seed The seed for reproducible invariant testing.
32
- * @param path The path for reproducible invariant testing.
33
32
  * @param runs The number of test runs.
34
33
  * @param bail Stop execution after the first failure and prevent further
35
34
  * shrinking.
@@ -37,7 +36,7 @@ const dialer_1 = require("./dialer");
37
36
  * @param radio The custom logging event emitter.
38
37
  * @returns void
39
38
  */
40
- const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
39
+ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
41
40
  const statistics = {
42
41
  sut: {
43
42
  successful: new Map(),
@@ -48,13 +47,14 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
48
47
  failed: new Map(),
49
48
  },
50
49
  };
50
+ // The Rendezvous identifier is the first one in the list. Only one contract
51
+ // can be fuzzed at a time.
52
+ const rendezvousContractId = rendezvousList[0];
51
53
  // A map where the keys are the Rendezvous identifiers and the values are
52
54
  // arrays of their SUT (System Under Test) functions. This map will be used
53
55
  // to access the SUT functions for each Rendezvous contract afterwards.
54
56
  const rendezvousSutFunctions = filterSutFunctions(rendezvousAllFunctions);
55
- // The Rendezvous identifier is the first one in the list. Only one contract
56
- // can be fuzzed at a time.
57
- const rendezvousContractId = rendezvousList[0];
57
+ // Initialize the statistics for the SUT functions.
58
58
  for (const functionInterface of rendezvousSutFunctions.get(rendezvousContractId)) {
59
59
  statistics.sut.successful.set(functionInterface.name, 0);
60
60
  statistics.sut.failed.set(functionInterface.name, 0);
@@ -63,47 +63,79 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
63
63
  // arrays of their invariant functions. This map will be used to access the
64
64
  // invariant functions for each Rendezvous contract afterwards.
65
65
  const rendezvousInvariantFunctions = filterInvariantFunctions(rendezvousAllFunctions);
66
+ // Initialize the statistics for the invariant functions.
66
67
  for (const functionInterface of rendezvousInvariantFunctions.get(rendezvousContractId)) {
67
68
  statistics.invariant.successful.set(functionInterface.name, 0);
68
69
  statistics.invariant.failed.set(functionInterface.name, 0);
69
70
  }
70
- const traitReferenceSutFunctions = rendezvousSutFunctions
71
- .get(rendezvousContractId)
72
- .filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
73
- const traitReferenceInvariantFunctions = rendezvousInvariantFunctions
74
- .get(rendezvousContractId)
75
- .filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
76
- const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
77
- if (Object.entries(projectTraitImplementations).length === 0 &&
78
- (traitReferenceSutFunctions.length > 0 ||
79
- traitReferenceInvariantFunctions.length > 0)) {
80
- const foundTraitReferenceMessage = traitReferenceSutFunctions.length > 0 &&
81
- traitReferenceInvariantFunctions.length > 0
82
- ? "public functions and invariants"
83
- : traitReferenceSutFunctions.length > 0
84
- ? "public functions"
85
- : "invariants";
86
- radio.emit("logMessage", (0, ansicolor_1.red)(`\nFound ${foundTraitReferenceMessage} referencing traits, but no trait implementations were found in the project.
87
- \nNote: You can add contracts implementing traits either as project contracts or as Clarinet requirements. For more details, visit: https://www.hiro.so/clarinet/.
88
- \n`));
89
- return;
90
- }
71
+ const sutFunctions = rendezvousSutFunctions.get(rendezvousContractId);
72
+ const traitReferenceSutFunctions = sutFunctions.filter(traits_1.isTraitReferenceFunction);
73
+ const invariantFunctions = rendezvousInvariantFunctions.get(rendezvousContractId);
74
+ const traitReferenceInvariantFunctions = invariantFunctions.filter(traits_1.isTraitReferenceFunction);
75
+ const sutTraitReferenceMap = (0, traits_1.buildTraitReferenceMap)(sutFunctions);
76
+ const invariantTraitReferenceMap = (0, traits_1.buildTraitReferenceMap)(invariantFunctions);
91
77
  const enrichedSutFunctionsInterfaces = traitReferenceSutFunctions.length > 0
92
- ? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), (0, traits_1.buildTraitReferenceMap)(rendezvousSutFunctions.get(rendezvousContractId)), rendezvousSutFunctions.get(rendezvousContractId), rendezvousContractId)
78
+ ? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), sutTraitReferenceMap, sutFunctions, rendezvousContractId)
93
79
  : rendezvousSutFunctions;
94
80
  const enrichedInvariantFunctionsInterfaces = traitReferenceInvariantFunctions.length > 0
95
- ? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), (0, traits_1.buildTraitReferenceMap)(rendezvousInvariantFunctions.get(rendezvousContractId)), rendezvousInvariantFunctions.get(rendezvousContractId), rendezvousContractId)
81
+ ? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), invariantTraitReferenceMap, invariantFunctions, rendezvousContractId)
96
82
  : rendezvousInvariantFunctions;
83
+ // Map all the project/requirement contracts to the traits they implement.
84
+ const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
85
+ // Extract SUT functions with missing trait implementations. These functions
86
+ // will be skipped during invariant testing. Otherwise, the invariant testing
87
+ // routine can fail during argument generation.
88
+ const sutFunctionsWithMissingTraits = (0, traits_1.getNonTestableTraitFunctions)(enrichedSutFunctionsInterfaces, sutTraitReferenceMap, projectTraitImplementations, rendezvousContractId);
89
+ // Extract invariant functions with missing trait implementations. These
90
+ // functions will be skipped during invariant testing. Otherwise, the
91
+ // invariant testing routine can fail during argument generation.
92
+ const invariantFunctionsWithMissingTraits = (0, traits_1.getNonTestableTraitFunctions)(enrichedInvariantFunctionsInterfaces, invariantTraitReferenceMap, projectTraitImplementations, rendezvousContractId);
93
+ if (sutFunctionsWithMissingTraits.length > 0 ||
94
+ invariantFunctionsWithMissingTraits.length > 0) {
95
+ if (sutFunctionsWithMissingTraits.length > 0) {
96
+ const functionList = sutFunctionsWithMissingTraits
97
+ .map((fn) => ` - ${fn}`)
98
+ .join("\n");
99
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following SUT functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
100
+ }
101
+ if (invariantFunctionsWithMissingTraits.length > 0) {
102
+ const functionList = invariantFunctionsWithMissingTraits
103
+ .map((fn) => ` - ${fn}`)
104
+ .join("\n");
105
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following invariant functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
106
+ }
107
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as Clarinet requirements.\n`));
108
+ }
109
+ // Filter out functions with missing trait implementations from the enriched
110
+ // map.
111
+ const executableSutFunctions = new Map([
112
+ [
113
+ rendezvousContractId,
114
+ enrichedSutFunctionsInterfaces
115
+ .get(rendezvousContractId)
116
+ .filter((f) => !sutFunctionsWithMissingTraits.includes(f.name)),
117
+ ],
118
+ ]);
119
+ // Filter out functions with missing trait implementations from the enriched
120
+ // map.
121
+ const executableInvariantFunctions = new Map([
122
+ [
123
+ rendezvousContractId,
124
+ enrichedInvariantFunctionsInterfaces
125
+ .get(rendezvousContractId)
126
+ .filter((f) => !invariantFunctionsWithMissingTraits.includes(f.name)),
127
+ ],
128
+ ]);
97
129
  // Set up local context to track SUT function call counts.
98
- const localContext = (0, exports.initializeLocalContext)(enrichedSutFunctionsInterfaces);
130
+ const localContext = (0, exports.initializeLocalContext)(executableSutFunctions);
99
131
  // Set up context in simnet by initializing state for SUT.
100
- (0, exports.initializeClarityContext)(simnet, enrichedSutFunctionsInterfaces);
132
+ (0, exports.initializeClarityContext)(simnet, executableSutFunctions);
101
133
  radio.emit("logMessage", `\nStarting invariant testing type for the ${targetContractName} contract...\n`);
102
134
  const simnetAccounts = simnet.getAccounts();
103
135
  const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
104
136
  const simnetAddresses = Array.from(simnetAccounts.values());
105
- const functions = (0, shared_1.getFunctionsListForContract)(enrichedSutFunctionsInterfaces, rendezvousContractId);
106
- const invariants = (0, shared_1.getFunctionsListForContract)(enrichedInvariantFunctionsInterfaces, rendezvousContractId);
137
+ const functions = (0, shared_1.getFunctionsListForContract)(executableSutFunctions, rendezvousContractId);
138
+ const invariants = (0, shared_1.getFunctionsListForContract)(executableInvariantFunctions, rendezvousContractId);
107
139
  if ((functions === null || functions === void 0 ? void 0 : functions.length) === 0) {
108
140
  radio.emit("logMessage", (0, ansicolor_1.red)(`No public functions found for the "${targetContractName}" contract. Without public functions, no state transitions can happen inside the contract, and the invariant test is not meaningful.\n`));
109
141
  return;
@@ -325,7 +357,6 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
325
357
  })), {
326
358
  endOnFailure: bail,
327
359
  numRuns: runs,
328
- path: path,
329
360
  reporter: radioReporter,
330
361
  seed: seed,
331
362
  verbose: true,
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "build": "npx -p typescript tsc --project tsconfig.json && node -e \"if (process.platform !== 'win32') require('fs').chmodSync('./dist/app.js', 0o755);\"",
11
11
  "test": "npx jest",
12
- "test:coverage": "npx tsc --project tsconfig.json && npx jest --coverage"
12
+ "test:coverage": "npx jest --coverage"
13
13
  },
14
14
  "keywords": [
15
15
  "stacks",
@@ -30,15 +30,15 @@
30
30
  "url": "https://github.com/stacks-network/rendezvous.git"
31
31
  },
32
32
  "dependencies": {
33
- "@hirosystems/clarinet-sdk": "^3.0.1",
34
- "@stacks/transactions": "^7.0.6",
33
+ "@hirosystems/clarinet-sdk": "^3.7.0",
34
+ "@stacks/transactions": "^7.2.0",
35
35
  "ansicolor": "^2.0.3",
36
36
  "fast-check": "^3.20.0",
37
37
  "toml": "^3.0.0",
38
38
  "yaml": "^2.6.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@hirosystems/clarinet-sdk-wasm": "^3.0.1",
41
+ "@hirosystems/clarinet-sdk-wasm": "^3.7.0",
42
42
  "@types/jest": "^29.5.12",
43
43
  "jest": "^29.7.0",
44
44
  "ts-jest": "^29.2.5",
package/dist/property.js CHANGED
@@ -19,14 +19,13 @@ const traits_1 = require("./traits");
19
19
  * @param rendezvousAllFunctions A map of all target contract IDs to their
20
20
  * function interfaces.
21
21
  * @param seed The seed for reproducible property-based tests.
22
- * @param path The path for reproducible property-based tests.
23
22
  * @param runs The number of test runs.
24
23
  * @param bail Stop execution after the first failure and prevent further
25
24
  * shrinking.
26
25
  * @param radio The custom logging event emitter.
27
26
  * @returns void
28
27
  */
29
- const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, radio) => {
28
+ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, radio) => {
30
29
  const statistics = {
31
30
  test: {
32
31
  successful: new Map(),
@@ -44,20 +43,39 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
44
43
  statistics.test.discarded.set(functionInterface.name, 0);
45
44
  statistics.test.failed.set(functionInterface.name, 0);
46
45
  }
47
- const traitReferenceFunctions = testContractsTestFunctions
48
- .get(testContractId)
49
- .filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
46
+ const allTestFunctions = testContractsTestFunctions.get(testContractId);
47
+ const traitReferenceFunctionsCount = allTestFunctions.filter(traits_1.isTraitReferenceFunction).length;
48
+ const traitReferenceMap = (0, traits_1.buildTraitReferenceMap)(allTestFunctions);
49
+ const enrichedTestFunctionsInterfaces = traitReferenceFunctionsCount > 0
50
+ ? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), traitReferenceMap, allTestFunctions, testContractId)
51
+ : testContractsTestFunctions;
50
52
  const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
51
- if (Object.entries(projectTraitImplementations).length === 0 &&
52
- traitReferenceFunctions.length > 0) {
53
- radio.emit("logMessage", (0, ansicolor_1.red)(`\nFound test functions referencing traits, but no trait implementations were found in the project.
54
- \nNote: You can add contracts implementing traits either as project contracts or as Clarinet requirements. For more details, visit: https://www.hiro.so/clarinet/.
55
- \n`));
56
- return;
53
+ // If the tests contain trait reference functions, extract the functions with
54
+ // missing trait implementations. These functions need to be skipped during
55
+ // property-based testing. Otherwise, the property-based testing routine will
56
+ // eventually fail during argument generation.
57
+ const functionsMissingTraitImplementations = traitReferenceFunctionsCount > 0
58
+ ? (0, traits_1.getNonTestableTraitFunctions)(enrichedTestFunctionsInterfaces, traitReferenceMap, projectTraitImplementations, testContractId)
59
+ : [];
60
+ // If the tests contain trait reference functions without eligible trait
61
+ // implementations, log a warning and filter out the functions.
62
+ if (functionsMissingTraitImplementations.length > 0) {
63
+ const functionList = functionsMissingTraitImplementations
64
+ .map((fn) => ` - ${fn}`)
65
+ .join("\n");
66
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following test functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
67
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as requirements.\n`));
57
68
  }
58
- const enrichedTestFunctionsInterfaces = traitReferenceFunctions.length > 0
59
- ? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), (0, traits_1.buildTraitReferenceMap)(testContractsTestFunctions.get(testContractId)), testContractsTestFunctions.get(testContractId), testContractId)
60
- : testContractsTestFunctions;
69
+ // Filter out test functions with missing trait implementations from the
70
+ // enriched map.
71
+ const executableTestContractsTestFunctions = new Map([
72
+ [
73
+ testContractId,
74
+ enrichedTestFunctionsInterfaces
75
+ .get(testContractId)
76
+ .filter((functionInterface) => !functionsMissingTraitImplementations.includes(functionInterface.name)),
77
+ ],
78
+ ]);
61
79
  radio.emit("logMessage", `\nStarting property testing type for the ${targetContractName} contract...\n`);
62
80
  // Search for discard functions, for each test function. This map will
63
81
  // be used to pair the test functions with their corresponding discard
@@ -69,7 +87,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
69
87
  // Pair each test function with its corresponding discard function. When a
70
88
  // test function is selected, Rendezvous will first call its discard
71
89
  // function, to allow or prevent the property test from running.
72
- const testContractsPairedFunctions = new Map(Array.from(testContractsTestFunctions, ([contractId, functions]) => [
90
+ const testContractsPairedFunctions = new Map(Array.from(executableTestContractsTestFunctions, ([contractId, functions]) => [
73
91
  contractId,
74
92
  new Map(functions.map((f) => {
75
93
  var _a;
@@ -87,7 +105,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
87
105
  const simnetAccounts = simnet.getAccounts();
88
106
  const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
89
107
  const simnetAddresses = Array.from(simnetAccounts.values());
90
- const testFunctions = (0, shared_1.getFunctionsListForContract)(enrichedTestFunctionsInterfaces, testContractId);
108
+ const testFunctions = (0, shared_1.getFunctionsListForContract)(executableTestContractsTestFunctions, testContractId);
91
109
  if ((testFunctions === null || testFunctions === void 0 ? void 0 : testFunctions.length) === 0) {
92
110
  radio.emit("logMessage", (0, ansicolor_1.red)(`No test functions found for the "${targetContractName}" contract.\n`));
93
111
  return;
@@ -223,7 +241,6 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
223
241
  }), {
224
242
  endOnFailure: bail,
225
243
  numRuns: runs,
226
- path: path,
227
244
  reporter: radioReporter,
228
245
  seed: seed,
229
246
  verbose: true,
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createIsolatedTestEnvironment = createIsolatedTestEnvironment;
4
+ const path_1 = require("path");
5
+ const fs_1 = require("fs");
6
+ const os_1 = require("os");
7
+ /**
8
+ * Creates an isolated test environment by copying the Clarinet project to a
9
+ * unique temporary directory. This prevents race conditions when multiple
10
+ * tests try to initialize simnet concurrently.
11
+ *
12
+ * @param manifestDir - The absolute path to the manifest directory.
13
+ * @param testPrefix - Prefix for the temporary directory name.
14
+ * @returns The path to the temporary directory containing the isolated project
15
+ * copy.
16
+ */
17
+ function createIsolatedTestEnvironment(manifestDir, testPrefix) {
18
+ const tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), testPrefix));
19
+ (0, fs_1.cpSync)(manifestDir, tempDir, { recursive: true });
20
+ return tempDir;
21
+ }
package/dist/traits.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractProjectTraitImplementations = exports.isTraitReferenceFunction = exports.getContractIdsImplementingTrait = exports.buildTraitReferenceMap = exports.getTraitReferenceData = exports.enrichInterfaceWithTraitData = void 0;
3
+ exports.getNonTestableTraitFunctions = exports.extractProjectTraitImplementations = exports.isTraitReferenceFunction = exports.getContractIdsImplementingTrait = exports.buildTraitReferenceMap = exports.getTraitReferenceData = exports.enrichInterfaceWithTraitData = void 0;
4
4
  /**
5
5
  * Enriches a contract interface with trait reference data. Before enrichment,
6
6
  * the contract interface lacks trait reference data for parameters. This
@@ -329,10 +329,15 @@ const getContractIdsImplementingTrait = (trait, projectTraitImplementations) =>
329
329
  const filteredContracts = contracts.filter((contractId) => {
330
330
  var _a;
331
331
  const traitImplemented = (_a = projectTraitImplementations[contractId]) === null || _a === void 0 ? void 0 : _a.some((implementedTrait) => {
332
- var _a, _b;
333
- const isTraitNamesMatch = implementedTrait.name === ((_a = trait.import.Imported) === null || _a === void 0 ? void 0 : _a.name);
332
+ var _a, _b, _c, _d;
333
+ const isTraitNamesMatch = implementedTrait.name ===
334
+ ((_a = trait.import.Imported) === null || _a === void 0 ? void 0 : _a.name) ||
335
+ implementedTrait.name ===
336
+ ((_b = trait.import.Defined) === null || _b === void 0 ? void 0 : _b.name);
334
337
  const isTraitIssuersMatch = JSON.stringify(implementedTrait.contract_identifier.issuer) ===
335
- JSON.stringify((_b = trait.import.Imported) === null || _b === void 0 ? void 0 : _b.contract_identifier.issuer);
338
+ JSON.stringify((_c = trait.import.Imported) === null || _c === void 0 ? void 0 : _c.contract_identifier.issuer) ||
339
+ JSON.stringify(implementedTrait.contract_identifier.issuer) ===
340
+ JSON.stringify((_d = trait.import.Defined) === null || _d === void 0 ? void 0 : _d.contract_identifier.issuer);
336
341
  return isTraitNamesMatch && isTraitIssuersMatch;
337
342
  });
338
343
  return traitImplemented;
@@ -403,3 +408,45 @@ const extractProjectTraitImplementations = (simnet) => {
403
408
  return projectTraitImplementations;
404
409
  };
405
410
  exports.extractProjectTraitImplementations = extractProjectTraitImplementations;
411
+ /**
412
+ * Filters functions that reference traits without eligible implementations in
413
+ * the project. Recursively checks nested parameters for trait reference types.
414
+ * @param enrichedFunctionsInterfaces The map of contract IDs to enriched
415
+ * function interfaces with trait reference data.
416
+ * @param traitReferenceMap The map of function names to their trait reference
417
+ * parameter data.
418
+ * @param projectTraitImplementations The record of contract IDs to their
419
+ * implemented traits.
420
+ * @param contractId The contract ID to filter functions for.
421
+ * @returns An array of function names with trait references without eligible
422
+ * trait implementations.
423
+ */
424
+ const getNonTestableTraitFunctions = (enrichedFunctionsInterfaces, traitReferenceMap, projectTraitImplementations, contractId) => {
425
+ const hasTraitReferenceWithoutImplementation = (type) => {
426
+ if (!type)
427
+ return false;
428
+ if (typeof type === "object" && "trait_reference" in type) {
429
+ const contractIdsImplementingTrait = (0, exports.getContractIdsImplementingTrait)(type.trait_reference, projectTraitImplementations);
430
+ return contractIdsImplementingTrait.length === 0;
431
+ }
432
+ if (typeof type === "object") {
433
+ return (("list" in type &&
434
+ hasTraitReferenceWithoutImplementation(type.list.type)) ||
435
+ ("tuple" in type &&
436
+ type.tuple.some((item) => hasTraitReferenceWithoutImplementation(item.type))) ||
437
+ ("optional" in type &&
438
+ hasTraitReferenceWithoutImplementation(type.optional)) ||
439
+ ("response" in type &&
440
+ (hasTraitReferenceWithoutImplementation(type.response.ok) ||
441
+ hasTraitReferenceWithoutImplementation(type.response.error))));
442
+ }
443
+ return false;
444
+ };
445
+ return Array.from(traitReferenceMap.keys()).filter((functionName) => {
446
+ var _a, _b;
447
+ const enrichedFunctionInterface = (_a = enrichedFunctionsInterfaces
448
+ .get(contractId)) === null || _a === void 0 ? void 0 : _a.find((f) => f.name === functionName);
449
+ return ((_b = enrichedFunctionInterface === null || enrichedFunctionInterface === void 0 ? void 0 : enrichedFunctionInterface.args.some((param) => hasTraitReferenceWithoutImplementation(param.type))) !== null && _b !== void 0 ? _b : false);
450
+ });
451
+ };
452
+ exports.getNonTestableTraitFunctions = getNonTestableTraitFunctions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "build": "npx -p typescript tsc --project tsconfig.json && node -e \"if (process.platform !== 'win32') require('fs').chmodSync('./dist/app.js', 0o755);\"",
11
11
  "test": "npx jest",
12
- "test:coverage": "npx tsc --project tsconfig.json && npx jest --coverage"
12
+ "test:coverage": "npx jest --coverage"
13
13
  },
14
14
  "keywords": [
15
15
  "stacks",
@@ -30,15 +30,15 @@
30
30
  "url": "https://github.com/stacks-network/rendezvous.git"
31
31
  },
32
32
  "dependencies": {
33
- "@hirosystems/clarinet-sdk": "^3.0.1",
34
- "@stacks/transactions": "^7.0.6",
33
+ "@hirosystems/clarinet-sdk": "^3.7.0",
34
+ "@stacks/transactions": "^7.2.0",
35
35
  "ansicolor": "^2.0.3",
36
36
  "fast-check": "^3.20.0",
37
37
  "toml": "^3.0.0",
38
38
  "yaml": "^2.6.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@hirosystems/clarinet-sdk-wasm": "^3.0.1",
41
+ "@hirosystems/clarinet-sdk-wasm": "^3.7.0",
42
42
  "@types/jest": "^29.5.12",
43
43
  "jest": "^29.7.0",
44
44
  "ts-jest": "^29.2.5",