Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions lib.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { join, resolve } from "node:path";

import { initSimnet } from "@stacks/clarinet-sdk";
import type { ClarityValue } from "@stacks/transactions";
import fc from "fast-check";

import { getContractFunction, strategyFor } from "./lib";

const manifestPath = join(resolve(__dirname, "example"), "Clarinet.toml");

describe("getContractFunction", () => {
it("returns the function interface for a known contract and function", async () => {
const simnet = await initSimnet(manifestPath);
const fn = getContractFunction(simnet, "counter", "increment");

expect(fn.name).toBe("increment");
expect(fn.access).toBe("public");
});

it("returns a function with arguments", async () => {
const simnet = await initSimnet(manifestPath);
const fn = getContractFunction(simnet, "counter", "add");

expect(fn.name).toBe("add");
expect(fn.args.length).toBe(1);
});

it("throws when the contract does not exist", async () => {
const simnet = await initSimnet(manifestPath);

expect(() => getContractFunction(simnet, "nonexistent", "foo")).toThrow(
'Contract "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.nonexistent" not found.',
);
});

it("throws when the function does not exist", async () => {
const simnet = await initSimnet(manifestPath);

expect(() => getContractFunction(simnet, "counter", "nonexistent")).toThrow(
'Function "nonexistent" not found in contract "counter".',
);
});

it("accepts a custom deployer address", async () => {
const simnet = await initSimnet(manifestPath);

// Using the actual deployer explicitly should work the same.
const fn = getContractFunction(
simnet,
"counter",
"increment",
simnet.deployer,
);
expect(fn.name).toBe("increment");
});
});

describe("strategyFor", () => {
it("produces ClarityValue arrays for a function with arguments", async () => {
const simnet = await initSimnet(manifestPath);
const fn = getContractFunction(simnet, "counter", "add");
const arb = strategyFor(fn, simnet);

fc.assert(
fc.property(arb, (args: ClarityValue[]) => {
expect(Array.isArray(args)).toBe(true);
expect(args.length).toBe(fn.args.length);
args.forEach((arg) => {
expect(arg).toHaveProperty("type");
});
}),
{ numRuns: 10 },
);
});

it("produces an empty array for a function with no arguments", async () => {
const simnet = await initSimnet(manifestPath);
const fn = getContractFunction(simnet, "counter", "increment");
const arb = strategyFor(fn, simnet);

fc.assert(
fc.property(arb, (args: ClarityValue[]) => {
expect(args).toEqual([]);
}),
{ numRuns: 1 },
);
});

it("produces arguments usable with simnet.callPublicFn", async () => {
const simnet = await initSimnet(manifestPath);
const fn = getContractFunction(simnet, "counter", "add");
const arb = strategyFor(fn, simnet);

fc.assert(
fc.property(arb, (args: ClarityValue[]) => {
// Should not throw — arguments are valid Clarity values.
const { result } = simnet.callPublicFn(
`${simnet.deployer}.counter`,
"add",
args,
simnet.deployer,
);
expect(result).toBeDefined();
}),
{ numRuns: 5 },
);
});
});
127 changes: 127 additions & 0 deletions lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Simnet } from "@stacks/clarinet-sdk";
import type { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm";
import type { ClarityValue } from "@stacks/transactions";
import fc from "fast-check";

import { argsToCV, functionToArbitrary } from "./shared";
import type { EnrichedContractInterfaceFunction } from "./shared.types";
import {
buildTraitReferenceMap,
enrichInterfaceWithTraitData,
extractProjectTraitImplementations,
isTraitReferenceFunction,
} from "./traits";

export type { EnrichedContractInterfaceFunction } from "./shared.types";

/**
* Retrieves a function interface from a simnet contract by name. The returned
* interface is enriched with trait reference data when applicable, making it
* ready for use with {@link strategyFor}.
*
* @param simnet The simnet instance.
* @param contractName The contract name (e.g., "counter").
* @param functionName The function name (e.g., "increment").
* @param deployer The deployer address. Defaults to `simnet.deployer`.
* @returns The enriched function interface.
*
* @example
* ```ts
* const simnet = await initSimnet("./Clarinet.toml");
* const increment = getContractFunction(simnet, "counter", "increment");
* ```
*/
export const getContractFunction = (
simnet: Simnet,
contractName: string,
functionName: string,
deployer?: string,
): EnrichedContractInterfaceFunction => {
const targetDeployer = deployer ?? simnet.deployer;
const allContracts = simnet.getContractsInterfaces();

const contractId = `${targetDeployer}.${contractName}`;
const contractInterface = allContracts.get(contractId);

if (!contractInterface) {
throw new Error(`Contract "${contractId}" not found.`);
}

const fn = contractInterface.functions.find(
(f: ContractInterfaceFunction) => f.name === functionName,
);

if (!fn) {
throw new Error(
`Function "${functionName}" not found in contract "${contractName}".`,
);
}

// Enrich with trait data if the function uses trait references.
if (isTraitReferenceFunction(fn)) {
const traitReferenceMap = buildTraitReferenceMap([fn]);
const enriched = enrichInterfaceWithTraitData(
simnet.getContractAST(contractName),
traitReferenceMap,
[fn],
contractId,
);
return enriched.get(contractId)![0];
}

return fn as EnrichedContractInterfaceFunction;
};

/**
* Generates a fast-check arbitrary that produces `ClarityValue[]` arrays
* ready for use with `simnet.callPublicFn` or similar.
*
* Automatically resolves principal addresses and trait implementations from
* the simnet instance.
*
* @param fn The enriched function interface (from {@link getContractFunction}).
* @param simnet The simnet instance.
* @returns A fast-check arbitrary producing Clarity argument arrays.
*
* @example
* ```ts
* import fc from "fast-check";
* import { initSimnet } from "@stacks/clarinet-sdk";
* import { getContractFunction, strategyFor } from "@stacks/rendezvous";
*
* const simnet = await initSimnet("./Clarinet.toml");
* const increment = getContractFunction(simnet, "counter", "increment");
* const argsArb = strategyFor(increment, simnet);
*
* fc.assert(
* fc.asyncProperty(argsArb, async (args) => {
* const { result } = simnet.callPublicFn(
* "deployer.counter", "increment", args, "deployer"
* );
* return result.type !== "err";
* })
* );
* ```
*/
export const strategyFor = (
fn: EnrichedContractInterfaceFunction,
simnet: Simnet,
): fc.Arbitrary<ClarityValue[]> => {
const allAddresses = [...simnet.getAccounts().values()];
const projectTraitImplementations =
extractProjectTraitImplementations(simnet);

const arbitraries = functionToArbitrary(
fn,
allAddresses,
projectTraitImplementations,
);

if (arbitraries.length === 0) {
return fc.constant([]);
}

return fc
.tuple(...arbitraries)
.map((generated: unknown[]) => argsToCV(fn, generated));
};
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
"name": "@stacks/rendezvous",
"version": "0.14.0",
"description": "Meet your contract's vulnerabilities head-on.",
"main": "app.js",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
"bin": {
"rv": "./dist/app.js"
},
"exports": {
".": {
"types": "./dist/lib.d.ts",
"default": "./dist/lib.js"
}
},
"scripts": {
"build": "npx -p typescript tsc --project tsconfig.json && node -e \"if (process.platform !== 'win32') require('fs').chmodSync('./dist/app.js', 0o755);\"",
"fmt": "prettier --write .",
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
Expand Down
Loading