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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ coverage
costs-reports.json
node_modules
*.log.txt
history.txt
history.txt
.cache
28 changes: 27 additions & 1 deletion Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,25 @@ description = ''
authors = []
telemetry = false
cache_dir = './.cache'
requirements = []
requirements = [
{ contract_id = "SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd001-direct-execute"},
{ contract_id = "SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccip024-miamicoin-signal-vote"}
]

[contracts.annotations_test]
path = "tests/contracts/generator-tests/annotations_test.clar"
clarity_version = 3
epoch = "3.1"

[contracts.annotations_flow_test]
path = "tests/contracts/generator-tests/annotations_flow_test.clar"
clarity_version = 3
epoch = "3.1"

[contracts.contract_call_flow_test]
path = "tests/contracts/generator-tests/contract-call_flow_test.clar"
clarity_version = 3
epoch = "3.1"

[repl.analysis]
passes = ['check_checker']
Expand All @@ -14,3 +32,11 @@ strict = false
trusted_sender = false
trusted_caller = false
callee_filter = false


[repl.remote_data]
# Enable mainnet execution simulation
enabled = true
# Specify the Stacks block height to fork from
initial_height = 3491155
use_mainnet_wallets = true
53 changes: 37 additions & 16 deletions deployments/default.simnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,76 @@ network: simnet
genesis:
wallets:
- name: deployer
address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
address: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: faucet
address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6
address: SPNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2C69MJD9
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_1
address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
address: SP1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2XG1V316
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_2
address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG
address: SP2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CH94GRJ
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_3
address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
address: SP2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1J5QKA2F
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_4
address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND
address: SP2NEB84ASENDXKYGJPQW86YXQCEFEX2ZPB1S2EP
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_5
address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB
address: SP2REHHS5J3CERCRBEPMGH7921Q6PYKAADR2V8W5C
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_6
address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0
address: SP3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ3JJEPMJ
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_7
address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ
address: SP3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXFA1W3C2
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_8
address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP
address: SP3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N6R5192Q
balance: "100000000000000"
sbtc-balance: "1000000000"
contracts:
- genesis
- lockup
- bns
- cost-voting
- costs
- pox
- costs-2
- pox-2
- costs-3
- pox-3
- pox-4
- lockup
- costs-2
- costs-3
- cost-voting
- bns
- signers
- signers-voting
plan:
batches: []
batches:
- id: 0
transactions:
- emulated-contract-publish:
contract-name: annotations_flow_test
emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R
path: tests/contracts/generator-tests/annotations_flow_test.clar
clarity-version: 3
- emulated-contract-publish:
contract-name: annotations_test
emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R
path: tests/contracts/generator-tests/annotations_test.clar
clarity-version: 3
- emulated-contract-publish:
contract-name: contract_call_flow_test
emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R
path: tests/contracts/generator-tests/contract-call_flow_test.clar
clarity-version: 3
epoch: "3.1"
1 change: 1 addition & 0 deletions example/tests/my-contract_test.clar
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
)

;; @caller 'ST000000000000000000002AMW42H
(define-public (test-a-times-b2)
(begin
(asserts! (is-eq (ok u108) (contract-call? .my-contract a-times-b u9 u12))
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
},
"homepage": "https://github.com/stacks-network/clarunit#readme",
"dependencies": {
"@hirosystems/clarinet-sdk": "3.1.0",
"@hirosystems/clarinet-sdk": "3.7.0",
"@stacks/common": "^7.0.2",
"@stacks/transactions": "7.1.0",
"@stacks/transactions": "7.2.0",
"chokidar-cli": "^3.0.0",
"typescript": "5.8.3",
"typescript": "5.9.2",
"vite": "6.3.5",
"vitest": "3.2.3",
"vitest-environment-clarinet": "2.3.0"
Expand Down
5 changes: 2 additions & 3 deletions src/clarunit-flow-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
extractTestAnnotationsAndCalls,
} from "./parser/clarity-parser-flow-tests";
import { expectOk, isValidTestFunction } from "./parser/test-helpers";
import { getCaller } from "./clarunit-utils";
import path from "path";

/**
Expand Down Expand Up @@ -100,9 +101,7 @@ function mineBlocksFromFunctionBody(
const mineBlocksBefore =
parseInt(callAnnotations["mine-blocks-before"] as string) || 0;
// get caller address
const caller = accounts.get(
(callAnnotations["caller"] as string) || "deployer"
)!;
const caller = getCaller(callAnnotations, accounts);

if (mineBlocksBefore >= 1) {
if (blockStarted) {
Expand Down
20 changes: 8 additions & 12 deletions src/clarunit-generator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Simnet, tx } from "@hirosystems/clarinet-sdk";
import { describe, it } from "vitest";
import {
extractTestAnnotations,
} from "./parser/clarity-parser";
import { extractTestAnnotations } from "./parser/clarity-parser";
import { expectOkTrue, isValidTestFunction } from "./parser/test-helpers";
import { FunctionAnnotations } from "./parser/clarity-parser-flow-tests";
import { getCaller } from "./clarunit-utils";

/**
* Returns true if the contract is a test contract
* @param contractName name of the contract
* @returns
* @returns
*/
function isTestContract(contractName: string) {
return (
Expand Down Expand Up @@ -44,10 +43,11 @@ export function generateUnitTests(simnet: Simnet) {
annotations[functionName] || {};

const mineBlocksBefore =
parseInt(annotations["mine-blocks-before"] as string) || 0;
parseInt(functionAnnotations["mine-blocks-before"] as string) || 0;

const testDescription = `${functionCall.name}${functionAnnotations.name ? `: ${functionAnnotations.name}` : ""
}`;
const testDescription = `${functionCall.name}${
functionAnnotations.name ? `: ${functionAnnotations.name}` : ""
}`;
it(testDescription, () => {
// handle prepare function for this test
if (hasDefaultPrepareFunction && !functionAnnotations.prepare)
Expand All @@ -56,11 +56,7 @@ export function generateUnitTests(simnet: Simnet) {
delete functionAnnotations.prepare;

// handle caller address for this test
const callerAddress = functionAnnotations.caller
? annotations.caller[0] === "'"
? `${(annotations.caller as string).substring(1)}`
: accounts.get(annotations.caller)!
: accounts.get("deployer")!;
const callerAddress = getCaller(functionAnnotations, accounts);

if (functionAnnotations.prepare) {
// mine block with prepare function call
Expand Down
7 changes: 7 additions & 0 deletions src/clarunit-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const getCaller = (annotations: any, accounts: Map<string, string>) => {
return annotations.caller && typeof annotations.caller === "string"
? annotations.caller[0] === "'"
? `${(annotations.caller as string).substring(1)}`
: accounts.get(annotations.caller)!
: accounts.get("deployer")!;
};
63 changes: 59 additions & 4 deletions src/parser/clarity-parser-flow-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function extractContractCalls(lastFunctionBody: string, simnet: Simnet) {
if (prop) callAnnotations[prop] = value ?? true;
}
// try to extract call info from (unwrap! (contract-call? ...))
let callInfo = extractUnwrapInfo(call, simnet);
let callInfo = extractUnwrapInfo(call, simnet, callAnnotations);
if (!callInfo) {
// try to extract call info from (try! (my-function))
callInfo = extractTryInfo(call);
Expand All @@ -133,9 +133,13 @@ export function extractContractCalls(lastFunctionBody: string, simnet: Simnet) {
* @param statement
* @returns
*/
function extractUnwrapInfo(statement: string, simnet: Simnet): CallInfo | null {
function extractUnwrapInfo(
statement: string,
simnet: Simnet,
callAnnotations: FunctionAnnotations
): CallInfo | null {
const match = statement.match(
/\(unwrap! \(contract-call\? (?:\.(.+?)|'(.+?)) (.+?)(( .+?)*)\)/
/\(unwrap!\s+\(contract-call\?\s+(?:\.(.+?)|'(.+?))\s+(.+?)((\s+.+?)*)\)/m
);
if (!match) return null;
// match[1] is the contract address,
Expand All @@ -155,7 +159,15 @@ function extractUnwrapInfo(statement: string, simnet: Simnet): CallInfo | null {
}
});
if (!fn) {
throw `function ${functionName} not found in contract ${contractName}`;
if (callAnnotations["type-hints"]) {
fn = {
args: (callAnnotations["type-hints"] as string)
.split(",")
.map((s) => ({ type: parseTypeHint(s.trim()) })),
};
} else {
throw `function ${functionName} of ${contractName} not found in Clarinet toml and no type-hints provided`;
}
}
const args = fn.args.map((arg: any, index: number) =>
stringToCV(argStrings[index], arg.type)
Expand Down Expand Up @@ -208,3 +220,46 @@ function splitArgs(argString: string): string[] {

return splitArgs;
}

/**
* Parse type hint string into ContractInterfaceAtomType
* @param typeHint string like "uint128", "(optional uint128)", etc.
* @returns ContractInterfaceAtomType
*/
function parseTypeHint(typeHint: string): any {
typeHint = typeHint.trim();

// Handle parentheses wrapped types like (optional uint128)
if (typeHint.startsWith("(") && typeHint.endsWith(")")) {
const inner = typeHint.slice(1, -1).trim();
const parts = inner.split(" ");

if (parts[0] === "optional") {
return {
optional: parseTypeHint(parts.slice(1).join(" ")),
};
}

// Add other complex type handling as needed
}

// Handle simple types
switch (typeHint) {
case "uint":
case "uint128":
return "uint128";
case "int":
case "int128":
return "int128";
case "bool":
return "bool";
case "principal":
return "principal";
case "trait_reference":
return "trait_reference";
case "none":
return "none";
default:
throw new Error(`Unsupported type hint: ${typeHint}`);
}
}
16 changes: 13 additions & 3 deletions src/parser/string-to-cv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export function stringToCV(
return { type: "uint", value: Cl.uint(arg.slice(1)) };
case "int128":
return { type: "int", value: Cl.int(arg) };
case "bool":
return { type: "bool", value: Cl.bool(arg === "true") };
case "principal":
const [address, name] = arg.split(".");
return name
Expand All @@ -73,8 +75,16 @@ export function stringToCV(
value: Cl.contractPrincipal(simnet.deployer, name),
}
: { type: "principal", value: Cl.standardPrincipal(address) };
case "bool":
return { type: "bool", value: Cl.bool(arg === "true") };
case "trait_reference":
const [addressTrait, nameTrait] = arg.split(".");
return {
type: "trait_reference",
value: Cl.contractPrincipal(
// handle both fully qualified contract ids and .contract-name
addressTrait.length > 1 ? addressTrait.substring(1) : simnet.deployer,
nameTrait
),
};
}
const typeDescriptor = Object.keys(type)[0];
switch (typeDescriptor) {
Expand Down Expand Up @@ -111,7 +121,7 @@ export function stringToCV(
};
}
default:
throw new Error(`Unsupported type ${type}`);
throw new Error(`Unsupported type ${arg}, ${typeDescriptor}`);
}
}

Expand Down
Loading