@stacks/rendezvous 0.7.1 → 0.7.2

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/dist/citizen.js CHANGED
@@ -12,10 +12,11 @@ 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.groupContractsByEpochFromSimnetPlan = exports.issueFirstClassCitizenship = void 0;
15
+ exports.getSbtcBalancesFromSimnet = exports.getTestContractSource = exports.buildRendezvousData = exports.getContractSource = exports.deployContracts = exports.groupContractsByEpochFromDeploymentPlan = 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
20
  const yaml_1 = __importDefault(require("yaml"));
20
21
  const clarinet_sdk_1 = require("@hirosystems/clarinet-sdk");
21
22
  const transactions_1 = require("@stacks/transactions");
@@ -35,14 +36,15 @@ const transactions_1 = require("@stacks/transactions");
35
36
  * the test contract treated as a first-class citizen of the target contract.
36
37
  */
37
38
  const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSettings, sutContractName) => __awaiter(void 0, void 0, void 0, function* () {
38
- // Initialize the simnet, to generate the simnet plan and instance. The empty
39
- // session will be set up, and contracts will be deployed in the correct
40
- // order based on the simnet plan a few lines below.
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.
41
43
  const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestPath);
42
- const simnetPlan = yaml_1.default.parse((0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "deployments", "default.simnet-plan.yaml"), {
44
+ const deploymentPlan = yaml_1.default.parse((0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "deployments", "default.simnet-plan.yaml"), {
43
45
  encoding: "utf-8",
44
46
  }));
45
- const sortedContractsByEpoch = (0, exports.groupContractsByEpochFromSimnetPlan)(simnetPlan);
47
+ const sortedContractsByEpoch = (0, exports.groupContractsByEpochFromDeploymentPlan)(deploymentPlan);
46
48
  const simnetAddresses = [...simnet.getAccounts().values()];
47
49
  const stxBalancesMap = new Map(simnetAddresses.map((address) => {
48
50
  const balanceHex = simnet.runSnippet(`(stx-get-balance '${address})`);
@@ -57,13 +59,19 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSetting
57
59
  // resulting contract will replace the target contract in the simnet.
58
60
  /** The contract names mapped to the concatenated source code. */
59
61
  const rendezvousSources = new Map([sutContractName]
60
- .map((contractName) => (0, exports.buildRendezvousData)(simnetPlan, contractName, manifestDir))
62
+ // For each target contract name, execute the processing steps to get the
63
+ // concatenated contract source code and the contract ID.
64
+ .map((contractName) => (0, exports.buildRendezvousData)(deploymentPlan, contractName, manifestDir))
65
+ // Use the contract ID as a key, mapping to the concatenated contract
66
+ // source code.
61
67
  .map((rendezvousContractData) => [
62
- rendezvousContractData.rendezvousContractName,
63
- rendezvousContractData.rendezvousSource,
68
+ rendezvousContractData.rendezvousContractId,
69
+ rendezvousContractData.rendezvousSourceCode,
64
70
  ]));
65
- // Deploy the contracts to the simnet in the correct order.
66
- yield deployContracts(simnet, sortedContractsByEpoch, (name, props) => (0, exports.getContractSource)([sutContractName], rendezvousSources, name, props, manifestDir));
71
+ const clarinetToml = toml_1.default.parse((0, fs_1.readFileSync)(manifestPath, { encoding: "utf-8" }));
72
+ const cacheDir = ((_a = clarinetToml.project) === null || _a === void 0 ? void 0 : _a.cache_dir) || "./.cache";
73
+ // Deploy the contracts to the empty simnet session in the correct order.
74
+ yield (0, exports.deployContracts)(simnet, sortedContractsByEpoch, manifestDir, cacheDir, (name, sender, props) => (0, exports.getContractSource)([sutContractName], rendezvousSources, name, sender, props, manifestDir));
67
75
  // Filter out addresses with zero balance. They do not need to be restored.
68
76
  const sbtcBalancesToRestore = new Map([...sbtcBalancesMap.entries()].filter(([_, balance]) => balance !== 0));
69
77
  // After all the contracts and requirements are deployed, if the test wallets
@@ -76,15 +84,15 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSetting
76
84
  });
77
85
  exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
78
86
  /**
79
- * Groups contracts by epoch from the simnet plan.
80
- * @param simnetPlan The simnet plan.
87
+ * Groups contracts by epoch from the deployment plan.
88
+ * @param deploymentPlan The parsed deployment plan.
81
89
  * @returns A record of contracts grouped by epoch. The record key is the epoch
82
90
  * string, and the value is an array of contracts. Each contract is represented
83
91
  * as a record with the contract name as the key and a record containing the
84
92
  * contract path and clarity version as the value.
85
93
  */
86
- const groupContractsByEpochFromSimnetPlan = (simnetPlan) => {
87
- return simnetPlan.plan.batches.reduce((acc, batch) => {
94
+ const groupContractsByEpochFromDeploymentPlan = (deploymentPlan) => {
95
+ return deploymentPlan.plan.batches.reduce((acc, batch) => {
88
96
  const epoch = batch.epoch;
89
97
  const contracts = batch.transactions
90
98
  .filter((tx) => tx["emulated-contract-publish"])
@@ -103,44 +111,56 @@ const groupContractsByEpochFromSimnetPlan = (simnetPlan) => {
103
111
  return acc;
104
112
  }, {});
105
113
  };
106
- exports.groupContractsByEpochFromSimnetPlan = groupContractsByEpochFromSimnetPlan;
114
+ exports.groupContractsByEpochFromDeploymentPlan = groupContractsByEpochFromDeploymentPlan;
107
115
  /**
108
116
  * Deploys the contracts to the simnet in the correct order.
109
117
  * @param simnet The simnet instance.
110
118
  * @param contractsByEpoch The record of contracts by epoch.
111
119
  * @param getContractSourceFn The function to retrieve the contract source.
112
120
  */
113
- const deployContracts = (simnet, contractsByEpoch, getContractSourceFn) => __awaiter(void 0, void 0, void 0, function* () {
121
+ const deployContracts = (simnet, contractsByEpoch, manifestDir, cacheDir, getContractSourceFn) => __awaiter(void 0, void 0, void 0, function* () {
114
122
  for (const [epoch, contracts] of Object.entries(contractsByEpoch)) {
115
123
  // Move to the next epoch and deploy the contracts in the correct order.
116
124
  simnet.setEpoch(epoch);
117
125
  for (const contract of contracts.flatMap(Object.entries)) {
118
126
  const [name, props] = contract;
119
- const source = getContractSourceFn(name, props);
120
- // For requirement contracts, use the original sender. The sender address
121
- // is included in the path:
122
- // "./.cache/requirements/<address>.contract-name.clar".
123
- const sender = props.path.includes(".cache")
124
- ? props.path.split("requirements")[1].slice(1).split(".")[0]
127
+ // Resolve paths to absolute for proper comparison.
128
+ const absoluteContractPath = (0, path_1.resolve)(manifestDir, props.path);
129
+ const absoluteRequirementsPath = (0, path_1.resolve)(manifestDir, cacheDir, "requirements");
130
+ // Check if contract is in requirements directory.
131
+ const isRequirement = absoluteContractPath.startsWith(absoluteRequirementsPath);
132
+ const sender = isRequirement
133
+ ? (0, path_1.basename)(props.path).split(".")[0]
125
134
  : simnet.deployer;
135
+ const source = getContractSourceFn(name, sender, props);
126
136
  simnet.deployContract(name, source, { clarityVersion: props.clarity_version }, sender);
127
137
  }
128
138
  }
129
139
  });
140
+ exports.deployContracts = deployContracts;
130
141
  /**
131
142
  * Conditionally retrieves the contract source based on whether the contract is
132
143
  * a SUT contract or not.
133
144
  * @param targetContractNames The list of target contract names.
134
- * @param rendezvousSourcesMap The contract names mapped to the concatenated
135
- * source code.
145
+ * @param rendezvousSourcesMap The target contract IDs mapped to the resulting
146
+ * concatenated source code.
136
147
  * @param contractName The contract name.
148
+ * @param contractSender The emulated sender of the contract according to the
149
+ * deployment plan.
137
150
  * @param contractProps The contract deployment properties.
138
151
  * @param manifestDir The relative path to the manifest directory.
139
152
  * @returns The contract source code.
140
153
  */
141
- const getContractSource = (targetContractNames, rendezvousSourcesMap, contractName, contractProps, manifestDir) => {
142
- if (targetContractNames.includes(contractName)) {
143
- const contractSource = rendezvousSourcesMap.get(contractName);
154
+ const getContractSource = (targetContractNames, rendezvousSourcesMap, contractName, contractSender, contractProps, manifestDir) => {
155
+ const contractId = `${contractSender}.${contractName}`;
156
+ // Checking if a contract is a SUT one just by using the name is not enough.
157
+ // There can be multiple contracts with the same name, but different senders
158
+ // in the deployment plan. The contract ID is the unique identifier used to
159
+ // store the concatenated Rendezvous source codes in the
160
+ // `rendezvousSourcesMap`.
161
+ if (targetContractNames.includes(contractName) &&
162
+ rendezvousSourcesMap.has(contractId)) {
163
+ const contractSource = rendezvousSourcesMap.get(contractId);
144
164
  if (!contractSource) {
145
165
  throw new Error(`Contract source not found for ${contractName}`);
146
166
  }
@@ -155,20 +175,24 @@ const getContractSource = (targetContractNames, rendezvousSourcesMap, contractNa
155
175
  exports.getContractSource = getContractSource;
156
176
  /**
157
177
  * Builds the Rendezvous data.
158
- * @param simnetPlan The parsed simnet plan.
178
+ * @param deploymentPlan The parsed deployment plan.
159
179
  * @param contractName The contract name.
160
180
  * @param manifestDir The relative path to the manifest directory.
161
181
  * @returns The Rendezvous data representing a record. The returned record
162
- * contains the Rendezvous source code and the Rendezvous contract name.
182
+ * contains the Rendezvous source code and the unique Rendezvous contract ID.
163
183
  */
164
- const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
184
+ const buildRendezvousData = (deploymentPlan, contractName, manifestDir) => {
165
185
  try {
166
- const sutContractSource = getSimnetPlanContractSource(simnetPlan, manifestDir, contractName);
167
- const testContractSource = (0, exports.getTestContractSource)(simnetPlan, contractName, manifestDir);
186
+ const sutContractSource = getDeploymentPlanContractSource(deploymentPlan, contractName, manifestDir);
187
+ const testContractSource = (0, exports.getTestContractSource)(deploymentPlan, contractName, manifestDir);
168
188
  const rendezvousSource = scheduleRendezvous(sutContractSource, testContractSource);
189
+ const rendezvousContractEmulatedSender = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, contractName)["emulated-sender"];
190
+ // Use the contract ID as a unique identifier of the contract within the
191
+ // deployment plan.
192
+ const rendezvousContractId = `${rendezvousContractEmulatedSender}.${contractName}`;
169
193
  return {
170
- rendezvousSource: rendezvousSource,
171
- rendezvousContractName: contractName,
194
+ rendezvousContractId: rendezvousContractId,
195
+ rendezvousSourceCode: rendezvousSource,
172
196
  };
173
197
  }
174
198
  catch (error) {
@@ -177,47 +201,45 @@ const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
177
201
  };
178
202
  exports.buildRendezvousData = buildRendezvousData;
179
203
  /**
180
- * Retrieves the contract source code using the simnet plan.
181
- * @param simnetPlan The parsed simnet plan.
182
- * @param manifestDir The relative path to the manifest directory.
204
+ * Retrieves the contract source code using the deployment plan.
205
+ * @param deploymentPlan The parsed deployment plan.
183
206
  * @param sutContractName The target contract name.
207
+ * @param manifestDir The relative path to the manifest directory.
184
208
  * @returns The contract source code.
185
209
  */
186
- const getSimnetPlanContractSource = (simnetPlan, manifestDir, sutContractName) => {
187
- var _a;
188
- // Filter for transactions that contain "emulated-contract-publish".
189
- const contractInfo = (_a = simnetPlan.plan.batches
190
- .flatMap((batch) => batch.transactions)
191
- .find((transaction) => transaction["emulated-contract-publish"] &&
192
- transaction["emulated-contract-publish"]["contract-name"] ===
193
- sutContractName)) === null || _a === void 0 ? void 0 : _a["emulated-contract-publish"];
194
- if (contractInfo == undefined) {
195
- throw new Error(`"${sutContractName}" contract not found in Clarinet.toml.`);
196
- }
197
- return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, contractInfo.path), {
210
+ const getDeploymentPlanContractSource = (deploymentPlan, sutContractName, manifestDir) => {
211
+ const sutContractPath = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, sutContractName).path;
212
+ return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, sutContractPath), {
198
213
  encoding: "utf-8",
199
214
  }).toString();
200
215
  };
201
216
  /**
202
217
  * Retrieves the test contract source code.
203
- * @param simnetPlan The parsed simnet plan.
218
+ * @param deploymentPlan The parsed deployment plan.
204
219
  * @param sutContractName The target contract name.
205
220
  * @param manifestDir The relative path to the manifest directory.
206
221
  * @returns The test contract source code.
207
222
  */
208
- const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
209
- var _a;
210
- const contractInfo = (_a = simnetPlan.plan.batches
211
- .flatMap((batch) => batch.transactions)
212
- .find((transaction) => transaction["emulated-contract-publish"] &&
213
- transaction["emulated-contract-publish"]["contract-name"] ===
214
- sutContractName)) === null || _a === void 0 ? void 0 : _a["emulated-contract-publish"];
215
- const sutContractPath = contractInfo.path;
216
- const extension = ".clar";
217
- if (!sutContractPath.endsWith(extension)) {
223
+ const getTestContractSource = (deploymentPlan, sutContractName, manifestDir) => {
224
+ const sutContractPath = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, sutContractName).path;
225
+ const clarityExtension = ".clar";
226
+ if (!sutContractPath.endsWith(clarityExtension)) {
218
227
  throw new Error(`Invalid contract extension for the "${sutContractName}" contract.`);
219
228
  }
220
- const testContractPath = sutContractPath.replace(extension, `.tests${extension}`);
229
+ // If the sutContractPath is located under .cache/requirements/ path, search
230
+ // for the test contract in the classic `contracts` directory.
231
+ if (sutContractPath.includes(".cache")) {
232
+ const relativePath = sutContractPath.split(".cache/requirements/")[1];
233
+ const relativePathTestContract = relativePath.replace(clarityExtension, `.tests${clarityExtension}`);
234
+ return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "contracts", relativePathTestContract), {
235
+ encoding: "utf-8",
236
+ }).toString();
237
+ }
238
+ // If the contract is not under the `.cache/requirements/` path, we assume it
239
+ // is located in a regular path specified in the manifest file. Just search
240
+ // for the test contract near the SUT one, following the naming
241
+ // convention: `<contract-name>.tests.clar`.
242
+ const testContractPath = sutContractPath.replace(clarityExtension, `.tests${clarityExtension}`);
221
243
  try {
222
244
  return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, testContractPath), {
223
245
  encoding: "utf-8",
@@ -228,6 +250,58 @@ const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
228
250
  }
229
251
  };
230
252
  exports.getTestContractSource = getTestContractSource;
253
+ /**
254
+ * Retrieves the emulated contract publish data of the target contract from the
255
+ * deployment plan. If multiple contracts share the same name in the deployment
256
+ * plan, this utility will prioritize the one defined in `Clarinet.toml` as a
257
+ * project contract over a requirement. The prioritization is made comparing
258
+ * the deployment plan emulated sender with the deployer of the Clarinet project.
259
+ * @param deploymentPlan The parsed deployment plan.
260
+ * @param sutContractName The target contract name.
261
+ * @returns The emulated contract publish data of the SUT contract as present
262
+ * in the deployment plan.
263
+ */
264
+ const getSutContractDeploymentPlanEmulatedPublish = (deploymentPlan, sutContractName) => {
265
+ var _a, _b;
266
+ // Filter all emulated contract publish transactions matching the target
267
+ // contract name from the deployment plan.
268
+ const contractPublishMatchesByName = deploymentPlan.plan.batches
269
+ .flatMap((batch) => batch.transactions)
270
+ .filter((transaction) => transaction["emulated-contract-publish"] &&
271
+ transaction["emulated-contract-publish"]["contract-name"] ===
272
+ sutContractName);
273
+ // If no matches are found, something went wrong.
274
+ if (contractPublishMatchesByName.length === 0) {
275
+ throw new Error(`"${sutContractName}" contract not found in Clarinet.toml.`);
276
+ }
277
+ // If multiple matches are found, search for the one deployed by the deployer
278
+ // defined in the `Devnet.toml` file and present in the deployment plan. This
279
+ // is the project contract.
280
+ if (contractPublishMatchesByName.length > 1) {
281
+ const deployer = (_a = deploymentPlan.genesis.wallets.find((wallet) => wallet.name === "deployer")) === null || _a === void 0 ? void 0 : _a.address;
282
+ if (!deployer) {
283
+ throw new Error(`Something went wrong. Deployer not found in the deployment plan.`);
284
+ }
285
+ // From the list of filtered emulated contract publish transactions with
286
+ // having the same name, select the one deployed by the deployer.
287
+ const targetContractDeploymentData = (_b = contractPublishMatchesByName.find((transaction) => transaction["emulated-contract-publish"]["emulated-sender"] ===
288
+ deployer)) === null || _b === void 0 ? void 0 : _b["emulated-contract-publish"];
289
+ // This is an edge case that can happen in practice. If the project has two
290
+ // requirements that share the same contract name, Rendezvous will not be
291
+ // able to select the one to be fuzzed. The recommendation for users would
292
+ // be to include the target contract in the Clarinet project.
293
+ if (!targetContractDeploymentData) {
294
+ throw new Error(`Multiple contracts named "${sutContractName}" found in the deployment plan, no one deployed by the deployer.`);
295
+ }
296
+ return targetContractDeploymentData;
297
+ }
298
+ // Only one match was found, return the path to the contract.
299
+ const contractNameMatch = contractPublishMatchesByName[0]["emulated-contract-publish"];
300
+ if (!contractNameMatch) {
301
+ throw new Error(`Could not locate "${sutContractName}" contract.`);
302
+ }
303
+ return contractNameMatch;
304
+ };
231
305
  /**
232
306
  * Schedules a Rendezvous between the System Under Test (`SUT`) and the test
233
307
  * contract.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {