@stacks/rendezvous
Advanced tools
+2
-30
@@ -12,11 +12,7 @@ #!/usr/bin/env node | ||
| }; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.tryParseRemoteDataSettings = exports.getManifestFileName = exports.noRemoteData = void 0; | ||
| exports.getManifestFileName = void 0; | ||
| exports.main = main; | ||
| const path_1 = require("path"); | ||
| const events_1 = require("events"); | ||
| const toml_1 = __importDefault(require("toml")); | ||
| const property_1 = require("./property"); | ||
@@ -35,9 +31,2 @@ const invariant_1 = require("./invariant"); | ||
| /** | ||
| * The object used to initialize an empty simnet session with, when no remote | ||
| * data is enabled in the `Clarinet.toml` file. | ||
| */ | ||
| exports.noRemoteData = { | ||
| enabled: false, | ||
| }; | ||
| /** | ||
| * Gets the manifest file name for a Clarinet project. | ||
@@ -58,18 +47,2 @@ * If a custom manifest exists (`Clarinet-<contract-name>.toml`), it is used. | ||
| exports.getManifestFileName = getManifestFileName; | ||
| const tryParseRemoteDataSettings = (manifestPath, radio) => { | ||
| var _a, _b; | ||
| const clarinetToml = toml_1.default.parse((0, fs_1.readFileSync)((0, path_1.resolve)(manifestPath), "utf-8")); | ||
| const remoteDataUserSettings = (_b = (_a = clarinetToml.repl) === null || _a === void 0 ? void 0 : _a.remote_data) !== null && _b !== void 0 ? _b : undefined; | ||
| if (remoteDataUserSettings && (remoteDataUserSettings === null || remoteDataUserSettings === void 0 ? void 0 : remoteDataUserSettings.enabled) === true) { | ||
| radio.emit("logMessage", (0, ansicolor_1.yellow)("\nUsing remote data. Setting up the environment can take up to a minute...")); | ||
| } | ||
| // If no remote data settings are provided, we still need to return an object | ||
| // with the `enabled` property set to `false`. That is what simnet expects | ||
| // at least in order to initialize an empty simnet session. | ||
| if (!remoteDataUserSettings) { | ||
| return exports.noRemoteData; | ||
| } | ||
| return remoteDataUserSettings; | ||
| }; | ||
| exports.tryParseRemoteDataSettings = tryParseRemoteDataSettings; | ||
| const helpMessage = ` | ||
@@ -175,4 +148,3 @@ rv v${package_json_1.version} | ||
| } | ||
| const remoteDataSettings = (0, exports.tryParseRemoteDataSettings)(manifestPath, radio); | ||
| const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, remoteDataSettings, runConfig.sutContractName); | ||
| const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, runConfig.sutContractName, radio); | ||
| /** | ||
@@ -179,0 +151,0 @@ * The list of contract IDs for the SUT contract names, as per the simnet. |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| // This file is a placeholder for the Rendezvous CLI-related types. |
+179
-317
@@ -15,167 +15,116 @@ "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.getSbtcBalancesFromSimnet = exports.getTestContractSource = exports.buildRendezvousData = exports.getContractSource = exports.deployContracts = exports.groupContractsByEpochFromDeploymentPlan = exports.issueFirstClassCitizenship = void 0; | ||
| exports.getTestContractSource = exports.buildRendezvousData = exports.issueFirstClassCitizenship = void 0; | ||
| exports.scheduleRendezvous = scheduleRendezvous; | ||
| const fs_1 = require("fs"); | ||
| const path_1 = require("path"); | ||
| const toml_1 = __importDefault(require("toml")); | ||
| const os_1 = require("os"); | ||
| const toml_1 = require("@iarna/toml"); | ||
| const yaml_1 = __importDefault(require("yaml")); | ||
| const clarinet_sdk_1 = require("@stacks/clarinet-sdk"); | ||
| const transactions_1 = require("@stacks/transactions"); | ||
| const ansicolor_1 = require("ansicolor"); | ||
| /** | ||
| * Prepares the simnet instance and assures the target contract's corresponding | ||
| * test contract is treated as a first-class citizen, relying on their | ||
| * concatenation. This function handles: | ||
| * - Contract sorting by epoch based on the deployment plan. | ||
| * - Combining the target contract with its tests and deploying all contracts | ||
| * to the simnet. | ||
| * Prepares the simnet with the Rendezvous tests as first-class citizens of the | ||
| * target contract. | ||
| * | ||
| * This function works in an isolated temporary copy of the Clarinet project | ||
| * in /tmp/ to avoid lingering temporary files in the user's project directory. | ||
| * In case of system crashes, power outages, etc., the temp directory is | ||
| * automatically cleaned up by the OS on reboot. | ||
| * | ||
| * @param manifestDir The relative path to the manifest directory. | ||
| * @param manifestPath The absolute path to the manifest file. | ||
| * @param remoteDataSettings The remote data settings. | ||
| * @param manifestPath The path to the manifest file. | ||
| * @param sutContractName The target contract name. | ||
| * @returns The initialized simnet instance with all contracts deployed, with | ||
| * the test contract treated as a first-class citizen of the target contract. | ||
| * @param radio The event emitter to send log messages to. | ||
| * @returns The initialized simnet. | ||
| */ | ||
| const issueFirstClassCitizenship = (manifestDir, manifestPath, remoteDataSettings, sutContractName) => __awaiter(void 0, void 0, void 0, function* () { | ||
| var _a; | ||
| // Initialize the simnet, to generate the deployment plan and instance. The | ||
| // empty session will be set up, and contracts will be deployed in the | ||
| // correct order based on the deployment plan a few lines below. | ||
| const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestPath); | ||
| const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName, radio) => __awaiter(void 0, void 0, void 0, function* () { | ||
| var _a, _b, _c, _d; | ||
| // First simnet initialization: This will generate the deployment plan and | ||
| // will type check the project without any Rendezvous tests. | ||
| try { | ||
| radio.emit("logMessage", `\nType-checking your Clarinet project...`); | ||
| yield (0, clarinet_sdk_1.generateDeployement)(manifestPath); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Error initializing simnet: ${(_a = error.message) !== null && _a !== void 0 ? _a : error}`); | ||
| } | ||
| const deploymentPlan = yaml_1.default.parse((0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "deployments", "default.simnet-plan.yaml"), { | ||
| encoding: "utf-8", | ||
| })); | ||
| const sortedContractsByEpoch = (0, exports.groupContractsByEpochFromDeploymentPlan)(deploymentPlan); | ||
| const simnetAddresses = [...simnet.getAccounts().values()]; | ||
| const stxBalancesMap = new Map(simnetAddresses.map((address) => { | ||
| const balanceHex = simnet.runSnippet(`(stx-get-balance '${address})`); | ||
| return [address, (0, transactions_1.cvToValue)((0, transactions_1.hexToCV)(balanceHex))]; | ||
| })); | ||
| // If the sbtc-token contract is included in the deployment plan, we need to | ||
| // restore the sBTC balances. Otherwise, use an empty map. | ||
| const sbtcBalancesMap = (0, exports.getSbtcBalancesFromSimnet)(simnet, deploymentPlan, remoteDataSettings); | ||
| yield simnet.initEmptySession(remoteDataSettings); | ||
| simnetAddresses.forEach((address) => { | ||
| simnet.mintSTX(address, stxBalancesMap.get(address)); | ||
| }); | ||
| // Combine the target contract with its tests into a single contract. The | ||
| // resulting contract will replace the target contract in the simnet. | ||
| /** The contract names mapped to the concatenated source code. */ | ||
| const rendezvousSources = new Map([sutContractName] | ||
| // For each target contract name, execute the processing steps to get the | ||
| // concatenated contract source code and the contract ID. | ||
| .map((contractName) => (0, exports.buildRendezvousData)(deploymentPlan, contractName, manifestDir)) | ||
| // Use the contract ID as a key, mapping to the concatenated contract | ||
| // source code. | ||
| .map((rendezvousContractData) => [ | ||
| rendezvousContractData.rendezvousContractId, | ||
| rendezvousContractData.rendezvousSourceCode, | ||
| ])); | ||
| const clarinetToml = toml_1.default.parse((0, fs_1.readFileSync)(manifestPath, { encoding: "utf-8" })); | ||
| const cacheDir = ((_a = clarinetToml.project) === null || _a === void 0 ? void 0 : _a.cache_dir) || "./.cache"; | ||
| // Deploy the contracts to the empty simnet session in the correct order. | ||
| yield (0, exports.deployContracts)(simnet, sortedContractsByEpoch, manifestDir, cacheDir, (name, sender, props) => (0, exports.getContractSource)([sutContractName], rendezvousSources, name, sender, props, manifestDir)); | ||
| // Filter out addresses with zero balance. They do not need to be restored. | ||
| const sbtcBalancesToRestore = new Map([...sbtcBalancesMap.entries()].filter(([_, balance]) => balance !== 0)); | ||
| // After all the contracts and requirements are deployed, if the test wallets | ||
| // had sBTC balances previously, restore them. If no test wallet previously | ||
| // owned sBTC, skip this step. | ||
| if ([...sbtcBalancesToRestore.keys()].length > 0) { | ||
| restoreSbtcBalances(simnet, sbtcBalancesToRestore); | ||
| const parsedManifest = (0, toml_1.parse)((0, fs_1.readFileSync)(manifestPath, { encoding: "utf-8" })); | ||
| const cacheDir = (_c = (_b = parsedManifest.project) === null || _b === void 0 ? void 0 : _b.cache_dir) !== null && _c !== void 0 ? _c : "./.cache"; | ||
| const rendezvousData = (0, exports.buildRendezvousData)(cacheDir, deploymentPlan, sutContractName, manifestDir); | ||
| // Create isolated temp directory for the Rendezvous testing run. | ||
| const tempProjectDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "rendezvous-run-")); | ||
| (0, fs_1.cpSync)(manifestDir, tempProjectDir, { recursive: true }); | ||
| const [, contractName] = rendezvousData.rendezvousContractId.split("."); | ||
| const rendezvousContractsDir = (0, path_1.join)(tempProjectDir, "contracts"); | ||
| const rendezvousPath = (0, path_1.join)(rendezvousContractsDir, `${contractName}-rendezvous.clar`); | ||
| (0, fs_1.writeFileSync)(rendezvousPath, rendezvousData.rendezvousSourceCode); | ||
| radio.emit("logMessage", `\nType-checking your Rendezvous project...`); | ||
| // Update the manifest in the temp directory to point to the Rendezvous | ||
| // concatenation. | ||
| const manifestFileName = (0, path_1.basename)(manifestPath); | ||
| const tempManifestPath = (0, path_1.join)(tempProjectDir, manifestFileName); | ||
| const tempParsedManifest = (0, toml_1.parse)((0, fs_1.readFileSync)(tempManifestPath, { encoding: "utf-8" })); | ||
| if (!tempParsedManifest.contracts) { | ||
| tempParsedManifest.contracts = {}; | ||
| } | ||
| return simnet; | ||
| }); | ||
| exports.issueFirstClassCitizenship = issueFirstClassCitizenship; | ||
| /** | ||
| * Groups contracts by epoch from the deployment plan. | ||
| * @param deploymentPlan The parsed deployment plan. | ||
| * @returns A record of contracts grouped by epoch. The record key is the epoch | ||
| * string, and the value is an array of contracts. Each contract is represented | ||
| * as a record with the contract name as the key and a record containing the | ||
| * contract path and clarity version as the value. | ||
| */ | ||
| const groupContractsByEpochFromDeploymentPlan = (deploymentPlan) => { | ||
| return deploymentPlan.plan.batches.reduce((acc, batch) => { | ||
| const epoch = batch.epoch; | ||
| const contracts = batch.transactions | ||
| .filter((tx) => tx["emulated-contract-publish"]) | ||
| .map((tx) => { | ||
| const contract = tx["emulated-contract-publish"]; | ||
| return { | ||
| [contract["contract-name"]]: { | ||
| path: contract.path, | ||
| clarity_version: contract["clarity-version"], | ||
| }, | ||
| }; | ||
| }); | ||
| if (contracts.length > 0) { | ||
| acc[epoch] = (acc[epoch] || []).concat(contracts); | ||
| if (!tempParsedManifest.contracts[sutContractName]) { | ||
| tempParsedManifest.contracts[sutContractName] = {}; | ||
| } | ||
| const relativeRendezvousPath = (0, path_1.relative)(tempProjectDir, rendezvousPath); | ||
| tempParsedManifest.contracts[sutContractName] = { | ||
| epoch: ((_d = tempParsedManifest.contracts[sutContractName].epoch) !== null && _d !== void 0 ? _d : "latest"), | ||
| path: relativeRendezvousPath, | ||
| }; | ||
| // Convert epoch values to strings for TOML compatibility. | ||
| for (const contractName in tempParsedManifest.contracts) { | ||
| const contract = tempParsedManifest.contracts[contractName]; | ||
| if ((contract === null || contract === void 0 ? void 0 : contract.epoch) && typeof contract.epoch === "number") { | ||
| contract.epoch = String(contract.epoch); | ||
| } | ||
| return acc; | ||
| }, {}); | ||
| }; | ||
| exports.groupContractsByEpochFromDeploymentPlan = groupContractsByEpochFromDeploymentPlan; | ||
| /** | ||
| * Deploys the contracts to the simnet in the correct order. | ||
| * @param simnet The simnet instance. | ||
| * @param contractsByEpoch The record of contracts by epoch. | ||
| * @param getContractSourceFn The function to retrieve the contract source. | ||
| */ | ||
| const deployContracts = (simnet, contractsByEpoch, manifestDir, cacheDir, getContractSourceFn) => __awaiter(void 0, void 0, void 0, function* () { | ||
| for (const [epoch, contracts] of Object.entries(contractsByEpoch)) { | ||
| // Move to the next epoch and deploy the contracts in the correct order. | ||
| simnet.setEpoch(epoch); | ||
| for (const contract of contracts.flatMap(Object.entries)) { | ||
| const [name, props] = contract; | ||
| // Resolve paths to absolute for proper comparison. | ||
| const absoluteContractPath = (0, path_1.resolve)(manifestDir, props.path); | ||
| const absoluteRequirementsPath = (0, path_1.resolve)(manifestDir, cacheDir, "requirements"); | ||
| // Check if contract is in requirements directory. | ||
| const isRequirement = absoluteContractPath.startsWith(absoluteRequirementsPath); | ||
| const sender = isRequirement | ||
| ? (0, path_1.basename)(props.path).split(".")[0] | ||
| : simnet.deployer; | ||
| const source = getContractSourceFn(name, sender, props); | ||
| simnet.deployContract(name, source, { clarityVersion: props.clarity_version }, sender); | ||
| } | ||
| (0, fs_1.writeFileSync)(tempManifestPath, (0, toml_1.stringify)(tempParsedManifest)); | ||
| // Final simnet initialization: This will initialize the simnet with the | ||
| // target contract containing Rendezvous tests as first-class citizens. | ||
| // | ||
| // Windows cannot initialize simnet with absolute paths. Use relative path | ||
| // from the temp project directory. | ||
| // See: https://github.com/stx-labs/clarinet/issues/1634 | ||
| const originalCwd = process.cwd(); | ||
| try { | ||
| // Change the current working directory to the temp project directory. | ||
| // This is necessary because the simnet initialization requires the | ||
| // manifest file to be in the current working directory. | ||
| process.chdir(tempProjectDir); | ||
| // Initialize the simnet while suppressing stdout to avoid polluting output. | ||
| // Errors are still printed to stderr to help troubleshoot issues. | ||
| const originalWrite = process.stdout.write; | ||
| process.stdout.write = () => true; | ||
| try { | ||
| const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestFileName); | ||
| return simnet; | ||
| } | ||
| finally { | ||
| // Restore stdout. | ||
| process.stdout.write = originalWrite; | ||
| } | ||
| } | ||
| }); | ||
| exports.deployContracts = deployContracts; | ||
| /** | ||
| * Conditionally retrieves the contract source based on whether the contract is | ||
| * a SUT contract or not. | ||
| * @param targetContractNames The list of target contract names. | ||
| * @param rendezvousSourcesMap The target contract IDs mapped to the resulting | ||
| * concatenated source code. | ||
| * @param contractName The contract name. | ||
| * @param contractSender The emulated sender of the contract according to the | ||
| * deployment plan. | ||
| * @param contractProps The contract deployment properties. | ||
| * @param manifestDir The relative path to the manifest directory. | ||
| * @returns The contract source code. | ||
| */ | ||
| const getContractSource = (targetContractNames, rendezvousSourcesMap, contractName, contractSender, contractProps, manifestDir) => { | ||
| const contractId = `${contractSender}.${contractName}`; | ||
| // Checking if a contract is a SUT one just by using the name is not enough. | ||
| // There can be multiple contracts with the same name, but different senders | ||
| // in the deployment plan. The contract ID is the unique identifier used to | ||
| // store the concatenated Rendezvous source codes in the | ||
| // `rendezvousSourcesMap`. | ||
| if (targetContractNames.includes(contractName) && | ||
| rendezvousSourcesMap.has(contractId)) { | ||
| const contractSource = rendezvousSourcesMap.get(contractId); | ||
| if (!contractSource) { | ||
| throw new Error(`Contract source not found for ${contractName}`); | ||
| finally { | ||
| // Restore the original current working directory. | ||
| process.chdir(originalCwd); | ||
| // Cleanup the temp project directory. | ||
| try { | ||
| (0, fs_1.rmSync)(tempProjectDir, { recursive: true, force: true }); | ||
| } | ||
| return contractSource; | ||
| catch (error) { | ||
| radio.emit("logMessage", (0, ansicolor_1.yellow)(`Error cleaning up temporary project directory ${tempProjectDir}: ${error.message}. Remove it manually to avoid unnecessary disk space usage.`)); | ||
| } | ||
| } | ||
| else { | ||
| return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, contractProps.path), { | ||
| encoding: "utf-8", | ||
| }); | ||
| } | ||
| }; | ||
| exports.getContractSource = getContractSource; | ||
| }); | ||
| exports.issueFirstClassCitizenship = issueFirstClassCitizenship; | ||
| /** | ||
| * Builds the Rendezvous data. | ||
| * @param cacheDir The cache directory path. | ||
| * @param deploymentPlan The parsed deployment plan. | ||
@@ -187,6 +136,6 @@ * @param contractName The contract name. | ||
| */ | ||
| const buildRendezvousData = (deploymentPlan, contractName, manifestDir) => { | ||
| const buildRendezvousData = (cacheDir, deploymentPlan, contractName, manifestDir) => { | ||
| try { | ||
| const sutContractSource = getDeploymentPlanContractSource(deploymentPlan, contractName, manifestDir); | ||
| const testContractSource = (0, exports.getTestContractSource)(deploymentPlan, contractName, manifestDir); | ||
| const testContractSource = (0, exports.getTestContractSource)(cacheDir, deploymentPlan, contractName, manifestDir); | ||
| const rendezvousSource = scheduleRendezvous(sutContractSource, testContractSource); | ||
@@ -221,39 +170,2 @@ const rendezvousContractEmulatedSender = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, contractName)["emulated-sender"]; | ||
| /** | ||
| * Retrieves the test contract source code. | ||
| * @param deploymentPlan The parsed deployment plan. | ||
| * @param sutContractName The target contract name. | ||
| * @param manifestDir The relative path to the manifest directory. | ||
| * @returns The test contract source code. | ||
| */ | ||
| const getTestContractSource = (deploymentPlan, sutContractName, manifestDir) => { | ||
| const sutContractPath = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, sutContractName).path; | ||
| const clarityExtension = ".clar"; | ||
| if (!sutContractPath.endsWith(clarityExtension)) { | ||
| throw new Error(`Invalid contract extension for the "${sutContractName}" contract.`); | ||
| } | ||
| // If the sutContractPath is located under .cache/requirements/ path, search | ||
| // for the test contract in the classic `contracts` directory. | ||
| if (sutContractPath.includes(".cache")) { | ||
| const relativePath = sutContractPath.split(".cache/requirements/")[1]; | ||
| const relativePathTestContract = relativePath.replace(clarityExtension, `.tests${clarityExtension}`); | ||
| return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "contracts", relativePathTestContract), { | ||
| encoding: "utf-8", | ||
| }).toString(); | ||
| } | ||
| // If the contract is not under the `.cache/requirements/` path, we assume it | ||
| // is located in a regular path specified in the manifest file. Just search | ||
| // for the test contract near the SUT one, following the naming | ||
| // convention: `<contract-name>.tests.clar`. | ||
| const testContractPath = sutContractPath.replace(clarityExtension, `.tests${clarityExtension}`); | ||
| try { | ||
| return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, testContractPath), { | ||
| encoding: "utf-8", | ||
| }).toString(); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract. ${error.message}`); | ||
| } | ||
| }; | ||
| exports.getTestContractSource = getTestContractSource; | ||
| /** | ||
| * Retrieves the emulated contract publish data of the target contract from the | ||
@@ -294,2 +206,5 @@ * deployment plan. If multiple contracts share the same name in the deployment | ||
| deployer)) === null || _b === void 0 ? void 0 : _b["emulated-contract-publish"]; | ||
| // TODO: Consider handling requirements and project contracts separately. | ||
| // Eventually let the user specify if the contract is a requirement or a | ||
| // project contract. | ||
| // This is an edge case that can happen in practice. If the project has two | ||
@@ -312,2 +227,83 @@ // requirements that share the same contract name, Rendezvous will not be | ||
| /** | ||
| * Retrieves the test contract source code for a project contract. | ||
| * @param contractPath The relative path to the contract. | ||
| * @param manifestDir The relative path to the manifest directory. | ||
| * @returns The test contract source code or `null` if the test contract is not | ||
| * found. | ||
| */ | ||
| const getProjectContractTestSrc = (contractPath, manifestDir) => { | ||
| const clarityExtension = ".clar"; | ||
| const lastExtensionIndex = contractPath.lastIndexOf(clarityExtension); | ||
| const testContractPath = lastExtensionIndex !== -1 | ||
| ? contractPath.slice(0, lastExtensionIndex) + | ||
| `.tests${clarityExtension}` + | ||
| contractPath.slice(lastExtensionIndex + clarityExtension.length) | ||
| : `${contractPath}.tests${clarityExtension}`; | ||
| try { | ||
| const fullPath = (0, path_1.join)(manifestDir, testContractPath); | ||
| const content = (0, fs_1.readFileSync)(fullPath, { | ||
| encoding: "utf-8", | ||
| }).toString(); | ||
| return content; | ||
| } | ||
| catch (error) { | ||
| return null; | ||
| } | ||
| }; | ||
| /** | ||
| * Retrieves the test contract source code for a requirement contract. It | ||
| * searches for the test contract in the `contracts` directory of the Clarinet | ||
| * project. | ||
| * @param cacheDir The cache directory path. | ||
| * @param sutContractPath The path to the SUT contract. | ||
| * @param manifestDir The relative path to the manifest directory. | ||
| * @returns The test contract source code or `null` if the test contract is not | ||
| * found. | ||
| */ | ||
| const getRequirementContractTestSrc = (cacheDir, sutContractPath, manifestDir) => { | ||
| const normalizedCacheDir = cacheDir.replace(/[\/\\]$/, ""); | ||
| const requirementsRelativePath = `${normalizedCacheDir}/requirements/`; | ||
| if (!sutContractPath.includes(requirementsRelativePath)) { | ||
| return null; | ||
| } | ||
| const relativePath = sutContractPath.split(requirementsRelativePath)[1]; | ||
| const clarityExtension = ".clar"; | ||
| const lastExtensionIndex = relativePath.lastIndexOf(clarityExtension); | ||
| const relativePathTestContract = lastExtensionIndex !== -1 | ||
| ? relativePath.slice(0, lastExtensionIndex) + | ||
| `.tests${clarityExtension}` + | ||
| relativePath.slice(lastExtensionIndex + clarityExtension.length) | ||
| : `${relativePath}.tests${clarityExtension}`; | ||
| return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "contracts", relativePathTestContract), { | ||
| encoding: "utf-8", | ||
| }).toString(); | ||
| }; | ||
| /** | ||
| * Retrieves the test contract source code. | ||
| * Project contracts have priority. Requirement contracts are only checked | ||
| * if project contract test is not found. | ||
| * @param cacheDir The cache directory path. | ||
| * @param deploymentPlan The parsed deployment plan. | ||
| * @param sutContractName The target contract name. | ||
| * @param manifestDir The relative path to the manifest directory. | ||
| * @returns The test contract source code. | ||
| */ | ||
| const getTestContractSource = (cacheDir, deploymentPlan, sutContractName, manifestDir) => { | ||
| const sutContractPath = getSutContractDeploymentPlanEmulatedPublish(deploymentPlan, sutContractName).path; | ||
| // Prioritize project contracts. Try project contract test first. | ||
| const projectTestContract = getProjectContractTestSrc(sutContractPath, manifestDir); | ||
| if (projectTestContract !== null) { | ||
| return projectTestContract; | ||
| } | ||
| // Fallback to requirement contract test if project contract test not found. | ||
| const normalizedCacheDir = cacheDir || "./.cache"; | ||
| const requirementTestContract = getRequirementContractTestSrc(normalizedCacheDir, sutContractPath, manifestDir); | ||
| if (requirementTestContract !== null) { | ||
| return requirementTestContract; | ||
| } | ||
| // No corresponding test contract was found for the SUT contract. | ||
| throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract.`); | ||
| }; | ||
| exports.getTestContractSource = getTestContractSource; | ||
| /** | ||
| * Schedules a Rendezvous between the System Under Test (`SUT`) and the test | ||
@@ -333,135 +329,1 @@ * contract. | ||
| } | ||
| /** | ||
| * Checks if a contract can be found in the deployment plan. | ||
| * @param deploymentPlan The parsed deployment plan. | ||
| * @param contractAddress The address of the contract. | ||
| * @param contractName The name of the contract. | ||
| * @returns True if the contract can be found in the deployment plan, false | ||
| * otherwise. | ||
| */ | ||
| const isContractInDeploymentPlan = (deploymentPlan, contractAddress, contractName) => { | ||
| return deploymentPlan.plan.batches.some((batch) => batch.transactions.some((transaction) => { | ||
| var _a, _b; | ||
| return ((_a = transaction["emulated-contract-publish"]) === null || _a === void 0 ? void 0 : _a["contract-name"]) === | ||
| contractName && | ||
| ((_b = transaction["emulated-contract-publish"]) === null || _b === void 0 ? void 0 : _b["emulated-sender"]) === | ||
| contractAddress; | ||
| })); | ||
| }; | ||
| /** | ||
| * Maps the simnet accounts to their sBTC balances. The function tries to call | ||
| * the `get-balance` function of the `sbtc-token` contract for each address. If | ||
| * the call fails, it returns a balance of 0 for that address. The call fails | ||
| * if the user is not working with sBTC. | ||
| * @param simnet The simnet instance. | ||
| * @param deploymentPlan The parsed deployment plan. | ||
| * @param remoteDataSettings The remote data settings. | ||
| * @returns A map of addresses to their sBTC balances. | ||
| */ | ||
| const getSbtcBalancesFromSimnet = (simnet, deploymentPlan, remoteDataSettings) => { | ||
| // If the user is not using remote data and the deployment plan does not | ||
| // contain the `sbtc-token` contract, return a map with 0 balances for all | ||
| // addresses. When remote data is enabled, the sbtc-token contract will not | ||
| // necessarily be present in the deployment plan. | ||
| if (!remoteDataSettings.enabled && | ||
| !isContractInDeploymentPlan(deploymentPlan, "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4", "sbtc-token")) { | ||
| return new Map([...simnet.getAccounts().values()].map((address) => [address, 0])); | ||
| } | ||
| return new Map([...simnet.getAccounts().values()].map((address) => { | ||
| try { | ||
| const { result: getBalanceResult } = simnet.callReadOnlyFn("SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token", "get-balance", [transactions_1.Cl.principal(address)], address); | ||
| // If the previous read-only call works, the user is working with | ||
| // sBTC. This means we can proceed with restoring sBTC balances. | ||
| const sbtcBalanceJSON = (0, transactions_1.cvToJSON)(getBalanceResult); | ||
| // The `get-balance` function returns a response containing the uint | ||
| // balance of the address. In the JSON representation, the balance is | ||
| // represented as a string. We need to parse it to an integer. | ||
| const sbtcBalance = parseInt(sbtcBalanceJSON.value.value, 10); | ||
| return [address, sbtcBalance]; | ||
| } | ||
| catch (e) { | ||
| return [address, 0]; | ||
| } | ||
| })); | ||
| }; | ||
| exports.getSbtcBalancesFromSimnet = getSbtcBalancesFromSimnet; | ||
| /** | ||
| * Utility function that restores the test wallets' initial sBTC balances in | ||
| * the re-initialized first-class citizenship simnet. | ||
| * | ||
| * @param simnet The simnet instance. | ||
| * @param sbtcBalancesMap A map containing the test wallets' balances to be | ||
| * restored. | ||
| */ | ||
| const restoreSbtcBalances = (simnet, sbtcBalancesMap) => { | ||
| // For each address present in the balances map, restore the balance. | ||
| [...sbtcBalancesMap.entries()] | ||
| // Re-assure the map does not contain nil balances. | ||
| .filter(([_, balance]) => balance !== 0) | ||
| .forEach(([address, balance]) => { | ||
| // To deposit sBTC, one needs a txId and a sweep txId. A deposit transaction | ||
| // must have a unique txId and sweep txId. | ||
| const txId = getUniqueHex(); | ||
| const sweepTxId = getUniqueHex(); | ||
| mintSbtc(simnet, balance, address, txId, sweepTxId); | ||
| }); | ||
| }; | ||
| /** | ||
| * Utility function to deposit an amount of sBTC to a Stacks address. | ||
| * | ||
| * @param simnet The simnet instance. | ||
| * @param amountSats The amount to mint in sats. | ||
| * @param recipient The Stacks address to mint sBTC to. | ||
| * @param txId A unique hex to use for the deposit. | ||
| * @param sweepTxId A unique hex to use for the deposit. | ||
| */ | ||
| const mintSbtc = (simnet, amountSats, recipient, txId, sweepTxId) => { | ||
| // Calling `get-burn-block-info?` only works for past burn heights. We mine | ||
| // one empty Bitcoin block if the initial height is 0 and use the previous | ||
| // burn height to retrieve the burn header hash. | ||
| if (simnet.burnBlockHeight === 0) { | ||
| simnet.mineEmptyBurnBlock(); | ||
| } | ||
| const previousBurnHeight = simnet.burnBlockHeight - 1; | ||
| const burnHash = (0, transactions_1.hexToCV)(simnet.runSnippet(`(get-burn-block-info? header-hash u${previousBurnHeight})`)); | ||
| if (burnHash === null || burnHash.type === transactions_1.ClarityType.OptionalNone) { | ||
| throw new Error("Something went wrong trying to retrieve the burn header."); | ||
| } | ||
| const completeDepositTx = (0, transactions_1.cvToJSON)(simnet.callPublicFn("SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-deposit", "complete-deposit-wrapper", [ | ||
| // (txid (buff 32)) | ||
| transactions_1.Cl.bufferFromHex(txId), | ||
| // (vout-index uint) | ||
| transactions_1.Cl.uint(1), | ||
| // (amount uint) | ||
| transactions_1.Cl.uint(amountSats), | ||
| // (recipient principal) | ||
| transactions_1.Cl.principal(recipient), | ||
| // (burn-hash (buff 32)) | ||
| burnHash.value, | ||
| // (burn-height uint) | ||
| transactions_1.Cl.uint(previousBurnHeight), | ||
| // (sweep-txid (buff 32)) | ||
| transactions_1.Cl.bufferFromHex(sweepTxId), | ||
| ], "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4").result); | ||
| // If the deposit transaction fails, an unexpected outcome can happen. Throw | ||
| // an error if the transaction is not successful. | ||
| if (!completeDepositTx.success) { | ||
| throw new Error("Something went wrong trying to restore sBTC balances."); | ||
| } | ||
| }; | ||
| /** | ||
| * Utility function that generates a random, unique hex to be used as txId in | ||
| * `mintSbtc`. | ||
| * | ||
| * @returns A random hex string. | ||
| */ | ||
| const getUniqueHex = () => { | ||
| let hex; | ||
| // Generate a 32-byte (64 character) random hex string. | ||
| const bytes = new Uint8Array(32); | ||
| crypto.getRandomValues(bytes); | ||
| hex = Array.from(bytes) | ||
| .map((byte) => byte.toString(16).padStart(2, "0")) | ||
| .join(""); | ||
| return hex; | ||
| }; |
| { | ||
| "name": "@stacks/rendezvous", | ||
| "version": "0.12.0", | ||
| "version": "0.13.0", | ||
| "description": "Meet your contract's vulnerabilities head-on.", | ||
@@ -33,11 +33,11 @@ "main": "app.js", | ||
| "dependencies": { | ||
| "@stacks/clarinet-sdk": "^3.9.1", | ||
| "@iarna/toml": "^2.2.5", | ||
| "@stacks/clarinet-sdk": "^3.12.0", | ||
| "@stacks/transactions": "^7.2.0", | ||
| "ansicolor": "^2.0.3", | ||
| "fast-check": "^4.3.0", | ||
| "toml": "^3.0.0", | ||
| "yaml": "^2.8.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@stacks/clarinet-sdk-wasm": "^3.9.1", | ||
| "@stacks/clarinet-sdk-wasm": "^3.12.0", | ||
| "@types/jest": "^30.0.0", | ||
@@ -44,0 +44,0 @@ "jest": "^30.2.0", |
+4
-4
| { | ||
| "name": "@stacks/rendezvous", | ||
| "version": "0.12.0", | ||
| "version": "0.13.0", | ||
| "description": "Meet your contract's vulnerabilities head-on.", | ||
@@ -33,11 +33,11 @@ "main": "app.js", | ||
| "dependencies": { | ||
| "@stacks/clarinet-sdk": "^3.9.1", | ||
| "@iarna/toml": "^2.2.5", | ||
| "@stacks/clarinet-sdk": "^3.12.0", | ||
| "@stacks/transactions": "^7.2.0", | ||
| "ansicolor": "^2.0.3", | ||
| "fast-check": "^4.3.0", | ||
| "toml": "^3.0.0", | ||
| "yaml": "^2.8.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@stacks/clarinet-sdk-wasm": "^3.9.1", | ||
| "@stacks/clarinet-sdk-wasm": "^3.12.0", | ||
| "@types/jest": "^30.0.0", | ||
@@ -44,0 +44,0 @@ "jest": "^30.2.0", |
+1
-1
@@ -11,3 +11,3 @@ <div align="center"> | ||
| - **Node.js**: Supported versions include 20, 22, and 23. Other versions may work, but they are untested. | ||
| - **Node.js**: Supported versions include 20, 22, and 24. Other versions may work, but they are untested. | ||
@@ -14,0 +14,0 @@ ### Inspiration |
161959
-5.18%2304
-6.72%+ Added
+ Added
- Removed
- Removed
Updated