From 71e61e3866d3695271461d1d7d35159ce4f93e0f Mon Sep 17 00:00:00 2001 From: kilianmh Date: Mon, 13 Nov 2023 16:50:35 +0100 Subject: [PATCH] Update multisig-vault for clarinet 2 Update the multisig-vault project for clarinet 2. Project was generated with following commands: clarinet new multisig-vault clarinet contract new multisig-vault .gitignore file removed multisig-vault.clar is unchanged. The tests have been moved to multisig-vault.test.ts and changed to work with clarinet-sdk and vitest. --- projects/multisig-vault/.vscode/settings.json | 2 +- projects/multisig-vault/.vscode/tasks.json | 19 ++ projects/multisig-vault/Clarinet.toml | 18 +- projects/multisig-vault/package.json | 23 +++ projects/multisig-vault/settings/Devnet.toml | 152 +++++++++++++--- projects/multisig-vault/settings/Mainnet.toml | 7 + projects/multisig-vault/settings/Mocknet.toml | 6 - projects/multisig-vault/settings/Testnet.toml | 7 + .../tests/multisig-vault.test.ts | 155 +++++++++++++++++ .../tests/multisig-vault_test.ts | 162 ------------------ projects/multisig-vault/tsconfig.json | 26 +++ projects/multisig-vault/vitest.config.js | 37 ++++ 12 files changed, 421 insertions(+), 193 deletions(-) create mode 100644 projects/multisig-vault/.vscode/tasks.json create mode 100644 projects/multisig-vault/package.json create mode 100644 projects/multisig-vault/settings/Mainnet.toml delete mode 100644 projects/multisig-vault/settings/Mocknet.toml create mode 100644 projects/multisig-vault/settings/Testnet.toml create mode 100644 projects/multisig-vault/tests/multisig-vault.test.ts delete mode 100644 projects/multisig-vault/tests/multisig-vault_test.ts create mode 100644 projects/multisig-vault/tsconfig.json create mode 100644 projects/multisig-vault/vitest.config.js diff --git a/projects/multisig-vault/.vscode/settings.json b/projects/multisig-vault/.vscode/settings.json index 02e21eb..3062519 100644 --- a/projects/multisig-vault/.vscode/settings.json +++ b/projects/multisig-vault/.vscode/settings.json @@ -1,4 +1,4 @@ { - "deno.enable": true, + "files.eol": "\n" } diff --git a/projects/multisig-vault/.vscode/tasks.json b/projects/multisig-vault/.vscode/tasks.json new file mode 100644 index 0000000..4dec0ff --- /dev/null +++ b/projects/multisig-vault/.vscode/tasks.json @@ -0,0 +1,19 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "type": "npm", + "script": "test", + "group": "test", + "problemMatcher": [], + "label": "npm test" + } + ] +} diff --git a/projects/multisig-vault/Clarinet.toml b/projects/multisig-vault/Clarinet.toml index 24e43be..5883d73 100644 --- a/projects/multisig-vault/Clarinet.toml +++ b/projects/multisig-vault/Clarinet.toml @@ -1,5 +1,19 @@ [project] -name = "multisig-vault" +name = 'multisig-vault' +description = '' +authors = [] +telemetry = false +cache_dir = './.cache' requirements = [] [contracts.multisig-vault] -path = "contracts/multisig-vault.clar" +path = 'contracts/multisig-vault.clar' +clarity_version = 2 +epoch = 2.4 +[repl.analysis] +passes = ['check_checker'] + +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/projects/multisig-vault/package.json b/projects/multisig-vault/package.json new file mode 100644 index 0000000..2efe649 --- /dev/null +++ b/projects/multisig-vault/package.json @@ -0,0 +1,23 @@ + +{ + "name": "multisig-vault-tests", + "version": "1.0.0", + "description": "Run unit tests on this project.", + "private": true, + "scripts": { + "test": "vitest run", + "test:report": "vitest run -- --coverage --costs", + "test:watch": "chokidar \"tests/**/*.ts\" \"contracts/**/*.clar\" -c \"npm run test:report\"" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@hirosystems/clarinet-sdk": "^1.0.0", + "@stacks/transactions": "^6.9.0", + "chokidar-cli": "^3.0.0", + "typescript": "^5.2.2", + "vite": "^4.4.9", + "vitest": "^0.34.4", + "vitest-environment-clarinet": "^1.0.0" + } +} diff --git a/projects/multisig-vault/settings/Devnet.toml b/projects/multisig-vault/settings/Devnet.toml index fc03ebd..68f53cc 100644 --- a/projects/multisig-vault/settings/Devnet.toml +++ b/projects/multisig-vault/settings/Devnet.toml @@ -1,42 +1,150 @@ [network] -name = "Devnet" +name = "devnet" +deployment_fee_rate = 10 [accounts.deployer] -mnemonic = "fetch outside black test wash cover just actual execute nice door want airport betray quantum stamp fish act pen trust portion fatigue scissors vague" -balance = 1_000_000 +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH [accounts.wallet_1] -mnemonic = "spoil sock coyote include verify comic jacket gain beauty tank flush victory illness edge reveal shallow plug hobby usual juice harsh pact wreck eight" -balance = 1_000_000 +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC [accounts.wallet_2] -mnemonic = "arrange scale orient half ugly kid bike twin magnet joke hurt fiber ethics super receive version wreck media fluid much abstract reward street alter" -balance = 1_000_000 +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG [accounts.wallet_3] -mnemonic = "glide clown kitchen picnic basket hidden asset beyond kid plug carbon talent drama wet pet rhythm hero nest purity baby bicycle ghost sponsor dragon" -balance = 1_000_000 +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 [accounts.wallet_4] -mnemonic = "pulp when detect fun unaware reduce promote tank success lecture cool cheese object amazing hunt plug wing month hello tunnel detect connect floor brush" -balance = 1_000_000 +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 [accounts.wallet_5] -mnemonic = "replace swing shove congress smoke banana tired term blanket nominee leave club myself swing egg virus answer bulk useful start decrease family energy february" -balance = 1_000_000 +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx [accounts.wallet_6] -mnemonic = "apology together shy taxi glare struggle hip camp engage lion possible during squeeze hen exotic marriage misery kiwi once quiz enough exhibit immense tooth" -balance = 1_000_000 +mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt [accounts.wallet_7] -mnemonic = "antenna bitter find rely gadget father exact excuse cross easy elbow alcohol injury loud silk bird crime cabbage winter fit wide screen update october" -balance = 1_000_000 +mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 [accounts.wallet_8] -mnemonic = "east load echo merit ignore hip tag obvious truly adjust smart panther deer aisle north hotel process frown lock property catch bless notice topple" -balance = 1_000_000 +mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw -[accounts.wallet_9] -mnemonic = "market ocean tortoise venue vivid coach machine category conduct enable insect jump fog file test core book chaos crucial burst version curious prosper fever" -balance = 1_000_000 +[accounts.faucet] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[devnet] +disable_stacks_explorer = false +disable_stacks_api = false +# disable_subnet_api = false +# disable_bitcoin_explorer = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_mnemonic = "fragile loan twenty basic net assault jazz absorb diet talk art shock innocent float punch travel gadget embrace caught blossom hockey surround initial reduce" +# miner_derivation_path = "m/44'/5757'/0'/0/0" +# faucet_mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +# faucet_derivation_path = "m/44'/5757'/0'/0/0" +# orchestrator_port = 20445 +# bitcoin_node_p2p_port = 18444 +# bitcoin_node_rpc_port = 18443 +# bitcoin_node_username = "devnet" +# bitcoin_node_password = "devnet" +# bitcoin_controller_block_time = 30_000 +# stacks_node_rpc_port = 20443 +# stacks_node_p2p_port = 20444 +# stacks_api_port = 3999 +# stacks_api_events_port = 3700 +# bitcoin_explorer_port = 8001 +# stacks_explorer_port = 8000 +# postgres_port = 5432 +# postgres_username = "postgres" +# postgres_password = "postgres" +# postgres_database = "postgres" +# bitcoin_node_image_url = "quay.io/hirosystems/bitcoind:devnet-v3" +# stacks_node_image_url = "quay.io/hirosystems/stacks-node:devnet-2.4.0.0.0" +# stacks_api_image_url = "hirosystems/stacks-blockchain-api:latest" +# stacks_explorer_image_url = "hirosystems/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:14" +# enable_subnet_node = true +# subnet_node_image_url = "hirosystems/stacks-subnets:0.8.1" +# subnet_leader_mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +# subnet_leader_derivation_path = "m/44'/5757'/0'/0/0" +# subnet_contract_id = "ST173JK7NZBA4BS05ZRATQH1K89YJMTGEH1Z5J52E.subnet-v3-0-1" +# subnet_node_rpc_port = 30443 +# subnet_node_p2p_port = 30444 +# subnet_events_ingestion_port = 30445 +# subnet_node_events_observers = ["host.docker.internal:8002"] +# subnet_api_image_url = "hirosystems/stacks-blockchain-api:latest" +# subnet_api_postgres_database = "subnet_api" + +# For testing in epoch 2.1 / using Clarity2 +# epoch_2_0 = 100 +# epoch_2_05 = 100 +# epoch_2_1 = 101 +# pox_2_activation = 102 +# epoch_2_2 = 103 +# epoch_2_3 = 104 +# epoch_2_4 = 105 + + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_2" +slots = 1 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_3" +slots = 1 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" diff --git a/projects/multisig-vault/settings/Mainnet.toml b/projects/multisig-vault/settings/Mainnet.toml new file mode 100644 index 0000000..b39892e --- /dev/null +++ b/projects/multisig-vault/settings/Mainnet.toml @@ -0,0 +1,7 @@ +[network] +name = "mainnet" +stacks_node_rpc_address = "https://api.hiro.so" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "" diff --git a/projects/multisig-vault/settings/Mocknet.toml b/projects/multisig-vault/settings/Mocknet.toml deleted file mode 100644 index eee3ad6..0000000 --- a/projects/multisig-vault/settings/Mocknet.toml +++ /dev/null @@ -1,6 +0,0 @@ -[network] -name = "mocknet" -node_rpc_address = "http://localhost:20443" - -[accounts.deployer] -mnemonic = "point approve language letter cargo rough similar wrap focus edge polar task olympic tobacco cinnamon drop lawn boring sort trade senior screen tiger climb" diff --git a/projects/multisig-vault/settings/Testnet.toml b/projects/multisig-vault/settings/Testnet.toml new file mode 100644 index 0000000..b9cfb45 --- /dev/null +++ b/projects/multisig-vault/settings/Testnet.toml @@ -0,0 +1,7 @@ +[network] +name = "testnet" +stacks_node_rpc_address = "https://api.testnet.hiro.so" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "" diff --git a/projects/multisig-vault/tests/multisig-vault.test.ts b/projects/multisig-vault/tests/multisig-vault.test.ts new file mode 100644 index 0000000..db76847 --- /dev/null +++ b/projects/multisig-vault/tests/multisig-vault.test.ts @@ -0,0 +1,155 @@ +import { tx } from "@hirosystems/clarinet-sdk"; +import { test, expect } from "vitest"; +import { Cl } from '@stacks/transactions'; + + +const accounts = simnet.getAccounts(); +const deployer = accounts.get('deployer')!; +const memberB = accounts.get('wallet_1')!; +const memberList = Cl.list([Cl.standardPrincipal(deployer), Cl.standardPrincipal(memberB)]); +const contractName = 'multisig-vault'; +const contractPrinicipal = deployer + "." + contractName; +const votesRequired = 1; +const defaultStxVaultAmount = 5000; +const defaultMembers = ['deployer', 'wallet_1', 'wallet_2', 'wallet_3', 'wallet_4']; + +const defaultVotesRequired = defaultMembers.length - 1; + +type InitContractOptions = { + chain: Chain, + accounts: Map, + members?: Array, + votesRequired?: number, + stxVaultAmount?: number +}; + +function initContract({ chain, accounts, members = defaultMembers, votesRequired = defaultVotesRequired, stxVaultAmount = defaultStxVaultAmount }: InitContractOptions) { + const allAccounts = simnet.getAccounts(); + const contractPrincipal = Cl.contractPrincipal(deployer, contractName); + const memberAccounts = defaultMembers.map(name => allAccounts.get(name)!); + const nonMemberAccounts = Array.from(allAccounts.keys()).filter(key => !members.includes(key)).map(name => allAccounts.get(name)!); + const startBlock = simnet.mineBlock([ + tx.callPublicFn(contractName, 'start', [Cl.list(memberAccounts.map(account => Cl.standardPrincipal(account))), Cl.uint(votesRequired)], deployer), + tx.callPublicFn(contractName, 'deposit', [Cl.uint(stxVaultAmount)], deployer), + ]); + return { deployer, contractPrincipal, memberAccounts, nonMemberAccounts, startBlock }; +}; + +test("Allows the contract owner to indescribe,itialise the vault", + async (chain: Chain) => { + const block = simnet.mineBlock([ + tx.callPublicFn(contractName, 'start', [memberList, Cl.uint(votesRequired)], deployer) + ]); + expect(block[0].result).toBeOk(Cl.bool(true)); + } +); + +test("Does not allow anyone else to initialise the vault", + async (chain: Chain) => { + const block = simnet.mineBlock([ + tx.callPublicFn(contractName, 'start', [memberList, Cl.uint(votesRequired)], memberB) + ]); + expect(block[0].result).toBeErr(Cl.uint(100)); + } +); + +test("Cannot start the vault more than once", + async (chain: Chain, accounts: Map) => { + const block = simnet.mineBlock([ + tx.callPublicFn(contractName, 'start', [memberList, Cl.uint(votesRequired)], deployer), + tx.callPublicFn(contractName, 'start', [memberList, Cl.uint(votesRequired)], deployer) + ]); + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr(Cl.uint(101)); + } +); + +test("Cannot require more votes than members", + async (chain: Chain, accounts: Map) => { + const { startBlock } = initContract({ chain, accounts, votesRequired: defaultMembers.length + 1 }); + expect(startBlock[0].result).toBeErr(Cl.uint(102)); + } +); + +test("Allows members to vote", + async (chain: Chain, accounts: Map) => { + const { memberAccounts, deployer } = initContract({ chain, accounts }); + const votes = memberAccounts.map(account => tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(deployer), Cl.bool(true)], account)); + const block = simnet.mineBlock(votes); + block.map(block => expect(block.result).toBeOk(Cl.bool(true))); + } +); + +test("Does not allow non-members to vote", + async (chain: Chain, accounts: Map) => { + const { nonMemberAccounts, deployer } = initContract({ chain, accounts }); + const votes = nonMemberAccounts.map(account => tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(deployer), Cl.bool(true)], account)); + const block = simnet.mineBlock(votes); + block.map(block => expect(block.result).toBeErr(Cl.uint(103))); + } +); + +test("Can retrieve a member's vote for a principal", + async (chain: Chain, accounts: Map) => { + const { memberAccounts, deployer } = initContract({ chain, accounts }); + const allAccounts = simnet.getAccounts(); + const memberA = allAccounts.get('wallet_2')!; + const vote = Cl.bool(true); + simnet.mineBlock([ + tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(deployer), vote], memberA) + ]); + const receipt = simnet.callReadOnlyFn(contractName, 'get-vote', [Cl.standardPrincipal(memberA), Cl.standardPrincipal(deployer)], memberA); + expect(receipt.result).toStrictEqual(Cl.bool(true)); + } +); + +test("Principal that meets the vote threshold can withdraw the vault balance", + async (chain: Chain, accounts: Map) => { + const { contractPrincipal, memberAccounts } = initContract({ chain, accounts }); + const recipient = memberAccounts.shift()!; + const votes = memberAccounts.map(account => tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(recipient), Cl.bool(true)], account)); + simnet.mineBlock(votes); + const block = simnet.mineBlock([ + tx.callPublicFn(contractName, 'withdraw', [], recipient) + ]); + expect(block[0].result).toBeOk(Cl.uint(votes.length)); + + expect(block[0].events[0].data).toStrictEqual( + JSON.parse('{"amount": "' + defaultStxVaultAmount + + '", "memo":"", "recipient":"' + recipient + + '", "sender":"' + contractPrinicipal +'"}') + ) + } +); + +test("Principals that do not meet the vote threshold cannot withdraw the vault balance", + async (chain: Chain, accounts: Map) => { + const { memberAccounts, nonMemberAccounts } = initContract({ chain, accounts }); + const recipient = memberAccounts.shift()!; + const [nonMemberA] = nonMemberAccounts; + const votes = memberAccounts.slice(0, defaultVotesRequired - 1).map(account => tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(recipient), Cl.bool(true)], account)); + simnet.mineBlock(votes); + const block = simnet.mineBlock([ + tx.callPublicFn(contractName, 'withdraw', [], recipient), + tx.callPublicFn(contractName, 'withdraw', [], nonMemberA) + ]); + block.map(block => expect(block.result).toBeErr(Cl.uint(104))); + } +); + +test("Members can change votes at-will, thus making an eligible recipient uneligible again", + async (chain: Chain, accounts: Map) => { + const { memberAccounts } = initContract({ chain, accounts }); + const recipient = memberAccounts.shift()!; + const votes = memberAccounts.map(account => tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(recipient), Cl.bool(true)], account)); + simnet.mineBlock(votes); + const receipt = simnet.callReadOnlyFn(contractName, 'tally-votes', [], recipient); + expect(receipt.result).toStrictEqual(Cl.uint(votes.length)); + const block = simnet.mineBlock([ + tx.callPublicFn(contractName, 'vote', [Cl.standardPrincipal(recipient), Cl.bool(false)], memberAccounts[0]), + tx.callPublicFn(contractName, 'withdraw', [], recipient), + ]); + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr(Cl.uint(104)); + } +); \ No newline at end of file diff --git a/projects/multisig-vault/tests/multisig-vault_test.ts b/projects/multisig-vault/tests/multisig-vault_test.ts deleted file mode 100644 index 24908ba..0000000 --- a/projects/multisig-vault/tests/multisig-vault_test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.31.0/index.ts'; - -const contractName = 'multisig-vault'; - -const defaultStxVaultAmount = 5000; -const defaultMembers = ['deployer', 'wallet_1', 'wallet_2', 'wallet_3', 'wallet_4']; -const defaultVotesRequired = defaultMembers.length - 1; - -type InitContractOptions = { - chain: Chain, - accounts: Map, - members?: Array, - votesRequired?: number, - stxVaultAmount?: number -}; - -function initContract({ chain, accounts, members = defaultMembers, votesRequired = defaultVotesRequired, stxVaultAmount = defaultStxVaultAmount }: InitContractOptions) { - const deployer = accounts.get('deployer')!; - const contractPrincipal = `${deployer.address}.${contractName}`; - const memberAccounts = members.map(name => accounts.get(name)!); - const nonMemberAccounts = Array.from(accounts.keys()).filter(key => !members.includes(key)).map(name => accounts.get(name)!); - const startBlock = chain.mineBlock([ - Tx.contractCall(contractName, 'start', [types.list(memberAccounts.map(account => types.principal(account.address))), types.uint(votesRequired)], deployer.address), - Tx.contractCall(contractName, 'deposit', [types.uint(stxVaultAmount)], deployer.address), - ]); - return { deployer, contractPrincipal, memberAccounts, nonMemberAccounts, startBlock }; -} - -Clarinet.test({ - name: "Allows the contract owner to initialise the vault", - async fn(chain: Chain, accounts: Map) { - const deployer = accounts.get('deployer')!; - const memberB = accounts.get('wallet_1')!; - const votesRequired = 1; - const memberList = types.list([types.principal(deployer.address), types.principal(memberB.address)]); - const block = chain.mineBlock([ - Tx.contractCall(contractName, 'start', [memberList, types.uint(votesRequired)], deployer.address) - ]); - block.receipts[0].result.expectOk().expectBool(true); - } -}); - -Clarinet.test({ - name: "Does not allow anyone else to initialise the vault", - async fn(chain: Chain, accounts: Map) { - const deployer = accounts.get('deployer')!; - const memberB = accounts.get('wallet_1')!; - const votesRequired = 1; - const memberList = types.list([types.principal(deployer.address), types.principal(memberB.address)]); - const block = chain.mineBlock([ - Tx.contractCall(contractName, 'start', [memberList, types.uint(votesRequired)], memberB.address) - ]); - block.receipts[0].result.expectErr().expectUint(100); - } -}); - -Clarinet.test({ - name: "Cannot start the vault more than once", - async fn(chain: Chain, accounts: Map) { - const deployer = accounts.get('deployer')!; - const memberB = accounts.get('wallet_1')!; - const votesRequired = 1; - const memberList = types.list([types.principal(deployer.address), types.principal(memberB.address)]); - const block = chain.mineBlock([ - Tx.contractCall(contractName, 'start', [memberList, types.uint(votesRequired)], deployer.address), - Tx.contractCall(contractName, 'start', [memberList, types.uint(votesRequired)], deployer.address) - ]); - block.receipts[0].result.expectOk().expectBool(true); - block.receipts[1].result.expectErr().expectUint(101); - } -}); - -Clarinet.test({ - name: "Cannot require more votes than members", - async fn(chain: Chain, accounts: Map) { - const { startBlock } = initContract({ chain, accounts, votesRequired: defaultMembers.length + 1 }); - startBlock.receipts[0].result.expectErr().expectUint(102); - } -}); - -Clarinet.test({ - name: "Allows members to vote", - async fn(chain: Chain, accounts: Map) { - const { memberAccounts, deployer } = initContract({ chain, accounts }); - const votes = memberAccounts.map(account => Tx.contractCall(contractName, 'vote', [types.principal(deployer.address), types.bool(true)], account.address)); - const block = chain.mineBlock(votes); - block.receipts.map(receipt => receipt.result.expectOk().expectBool(true)); - } -}); - -Clarinet.test({ - name: "Does not allow non-members to vote", - async fn(chain: Chain, accounts: Map) { - const { nonMemberAccounts, deployer } = initContract({ chain, accounts }); - const votes = nonMemberAccounts.map(account => Tx.contractCall(contractName, 'vote', [types.principal(deployer.address), types.bool(true)], account.address)); - const block = chain.mineBlock(votes); - block.receipts.map(receipt => receipt.result.expectErr().expectUint(103)); - } -}); - -Clarinet.test({ - name: "Can retrieve a member's vote for a principal", - async fn(chain: Chain, accounts: Map) { - const { memberAccounts, deployer } = initContract({ chain, accounts }); - const [memberA] = memberAccounts; - const vote = types.bool(true); - chain.mineBlock([ - Tx.contractCall(contractName, 'vote', [types.principal(deployer.address), vote], memberA.address) - ]); - const receipt = chain.callReadOnlyFn(contractName, 'get-vote', [types.principal(memberA.address), types.principal(deployer.address)], memberA.address); - receipt.result.expectBool(true); - } -}); - -Clarinet.test({ - name: "Principal that meets the vote threshold can withdraw the vault balance", - async fn(chain: Chain, accounts: Map) { - const { contractPrincipal, memberAccounts } = initContract({ chain, accounts }); - const recipient = memberAccounts.shift()!; - const votes = memberAccounts.map(account => Tx.contractCall(contractName, 'vote', [types.principal(recipient.address), types.bool(true)], account.address)); - chain.mineBlock(votes); - const block = chain.mineBlock([ - Tx.contractCall(contractName, 'withdraw', [], recipient.address) - ]); - block.receipts[0].result.expectOk().expectUint(votes.length); - block.receipts[0].events.expectSTXTransferEvent(defaultStxVaultAmount, contractPrincipal, recipient.address); - } -}); - -Clarinet.test({ - name: "Principals that do not meet the vote threshold cannot withdraw the vault balance", - async fn(chain: Chain, accounts: Map) { - const { memberAccounts, nonMemberAccounts } = initContract({ chain, accounts }); - const recipient = memberAccounts.shift()!; - const [nonMemberA] = nonMemberAccounts; - const votes = memberAccounts.slice(0, defaultVotesRequired - 1).map(account => Tx.contractCall(contractName, 'vote', [types.principal(recipient.address), types.bool(true)], account.address)); - chain.mineBlock(votes); - const block = chain.mineBlock([ - Tx.contractCall(contractName, 'withdraw', [], recipient.address), - Tx.contractCall(contractName, 'withdraw', [], nonMemberA.address) - ]); - block.receipts.map(receipt => receipt.result.expectErr().expectUint(104)); - } -}); - -Clarinet.test({ - name: "Members can change votes at-will, thus making an eligible recipient uneligible again", - async fn(chain: Chain, accounts: Map) { - const { memberAccounts } = initContract({ chain, accounts }); - const recipient = memberAccounts.shift()!; - const votes = memberAccounts.map(account => Tx.contractCall(contractName, 'vote', [types.principal(recipient.address), types.bool(true)], account.address)); - chain.mineBlock(votes); - const receipt = chain.callReadOnlyFn(contractName, 'tally-votes', [], recipient.address); - receipt.result.expectUint(votes.length); - const block = chain.mineBlock([ - Tx.contractCall(contractName, 'vote', [types.principal(recipient.address), types.bool(false)], memberAccounts[0].address), - Tx.contractCall(contractName, 'withdraw', [], recipient.address), - ]); - block.receipts[0].result.expectOk().expectBool(true); - block.receipts[1].result.expectErr().expectUint(104); - } -}); diff --git a/projects/multisig-vault/tsconfig.json b/projects/multisig-vault/tsconfig.json new file mode 100644 index 0000000..1bdaf36 --- /dev/null +++ b/projects/multisig-vault/tsconfig.json @@ -0,0 +1,26 @@ + +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", + "tests" + ] +} diff --git a/projects/multisig-vault/vitest.config.js b/projects/multisig-vault/vitest.config.js new file mode 100644 index 0000000..36a2261 --- /dev/null +++ b/projects/multisig-vault/vitest.config.js @@ -0,0 +1,37 @@ + +/// + +import { defineConfig } from "vite"; +import { vitestSetupFilePath, getClarinetVitestsArgv } from "@hirosystems/clarinet-sdk/vitest"; + +/* + In this file, Vitest is configured so that it works seamlessly with Clarinet and the Simnet. + + 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: + - 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()`) + + The `getClarinetVitestsArgv()` will parse options passed to the command `vitest run --` + - vitest run -- --manifest ./Clarinet.toml # pass a custom path + - vitest run -- --coverage --costs # collect coverage and cost reports +*/ + +export default defineConfig({ + test: { + environment: "clarinet", // use vitest-environment-clarinet + singleThread: true, + setupFiles: [ + vitestSetupFilePath, + // custom setup files can be added here + ], + environmentOptions: { + clarinet: { + ...getClarinetVitestsArgv(), + // add or override options + }, + }, + }, +});