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..b9edec7 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -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'] @@ -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 \ No newline at end of file diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index 81393c0..0cd8dcf 100644 --- a/deployments/default.simnet-plan.yaml +++ b/deployments/default.simnet-plan.yaml @@ -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" 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/package.json b/package.json index 01405a1..820b423 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/clarunit-flow-generator.ts b/src/clarunit-flow-generator.ts index 19b3303..db83778 100644 --- a/src/clarunit-flow-generator.ts +++ b/src/clarunit-flow-generator.ts @@ -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..a211cb5 100644 --- a/src/clarunit-generator.ts +++ b/src/clarunit-generator.ts @@ -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 ( @@ -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/parser/clarity-parser-flow-tests.ts b/src/parser/clarity-parser-flow-tests.ts index 3ac09f3..6c5898f 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,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, @@ -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) @@ -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}`); + } +} diff --git a/src/parser/string-to-cv.ts b/src/parser/string-to-cv.ts index 912c3ac..f04cb16 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) { @@ -111,7 +121,7 @@ export function stringToCV( }; } default: - throw new Error(`Unsupported type ${type}`); + throw new Error(`Unsupported type ${arg}, ${typeDescriptor}`); } } diff --git a/tests/clarity-parser-flow.test.ts b/tests/clarity-parser-flow.test.ts index b920136..5e88b8a 100644 --- a/tests/clarity-parser-flow.test.ts +++ b/tests/clarity-parser-flow.test.ts @@ -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,6 +56,14 @@ 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", () => { 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/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)