@stacks/rendezvous 0.7.0 → 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.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,31 +36,21 @@ 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})`);
49
51
  return [address, (0, transactions_1.cvToValue)((0, transactions_1.hexToCV)(balanceHex))];
50
52
  }));
51
- const sbtcBalancesMap = new Map(simnetAddresses.map((address) => {
52
- try {
53
- const { result: getBalanceResult } = simnet.callReadOnlyFn("SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token", "get-balance", [transactions_1.Cl.principal(address)], address);
54
- // If the previous read-only call works, the user is working with
55
- // sBTC. This means we can proceed with restoring sBTC balances.
56
- const sbtcBalance = (0, transactions_1.cvToJSON)(getBalanceResult).value.value;
57
- return [address, sbtcBalance];
58
- }
59
- catch (e) {
60
- return [address, 0];
61
- }
62
- }));
53
+ const sbtcBalancesMap = (0, exports.getSbtcBalancesFromSimnet)(simnet);
63
54
  yield simnet.initEmptySession(remoteDataSettings);
64
55
  simnetAddresses.forEach((address) => {
65
56
  simnet.mintSTX(address, stxBalancesMap.get(address));
@@ -68,13 +59,19 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSetting
68
59
  // resulting contract will replace the target contract in the simnet.
69
60
  /** The contract names mapped to the concatenated source code. */
70
61
  const rendezvousSources = new Map([sutContractName]
71
- .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.
72
67
  .map((rendezvousContractData) => [
73
- rendezvousContractData.rendezvousContractName,
74
- rendezvousContractData.rendezvousSource,
68
+ rendezvousContractData.rendezvousContractId,
69
+ rendezvousContractData.rendezvousSourceCode,
75
70
  ]));
76
- // Deploy the contracts to the simnet in the correct order.
77
- 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));
78
75
  // Filter out addresses with zero balance. They do not need to be restored.
79
76
  const sbtcBalancesToRestore = new Map([...sbtcBalancesMap.entries()].filter(([_, balance]) => balance !== 0));
80
77
  // After all the contracts and requirements are deployed, if the test wallets
@@ -87,15 +84,15 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSetting
87
84
  });
88
85
  exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
89
86
  /**
90
- * Groups contracts by epoch from the simnet plan.
91
- * @param simnetPlan The simnet plan.
87
+ * Groups contracts by epoch from the deployment plan.
88
+ * @param deploymentPlan The parsed deployment plan.
92
89
  * @returns A record of contracts grouped by epoch. The record key is the epoch
93
90
  * string, and the value is an array of contracts. Each contract is represented
94
91
  * as a record with the contract name as the key and a record containing the
95
92
  * contract path and clarity version as the value.
96
93
  */
97
- const groupContractsByEpochFromSimnetPlan = (simnetPlan) => {
98
- return simnetPlan.plan.batches.reduce((acc, batch) => {
94
+ const groupContractsByEpochFromDeploymentPlan = (deploymentPlan) => {
95
+ return deploymentPlan.plan.batches.reduce((acc, batch) => {
99
96
  const epoch = batch.epoch;
100
97
  const contracts = batch.transactions
101
98
  .filter((tx) => tx["emulated-contract-publish"])
@@ -114,44 +111,56 @@ const groupContractsByEpochFromSimnetPlan = (simnetPlan) => {
114
111
  return acc;
115
112
  }, {});
116
113
  };
117
- exports.groupContractsByEpochFromSimnetPlan = groupContractsByEpochFromSimnetPlan;
114
+ exports.groupContractsByEpochFromDeploymentPlan = groupContractsByEpochFromDeploymentPlan;
118
115
  /**
119
116
  * Deploys the contracts to the simnet in the correct order.
120
117
  * @param simnet The simnet instance.
121
118
  * @param contractsByEpoch The record of contracts by epoch.
122
119
  * @param getContractSourceFn The function to retrieve the contract source.
123
120
  */
124
- 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* () {
125
122
  for (const [epoch, contracts] of Object.entries(contractsByEpoch)) {
126
123
  // Move to the next epoch and deploy the contracts in the correct order.
127
124
  simnet.setEpoch(epoch);
128
125
  for (const contract of contracts.flatMap(Object.entries)) {
129
126
  const [name, props] = contract;
130
- const source = getContractSourceFn(name, props);
131
- // For requirement contracts, use the original sender. The sender address
132
- // is included in the path:
133
- // "./.cache/requirements/<address>.contract-name.clar".
134
- const sender = props.path.includes(".cache")
135
- ? 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]
136
134
  : simnet.deployer;
135
+ const source = getContractSourceFn(name, sender, props);
137
136
  simnet.deployContract(name, source, { clarityVersion: props.clarity_version }, sender);
138
137
  }
139
138
  }
140
139
  });
140
+ exports.deployContracts = deployContracts;
141
141
  /**
142
142
  * Conditionally retrieves the contract source based on whether the contract is
143
143
  * a SUT contract or not.
144
144
  * @param targetContractNames The list of target contract names.
145
- * @param rendezvousSourcesMap The contract names mapped to the concatenated
146
- * source code.
145
+ * @param rendezvousSourcesMap The target contract IDs mapped to the resulting
146
+ * concatenated source code.
147
147
  * @param contractName The contract name.
148
+ * @param contractSender The emulated sender of the contract according to the
149
+ * deployment plan.
148
150
  * @param contractProps The contract deployment properties.
149
151
  * @param manifestDir The relative path to the manifest directory.
150
152
  * @returns The contract source code.
151
153
  */
152
- const getContractSource = (targetContractNames, rendezvousSourcesMap, contractName, contractProps, manifestDir) => {
153
- if (targetContractNames.includes(contractName)) {
154
- 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);
155
164
  if (!contractSource) {
156
165
  throw new Error(`Contract source not found for ${contractName}`);
157
166
  }
@@ -166,20 +175,24 @@ const getContractSource = (targetContractNames, rendezvousSourcesMap, contractNa
166
175
  exports.getContractSource = getContractSource;
167
176
  /**
168
177
  * Builds the Rendezvous data.
169
- * @param simnetPlan The parsed simnet plan.
178
+ * @param deploymentPlan The parsed deployment plan.
170
179
  * @param contractName The contract name.
171
180
  * @param manifestDir The relative path to the manifest directory.
172
181
  * @returns The Rendezvous data representing a record. The returned record
173
- * contains the Rendezvous source code and the Rendezvous contract name.
182
+ * contains the Rendezvous source code and the unique Rendezvous contract ID.
174
183
  */
175
- const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
184
+ const buildRendezvousData = (deploymentPlan, contractName, manifestDir) => {
176
185
  try {
177
- const sutContractSource = getSimnetPlanContractSource(simnetPlan, manifestDir, contractName);
178
- const testContractSource = (0, exports.getTestContractSource)(simnetPlan, contractName, manifestDir);
186
+ const sutContractSource = getDeploymentPlanContractSource(deploymentPlan, contractName, manifestDir);
187
+ const testContractSource = (0, exports.getTestContractSource)(deploymentPlan, contractName, manifestDir);
179
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}`;
180
193
  return {
181
- rendezvousSource: rendezvousSource,
182
- rendezvousContractName: contractName,
194
+ rendezvousContractId: rendezvousContractId,
195
+ rendezvousSourceCode: rendezvousSource,
183
196
  };
184
197
  }
185
198
  catch (error) {
@@ -188,47 +201,45 @@ const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
188
201
  };
189
202
  exports.buildRendezvousData = buildRendezvousData;
190
203
  /**
191
- * Retrieves the contract source code using the simnet plan.
192
- * @param simnetPlan The parsed simnet plan.
193
- * @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.
194
206
  * @param sutContractName The target contract name.
207
+ * @param manifestDir The relative path to the manifest directory.
195
208
  * @returns The contract source code.
196
209
  */
197
- const getSimnetPlanContractSource = (simnetPlan, manifestDir, sutContractName) => {
198
- var _a;
199
- // Filter for transactions that contain "emulated-contract-publish".
200
- const contractInfo = (_a = simnetPlan.plan.batches
201
- .flatMap((batch) => batch.transactions)
202
- .find((transaction) => transaction["emulated-contract-publish"] &&
203
- transaction["emulated-contract-publish"]["contract-name"] ===
204
- sutContractName)) === null || _a === void 0 ? void 0 : _a["emulated-contract-publish"];
205
- if (contractInfo == undefined) {
206
- throw new Error(`"${sutContractName}" contract not found in Clarinet.toml.`);
207
- }
208
- 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), {
209
213
  encoding: "utf-8",
210
214
  }).toString();
211
215
  };
212
216
  /**
213
217
  * Retrieves the test contract source code.
214
- * @param simnetPlan The parsed simnet plan.
218
+ * @param deploymentPlan The parsed deployment plan.
215
219
  * @param sutContractName The target contract name.
216
220
  * @param manifestDir The relative path to the manifest directory.
217
221
  * @returns The test contract source code.
218
222
  */
219
- const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
220
- var _a;
221
- const contractInfo = (_a = simnetPlan.plan.batches
222
- .flatMap((batch) => batch.transactions)
223
- .find((transaction) => transaction["emulated-contract-publish"] &&
224
- transaction["emulated-contract-publish"]["contract-name"] ===
225
- sutContractName)) === null || _a === void 0 ? void 0 : _a["emulated-contract-publish"];
226
- const sutContractPath = contractInfo.path;
227
- const extension = ".clar";
228
- 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)) {
229
227
  throw new Error(`Invalid contract extension for the "${sutContractName}" contract.`);
230
228
  }
231
- 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}`);
232
243
  try {
233
244
  return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, testContractPath), {
234
245
  encoding: "utf-8",
@@ -239,6 +250,58 @@ const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
239
250
  }
240
251
  };
241
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
+ };
242
305
  /**
243
306
  * Schedules a Rendezvous between the System Under Test (`SUT`) and the test
244
307
  * contract.
@@ -260,6 +323,31 @@ function scheduleRendezvous(targetContractSource, tests) {
260
323
  (ok (map-set context function-name {called: called})))`;
261
324
  return `${targetContractSource}\n\n${context}\n\n${tests}`;
262
325
  }
326
+ /**
327
+ * Maps the simnet accounts to their sBTC balances. The function tries to call
328
+ * the `get-balance` function of the `sbtc-token` contract for each address. If
329
+ * the call fails, it returns a balance of 0 for that address. The call fails
330
+ * if the user is not working with sBTC.
331
+ * @param simnet The simnet instance.
332
+ * @returns A map of addresses to their sBTC balances.
333
+ */
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];
348
+ }
349
+ }));
350
+ exports.getSbtcBalancesFromSimnet = getSbtcBalancesFromSimnet;
263
351
  /**
264
352
  * Utility function that restores the test wallets' initial sBTC balances in
265
353
  * the re-initialized first-class citizenship simnet.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.7.0",
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.0",
3
+ "version": "0.7.2",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {