diff --git a/.gitignore b/.gitignore index 4ddd794..e5d2b67 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ coverage costs-reports.json node_modules *.log.txt -history.txt \ No newline at end of file +history.txt +.cache \ No newline at end of file diff --git a/Clarinet.toml b/Clarinet.toml index 7731877..9bd1257 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -4,7 +4,35 @@ 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" + +[contracts.list-args_flow_test] +path = "tests/contracts/generator-tests/list-args_flow_test.clar" +clarity_version = 3 +epoch = "3.1" + +[contracts.some-contract] +path = "tests/contracts/some-contract.clar" +clarity_version = 3 +epoch = "3.1" [repl.analysis] passes = ['check_checker'] @@ -14,3 +42,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 \ No newline at end of file diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index 81393c0..cc221a3 100644 --- a/deployments/default.simnet-plan.yaml +++ b/deployments/default.simnet-plan.yaml @@ -1,59 +1,90 @@ ---- id: 0 -name: "Simulated deployment, used as a default for `clarinet console`, `clarinet test` and `clarinet check`" +name: Simulated deployment, used as a default for `clarinet console`, `clarinet test` and `clarinet check` network: simnet genesis: wallets: - - name: deployer - address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: faucet - address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_1 - address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_2 - address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_3 - address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_4 - address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_5 - address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_6 - address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_7 - address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ - balance: "100000000000000" - sbtc-balance: "1000000000" - - name: wallet_8 - address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP - balance: "100000000000000" - sbtc-balance: "1000000000" + - name: deployer + address: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: faucet + address: SPNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2C69MJD9 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_1 + address: SP1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2XG1V316 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_2 + address: SP2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CH94GRJ + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_3 + address: SP2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1J5QKA2F + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_4 + address: SP2NEB84ASENDXKYGJPQW86YXQCEFEX2ZPB1S2EP + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_5 + address: SP2REHHS5J3CERCRBEPMGH7921Q6PYKAADR2V8W5C + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_6 + address: SP3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ3JJEPMJ + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_7 + address: SP3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXFA1W3C2 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_8 + address: SP3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N6R5192Q + balance: '100000000000000' + sbtc-balance: '1000000000' contracts: - - costs - - pox - - pox-2 - - pox-3 - - pox-4 - - lockup - - costs-2 - - costs-3 - - cost-voting - - bns + - genesis + - lockup + - bns + - cost-voting + - costs + - pox + - costs-2 + - pox-2 + - costs-3 + - pox-3 + - pox-4 + - signers + - signers-voting + - costs-4 plan: - batches: [] + batches: + - id: 0 + transactions: + - transaction-type: emulated-contract-publish + contract-name: annotations_flow_test + emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R + path: tests/contracts/generator-tests/annotations_flow_test.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: annotations_test + emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R + path: tests/contracts/generator-tests/annotations_test.clar + clarity-version: 3 + - transaction-type: 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 + - transaction-type: emulated-contract-publish + contract-name: some-contract + emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R + path: tests/contracts/some-contract.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: list-args_flow_test + emulated-sender: SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R + path: tests/contracts/generator-tests/list-args_flow_test.clar + clarity-version: 3 + epoch: '3.1' diff --git a/example/package.json b/example/package.json index dbf1dbe..343ba3a 100644 --- a/example/package.json +++ b/example/package.json @@ -13,13 +13,13 @@ "author": "", "license": "ISC", "dependencies": { - "@hirosystems/clarinet-sdk": "3.1.0", + "@stacks/clarinet-sdk": "3.9.1", "@stacks/clarunit": "^0.1.0", "@stacks/transactions": "^7.1.0", "chokidar-cli": "^3.0.0", - "typescript": "^5.8.3", - "vite": "^7.0.0", - "vitest": "^3.2.4", - "vitest-environment-clarinet": "^2.3.0" + "typescript": "^5.9.3", + "vite": "^7.2.2", + "vitest": "^4.0.8", + "vitest-environment-clarinet": "^3.0.1" } } diff --git a/example/tests/my-contract_test.clar b/example/tests/my-contract_test.clar index 576f5ec..3a1623d 100644 --- a/example/tests/my-contract_test.clar +++ b/example/tests/my-contract_test.clar @@ -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)) diff --git a/example/tsconfig.json b/example/tsconfig.json index 1bdaf36..4601289 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -20,7 +20,7 @@ "noFallthroughCasesInSwitch": true }, "include": [ - "node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", + "node_modules/@stacks/clarinet-sdk/vitest-helpers/src", "tests" ] } diff --git a/example/vitest.config.js b/example/vitest.config.js index 364c55f..16e11e3 100644 --- a/example/vitest.config.js +++ b/example/vitest.config.js @@ -4,7 +4,7 @@ import { defineConfig } from "vite"; import { vitestSetupFilePath, getClarinetVitestsArgv, -} from "@hirosystems/clarinet-sdk/vitest"; +} from "@stacks/clarinet-sdk/vitest"; /* In this file, Vitest is configured so that it works seamlessly with Clarinet and the Simnet. diff --git a/package.json b/package.json index 01405a1..d444ee4 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,16 @@ }, "homepage": "https://github.com/stacks-network/clarunit#readme", "dependencies": { - "@hirosystems/clarinet-sdk": "3.1.0", - "@stacks/common": "^7.0.2", - "@stacks/transactions": "7.1.0", + "@stacks/clarinet-sdk": "3.15.1", + "@stacks/common": "^7.3.1", + "@stacks/transactions": "7.4.0", "chokidar-cli": "^3.0.0", - "typescript": "5.8.3", - "vite": "6.3.5", - "vitest": "3.2.3", - "vitest-environment-clarinet": "2.3.0" + "typescript": "6.0.2", + "vite": "8.0.3", + "vitest": "4.1.2", + "vitest-environment-clarinet": "3.0.2" }, "devDependencies": { - "fast-check": "4.1.1" + "fast-check": "4.6.0" } } diff --git a/src/clarunit-flow-generator.ts b/src/clarunit-flow-generator.ts index 19b3303..043eb12 100644 --- a/src/clarunit-flow-generator.ts +++ b/src/clarunit-flow-generator.ts @@ -1,4 +1,4 @@ -import { ParsedTransactionResult, tx } from "@hirosystems/clarinet-sdk"; +import { ParsedTransactionResult, tx } from "@stacks/clarinet-sdk"; import * as fs from "fs"; import { describe, it } from "vitest"; import { @@ -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"; /** @@ -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) { diff --git a/src/clarunit-generator.ts b/src/clarunit-generator.ts index 9e225c2..9d776c6 100644 --- a/src/clarunit-generator.ts +++ b/src/clarunit-generator.ts @@ -1,15 +1,14 @@ -import { Simnet, tx } from "@hirosystems/clarinet-sdk"; +import { Simnet, tx } from "@stacks/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 ( @@ -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) @@ -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 diff --git a/src/clarunit-utils.ts b/src/clarunit-utils.ts new file mode 100644 index 0000000..bf7e47a --- /dev/null +++ b/src/clarunit-utils.ts @@ -0,0 +1,7 @@ +export const getCaller = (annotations: any, accounts: Map) => { + return annotations.caller && typeof annotations.caller === "string" + ? annotations.caller[0] === "'" + ? `${(annotations.caller as string).substring(1)}` + : accounts.get(annotations.caller)! + : accounts.get("deployer")!; +}; diff --git a/src/index.ts b/src/index.ts index 0a02e08..edcb8f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { generateUnitTests } from "./clarunit-generator"; import { generateFlowTests } from "./clarunit-flow-generator"; -import { Simnet } from "@hirosystems/clarinet-sdk"; +import { Simnet } from "@stacks/clarinet-sdk"; export function clarunit(simnet: Simnet) { generateUnitTests(simnet); diff --git a/src/parser/clarity-parser-flow-tests.ts b/src/parser/clarity-parser-flow-tests.ts index 3ac09f3..b90b1ec 100644 --- a/src/parser/clarity-parser-flow-tests.ts +++ b/src/parser/clarity-parser-flow-tests.ts @@ -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); @@ -133,17 +133,42 @@ 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 { + // Match contract-call header only (contract + function name). + // Args are extracted separately using balanced-paren counting + // to avoid catastrophic backtracking with nested list/tuple args. const match = statement.match( - /\(unwrap! \(contract-call\? (?:\.(.+?)|'(.+?)) (.+?)(( .+?)*)\)/ + /\(unwrap!\s+\(contract-call\?\s+(?:\.(\S+)|'(\S+))\s+(\S+)/m ); if (!match) return null; + + // Find args: everything between the function name and the balanced + // closing paren of (contract-call? ...) + const headerEnd = match.index! + match[0].length; + let depth = 2; // we're inside (unwrap! (contract-call? ... + let contractCallClose = -1; + for (let i = headerEnd; i < statement.length; i++) { + if (statement[i] === "(") depth++; + if (statement[i] === ")") depth--; + if (depth === 1) { + // found closing ) of (contract-call? ...) + contractCallClose = i; + break; + } + } + if (contractCallClose === -1) return null; + const argsRaw = statement.slice(headerEnd, contractCallClose); + // match[1] is the contract address, const [contractAddress, contractName] = match[2] ? match[2].split(".") : [simnet.deployer, match[1]]; const functionName = match[3]; - const argStrings = splitArgs(match[4]); + const argStrings = splitArgs(argsRaw); let fn: any; simnet.getContractsInterfaces().forEach((contract, contractFQN) => { const [ctrAddress, ctrName] = contractFQN.split("."); @@ -155,7 +180,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) @@ -196,8 +229,9 @@ function splitArgs(argString: string): string[] { if (char === "(") rbrackets++; if (char === ")") rbrackets--; + const isWhitespace = char === " " || char === "\n" || char === "\t" || char === "\r"; const atLastChar = i === argString.length - 1; - if ((char === " " && brackets === 0 && rbrackets === 0) || atLastChar) { + if ((isWhitespace && brackets === 0 && rbrackets === 0) || atLastChar) { const newArg = argString.slice(argStart, i + (atLastChar ? 1 : 0)); if (newArg.trim()) { splitArgs.push(newArg.trim()); @@ -208,3 +242,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}`); + } +} diff --git a/src/parser/string-to-cv.ts b/src/parser/string-to-cv.ts index 912c3ac..b0b6ada 100644 --- a/src/parser/string-to-cv.ts +++ b/src/parser/string-to-cv.ts @@ -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 @@ -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) { @@ -110,8 +120,33 @@ export function stringToCV( ), }; } + case "list": { + const listType = (type as { list: { type: ContractInterfaceAtomType; length: number } }).list; + // strip outer (list ... ) + const inner = arg.replace(/^\(list\s*/, "").replace(/\)$/, "").trim(); + if (!inner) { + return { type: "list", value: Cl.list([]) }; + } + // split list elements respecting nested parens/braces + const elements: string[] = []; + let start = 0; + let depth = 0; + for (let i = 0; i < inner.length; i++) { + if (inner[i] === "(" || inner[i] === "{") depth++; + if (inner[i] === ")" || inner[i] === "}") depth--; + if ((inner[i] === " " && depth === 0) || i === inner.length - 1) { + const el = inner.slice(start, i + (i === inner.length - 1 ? 1 : 0)).trim(); + if (el) elements.push(el); + start = i + 1; + } + } + const values = elements.map( + (el) => stringToCV(el, listType.type).value + ); + return { type: "list", value: Cl.list(values) }; + } default: - throw new Error(`Unsupported type ${type}`); + throw new Error(`Unsupported type ${arg}, ${typeDescriptor}`); } } diff --git a/src/parser/test-helpers.ts b/src/parser/test-helpers.ts index b929fb7..b809380 100644 --- a/src/parser/test-helpers.ts +++ b/src/parser/test-helpers.ts @@ -1,4 +1,4 @@ -import { ParsedTransactionResult } from "@hirosystems/clarinet-sdk"; +import { ParsedTransactionResult } from "@stacks/clarinet-sdk"; import { Cl, ClarityType, cvToString } from "@stacks/transactions"; import { expect } from "vitest"; diff --git a/tests/clarity-parser-flow.test.ts b/tests/clarity-parser-flow.test.ts index b920136..01440a9 100644 --- a/tests/clarity-parser-flow.test.ts +++ b/tests/clarity-parser-flow.test.ts @@ -8,9 +8,9 @@ describe("verify clarity parser for flow tests", () => { const [annotations, callInfos] = extractTestAnnotationsAndCalls( fs.readFileSync( path.join(__dirname, "./contracts/parser-tests/simple-flow.clar"), - "utf8" + "utf8", ), - simnet + simnet, ); expect(annotations["test-simple-flow"]).toEqual({}); // check the two function calls @@ -31,7 +31,10 @@ describe("verify clarity parser for flow tests", () => { }, }); expect(callInfos["test-simple-flow"][2]).toEqual({ - callAnnotations: { caller: "wallet_1" }, + callAnnotations: { + caller: "wallet_1", + "type-hints": "principal, (optional uint)", + }, callInfo: { args: [ { @@ -39,7 +42,7 @@ describe("verify clarity parser for flow tests", () => { value: { type: "contract", value: - "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox4-self-service-multi", + "SP1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R.pox4-self-service-multi", }, }, { @@ -53,15 +56,23 @@ describe("verify clarity parser for flow tests", () => { functionName: "allow-contract-caller", }, }); + expect(callInfos["test-simple-flow"][3]).toEqual({ + callAnnotations: { caller: "'ST000000000000000000002AMW42H" }, + callInfo: { + args: [], + contractName: "", + functionName: "my-test-function", + }, + }); }); it("should parse flow test with bad annotations", () => { const [annotations, callInfos] = extractTestAnnotationsAndCalls( fs.readFileSync( path.join(__dirname, "./contracts/parser-tests/bad-flow.clar"), - "utf8" + "utf8", ), - simnet + simnet, ); expect(annotations["test-bad-flow"]).toEqual({}); expect(callInfos["test-bad-flow"][0]).toEqual({ @@ -74,4 +85,17 @@ describe("verify clarity parser for flow tests", () => { }); expect(callInfos["test-bad-flow"].length).toEqual(1); }); + + //assertion in clarity-parser-flow.test.ts verifying that: + // callInfo.args[0] is a list of two buffer values + // callInfo.args[1] is a list of two bool values + it("should parse list types and arguments across lines", () => { + const [annotations, callInfos] = extractTestAnnotationsAndCalls( + fs.readFileSync( + path.join(__dirname, "./contracts/parser-tests/bad-flow.clar"), + "utf8", + ), + simnet, + ); + }); }); diff --git a/tests/clarity-parser-string-to-cv.test.ts b/tests/clarity-parser-string-to-cv.test.ts index 6dc85e3..be65574 100644 --- a/tests/clarity-parser-string-to-cv.test.ts +++ b/tests/clarity-parser-string-to-cv.test.ts @@ -1,13 +1,16 @@ +import { Cl, ClarityType } from "@stacks/transactions"; import { describe, expect, it } from "vitest"; -import { stringToCV } from "../src/parser/string-to-cv"; -import { ClarityType, intCV, stringUtf8CV, uintCV } from "@stacks/transactions"; +import { + ContractInterfaceAtomType, + stringToCV, +} from "../src/parser/string-to-cv"; describe("verify string to cv conversion", () => { it("should convert string to cv", () => { const result = stringToCV("hello", { "string-utf8": { length: 100 } }); expect(result).toEqual({ type: "string", - value: stringUtf8CV("hello"), + value: Cl.stringUtf8("hello"), }); }); @@ -15,7 +18,7 @@ describe("verify string to cv conversion", () => { const result = stringToCV("u12345", "uint128"); expect(result).toEqual({ type: "uint", - value: uintCV(12345), + value: Cl.uint(12345), }); }); @@ -25,7 +28,61 @@ describe("verify string to cv conversion", () => { }); expect(result).toEqual({ type: "tuple", - value: { value: { a: intCV(12345) }, type: ClarityType.Tuple }, + value: { value: { a: Cl.int(12345) }, type: ClarityType.Tuple }, }); }); + + it("should convert list of uints to cv", () => { + const result = stringToCV("(list u1 u2 u3)", { + list: { type: "uint128", length: 3 }, + }); + expect(result).toEqual({ + type: "list", + value: { + value: [Cl.uint(1), Cl.uint(2), Cl.uint(3)], + type: ClarityType.List, + }, + }); + }); + + it("should convert list of tuples to cv", () => { + const result = stringToCV("(list {a: u1} {a: u2} {a: u3})", { + list: { type: { tuple: [{ name: "a", type: "uint128" }] }, length: 3 }, + }); + expect(result).toEqual({ + type: "list", + value: { + value: [ + { value: { a: Cl.uint(1) }, type: ClarityType.Tuple }, + { value: { a: Cl.uint(2) }, type: ClarityType.Tuple }, + { value: { a: Cl.uint(3) }, type: ClarityType.Tuple }, + ], + type: ClarityType.List, + }, + }); + }); + + it("should parse list arguments", () => { + // single-element buffer list + const bufListType = { + list: { type: { buffer: { length: 32 } }, length: 1 }, + }; + expect( + stringToCV( + "(list 0x43f51785937b153496e1bd092a4999405d697138273681aba55e841756043fe2)", + bufListType, + ).value, + ).toEqual(Cl.list([Cl.bufferFromHex("43f51785937b153496e1bd092a4999405d697138273681aba55e841756043fe2")])); + + // boolean list + const boolListType = { + list: { type: "bool" as ContractInterfaceAtomType, length: 2 }, + }; + expect(stringToCV("(list true false)", boolListType).value).toEqual( + Cl.list([Cl.bool(true), Cl.bool(false)]), + ); + + // empty list + expect(stringToCV("(list)", boolListType).value).toEqual(Cl.list([])); + }); }); diff --git a/tests/clarity-parser.test.ts b/tests/clarity-parser.test.ts index 3673afe..1721dff 100644 --- a/tests/clarity-parser.test.ts +++ b/tests/clarity-parser.test.ts @@ -46,6 +46,10 @@ describe("verify clarity parser", () => { "mine-before": "20", name: "all annotation test 2", }); + + expect(result["test-all-annotations-3"]).toEqual({ + caller: "'ST000000000000000000002AMW42H", + }); }); it("should parse with bad annotations", () => { diff --git a/tests/clarunit.test.ts b/tests/clarunit.test.ts new file mode 100644 index 0000000..3f19e33 --- /dev/null +++ b/tests/clarunit.test.ts @@ -0,0 +1,2 @@ +import { clarunit } from "../src/index"; +clarunit(simnet); diff --git a/tests/contracts/generator-tests/annotations_flow_test.clar b/tests/contracts/generator-tests/annotations_flow_test.clar new file mode 100644 index 0000000..f5cfae6 --- /dev/null +++ b/tests/contracts/generator-tests/annotations_flow_test.clar @@ -0,0 +1,33 @@ +;; @name test block height at launch +(define-public (test-block-height-at-launch) + (begin + ;; @caller 'SP1T91N2Y2TE5M937FE3R6DE0HGWD85SGCV50T95A + (try! (assert-block-height-3)) + ;; @mine-blocks-before 10 + ;; @caller wallet_1 + (try! (assert-block-height-13)) + (ok true) + ) +) + +(define-public (assert-block-height-3) + (begin + (asserts! (is-eq u3491158 stacks-block-height) + (err (concat "expected block height 3491158, found " + (int-to-ascii stacks-block-height) + )) + ) + (ok true) + ) +) + +(define-public (assert-block-height-13) + (begin + (asserts! (is-eq u3491168 stacks-block-height) + (err (concat "expected block height 3491168, found " + (int-to-ascii stacks-block-height) + )) + ) + (ok true) + ) +) diff --git a/tests/contracts/generator-tests/annotations_test.clar b/tests/contracts/generator-tests/annotations_test.clar new file mode 100644 index 0000000..16d5d4b --- /dev/null +++ b/tests/contracts/generator-tests/annotations_test.clar @@ -0,0 +1,50 @@ +;; test block-height at launch +;; One block is need to advance to epoch 2.5 +(define-public (test-block-height-at-launch) + (begin + (asserts! (is-eq u3491158 stacks-block-height) + (err (concat "expected block height 3491158, found " + (int-to-ascii stacks-block-height) + )) + ) + (ok true) + ) +) + +;; @mine-blocks-before 10 +(define-public (test-mine-blocks-before) + (begin + (asserts! (is-eq u3491168 stacks-block-height) + (err (concat "expected block height 3491168, found " + (int-to-ascii stacks-block-height) + )) + ) + (ok true) + ) +) + +;; @caller wallet_1 +(define-public (test-caller) + (begin + (asserts! (is-eq tx-sender 'SP1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2XG1V316) + (err tx-sender) + ) + (asserts! (is-eq contract-caller 'SP1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2XG1V316) + (err contract-caller) + ) + (ok true) + ) +) + +;; @caller 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE +(define-public (test-caller-2) + (begin + (asserts! (is-eq tx-sender 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE) + (err tx-sender) + ) + (asserts! (is-eq contract-caller 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE) + (err contract-caller) + ) + (ok true) + ) +) diff --git a/tests/contracts/generator-tests/contract-call_flow_test.clar b/tests/contracts/generator-tests/contract-call_flow_test.clar new file mode 100644 index 0000000..e383560 --- /dev/null +++ b/tests/contracts/generator-tests/contract-call_flow_test.clar @@ -0,0 +1,16 @@ +;; @name test external contract call +(define-public (test-contract-call) + (begin + ;; @caller 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE + ;; @type-hints trait_reference + (unwrap! + (contract-call? + 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd001-direct-execute + direct-execute + 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccip024-miamicoin-signal-vote + ) + (err "direct execute failed") + ) + (ok true) + ) +) diff --git a/tests/contracts/generator-tests/list-args_flow_test.clar b/tests/contracts/generator-tests/list-args_flow_test.clar new file mode 100644 index 0000000..a9f540c --- /dev/null +++ b/tests/contracts/generator-tests/list-args_flow_test.clar @@ -0,0 +1,15 @@ +;; @name test list args in flow test +(define-public (test-list-args) + (begin + ;; @caller wallet_1 + (unwrap! + (contract-call? .some-contract + fn-with-list-args + (list 0xdeadbeef 0xcafebabe) + (list true false) + ) + (err "failed") + ) + (ok true) + ) +) \ No newline at end of file diff --git a/tests/contracts/parser-tests/all-annotations.clar b/tests/contracts/parser-tests/all-annotations.clar index 3fec2c0..67a6c8b 100644 --- a/tests/contracts/parser-tests/all-annotations.clar +++ b/tests/contracts/parser-tests/all-annotations.clar @@ -10,4 +10,8 @@ ;; @mine-before 20 ;; @caller wallet_2 (define-public (test-all-annotations-2) + (ok true)) + +;; @caller 'ST000000000000000000002AMW42H +(define-public (test-all-annotations-3) (ok true)) \ No newline at end of file diff --git a/tests/contracts/parser-tests/simple-flow.clar b/tests/contracts/parser-tests/simple-flow.clar index 558dbbb..c470637 100644 --- a/tests/contracts/parser-tests/simple-flow.clar +++ b/tests/contracts/parser-tests/simple-flow.clar @@ -6,7 +6,10 @@ ;; @caller wallet_2 (try! (my-test-function2)) ;; @caller wallet_1 + ;; @type-hints principal, (optional uint) (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 allow-contract-caller .pox4-self-service-multi none) (err "allow-contract-caller failed")) + ;; @caller 'ST000000000000000000002AMW42H + (try! (my-test-function)) (ok true))) (define-public (my-test-function) diff --git a/tests/contracts/some-contract.clar b/tests/contracts/some-contract.clar new file mode 100644 index 0000000..f21b58f --- /dev/null +++ b/tests/contracts/some-contract.clar @@ -0,0 +1,3 @@ +(define-public (fn-with-list-args (arg1 (list 10 (buff 10))) (arg2 (list 10 bool))) + (ok true) +) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index aa218f6..f2e4302 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "noFallthroughCasesInSwitch": true }, "include": [ - "node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", + "node_modules/@stacks/clarinet-sdk/vitest-helpers/src", "tests" ] } diff --git a/vitest.config.js b/vitest.config.js index 3b16eca..1dfde20 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -5,7 +5,7 @@ import { configDefaults } from "vitest/config"; import { vitestSetupFilePath, getClarinetVitestsArgv, -} from "@hirosystems/clarinet-sdk/vitest"; +} from "@stacks/clarinet-sdk/vitest"; /* In this file, Vitest is configured so that it works seamlessly with Clarinet and the Simnet. @@ -13,7 +13,7 @@ import { The `vitest-environment-clarinet` will initialise the clarinet-sdk and make the `simnet` object available globally in the test files. - `vitestSetupFilePath` points to a file in the `@hirosystems/clarinet-sdk` package that does two things: + `vitestSetupFilePath` points to a file in the `@stacks/clarinet-sdk` package that does two things: - run `before` hooks to initialize the simnet and `after` hooks to collect costs and coverage reports. - load custom vitest matchers to work with Clarity values (such as `expect(...).toBeUint()`) @@ -26,8 +26,9 @@ export default defineConfig({ test: { exclude: [...configDefaults.exclude, "example/**"], environment: "clarinet", // use vitest-environment-clarinet - pool: "forks", + pool: "threads", poolOptions: { + threads: { singleThread: true }, forks: { singleFork: true }, }, setupFiles: [