@stacks/rendezvous 0.12.0 → 0.13.1

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 20, 22, and 23. Other versions may work, but they are untested.
11
+ - **Node.js**: Supported versions include 20, 22, and 24. Other versions may work, but they are untested.
12
12
 
13
13
  ### Inspiration
14
14
 
package/dist/app.js CHANGED
@@ -9,15 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  step((generator = generator.apply(thisArg, _arguments || [])).next());
10
10
  });
11
11
  };
12
- var __importDefault = (this && this.__importDefault) || function (mod) {
13
- return (mod && mod.__esModule) ? mod : { "default": mod };
14
- };
15
12
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.tryParseRemoteDataSettings = exports.getManifestFileName = exports.noRemoteData = void 0;
13
+ exports.getManifestFileName = void 0;
17
14
  exports.main = main;
18
15
  const path_1 = require("path");
19
16
  const events_1 = require("events");
20
- const toml_1 = __importDefault(require("toml"));
21
17
  const property_1 = require("./property");
22
18
  const invariant_1 = require("./invariant");
23
19
  const shared_1 = require("./shared");
@@ -30,13 +26,6 @@ const dialer_1 = require("./dialer");
30
26
  const logger = (log, logLevel = "log") => {
31
27
  console[logLevel](log);
32
28
  };
33
- /**
34
- * The object used to initialize an empty simnet session with, when no remote
35
- * data is enabled in the `Clarinet.toml` file.
36
- */
37
- exports.noRemoteData = {
38
- enabled: false,
39
- };
40
29
  /**
41
30
  * Gets the manifest file name for a Clarinet project.
42
31
  * If a custom manifest exists (`Clarinet-<contract-name>.toml`), it is used.
@@ -53,22 +42,6 @@ const getManifestFileName = (manifestDir, targetContractName) => {
53
42
  return "Clarinet.toml";
54
43
  };
55
44
  exports.getManifestFileName = getManifestFileName;
56
- const tryParseRemoteDataSettings = (manifestPath, radio) => {
57
- var _a, _b;
58
- const clarinetToml = toml_1.default.parse((0, fs_1.readFileSync)((0, path_1.resolve)(manifestPath), "utf-8"));
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) {
61
- radio.emit("logMessage", (0, ansicolor_1.yellow)("\nUsing remote data. Setting up the environment can take up to a minute..."));
62
- }
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) {
67
- return exports.noRemoteData;
68
- }
69
- return remoteDataUserSettings;
70
- };
71
- exports.tryParseRemoteDataSettings = tryParseRemoteDataSettings;
72
45
  const helpMessage = `
73
46
  rv v${package_json_1.version}
74
47
 
@@ -169,8 +142,7 @@ function main() {
169
142
  if (dialerRegistry !== undefined) {
170
143
  dialerRegistry.registerDialers();
171
144
  }
172
- const remoteDataSettings = (0, exports.tryParseRemoteDataSettings)(manifestPath, radio);
173
- const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, remoteDataSettings, runConfig.sutContractName);
145
+ const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, runConfig.sutContractName, radio);
174
146
  /**
175
147
  * The list of contract IDs for the SUT contract names, as per the simnet.
176
148
  */
package/dist/app.types.js CHANGED
@@ -1,2 +1,2 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
2
+ // This file is a placeholder for the Rendezvous CLI-related types.
package/dist/citizen.js CHANGED
@@ -12,181 +12,138 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.getSbtcBalancesFromSimnet = exports.getTestContractSource = exports.buildRendezvousData = exports.getContractSource = exports.deployContracts = exports.groupContractsByEpochFromDeploymentPlan = exports.issueFirstClassCitizenship = void 0;
15
+ exports.getTestContractSource = exports.buildRendezvousData = exports.issueFirstClassCitizenship = void 0;
16
16
  exports.scheduleRendezvous = scheduleRendezvous;
17
17
  const fs_1 = require("fs");
18
18
  const path_1 = require("path");
19
- const toml_1 = __importDefault(require("toml"));
19
+ const os_1 = require("os");
20
+ const toml_1 = require("@iarna/toml");
20
21
  const yaml_1 = __importDefault(require("yaml"));
21
22
  const clarinet_sdk_1 = require("@stacks/clarinet-sdk");
22
- const transactions_1 = require("@stacks/transactions");
23
+ const ansicolor_1 = require("ansicolor");
23
24
  /**
24
- * Prepares the simnet instance and assures the target contract's corresponding
25
- * test contract is treated as a first-class citizen, relying on their
26
- * concatenation. This function handles:
27
- * - Contract sorting by epoch based on the deployment plan.
28
- * - Combining the target contract with its tests and deploying all contracts
29
- * to the simnet.
25
+ * Prepares the simnet with the Rendezvous tests as first-class citizens of the
26
+ * target contract.
27
+ *
28
+ * This function works in an isolated temporary copy of the Clarinet project
29
+ * in /tmp/ to avoid lingering temporary files in the user's project directory.
30
+ * In case of system crashes, power outages, etc., the temp directory is
31
+ * automatically cleaned up by the OS on reboot.
30
32
  *
31
33
  * @param manifestDir The relative path to the manifest directory.
32
- * @param manifestPath The absolute path to the manifest file.
33
- * @param remoteDataSettings The remote data settings.
34
+ * @param manifestPath The path to the manifest file.
34
35
  * @param sutContractName The target contract name.
35
- * @returns The initialized simnet instance with all contracts deployed, with
36
- * the test contract treated as a first-class citizen of the target contract.
36
+ * @param radio The event emitter to send log messages to.
37
+ * @returns The initialized simnet.
37
38
  */
38
- const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSettings, sutContractName) => __awaiter(void 0, void 0, void 0, function* () {
39
- var _a;
40
- // Initialize the simnet, to generate the deployment plan and instance. The
41
- // empty session will be set up, and contracts will be deployed in the
42
- // correct order based on the deployment plan a few lines below.
43
- const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestPath);
44
- const deploymentPlan = yaml_1.default.parse((0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "deployments", "default.simnet-plan.yaml"), {
39
+ const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName, radio) => __awaiter(void 0, void 0, void 0, function* () {
40
+ var _a, _b, _c, _d;
41
+ // First simnet initialization: This will generate the deployment plan and
42
+ // will type check the project without any Rendezvous tests.
43
+ try {
44
+ radio.emit("logMessage", `\nType-checking your Clarinet project...`);
45
+ yield (0, clarinet_sdk_1.generateDeployement)(manifestPath);
46
+ }
47
+ catch (error) {
48
+ throw new Error(`Error initializing simnet: ${(_a = error.message) !== null && _a !== void 0 ? _a : error}`);
49
+ }
50
+ const deploymentPlanRelativePath = (0, path_1.relative)(manifestDir, (0, path_1.join)(manifestDir, "deployments", "default.simnet-plan.yaml"));
51
+ const deploymentPlanAbsolutePath = (0, path_1.join)(manifestDir, deploymentPlanRelativePath);
52
+ const deploymentPlan = yaml_1.default.parse((0, fs_1.readFileSync)(deploymentPlanAbsolutePath, {
45
53
  encoding: "utf-8",
46
54
  }));
47
- const sortedContractsByEpoch = (0, exports.groupContractsByEpochFromDeploymentPlan)(deploymentPlan);
48
- const simnetAddresses = [...simnet.getAccounts().values()];
49
- const stxBalancesMap = new Map(simnetAddresses.map((address) => {
50
- const balanceHex = simnet.runSnippet(`(stx-get-balance '${address})`);
51
- return [address, (0, transactions_1.cvToValue)((0, transactions_1.hexToCV)(balanceHex))];
52
- }));
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);
56
- yield simnet.initEmptySession(remoteDataSettings);
57
- simnetAddresses.forEach((address) => {
58
- simnet.mintSTX(address, stxBalancesMap.get(address));
59
- });
60
- // Combine the target contract with its tests into a single contract. The
61
- // resulting contract will replace the target contract in the simnet.
62
- /** The contract names mapped to the concatenated source code. */
63
- const rendezvousSources = new Map([sutContractName]
64
- // For each target contract name, execute the processing steps to get the
65
- // concatenated contract source code and the contract ID.
66
- .map((contractName) => (0, exports.buildRendezvousData)(deploymentPlan, contractName, manifestDir))
67
- // Use the contract ID as a key, mapping to the concatenated contract
68
- // source code.
69
- .map((rendezvousContractData) => [
70
- rendezvousContractData.rendezvousContractId,
71
- rendezvousContractData.rendezvousSourceCode,
72
- ]));
73
- const clarinetToml = toml_1.default.parse((0, fs_1.readFileSync)(manifestPath, { encoding: "utf-8" }));
74
- const cacheDir = ((_a = clarinetToml.project) === null || _a === void 0 ? void 0 : _a.cache_dir) || "./.cache";
75
- // Deploy the contracts to the empty simnet session in the correct order.
76
- yield (0, exports.deployContracts)(simnet, sortedContractsByEpoch, manifestDir, cacheDir, (name, sender, props) => (0, exports.getContractSource)([sutContractName], rendezvousSources, name, sender, props, manifestDir));
77
- // Filter out addresses with zero balance. They do not need to be restored.
78
- const sbtcBalancesToRestore = new Map([...sbtcBalancesMap.entries()].filter(([_, balance]) => balance !== 0));
79
- // After all the contracts and requirements are deployed, if the test wallets
80
- // had sBTC balances previously, restore them. If no test wallet previously
81
- // owned sBTC, skip this step.
82
- if ([...sbtcBalancesToRestore.keys()].length > 0) {
83
- restoreSbtcBalances(simnet, sbtcBalancesToRestore);
55
+ const parsedManifest = (0, toml_1.parse)((0, fs_1.readFileSync)(manifestPath, { encoding: "utf-8" }));
56
+ const cacheDir = (_c = (_b = parsedManifest.project) === null || _b === void 0 ? void 0 : _b.cache_dir) !== null && _c !== void 0 ? _c : "./.cache";
57
+ const rendezvousData = (0, exports.buildRendezvousData)(cacheDir, deploymentPlan, sutContractName, manifestDir);
58
+ // Create isolated temp directory for the Rendezvous testing run.
59
+ const tempProjectDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "rendezvous-run-"));
60
+ (0, fs_1.cpSync)(manifestDir, tempProjectDir, { recursive: true });
61
+ const [, contractName] = rendezvousData.rendezvousContractId.split(".");
62
+ const rendezvousContractsDir = (0, path_1.join)(tempProjectDir, "contracts");
63
+ const rendezvousPath = (0, path_1.join)(rendezvousContractsDir, `${contractName}-rendezvous.clar`);
64
+ (0, fs_1.writeFileSync)(rendezvousPath, rendezvousData.rendezvousSourceCode);
65
+ radio.emit("logMessage", `\nType-checking your Rendezvous project...`);
66
+ // Update the manifest in the temp directory to point to the Rendezvous
67
+ // concatenation.
68
+ const manifestFileName = (0, path_1.basename)(manifestPath);
69
+ const tempManifestPath = (0, path_1.join)(tempProjectDir, manifestFileName);
70
+ const tempParsedManifest = (0, toml_1.parse)((0, fs_1.readFileSync)(tempManifestPath, { encoding: "utf-8" }));
71
+ if (!tempParsedManifest.contracts) {
72
+ tempParsedManifest.contracts = {};
84
73
  }
85
- return simnet;
86
- });
87
- exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
88
- /**
89
- * Groups contracts by epoch from the deployment plan.
90
- * @param deploymentPlan The parsed deployment plan.
91
- * @returns A record of contracts grouped by epoch. The record key is the epoch
92
- * string, and the value is an array of contracts. Each contract is represented
93
- * as a record with the contract name as the key and a record containing the
94
- * contract path and clarity version as the value.
95
- */
96
- const groupContractsByEpochFromDeploymentPlan = (deploymentPlan) => {
97
- return deploymentPlan.plan.batches.reduce((acc, batch) => {
98
- const epoch = batch.epoch;
99
- const contracts = batch.transactions
100
- .filter((tx) => tx["emulated-contract-publish"])
101
- .map((tx) => {
102
- const contract = tx["emulated-contract-publish"];
103
- return {
104
- [contract["contract-name"]]: {
105
- path: contract.path,
106
- clarity_version: contract["clarity-version"],
107
- },
108
- };
109
- });
110
- if (contracts.length > 0) {
111
- acc[epoch] = (acc[epoch] || []).concat(contracts);
112
- }
113
- return acc;
114
- }, {});
115
- };
116
- exports.groupContractsByEpochFromDeploymentPlan = groupContractsByEpochFromDeploymentPlan;
117
- /**
118
- * Deploys the contracts to the simnet in the correct order.
119
- * @param simnet The simnet instance.
120
- * @param contractsByEpoch The record of contracts by epoch.
121
- * @param getContractSourceFn The function to retrieve the contract source.
122
- */
123
- const deployContracts = (simnet, contractsByEpoch, manifestDir, cacheDir, getContractSourceFn) => __awaiter(void 0, void 0, void 0, function* () {
124
- for (const [epoch, contracts] of Object.entries(contractsByEpoch)) {
125
- // Move to the next epoch and deploy the contracts in the correct order.
126
- simnet.setEpoch(epoch);
127
- for (const contract of contracts.flatMap(Object.entries)) {
128
- const [name, props] = contract;
129
- // Resolve paths to absolute for proper comparison.
130
- const absoluteContractPath = (0, path_1.resolve)(manifestDir, props.path);
131
- const absoluteRequirementsPath = (0, path_1.resolve)(manifestDir, cacheDir, "requirements");
132
- // Check if contract is in requirements directory.
133
- const isRequirement = absoluteContractPath.startsWith(absoluteRequirementsPath);
134
- const sender = isRequirement
135
- ? (0, path_1.basename)(props.path).split(".")[0]
136
- : simnet.deployer;
137
- const source = getContractSourceFn(name, sender, props);
138
- simnet.deployContract(name, source, { clarityVersion: props.clarity_version }, sender);
74
+ if (!tempParsedManifest.contracts[sutContractName]) {
75
+ tempParsedManifest.contracts[sutContractName] = {};
76
+ }
77
+ const relativeRendezvousPath = (0, path_1.relative)(tempProjectDir, rendezvousPath);
78
+ tempParsedManifest.contracts[sutContractName] = {
79
+ epoch: ((_d = tempParsedManifest.contracts[sutContractName].epoch) !== null && _d !== void 0 ? _d : "latest"),
80
+ path: relativeRendezvousPath,
81
+ };
82
+ // Convert epoch values to strings for TOML compatibility.
83
+ for (const contractName in tempParsedManifest.contracts) {
84
+ const contract = tempParsedManifest.contracts[contractName];
85
+ if ((contract === null || contract === void 0 ? void 0 : contract.epoch) && typeof contract.epoch === "number") {
86
+ contract.epoch = String(contract.epoch);
139
87
  }
140
88
  }
141
- });
142
- exports.deployContracts = deployContracts;
143
- /**
144
- * Conditionally retrieves the contract source based on whether the contract is
145
- * a SUT contract or not.
146
- * @param targetContractNames The list of target contract names.
147
- * @param rendezvousSourcesMap The target contract IDs mapped to the resulting
148
- * concatenated source code.
149
- * @param contractName The contract name.
150
- * @param contractSender The emulated sender of the contract according to the
151
- * deployment plan.
152
- * @param contractProps The contract deployment properties.
153
- * @param manifestDir The relative path to the manifest directory.
154
- * @returns The contract source code.
155
- */
156
- const getContractSource = (targetContractNames, rendezvousSourcesMap, contractName, contractSender, contractProps, manifestDir) => {
157
- const contractId = `${contractSender}.${contractName}`;
158
- // Checking if a contract is a SUT one just by using the name is not enough.
159
- // There can be multiple contracts with the same name, but different senders
160
- // in the deployment plan. The contract ID is the unique identifier used to
161
- // store the concatenated Rendezvous source codes in the
162
- // `rendezvousSourcesMap`.
163
- if (targetContractNames.includes(contractName) &&
164
- rendezvousSourcesMap.has(contractId)) {
165
- const contractSource = rendezvousSourcesMap.get(contractId);
166
- if (!contractSource) {
167
- throw new Error(`Contract source not found for ${contractName}`);
89
+ (0, fs_1.writeFileSync)(tempManifestPath, (0, toml_1.stringify)(tempParsedManifest));
90
+ // Final simnet initialization: This will initialize the simnet with the
91
+ // target contract containing Rendezvous tests as first-class citizens.
92
+ //
93
+ // Windows cannot initialize simnet with absolute paths. Use relative path
94
+ // from the temp project directory.
95
+ // See: https://github.com/stx-labs/clarinet/issues/1634
96
+ const originalCwd = process.cwd();
97
+ try {
98
+ // Change the current working directory to the temp project directory.
99
+ // This is necessary because the simnet initialization requires the
100
+ // manifest file to be in the current working directory.
101
+ process.chdir(tempProjectDir);
102
+ // Initialize the simnet while suppressing stdout to avoid polluting output.
103
+ // Errors are still printed to stderr to help troubleshoot issues.
104
+ const originalWrite = process.stdout.write;
105
+ process.stdout.write = () => true;
106
+ // Cleanup the deployment plan file if it exists. This will force clarinet
107
+ // to generate the deployment plan from scratch, accounting for the new
108
+ // Rendezvous contract.
109
+ if ((0, fs_1.existsSync)(deploymentPlanRelativePath)) {
110
+ (0, fs_1.rmSync)(deploymentPlanRelativePath, { force: true });
111
+ }
112
+ try {
113
+ const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestFileName);
114
+ return simnet;
115
+ }
116
+ finally {
117
+ // Restore stdout.
118
+ process.stdout.write = originalWrite;
168
119
  }
169
- return contractSource;
170
120
  }
171
- else {
172
- return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, contractProps.path), {
173
- encoding: "utf-8",
174
- });
121
+ finally {
122
+ // Restore the original current working directory.
123
+ process.chdir(originalCwd);
124
+ // Cleanup the temp project directory.
125
+ try {
126
+ (0, fs_1.rmSync)(tempProjectDir, { recursive: true, force: true });
127
+ }
128
+ catch (error) {
129
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`Error cleaning up temporary project directory ${tempProjectDir}: ${error.message}. Remove it manually to avoid unnecessary disk space usage.`));
130
+ }
175
131
  }
176
- };
177
- exports.getContractSource = getContractSource;
132
+ });
133
+ exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
178
134
  /**
179
135
  * Builds the Rendezvous data.
136
+ * @param cacheDir The cache directory path.
180
137
  * @param deploymentPlan The parsed deployment plan.
181
138
  * @param contractName The contract name.
182
139
  * @param manifestDir The relative path to the manifest directory.
183
140
  * @returns The Rendezvous data representing a record. The returned record
184
141
  * contains the Rendezvous source code and the unique Rendezvous contract ID.
185
142
  */
186
- const buildRendezvousData = (deploymentPlan, contractName, manifestDir) => {
143
+ const buildRendezvousData = (cacheDir, deploymentPlan, contractName, manifestDir) => {
187
144
  try {
188
145
  const sutContractSource = getDeploymentPlanContractSource(deploymentPlan, contractName, manifestDir);
189
- const testContractSource = (0, exports.getTestContractSource)(deploymentPlan, contractName, manifestDir);
146
+ const testContractSource = (0, exports.getTestContractSource)(cacheDir, deploymentPlan, contractName, manifestDir);
190
147
  const rendezvousSource = scheduleRendezvous(sutContractSource, testContractSource);
191
148
  const rendezvousContractEmulatedSender = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, contractName)["emulated-sender"];
192
149
  // Use the contract ID as a unique identifier of the contract within the
@@ -215,43 +172,6 @@ const getDeploymentPlanContractSource = (deploymentPlan, sutContractName, manife
215
172
  encoding: "utf-8",
216
173
  }).toString();
217
174
  };
218
- /**
219
- * Retrieves the test contract source code.
220
- * @param deploymentPlan The parsed deployment plan.
221
- * @param sutContractName The target contract name.
222
- * @param manifestDir The relative path to the manifest directory.
223
- * @returns The test contract source code.
224
- */
225
- const getTestContractSource = (deploymentPlan, sutContractName, manifestDir) => {
226
- const sutContractPath = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, sutContractName).path;
227
- const clarityExtension = ".clar";
228
- if (!sutContractPath.endsWith(clarityExtension)) {
229
- throw new Error(`Invalid contract extension for the "${sutContractName}" contract.`);
230
- }
231
- // If the sutContractPath is located under .cache/requirements/ path, search
232
- // for the test contract in the classic `contracts` directory.
233
- if (sutContractPath.includes(".cache")) {
234
- const relativePath = sutContractPath.split(".cache/requirements/")[1];
235
- const relativePathTestContract = relativePath.replace(clarityExtension, `.tests${clarityExtension}`);
236
- return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "contracts", relativePathTestContract), {
237
- encoding: "utf-8",
238
- }).toString();
239
- }
240
- // If the contract is not under the `.cache/requirements/` path, we assume it
241
- // is located in a regular path specified in the manifest file. Just search
242
- // for the test contract near the SUT one, following the naming
243
- // convention: `<contract-name>.tests.clar`.
244
- const testContractPath = sutContractPath.replace(clarityExtension, `.tests${clarityExtension}`);
245
- try {
246
- return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, testContractPath), {
247
- encoding: "utf-8",
248
- }).toString();
249
- }
250
- catch (error) {
251
- throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract. ${error.message}`);
252
- }
253
- };
254
- exports.getTestContractSource = getTestContractSource;
255
175
  /**
256
176
  * Retrieves the emulated contract publish data of the target contract from the
257
177
  * deployment plan. If multiple contracts share the same name in the deployment
@@ -288,6 +208,9 @@ const getSutContractDeploymentPlanEmulatedPublish = (deploymentPlan, sutContract
288
208
  // having the same name, select the one deployed by the deployer.
289
209
  const targetContractDeploymentData = (_b = contractPublishMatchesByName.find((transaction) => transaction["emulated-contract-publish"]["emulated-sender"] ===
290
210
  deployer)) === null || _b === void 0 ? void 0 : _b["emulated-contract-publish"];
211
+ // TODO: Consider handling requirements and project contracts separately.
212
+ // Eventually let the user specify if the contract is a requirement or a
213
+ // project contract.
291
214
  // This is an edge case that can happen in practice. If the project has two
292
215
  // requirements that share the same contract name, Rendezvous will not be
293
216
  // able to select the one to be fuzzed. The recommendation for users would
@@ -304,6 +227,87 @@ const getSutContractDeploymentPlanEmulatedPublish = (deploymentPlan, sutContract
304
227
  }
305
228
  return contractNameMatch;
306
229
  };
230
+ /**
231
+ * Retrieves the test contract source code for a project contract.
232
+ * @param contractPath The relative path to the contract.
233
+ * @param manifestDir The relative path to the manifest directory.
234
+ * @returns The test contract source code or `null` if the test contract is not
235
+ * found.
236
+ */
237
+ const getProjectContractTestSrc = (contractPath, manifestDir) => {
238
+ const clarityExtension = ".clar";
239
+ const lastExtensionIndex = contractPath.lastIndexOf(clarityExtension);
240
+ const testContractPath = lastExtensionIndex !== -1
241
+ ? contractPath.slice(0, lastExtensionIndex) +
242
+ `.tests${clarityExtension}` +
243
+ contractPath.slice(lastExtensionIndex + clarityExtension.length)
244
+ : `${contractPath}.tests${clarityExtension}`;
245
+ try {
246
+ const fullPath = (0, path_1.join)(manifestDir, testContractPath);
247
+ const content = (0, fs_1.readFileSync)(fullPath, {
248
+ encoding: "utf-8",
249
+ }).toString();
250
+ return content;
251
+ }
252
+ catch (error) {
253
+ return null;
254
+ }
255
+ };
256
+ /**
257
+ * Retrieves the test contract source code for a requirement contract. It
258
+ * searches for the test contract in the `contracts` directory of the Clarinet
259
+ * project.
260
+ * @param cacheDir The cache directory path.
261
+ * @param sutContractPath The path to the SUT contract.
262
+ * @param manifestDir The relative path to the manifest directory.
263
+ * @returns The test contract source code or `null` if the test contract is not
264
+ * found.
265
+ */
266
+ const getRequirementContractTestSrc = (cacheDir, sutContractPath, manifestDir) => {
267
+ const normalizedCacheDir = cacheDir.replace(/[\/\\]$/, "");
268
+ const requirementsRelativePath = `${normalizedCacheDir}/requirements/`;
269
+ if (!sutContractPath.includes(requirementsRelativePath)) {
270
+ return null;
271
+ }
272
+ const relativePath = sutContractPath.split(requirementsRelativePath)[1];
273
+ const clarityExtension = ".clar";
274
+ const lastExtensionIndex = relativePath.lastIndexOf(clarityExtension);
275
+ const relativePathTestContract = lastExtensionIndex !== -1
276
+ ? relativePath.slice(0, lastExtensionIndex) +
277
+ `.tests${clarityExtension}` +
278
+ relativePath.slice(lastExtensionIndex + clarityExtension.length)
279
+ : `${relativePath}.tests${clarityExtension}`;
280
+ return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "contracts", relativePathTestContract), {
281
+ encoding: "utf-8",
282
+ }).toString();
283
+ };
284
+ /**
285
+ * Retrieves the test contract source code.
286
+ * Project contracts have priority. Requirement contracts are only checked
287
+ * if project contract test is not found.
288
+ * @param cacheDir The cache directory path.
289
+ * @param deploymentPlan The parsed deployment plan.
290
+ * @param sutContractName The target contract name.
291
+ * @param manifestDir The relative path to the manifest directory.
292
+ * @returns The test contract source code.
293
+ */
294
+ const getTestContractSource = (cacheDir, deploymentPlan, sutContractName, manifestDir) => {
295
+ const sutContractPath = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, sutContractName).path;
296
+ // Prioritize project contracts. Try project contract test first.
297
+ const projectTestContract = getProjectContractTestSrc(sutContractPath, manifestDir);
298
+ if (projectTestContract !== null) {
299
+ return projectTestContract;
300
+ }
301
+ // Fallback to requirement contract test if project contract test not found.
302
+ const normalizedCacheDir = cacheDir || "./.cache";
303
+ const requirementTestContract = getRequirementContractTestSrc(normalizedCacheDir, sutContractPath, manifestDir);
304
+ if (requirementTestContract !== null) {
305
+ return requirementTestContract;
306
+ }
307
+ // No corresponding test contract was found for the SUT contract.
308
+ throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract.`);
309
+ };
310
+ exports.getTestContractSource = getTestContractSource;
307
311
  /**
308
312
  * Schedules a Rendezvous between the System Under Test (`SUT`) and the test
309
313
  * contract.
@@ -325,137 +329,3 @@ function scheduleRendezvous(targetContractSource, tests) {
325
329
  (ok (map-set context function-name {called: called})))`;
326
330
  return `${targetContractSource}\n\n${context}\n\n${tests}`;
327
331
  }
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
- };
345
- /**
346
- * Maps the simnet accounts to their sBTC balances. The function tries to call
347
- * the `get-balance` function of the `sbtc-token` contract for each address. If
348
- * the call fails, it returns a balance of 0 for that address. The call fails
349
- * if the user is not working with sBTC.
350
- * @param simnet The simnet instance.
351
- * @param deploymentPlan The parsed deployment plan.
352
- * @param remoteDataSettings The remote data settings.
353
- * @returns A map of addresses to their sBTC balances.
354
- */
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]));
363
- }
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
- };
381
- exports.getSbtcBalancesFromSimnet = getSbtcBalancesFromSimnet;
382
- /**
383
- * Utility function that restores the test wallets' initial sBTC balances in
384
- * the re-initialized first-class citizenship simnet.
385
- *
386
- * @param simnet The simnet instance.
387
- * @param sbtcBalancesMap A map containing the test wallets' balances to be
388
- * restored.
389
- */
390
- const restoreSbtcBalances = (simnet, sbtcBalancesMap) => {
391
- // For each address present in the balances map, restore the balance.
392
- [...sbtcBalancesMap.entries()]
393
- // Re-assure the map does not contain nil balances.
394
- .filter(([_, balance]) => balance !== 0)
395
- .forEach(([address, balance]) => {
396
- // To deposit sBTC, one needs a txId and a sweep txId. A deposit transaction
397
- // must have a unique txId and sweep txId.
398
- const txId = getUniqueHex();
399
- const sweepTxId = getUniqueHex();
400
- mintSbtc(simnet, balance, address, txId, sweepTxId);
401
- });
402
- };
403
- /**
404
- * Utility function to deposit an amount of sBTC to a Stacks address.
405
- *
406
- * @param simnet The simnet instance.
407
- * @param amountSats The amount to mint in sats.
408
- * @param recipient The Stacks address to mint sBTC to.
409
- * @param txId A unique hex to use for the deposit.
410
- * @param sweepTxId A unique hex to use for the deposit.
411
- */
412
- const mintSbtc = (simnet, amountSats, recipient, txId, sweepTxId) => {
413
- // Calling `get-burn-block-info?` only works for past burn heights. We mine
414
- // one empty Bitcoin block if the initial height is 0 and use the previous
415
- // burn height to retrieve the burn header hash.
416
- if (simnet.burnBlockHeight === 0) {
417
- simnet.mineEmptyBurnBlock();
418
- }
419
- const previousBurnHeight = simnet.burnBlockHeight - 1;
420
- const burnHash = (0, transactions_1.hexToCV)(simnet.runSnippet(`(get-burn-block-info? header-hash u${previousBurnHeight})`));
421
- if (burnHash === null || burnHash.type === transactions_1.ClarityType.OptionalNone) {
422
- throw new Error("Something went wrong trying to retrieve the burn header.");
423
- }
424
- const completeDepositTx = (0, transactions_1.cvToJSON)(simnet.callPublicFn("SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-deposit", "complete-deposit-wrapper", [
425
- // (txid (buff 32))
426
- transactions_1.Cl.bufferFromHex(txId),
427
- // (vout-index uint)
428
- transactions_1.Cl.uint(1),
429
- // (amount uint)
430
- transactions_1.Cl.uint(amountSats),
431
- // (recipient principal)
432
- transactions_1.Cl.principal(recipient),
433
- // (burn-hash (buff 32))
434
- burnHash.value,
435
- // (burn-height uint)
436
- transactions_1.Cl.uint(previousBurnHeight),
437
- // (sweep-txid (buff 32))
438
- transactions_1.Cl.bufferFromHex(sweepTxId),
439
- ], "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4").result);
440
- // If the deposit transaction fails, an unexpected outcome can happen. Throw
441
- // an error if the transaction is not successful.
442
- if (!completeDepositTx.success) {
443
- throw new Error("Something went wrong trying to restore sBTC balances.");
444
- }
445
- };
446
- /**
447
- * Utility function that generates a random, unique hex to be used as txId in
448
- * `mintSbtc`.
449
- *
450
- * @returns A random hex string.
451
- */
452
- const getUniqueHex = () => {
453
- let hex;
454
- // Generate a 32-byte (64 character) random hex string.
455
- const bytes = new Uint8Array(32);
456
- crypto.getRandomValues(bytes);
457
- hex = Array.from(bytes)
458
- .map((byte) => byte.toString(16).padStart(2, "0"))
459
- .join("");
460
- return hex;
461
- };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -30,15 +30,15 @@
30
30
  "url": "https://github.com/stacks-network/rendezvous.git"
31
31
  },
32
32
  "dependencies": {
33
- "@stacks/clarinet-sdk": "^3.9.1",
33
+ "@iarna/toml": "^2.2.5",
34
+ "@stacks/clarinet-sdk": "^3.12.0",
34
35
  "@stacks/transactions": "^7.2.0",
35
36
  "ansicolor": "^2.0.3",
36
37
  "fast-check": "^4.3.0",
37
- "toml": "^3.0.0",
38
38
  "yaml": "^2.8.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@stacks/clarinet-sdk-wasm": "^3.9.1",
41
+ "@stacks/clarinet-sdk-wasm": "^3.12.0",
42
42
  "@types/jest": "^30.0.0",
43
43
  "jest": "^30.2.0",
44
44
  "ts-jest": "^29.4.5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -30,15 +30,15 @@
30
30
  "url": "https://github.com/stacks-network/rendezvous.git"
31
31
  },
32
32
  "dependencies": {
33
- "@stacks/clarinet-sdk": "^3.9.1",
33
+ "@iarna/toml": "^2.2.5",
34
+ "@stacks/clarinet-sdk": "^3.12.0",
34
35
  "@stacks/transactions": "^7.2.0",
35
36
  "ansicolor": "^2.0.3",
36
37
  "fast-check": "^4.3.0",
37
- "toml": "^3.0.0",
38
38
  "yaml": "^2.8.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@stacks/clarinet-sdk-wasm": "^3.9.1",
41
+ "@stacks/clarinet-sdk-wasm": "^3.12.0",
42
42
  "@types/jest": "^30.0.0",
43
43
  "jest": "^30.2.0",
44
44
  "ts-jest": "^29.4.5",