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