@stacks/rendezvous 0.3.0 → 0.4.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
@@ -56,6 +56,8 @@ npx rv <path-to-clarinet-project> <contract-name> <type>
56
56
  - `--path` – The path to use for the replay functionality.
57
57
  - `--runs` – The number of test iterations to use for exercising the contracts.
58
58
  (default: `100`)
59
+ - `--dial` – The path to a JavaScript file containing custom pre- and
60
+ post-execution functions (dialers).
59
61
 
60
62
  ---
61
63
 
package/dist/app.js CHANGED
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  });
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.getManifestFileName = void 0;
13
14
  exports.main = main;
14
15
  const path_1 = require("path");
15
16
  const events_1 = require("events");
@@ -19,13 +20,31 @@ const shared_1 = require("./shared");
19
20
  const citizen_1 = require("./citizen");
20
21
  const package_json_1 = require("./package.json");
21
22
  const ansicolor_1 = require("ansicolor");
23
+ const fs_1 = require("fs");
24
+ const dialer_1 = require("./dialer");
22
25
  const logger = (log, logLevel = "log") => {
23
26
  console[logLevel](log);
24
27
  };
28
+ /**
29
+ * Gets the manifest file name for a Clarinet project.
30
+ * If a custom manifest exists (`Clarinet-<contract-name>.toml`), it is used.
31
+ * Otherwise, the default `Clarinet.toml` is returned.
32
+ * @param manifestDir The relative path to the Clarinet project directory.
33
+ * @param targetContractName The target contract name.
34
+ * @returns The manifest file name.
35
+ */
36
+ const getManifestFileName = (manifestDir, targetContractName) => {
37
+ const isCustomManifest = (0, fs_1.existsSync)((0, path_1.resolve)(manifestDir, `Clarinet-${targetContractName}.toml`));
38
+ if (isCustomManifest) {
39
+ return `Clarinet-${targetContractName}.toml`;
40
+ }
41
+ return "Clarinet.toml";
42
+ };
43
+ exports.getManifestFileName = getManifestFileName;
25
44
  const helpMessage = `
26
45
  rv v${package_json_1.version}
27
46
 
28
- Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--path=<path>] [--runs=<runs>]
47
+ Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--path=<path>] [--runs=<runs>] [--dial=<path-to-dialers-file>] [--help]
29
48
 
30
49
  Positional arguments:
31
50
  path-to-clarinet-project - The path to the Clarinet project.
@@ -36,6 +55,7 @@ const helpMessage = `
36
55
  --seed - The seed to use for the replay functionality.
37
56
  --path - The path to use for the replay functionality.
38
57
  --runs - The runs to use for iterating over the tests. Default: 100.
58
+ --dial – The path to a JavaScript file containing custom pre- and post-execution functions (dialers).
39
59
  --help - Show the help message.
40
60
  `;
41
61
  const parseOptionalArgument = (argName) => {
@@ -74,8 +94,11 @@ function main() {
74
94
  radio.emit("logMessage", helpMessage);
75
95
  return;
76
96
  }
77
- /** The relative path to `Clarinet.toml`. */
78
- const manifestPath = (0, path_1.join)(manifestDir, "Clarinet.toml");
97
+ /**
98
+ * The relative path to the manifest file, either `Clarinet.toml` or
99
+ * `Clarinet-<contract-name>.toml`. If the latter exists, it is used.
100
+ */
101
+ const manifestPath = (0, path_1.join)(manifestDir, (0, exports.getManifestFileName)(manifestDir, sutContractName));
79
102
  radio.emit("logMessage", `Using manifest path: ${manifestPath}`);
80
103
  radio.emit("logMessage", `Target contract: ${sutContractName}`);
81
104
  const seed = parseInt(parseOptionalArgument("seed"), 10) || undefined;
@@ -90,7 +113,24 @@ function main() {
90
113
  if (runs !== undefined) {
91
114
  radio.emit("logMessage", `Using runs: ${runs}`);
92
115
  }
93
- const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(manifestDir, sutContractName);
116
+ /**
117
+ * The path to the dialer file. The dialer file allows the user to register
118
+ * custom pre and post-execution JavaScript functions to be executed before
119
+ * and after the public function calls during invariant testing.
120
+ */
121
+ const dialPath = parseOptionalArgument("dial") || undefined;
122
+ if (dialPath !== undefined) {
123
+ radio.emit("logMessage", `Using dial path: ${dialPath}`);
124
+ }
125
+ /**
126
+ * The dialer registry, which is used to keep track of all the custom dialers
127
+ * registered by the user using the `--dial` flag.
128
+ */
129
+ const dialerRegistry = dialPath !== undefined ? new dialer_1.DialerRegistry(dialPath) : undefined;
130
+ if (dialerRegistry !== undefined) {
131
+ dialerRegistry.registerDialers();
132
+ }
133
+ const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(manifestDir, manifestPath, sutContractName);
94
134
  /**
95
135
  * The list of contract IDs for the SUT contract names, as per the simnet.
96
136
  */
@@ -101,7 +141,7 @@ function main() {
101
141
  // If "test", call `checkProperties` for property-based testing.
102
142
  switch (type) {
103
143
  case "invariant": {
104
- (0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio);
144
+ yield (0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, dialerRegistry, radio);
105
145
  break;
106
146
  }
107
147
  case "test": {
package/dist/citizen.js CHANGED
@@ -27,12 +27,12 @@ const clarinet_sdk_1 = require("@hirosystems/clarinet-sdk");
27
27
  * to the simnet.
28
28
  *
29
29
  * @param manifestDir The relative path to the manifest directory.
30
+ * @param manifestPath The absolute path to the manifest file.
30
31
  * @param sutContractName The target contract name.
31
32
  * @returns The initialized simnet instance with all contracts deployed, with
32
33
  * the test contract treated as a first-class citizen of the target contract.
33
34
  */
34
- const issueFirstClassCitizenship = (manifestDir, sutContractName) => __awaiter(void 0, void 0, void 0, function* () {
35
- const manifestPath = (0, path_1.join)(manifestDir, "Clarinet.toml");
35
+ const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName) => __awaiter(void 0, void 0, void 0, function* () {
36
36
  // Initialize the simnet, to generate the simnet plan and instance. The empty
37
37
  // session will be set up, and contracts will be deployed in the correct
38
38
  // order based on the simnet plan a few lines below.
@@ -152,8 +152,8 @@ const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
152
152
  rendezvousContractName: contractName,
153
153
  };
154
154
  }
155
- catch (e) {
156
- throw new Error(`Error processing "${contractName}" contract: ${e.message}`);
155
+ catch (error) {
156
+ throw new Error(`Error processing "${contractName}" contract: ${error.message}`);
157
157
  }
158
158
  };
159
159
  exports.buildRendezvousData = buildRendezvousData;
@@ -204,8 +204,8 @@ const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
204
204
  encoding: "utf-8",
205
205
  }).toString();
206
206
  }
207
- catch (e) {
208
- throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract. ${e.message}`);
207
+ catch (error) {
208
+ throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract. ${error.message}`);
209
209
  }
210
210
  };
211
211
  exports.getTestContractSource = getTestContractSource;
package/dist/dialer.js ADDED
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.PostDialerError = exports.PreDialerError = exports.DialerRegistry = void 0;
36
+ const fs_1 = require("fs");
37
+ const path_1 = require("path");
38
+ // In telephony, a registry is used for maintaining a known set of handlers,
39
+ // devices, or processes. This aligns with this class's purpose. Dialers are
40
+ // loaded/stored and run in a structured way, so "registry" fits both telephony
41
+ // and DI semantics.
42
+ class DialerRegistry {
43
+ constructor(dialPath) {
44
+ this.preDialers = [];
45
+ this.postDialers = [];
46
+ this.dialPath = dialPath;
47
+ }
48
+ registerPreDialer(dialer) {
49
+ this.preDialers.push(dialer);
50
+ }
51
+ registerPostDialer(dialer) {
52
+ this.postDialers.push(dialer);
53
+ }
54
+ registerDialers() {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ const resolvedDialPath = (0, path_1.resolve)(this.dialPath);
57
+ if (!(0, fs_1.existsSync)(resolvedDialPath)) {
58
+ console.error(`Error: Dialer file not found: ${resolvedDialPath}`);
59
+ process.exit(1);
60
+ }
61
+ try {
62
+ const userModule = yield Promise.resolve(`${resolvedDialPath}`).then(s => __importStar(require(s)));
63
+ Object.entries(userModule).forEach(([key, fn]) => {
64
+ if (typeof fn === "function") {
65
+ if (key.startsWith("pre")) {
66
+ this.registerPreDialer(fn);
67
+ }
68
+ else if (key.startsWith("post")) {
69
+ this.registerPostDialer(fn);
70
+ }
71
+ }
72
+ });
73
+ }
74
+ catch (error) {
75
+ console.error(`Failed to load dialers:`, error);
76
+ process.exit(1);
77
+ }
78
+ });
79
+ }
80
+ executePreDialers(context) {
81
+ return __awaiter(this, void 0, void 0, function* () {
82
+ if (this.preDialers.length === 0) {
83
+ return;
84
+ }
85
+ for (const dial of this.preDialers) {
86
+ yield dial(context);
87
+ }
88
+ });
89
+ }
90
+ executePostDialers(context) {
91
+ return __awaiter(this, void 0, void 0, function* () {
92
+ if (this.postDialers.length === 0) {
93
+ return;
94
+ }
95
+ for (const dial of this.postDialers) {
96
+ yield dial(context);
97
+ }
98
+ });
99
+ }
100
+ }
101
+ exports.DialerRegistry = DialerRegistry;
102
+ class PreDialerError extends Error {
103
+ constructor(message) {
104
+ super(message);
105
+ this.name = "Pre-dialer error";
106
+ }
107
+ }
108
+ exports.PreDialerError = PreDialerError;
109
+ class PostDialerError extends Error {
110
+ constructor(message) {
111
+ super(message);
112
+ this.name = "Post-dialer error";
113
+ }
114
+ }
115
+ exports.PostDialerError = PostDialerError;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/invariant.js CHANGED
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
@@ -10,6 +19,7 @@ const heatstroke_1 = require("./heatstroke");
10
19
  const fast_check_1 = __importDefault(require("fast-check"));
11
20
  const ansicolor_1 = require("ansicolor");
12
21
  const traits_1 = require("./traits");
22
+ const dialer_1 = require("./dialer");
13
23
  /**
14
24
  * Runs invariant testing on the target contract and logs the progress. Reports
15
25
  * the test results through a custom reporter.
@@ -21,10 +31,11 @@ const traits_1 = require("./traits");
21
31
  * @param seed The seed for reproducible invariant testing.
22
32
  * @param path The path for reproducible invariant testing.
23
33
  * @param runs The number of test runs.
34
+ * @param dialerRegistry The custom dialer registry.
24
35
  * @param radio The custom logging event emitter.
25
36
  * @returns void
26
37
  */
27
- const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio) => {
38
+ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
28
39
  // A map where the keys are the Rendezvous identifiers and the values are
29
40
  // arrays of their SUT (System Under Test) functions. This map will be used
30
41
  // to access the SUT functions for each Rendezvous contract afterwards.
@@ -84,7 +95,7 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
84
95
  const radioReporter = (runDetails) => {
85
96
  (0, heatstroke_1.reporter)(runDetails, radio, "invariant");
86
97
  };
87
- fast_check_1.default.assert(fast_check_1.default.property(fast_check_1.default
98
+ yield fast_check_1.default.assert(fast_check_1.default.asyncProperty(fast_check_1.default
88
99
  .record({
89
100
  // The target contract identifier. It is a constant value equal
90
101
  // to the first contract in the list. The arbitrary is still needed,
@@ -126,10 +137,10 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
126
137
  })
127
138
  : fast_check_1.default.constant(0),
128
139
  })
129
- .map((burnBlocks) => (Object.assign(Object.assign({}, r), burnBlocks)))), (r) => {
140
+ .map((burnBlocks) => (Object.assign(Object.assign({}, r), burnBlocks)))), (r) => __awaiter(void 0, void 0, void 0, function* () {
130
141
  const selectedFunctionsArgsCV = r.selectedFunctions.map((selectedFunction, index) => (0, shared_1.argsToCV)(selectedFunction, r.selectedFunctionsArgsList[index]));
131
142
  const selectedInvariantArgsCV = (0, shared_1.argsToCV)(r.selectedInvariant, r.invariantArgs);
132
- r.selectedFunctions.forEach((selectedFunction, index) => {
143
+ for (const [index, selectedFunction] of r.selectedFunctions.entries()) {
133
144
  const [sutCallerWallet, sutCallerAddress] = r.sutCallers[index];
134
145
  const printedFunctionArgs = r.selectedFunctionsArgsList[index]
135
146
  .map((arg) => {
@@ -144,8 +155,20 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
144
155
  })
145
156
  .join(" ");
146
157
  try {
147
- const { result: functionCallResult } = simnet.callPublicFn(r.rendezvousContractId, selectedFunction.name, selectedFunctionsArgsCV[index], sutCallerAddress);
148
- const functionCallResultJson = (0, transactions_1.cvToJSON)(functionCallResult);
158
+ if (dialerRegistry !== undefined) {
159
+ yield dialerRegistry.executePreDialers({
160
+ selectedFunction: selectedFunction,
161
+ functionCall: undefined,
162
+ clarityValueArguments: selectedFunctionsArgsCV[index],
163
+ });
164
+ }
165
+ }
166
+ catch (error) {
167
+ throw new dialer_1.PreDialerError(error.message);
168
+ }
169
+ try {
170
+ const functionCall = simnet.callPublicFn(r.rendezvousContractId, selectedFunction.name, selectedFunctionsArgsCV[index], sutCallerAddress);
171
+ const functionCallResultJson = (0, transactions_1.cvToJSON)(functionCall.result);
149
172
  if (functionCallResultJson.success) {
150
173
  localContext[r.rendezvousContractId][selectedFunction.name]++;
151
174
  simnet.callPublicFn(r.rendezvousContractId, "update-context", [
@@ -158,6 +181,18 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
158
181
  `${targetContractName} ` +
159
182
  `${(0, ansicolor_1.underline)(selectedFunction.name)} ` +
160
183
  printedFunctionArgs);
184
+ try {
185
+ if (dialerRegistry !== undefined) {
186
+ yield dialerRegistry.executePostDialers({
187
+ selectedFunction: selectedFunction,
188
+ functionCall: functionCall,
189
+ clarityValueArguments: selectedFunctionsArgsCV[index],
190
+ });
191
+ }
192
+ }
193
+ catch (error) {
194
+ throw new dialer_1.PostDialerError(error.message);
195
+ }
161
196
  }
162
197
  else {
163
198
  radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
@@ -169,17 +204,23 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
169
204
  }
170
205
  }
171
206
  catch (error) {
172
- // If the function call fails with a runtime error, log a dimmed
173
- // message. Since the public function result is ignored, there's
174
- // no need to throw an error.
175
- radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
176
- ${simnet.blockHeight.toString().padStart(8)} ` +
177
- `${sutCallerWallet} ` +
178
- `${targetContractName} ` +
179
- `${(0, ansicolor_1.underline)(selectedFunction.name)} ` +
180
- printedFunctionArgs));
207
+ if (error instanceof dialer_1.PreDialerError ||
208
+ error instanceof dialer_1.PostDialerError) {
209
+ throw error;
210
+ }
211
+ else {
212
+ // If the function call fails with a runtime error, log a dimmed
213
+ // message. Since the public function result is ignored, there's
214
+ // no need to throw an error.
215
+ radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
216
+ `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
217
+ `${sutCallerWallet} ` +
218
+ `${targetContractName} ` +
219
+ `${(0, ansicolor_1.underline)(selectedFunction.name)} ` +
220
+ printedFunctionArgs));
221
+ }
181
222
  }
182
- });
223
+ }
183
224
  const printedInvariantArgs = r.invariantArgs
184
225
  .map((arg) => {
185
226
  try {
@@ -226,14 +267,14 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
226
267
  if (r.canMineBlocks) {
227
268
  simnet.mineEmptyBurnBlocks(r.burnBlocks);
228
269
  }
229
- }), {
270
+ })), {
230
271
  verbose: true,
231
272
  reporter: radioReporter,
232
273
  seed: seed,
233
274
  path: path,
234
275
  numRuns: runs,
235
276
  });
236
- };
277
+ });
237
278
  exports.checkInvariants = checkInvariants;
238
279
  /**
239
280
  * Initializes the local context, setting the number of times each function
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {