diff --git a/Cargo.lock b/Cargo.lock index d92d1b032..920418aa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5116,6 +5116,18 @@ dependencies = [ "spl-token-2022-interface", ] +[[package]] +name = "token-fundraiser-pinocchio-program" +version = "0.1.0" +dependencies = [ + "pinocchio 0.10.2", + "pinocchio-associated-token-account", + "pinocchio-log", + "pinocchio-pubkey", + "pinocchio-system", + "pinocchio-token", +] + [[package]] name = "toml" version = "0.8.23" diff --git a/Cargo.toml b/Cargo.toml index aae171e04..3d0820af0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ # tokens "tokens/escrow/pinocchio/program", "tokens/transfer-tokens/pinocchio/program", + "tokens/token-fundraiser/pinocchio/program", "tokens/token-2022/mint-close-authority/native/program", "tokens/token-2022/non-transferable/native/program", "tokens/token-2022/default-account-state/native/program", diff --git a/README.md b/README.md index 0f7048fa8..37b13cfeb 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Allow two users to swap digital assets with each other, each getting 100% of wha Create a fundraiser account specifying a target mint and amount, allowing contributors to deposit tokens until the goal is reached. -[anchor](./tokens/token-fundraiser/anchor) +[anchor](./tokens/token-fundraiser/anchor) [pinocchio](./tokens/token-fundraiser/pinocchio) ### Minting a token from inside a program with a PDA as the mint authority diff --git a/tokens/token-fundraiser/pinocchio/cicd.sh b/tokens/token-fundraiser/pinocchio/cicd.sh new file mode 100644 index 000000000..b2407c75f --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/cicd.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# This script is for quick building & deploying of the program. +# It also serves as a reference for the commands used for building & deploying Solana programs. +# Run this bad boy with "bash cicd.sh" or "./cicd.sh" + +cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so +solana program deploy ./program/target/so/program.so diff --git a/tokens/token-fundraiser/pinocchio/package.json b/tokens/token-fundraiser/pinocchio/package.json new file mode 100644 index 000000000..f4b1d26d2 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/package.json @@ -0,0 +1,25 @@ +{ + "scripts": { + "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./tests/test.ts", + "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", + "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", + "deploy": "solana program deploy ./program/target/so/program.so" + }, + "dependencies": { + "@solana/spl-token": "^0.4.9", + "@solana/web3.js": "^1.98.4", + "bn.js": "^5.2.2" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.1", + "@types/mocha": "^10.0.9", + "@types/node": "^22.8.6", + "borsh": "^2.0.0", + "chai": "^4.3.4", + "mocha": "^10.8.2", + "solana-bankrun": "^0.4.0", + "ts-mocha": "^10.0.0", + "typescript": "^5" + } +} diff --git a/tokens/token-fundraiser/pinocchio/pnpm-lock.yaml b/tokens/token-fundraiser/pinocchio/pnpm-lock.yaml new file mode 100644 index 000000000..477c1da3a --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/pnpm-lock.yaml @@ -0,0 +1,1500 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@solana/spl-token': + specifier: ^0.4.9 + version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/web3.js': + specifier: ^1.98.4 + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: + specifier: ^5.2.2 + version: 5.2.3 + devDependencies: + '@types/bn.js': + specifier: ^5.1.0 + version: 5.2.0 + '@types/chai': + specifier: ^4.3.1 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.9 + version: 10.0.10 + '@types/node': + specifier: ^22.8.6 + version: 22.20.0 + borsh: + specifier: ^2.0.0 + version: 2.0.0 + chai: + specifier: ^4.3.4 + version: 4.5.0 + mocha: + specifier: ^10.8.2 + version: 10.8.2 + solana-bankrun: + specifier: ^0.4.0 + version: 0.4.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + ts-mocha: + specifier: ^10.0.0 + version: 10.1.0(mocha@10.8.2) + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@solana/buffer-layout-utils@0.2.0': + resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} + engines: {node: '>= 10'} + + '@solana/buffer-layout@4.0.1': + resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} + engines: {node: '>=5.10'} + + '@solana/codecs-core@2.0.0-rc.1': + resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-data-structures@2.0.0-rc.1': + resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-numbers@2.0.0-rc.1': + resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-strings@2.0.0-rc.1': + resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5' + + '@solana/codecs@2.0.0-rc.1': + resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} + peerDependencies: + typescript: '>=5' + + '@solana/errors@2.0.0-rc.1': + resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} + hasBin: true + peerDependencies: + typescript: '>=5' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/options@2.0.0-rc.1': + resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} + peerDependencies: + typescript: '>=5' + + '@solana/spl-token-group@0.0.7': + resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token-metadata@0.1.6': + resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token@0.4.14': + resolution: {integrity: sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.5 + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@types/bn.js@5.2.0': + resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==} + + '@types/chai@4.3.20': + resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@22.20.0': + resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-x@3.0.11: + resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bigint-buffer@1.1.5: + resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} + engines: {node: '>= 10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bn.js@5.2.3: + resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} + + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + + borsh@2.0.0: + resolution: {integrity: sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + diff@3.5.1: + resolution: {integrity: sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==} + engines: {node: '>=0.3.1'} + + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + + fast-stable-stringify@1.0.0: + resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + + fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + jayson@4.3.0: + resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==} + engines: {node: '>=8'} + hasBin: true + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mocha@10.8.2: + resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} + engines: {node: '>= 14.0.0'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rpc-websockets@9.3.9: + resolution: {integrity: sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + solana-bankrun-darwin-arm64@0.4.0: + resolution: {integrity: sha512-6dz78Teoz7ez/3lpRLDjktYLJb79FcmJk2me4/YaB8WiO6W43OdExU4h+d2FyuAryO2DgBPXaBoBNY/8J1HJmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + solana-bankrun-darwin-universal@0.4.0: + resolution: {integrity: sha512-zSSw/Jx3KNU42pPMmrEWABd0nOwGJfsj7nm9chVZ3ae7WQg3Uty0hHAkn5NSDCj3OOiN0py9Dr1l9vmRJpOOxg==} + engines: {node: '>= 10'} + os: [darwin] + + solana-bankrun-darwin-x64@0.4.0: + resolution: {integrity: sha512-LWjs5fsgHFtyr7YdJR6r0Ho5zrtzI6CY4wvwPXr8H2m3b4pZe6RLIZjQtabCav4cguc14G0K8yQB2PTMuGub8w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + solana-bankrun-linux-x64-gnu@0.4.0: + resolution: {integrity: sha512-SrlVrb82UIxt21Zr/XZFHVV/h9zd2/nP25PMpLJVLD7Pgl2yhkhfi82xj3OjxoQqWe+zkBJ+uszA0EEKr67yNw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + solana-bankrun-linux-x64-musl@0.4.0: + resolution: {integrity: sha512-Nv328ZanmURdYfcLL+jwB1oMzX4ZzK57NwIcuJjGlf0XSNLq96EoaO5buEiUTo4Ls7MqqMyLbClHcrPE7/aKyA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + solana-bankrun@0.4.0: + resolution: {integrity: sha512-NMmXUipPBkt8NgnyNO3SCnPERP6xT/AMNMBooljGA3+rG6NN8lmXJsKeLqQTiFsDeWD74U++QM/DgcueSWvrIg==} + engines: {node: '>= 10'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-mocha@10.1.0: + resolution: {integrity: sha512-T0C0Xm3/WqCuF2tpa0GNGESTBoKZaiqdUP8guNv4ZY316AFXlyidnrzQ1LUrCT0Wb1i3J0zFTgOh/55Un44WdA==} + engines: {node: '>= 6.X.X'} + hasBin: true + peerDependencies: + mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X || ^11.X.X + + ts-node@7.0.1: + resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} + engines: {node: '>=4.2.0'} + hasBin: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + utf-8-validate@6.0.6: + resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==} + engines: {node: '>=6.14.2'} + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.11: + resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + + yn@2.0.0: + resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} + engines: {node: '>=4'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/runtime@7.29.7': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bigint-buffer: 1.1.5 + bignumber.js: 9.3.1 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@solana/buffer-layout@4.0.1': + dependencies: + buffer: 6.0.3 + + '@solana/codecs-core@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-core@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + + '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/errors@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 12.1.0 + typescript: 5.9.3 + + '@solana/errors@2.3.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.9.3 + + '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + + '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@babel/runtime': 7.29.7 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + agentkeepalive: 4.6.0 + bn.js: 5.2.3 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + node-fetch: 2.7.0 + rpc-websockets: 9.3.9 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@types/bn.js@5.2.0': + dependencies: + '@types/node': 22.20.0 + + '@types/chai@4.3.20': {} + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.20.0 + + '@types/json5@0.0.29': + optional: true + + '@types/mocha@10.0.10': {} + + '@types/node@12.20.55': {} + + '@types/node@22.20.0': + dependencies: + undici-types: 6.21.0 + + '@types/uuid@10.0.0': {} + + '@types/ws@7.4.7': + dependencies: + '@types/node': 22.20.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.20.0 + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@2.0.1: {} + + arrify@1.0.1: {} + + assertion-error@1.1.0: {} + + balanced-match@1.0.2: {} + + base-x@3.0.11: + dependencies: + safe-buffer: 5.2.1 + + base64-js@1.5.1: {} + + bigint-buffer@1.1.5: + dependencies: + bindings: 1.5.0 + + bignumber.js@9.3.1: {} + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bn.js@5.2.3: {} + + borsh@0.7.0: + dependencies: + bn.js: 5.2.3 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + + borsh@2.0.0: {} + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-stdout@1.3.1: {} + + bs58@4.0.1: + dependencies: + base-x: 3.0.11 + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + camelcase@6.3.0: {} + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + commander@14.0.3: {} + + commander@2.20.3: {} + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + delay@5.0.0: {} + + diff@3.5.1: {} + + diff@5.2.2: {} + + emoji-regex@8.0.0: {} + + es6-promise@4.2.8: {} + + es6-promisify@5.0.0: + dependencies: + es6-promise: 4.2.8 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eventemitter3@5.0.4: {} + + eyes@0.1.8: {} + + fast-stable-stringify@1.0.0: {} + + fastestsmallesttextencoderdecoder@1.0.22: {} + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-func-name@2.0.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + has-flag@4.0.0: {} + + he@1.2.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + ieee754@1.2.1: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-obj@2.1.0: {} + + is-unicode-supported@0.1.0: {} + + isomorphic-ws@4.0.1(ws@7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + dependencies: + ws: 7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6) + + jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + json-stringify-safe@5.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + optional: true + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + make-error@1.3.6: {} + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.1 + + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mocha@10.8.2: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.4.3(supports-color@8.1.1) + diff: 5.2.2 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.2.0 + log-symbols: 4.1.0 + minimatch: 5.1.9 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.2 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + + ms@2.1.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: + optional: true + + normalize-path@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + pathval@1.1.1: {} + + picomatch@2.3.2: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + require-directory@2.1.1: {} + + rpc-websockets@9.3.9: + dependencies: + '@swc/helpers': 0.5.23 + '@types/uuid': 10.0.0 + '@types/ws': 8.18.1 + buffer: 6.0.3 + eventemitter3: 5.0.4 + uuid: 14.0.1 + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + safe-buffer@5.2.1: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + solana-bankrun-darwin-arm64@0.4.0: + optional: true + + solana-bankrun-darwin-universal@0.4.0: + optional: true + + solana-bankrun-darwin-x64@0.4.0: + optional: true + + solana-bankrun-linux-x64-gnu@0.4.0: + optional: true + + solana-bankrun-linux-x64-musl@0.4.0: + optional: true + + solana-bankrun@0.4.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6): + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bs58: 4.0.1 + optionalDependencies: + solana-bankrun-darwin-arm64: 0.4.0 + solana-bankrun-darwin-universal: 0.4.0 + solana-bankrun-darwin-x64: 0.4.0 + solana-bankrun-linux-x64-gnu: 0.4.0 + solana-bankrun-linux-x64-musl: 0.4.0 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: + optional: true + + strip-json-comments@3.1.1: {} + + superstruct@2.0.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + text-encoding-utf-8@1.0.2: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-mocha@10.1.0(mocha@10.8.2): + dependencies: + mocha: 10.8.2 + ts-node: 7.0.1 + optionalDependencies: + tsconfig-paths: 3.15.0 + + ts-node@7.0.1: + dependencies: + arrify: 1.0.1 + buffer-from: 1.1.2 + diff: 3.5.1 + make-error: 1.3.6 + minimist: 1.2.8 + mkdirp: 0.5.6 + source-map-support: 0.5.21 + yn: 2.0.0 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + optional: true + + tslib@2.8.1: {} + + type-detect@4.1.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + utf-8-validate@6.0.6: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + uuid@14.0.1: {} + + uuid@8.3.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + workerpool@6.5.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yn@2.0.0: {} + + yocto-queue@0.1.0: {} diff --git a/tokens/token-fundraiser/pinocchio/program/Cargo.toml b/tokens/token-fundraiser/pinocchio/program/Cargo.toml new file mode 100644 index 000000000..714ded74d --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "token-fundraiser-pinocchio-program" +version = "0.1.0" +edition = "2021" + +[dependencies] +pinocchio.workspace = true +pinocchio-log.workspace = true +pinocchio-pubkey.workspace = true +pinocchio-system.workspace = true +pinocchio-token.workspace = true +pinocchio-associated-token-account.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +custom-heap = [] +custom-panic = [] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/tokens/token-fundraiser/pinocchio/program/src/error.rs b/tokens/token-fundraiser/pinocchio/program/src/error.rs new file mode 100644 index 000000000..beda17bed --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/error.rs @@ -0,0 +1,39 @@ +use pinocchio::error::ProgramError; + +/// Errors returned by the fundraiser program. +/// +/// These mirror the named errors of the Anchor and native versions of this +/// example. Each variant is surfaced to clients as `ProgramError::Custom(n)`, +/// where `n` is the variant's discriminant below. +#[repr(u32)] +pub enum FundraiserError { + /// The vault does not yet hold the target amount. + TargetNotMet, + /// The target has already been reached, so a refund is not allowed. + TargetMet, + /// The contribution exceeds the per-contributor maximum. + ContributionTooBig, + /// The contribution is below the minimum. + ContributionTooSmall, + /// The contributor has reached their maximum total contribution. + MaximumContributionsReached, + /// The fundraiser has not ended yet, so a refund is not allowed. + FundraiserNotEnded, + /// The fundraiser has already ended, so contributions are closed. + FundraiserEnded, + /// The requested target is below the minimum allowed amount. + InvalidAmount, + /// A provided account is not the expected PDA for the given seeds. + InvalidSeeds, + /// A provided mint does not match the one the fundraiser is raising. + InvalidMint, + /// The provided vault is not a token account owned by the fundraiser for + /// the raised mint. + InvalidVault, +} + +impl From for ProgramError { + fn from(error: FundraiserError) -> Self { + ProgramError::Custom(error as u32) + } +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs new file mode 100644 index 000000000..439cd1943 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs @@ -0,0 +1,129 @@ +use pinocchio::{ + cpi::{Seed, Signer}, + error::ProgramError, + AccountView, Address, ProgramResult, +}; +use pinocchio_associated_token_account::instructions::CreateIdempotent; +use pinocchio_log::log; +use pinocchio_pubkey::derive_address; +use pinocchio_token::{ + instructions::{CloseAccount, Transfer}, + state::TokenAccount, +}; + +use crate::{error::FundraiserError, state::Fundraiser}; + +/// Settles a successful fundraiser. +/// +/// If the vault holds at least the target amount, all funds are transferred to +/// the maker, and both the vault and the fundraiser account are closed (their +/// rent returned to the maker). +/// +/// Accounts: +/// 0. `[signer, writable]` maker (receives the funds and the reclaimed rent) +/// 1. `[]` mint to raise +/// 2. `[writable]` fundraiser account (PDA, closed here) +/// 3. `[writable]` vault (fundraiser's token account, drained and closed here) +/// 4. `[writable]` maker's token account (created if needed) +/// 5. `[]` token program +/// 6. `[]` associated token program +/// 7. `[]` system program +/// +/// Instruction data: none. +pub fn check_contributions( + program_id: &Address, + accounts: &[AccountView], + _data: &[u8], +) -> ProgramResult { + let [maker, mint_to_raise, fundraiser, vault, maker_ata, token_program, _associated_token_program, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !maker.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Load the campaign and confirm the maker, mint and PDA all match. + let fundraiser_state = Fundraiser::deserialize(&fundraiser.try_borrow()?)?; + if &fundraiser_state.maker != maker.address().as_array() { + return Err(FundraiserError::InvalidSeeds.into()); + } + if &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() { + return Err(FundraiserError::InvalidMint.into()); + } + let fundraiser_pda = derive_address( + &[Fundraiser::SEED_PREFIX, maker.address().as_ref()], + Some(fundraiser_state.bump), + program_id.as_array(), + ); + if fundraiser.address().as_array() != &fundraiser_pda { + return Err(FundraiserError::InvalidSeeds.into()); + } + + // The vault must be the canonical vault recorded at initialization, so a + // maker cannot settle against a pre-funded substitute and strand the real + // contributions. + if vault.address().as_array() != &fundraiser_state.vault { + return Err(FundraiserError::InvalidVault.into()); + } + let vault_amount = TokenAccount::from_account_view(vault)?.amount(); + + // The campaign must have reached its target. + if vault_amount < fundraiser_state.amount_to_raise { + return Err(FundraiserError::TargetNotMet.into()); + } + + // Make sure the maker has a token account to receive into. + log!("Ensuring maker token account exists"); + CreateIdempotent { + funding_account: maker, + account: maker_ata, + wallet: maker, + mint: mint_to_raise, + system_program, + token_program, + } + .invoke()?; + + // Release the raised funds to the maker, signed by the fundraiser PDA. + let bump_bytes = [fundraiser_state.bump]; + let seeds = [ + Seed::from(Fundraiser::SEED_PREFIX), + Seed::from(maker.address().as_ref()), + Seed::from(&bump_bytes), + ]; + let signers = [Signer::from(&seeds)]; + + log!("Transferring raised funds to maker"); + Transfer { + from: vault, + to: maker_ata, + authority: fundraiser, + amount: vault_amount, + } + .invoke_signed(&signers)?; + + // Close the now-empty vault, returning its rent to the maker. + log!("Closing vault"); + CloseAccount { + account: vault, + destination: maker, + authority: fundraiser, + } + .invoke_signed(&signers)?; + + // Close the fundraiser account, returning its rent to the maker. + log!("Closing fundraiser account"); + let fundraiser_lamports = fundraiser.lamports(); + fundraiser.set_lamports(0); + maker.set_lamports(maker.lamports() + fundraiser_lamports); + fundraiser.resize(0)?; + unsafe { + fundraiser.assign(system_program.address()); + } + + log!("Fundraiser settled successfully"); + Ok(()) +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs new file mode 100644 index 000000000..d8936ad1e --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs @@ -0,0 +1,156 @@ +use pinocchio::{ + cpi::{Seed, Signer}, + error::ProgramError, + sysvars::{clock::Clock, rent::Rent, Sysvar}, + AccountView, Address, ProgramResult, +}; +use pinocchio_log::log; +use pinocchio_pubkey::derive_address; +use pinocchio_system::instructions::CreateAccount; +use pinocchio_token::instructions::Transfer; + +use crate::{ + error::FundraiserError, + instructions::{max_contribution, read_u64, SECONDS_TO_DAYS}, + state::{Contributor, Fundraiser}, +}; + +/// Contributes tokens to a fundraiser. +/// +/// Tokens are moved from the contributor's token account into the vault, and the +/// contributor's running total is recorded in a per-contributor PDA +/// (`[b"contributor", fundraiser, contributor]`), created on first contribution. +/// +/// Accounts: +/// 0. `[signer, writable]` contributor (funds the contributor account, sends tokens) +/// 1. `[]` mint to raise +/// 2. `[writable]` fundraiser account (PDA) +/// 3. `[writable]` contributor account (PDA, created on first contribution) +/// 4. `[writable]` contributor's token account (source of the tokens) +/// 5. `[writable]` vault (fundraiser's token account) +/// 6. `[]` token program +/// 7. `[]` system program +/// +/// Instruction data: `[amount: u64 (LE), contributor_bump: u8]` +pub fn contribute(program_id: &Address, accounts: &[AccountView], data: &[u8]) -> ProgramResult { + let [contributor, mint_to_raise, fundraiser, contributor_account, contributor_ata, vault, _token_program, _system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !contributor.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + let amount = read_u64(data, 0)?; + let contributor_bump = *data.get(8).ok_or(ProgramError::InvalidInstructionData)?; + + // Load the campaign and re-derive its PDA to confirm authenticity. + let mut fundraiser_state = Fundraiser::deserialize(&fundraiser.try_borrow()?)?; + if &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() { + return Err(FundraiserError::InvalidMint.into()); + } + let fundraiser_pda = derive_address( + &[Fundraiser::SEED_PREFIX, fundraiser_state.maker.as_ref()], + Some(fundraiser_state.bump), + program_id.as_array(), + ); + if fundraiser.address().as_array() != &fundraiser_pda { + return Err(FundraiserError::InvalidSeeds.into()); + } + + // The vault must be the canonical vault recorded at initialization. Without + // this check a contributor could pass an account they control as the vault, + // keep their tokens, and still inflate the recorded total. + if vault.address().as_array() != &fundraiser_state.vault { + return Err(FundraiserError::InvalidVault.into()); + } + + // A contribution must be non-zero (mirrors `1.pow(decimals) == 1`). + if amount < 1 { + return Err(FundraiserError::ContributionTooSmall.into()); + } + + // A single contribution cannot exceed the per-contributor maximum. + let max = max_contribution(fundraiser_state.amount_to_raise); + if amount > max { + return Err(FundraiserError::ContributionTooBig.into()); + } + + // Contributions are only accepted while the campaign is still open, i.e. + // before `duration` days have elapsed since it started. + let elapsed_days = + ((Clock::get()?.unix_timestamp - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; + if elapsed_days >= fundraiser_state.duration { + return Err(FundraiserError::FundraiserEnded.into()); + } + + // Track this contributor's running total, creating their account if needed. + let contributor_pda = derive_address( + &[ + Contributor::SEED_PREFIX, + fundraiser.address().as_ref(), + contributor.address().as_ref(), + ], + Some(contributor_bump), + program_id.as_array(), + ); + if contributor_account.address().as_array() != &contributor_pda { + return Err(FundraiserError::InvalidSeeds.into()); + } + + let previous_amount = if contributor_account.data_len() == 0 { + // First contribution: create the contributor account, signed by its PDA. + let lamports = Rent::get()?.try_minimum_balance(Contributor::LEN)?; + let bump_bytes = [contributor_bump]; + let seeds = [ + Seed::from(Contributor::SEED_PREFIX), + Seed::from(fundraiser.address().as_ref()), + Seed::from(contributor.address().as_ref()), + Seed::from(&bump_bytes), + ]; + let signers = [Signer::from(&seeds)]; + + log!("Creating contributor account"); + CreateAccount { + from: contributor, + to: contributor_account, + lamports, + space: Contributor::LEN as u64, + owner: program_id, + } + .invoke_signed(&signers)?; + + 0 + } else { + Contributor::deserialize(&contributor_account.try_borrow()?)?.amount + }; + + // The contributor's total contribution cannot exceed the per-contributor max. + if previous_amount > max || previous_amount + amount > max { + return Err(FundraiserError::MaximumContributionsReached.into()); + } + + // Move the tokens into the vault. + log!("Transferring contribution into vault"); + Transfer { + from: contributor_ata, + to: vault, + authority: contributor, + amount, + } + .invoke()?; + + // Update the campaign and contributor totals. + fundraiser_state.current_amount += amount; + fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?; + + let contributor_state = Contributor { + amount: previous_amount + amount, + }; + contributor_state.serialize(&mut contributor_account.try_borrow_mut()?)?; + + log!("Contribution recorded successfully"); + Ok(()) +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs new file mode 100644 index 000000000..668910f88 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs @@ -0,0 +1,123 @@ +use pinocchio::{ + cpi::{Seed, Signer}, + error::ProgramError, + sysvars::{clock::Clock, rent::Rent, Sysvar}, + AccountView, Address, ProgramResult, +}; +use pinocchio_associated_token_account::instructions::CreateIdempotent; +use pinocchio_log::log; +use pinocchio_pubkey::derive_address; +use pinocchio_system::instructions::CreateAccount; +use pinocchio_token::state::Mint; + +use crate::{ + error::FundraiserError, + instructions::{read_u64, MIN_AMOUNT_TO_RAISE}, + state::Fundraiser, +}; + +/// Creates a fundraiser and its vault. +/// +/// The fundraiser account is a PDA (`[b"fundraiser", maker]`) that records the +/// campaign terms and owns the vault, an associated token account for the +/// raised mint. +/// +/// Accounts: +/// 0. `[signer, writable]` maker (creates and funds the fundraiser + vault) +/// 1. `[]` mint to raise +/// 2. `[writable]` fundraiser account (PDA, created here) +/// 3. `[writable]` vault (fundraiser's associated token account, created here) +/// 4. `[]` token program +/// 5. `[]` associated token program +/// 6. `[]` system program +/// +/// Instruction data: `[amount: u64 (LE), duration: u16 (LE), bump: u8]` +pub fn initialize(program_id: &Address, accounts: &[AccountView], data: &[u8]) -> ProgramResult { + let [maker, mint_to_raise, fundraiser, vault, token_program, _associated_token_program, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !maker.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + let amount = read_u64(data, 0)?; + let duration = u16::from_le_bytes( + data.get(8..10) + .ok_or(ProgramError::InvalidInstructionData)? + .try_into() + .unwrap(), + ); + let bump = *data.get(10).ok_or(ProgramError::InvalidInstructionData)?; + + // The target must meet the minimum, scaled by the mint's decimals. This + // mirrors the Anchor/native versions: `MIN_AMOUNT_TO_RAISE.pow(decimals)`. + let decimals = Mint::from_account_view(mint_to_raise)?.decimals(); + if amount < MIN_AMOUNT_TO_RAISE.pow(decimals as u32) { + return Err(FundraiserError::InvalidAmount.into()); + } + + // Confirm the supplied fundraiser account is the canonical PDA for the maker. + let fundraiser_pda = derive_address( + &[Fundraiser::SEED_PREFIX, maker.address().as_ref()], + Some(bump), + program_id.as_array(), + ); + if fundraiser.address().as_array() != &fundraiser_pda { + return Err(FundraiserError::InvalidSeeds.into()); + } + + // Create the fundraiser account, signed by the fundraiser PDA itself. + let lamports = Rent::get()?.try_minimum_balance(Fundraiser::LEN)?; + let bump_bytes = [bump]; + let seeds = [ + Seed::from(Fundraiser::SEED_PREFIX), + Seed::from(maker.address().as_ref()), + Seed::from(&bump_bytes), + ]; + let signers = [Signer::from(&seeds)]; + + log!("Creating fundraiser account"); + CreateAccount { + from: maker, + to: fundraiser, + lamports, + space: Fundraiser::LEN as u64, + owner: program_id, + } + .invoke_signed(&signers)?; + + // Create the vault: an associated token account for the raised mint, owned + // by the fundraiser PDA. + log!("Creating vault"); + CreateIdempotent { + funding_account: maker, + account: vault, + wallet: fundraiser, + mint: mint_to_raise, + system_program, + token_program, + } + .invoke()?; + + // Persist the campaign terms. + let fundraiser_state = Fundraiser { + maker: *maker.address().as_array(), + mint_to_raise: *mint_to_raise.address().as_array(), + // The associated token program guaranteed this is the canonical ATA when + // it was created above, so recording it lets later instructions reject + // any substitute vault. + vault: *vault.address().as_array(), + amount_to_raise: amount, + current_amount: 0, + time_started: Clock::get()?.unix_timestamp, + duration, + bump, + }; + fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?; + + log!("Fundraiser initialized successfully"); + Ok(()) +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs new file mode 100644 index 000000000..9217087ae --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs @@ -0,0 +1,38 @@ +use pinocchio::error::ProgramError; + +mod check_contributions; +mod contribute; +mod initialize; +mod refund; + +pub use check_contributions::*; +pub use contribute::*; +pub use initialize::*; +pub use refund::*; + +/// Minimum target a fundraiser may set, before scaling by the mint's decimals. +pub const MIN_AMOUNT_TO_RAISE: u64 = 3; +/// Number of seconds in a day, used to convert the campaign duration to days. +pub const SECONDS_TO_DAYS: i64 = 86_400; +/// A single contributor may supply at most this percentage of the target. +pub const MAX_CONTRIBUTION_PERCENTAGE: u64 = 10; +/// Denominator for the percentage math above. +pub const PERCENTAGE_SCALER: u64 = 100; + +/// Reads a little-endian `u64` starting at `offset` within `data`. +pub(crate) fn read_u64(data: &[u8], offset: usize) -> Result { + let bytes: [u8; 8] = data + .get(offset..offset + 8) + .ok_or(ProgramError::InvalidInstructionData)? + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + Ok(u64::from_le_bytes(bytes)) +} + +/// Maximum amount a single contributor may supply: `MAX_CONTRIBUTION_PERCENTAGE` +/// percent of the campaign target. +pub(crate) fn max_contribution(amount_to_raise: u64) -> u64 { + amount_to_raise + .saturating_mul(MAX_CONTRIBUTION_PERCENTAGE) + .saturating_div(PERCENTAGE_SCALER) +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs new file mode 100644 index 000000000..0eac9170c --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs @@ -0,0 +1,134 @@ +use pinocchio::{ + cpi::{Seed, Signer}, + error::ProgramError, + sysvars::{clock::Clock, Sysvar}, + AccountView, Address, ProgramResult, +}; +use pinocchio_log::log; +use pinocchio_pubkey::derive_address; +use pinocchio_token::{instructions::Transfer, state::TokenAccount}; + +use crate::{ + error::FundraiserError, + instructions::SECONDS_TO_DAYS, + state::{Contributor, Fundraiser}, +}; + +/// Refunds a contributor after a failed fundraiser. +/// +/// Once the campaign has ended without reaching its target, a contributor can +/// reclaim their tokens. Their contributed amount is returned from the vault and +/// the contributor account is closed (its rent returned to the contributor). +/// +/// Accounts: +/// 0. `[signer, writable]` contributor (receives the refund and reclaimed rent) +/// 1. `[]` maker (part of the fundraiser PDA seeds) +/// 2. `[]` mint to raise +/// 3. `[writable]` fundraiser account (PDA) +/// 4. `[writable]` contributor account (PDA, closed here) +/// 5. `[writable]` contributor's token account (receives the refund) +/// 6. `[writable]` vault (fundraiser's token account) +/// 7. `[]` token program +/// 8. `[]` system program +/// +/// Instruction data: `[contributor_bump: u8]` +pub fn refund(program_id: &Address, accounts: &[AccountView], data: &[u8]) -> ProgramResult { + let [contributor, maker, mint_to_raise, fundraiser, contributor_account, contributor_ata, vault, _token_program, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !contributor.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + let contributor_bump = *data.first().ok_or(ProgramError::InvalidInstructionData)?; + + // Load the campaign and confirm the maker, mint and PDA all match. + let mut fundraiser_state = Fundraiser::deserialize(&fundraiser.try_borrow()?)?; + if &fundraiser_state.maker != maker.address().as_array() { + return Err(FundraiserError::InvalidSeeds.into()); + } + if &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() { + return Err(FundraiserError::InvalidMint.into()); + } + let fundraiser_pda = derive_address( + &[Fundraiser::SEED_PREFIX, maker.address().as_ref()], + Some(fundraiser_state.bump), + program_id.as_array(), + ); + if fundraiser.address().as_array() != &fundraiser_pda { + return Err(FundraiserError::InvalidSeeds.into()); + } + + // Confirm the contributor account is the canonical PDA for this contributor. + let contributor_pda = derive_address( + &[ + Contributor::SEED_PREFIX, + fundraiser.address().as_ref(), + contributor.address().as_ref(), + ], + Some(contributor_bump), + program_id.as_array(), + ); + if contributor_account.address().as_array() != &contributor_pda { + return Err(FundraiserError::InvalidSeeds.into()); + } + + // Refunds are only allowed once the campaign has ended, i.e. after + // `duration` days have elapsed since it started. + let elapsed_days = + ((Clock::get()?.unix_timestamp - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; + if elapsed_days < fundraiser_state.duration { + return Err(FundraiserError::FundraiserNotEnded.into()); + } + + // The vault must be the canonical vault recorded at initialization. + if vault.address().as_array() != &fundraiser_state.vault { + return Err(FundraiserError::InvalidVault.into()); + } + let vault_amount = TokenAccount::from_account_view(vault)?.amount(); + + // ...and only refund if the target was not met. + if vault_amount >= fundraiser_state.amount_to_raise { + return Err(FundraiserError::TargetMet.into()); + } + + let contributor_amount = Contributor::deserialize(&contributor_account.try_borrow()?)?.amount; + + // Return the contributor's tokens from the vault, signed by the fundraiser PDA. + let bump_bytes = [fundraiser_state.bump]; + let seeds = [ + Seed::from(Fundraiser::SEED_PREFIX), + Seed::from(maker.address().as_ref()), + Seed::from(&bump_bytes), + ]; + let signers = [Signer::from(&seeds)]; + + log!("Refunding contribution from vault"); + Transfer { + from: vault, + to: contributor_ata, + authority: fundraiser, + amount: contributor_amount, + } + .invoke_signed(&signers)?; + + // Reduce the campaign's recorded total. + fundraiser_state.current_amount -= contributor_amount; + fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?; + + // Close the contributor account, returning its rent to the contributor. + log!("Closing contributor account"); + let contributor_lamports = contributor_account.lamports(); + contributor_account.set_lamports(0); + contributor.set_lamports(contributor.lamports() + contributor_lamports); + contributor_account.resize(0)?; + unsafe { + contributor_account.assign(system_program.address()); + } + + log!("Refund completed successfully"); + Ok(()) +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/lib.rs b/tokens/token-fundraiser/pinocchio/program/src/lib.rs new file mode 100644 index 000000000..1e5b927ea --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] + +pub mod error; +pub mod instructions; +pub mod processor; +pub mod state; + +use pinocchio::{entrypoint, nostd_panic_handler}; + +entrypoint!(processor::process_instruction); +nostd_panic_handler!(); diff --git a/tokens/token-fundraiser/pinocchio/program/src/processor.rs b/tokens/token-fundraiser/pinocchio/program/src/processor.rs new file mode 100644 index 000000000..5738c907f --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/processor.rs @@ -0,0 +1,41 @@ +use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; +use pinocchio_log::log; + +use crate::instructions::{check_contributions, contribute, initialize, refund}; + +/// Dispatches an instruction based on its leading discriminator byte. +/// +/// Instruction data layout: `[discriminator: u8, ..args]` +/// - `0` -> Initialize (args: `[amount: u64 (LE), duration: u16 (LE), bump: u8]`) +/// - `1` -> Contribute (args: `[amount: u64 (LE), contributor_bump: u8]`) +/// - `2` -> CheckContributions (no args) +/// - `3` -> Refund (args: `[contributor_bump: u8]`) +pub fn process_instruction( + program_id: &Address, + accounts: &[AccountView], + instruction_data: &[u8], +) -> ProgramResult { + let (discriminator, args) = instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + match *discriminator { + 0 => { + log!("Instruction: Initialize"); + initialize(program_id, accounts, args) + } + 1 => { + log!("Instruction: Contribute"); + contribute(program_id, accounts, args) + } + 2 => { + log!("Instruction: CheckContributions"); + check_contributions(program_id, accounts, args) + } + 3 => { + log!("Instruction: Refund"); + refund(program_id, accounts, args) + } + _ => Err(ProgramError::InvalidInstructionData), + } +} diff --git a/tokens/token-fundraiser/pinocchio/program/src/state.rs b/tokens/token-fundraiser/pinocchio/program/src/state.rs new file mode 100644 index 000000000..86ca58c57 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/state.rs @@ -0,0 +1,113 @@ +use pinocchio::error::ProgramError; + +/// Persistent state for a fundraiser campaign, stored in the fundraiser PDA. +/// +/// The fundraiser PDA is derived from `[b"fundraiser", maker]` and is the +/// authority of the vault token account that custodies contributed tokens until +/// the campaign succeeds (funds go to the maker) or fails (funds are refunded). +/// +/// Serialized byte layout (little-endian), matching the field order below so a +/// Borsh client can deserialize it directly: +/// `[maker: 32][mint_to_raise: 32][vault: 32][amount_to_raise: u64] +/// [current_amount: u64][time_started: i64][duration: u16][bump: u8]` +pub struct Fundraiser { + /// The wallet that created the fundraiser and receives the funds on success. + pub maker: [u8; 32], + /// Mint of the token being raised. + pub mint_to_raise: [u8; 32], + /// The vault (the fundraiser PDA's associated token account) that holds the + /// raised funds. Recorded at initialization — where the associated token + /// program guarantees it is the canonical ATA — so later instructions can + /// reject any other token account passed in its place. + pub vault: [u8; 32], + /// Target amount (in base units) the campaign wants to raise. + pub amount_to_raise: u64, + /// Amount contributed so far. + pub current_amount: u64, + /// Unix timestamp at which the campaign started. + pub time_started: i64, + /// Campaign duration, in days. + pub duration: u16, + /// Canonical bump for the fundraiser PDA. + pub bump: u8, +} + +impl Fundraiser { + /// Seed prefix for the fundraiser PDA: `[SEED_PREFIX, maker]`. + pub const SEED_PREFIX: &'static [u8] = b"fundraiser"; + + /// Serialized size of a `Fundraiser` in bytes. + pub const LEN: usize = 32 + 32 + 32 + 8 + 8 + 8 + 2 + 1; + + /// Writes the fundraiser into `dst` using the layout documented above. + pub fn serialize(&self, dst: &mut [u8]) -> Result<(), ProgramError> { + let dst = dst + .get_mut(..Self::LEN) + .ok_or(ProgramError::AccountDataTooSmall)?; + dst[0..32].copy_from_slice(&self.maker); + dst[32..64].copy_from_slice(&self.mint_to_raise); + dst[64..96].copy_from_slice(&self.vault); + dst[96..104].copy_from_slice(&self.amount_to_raise.to_le_bytes()); + dst[104..112].copy_from_slice(&self.current_amount.to_le_bytes()); + dst[112..120].copy_from_slice(&self.time_started.to_le_bytes()); + dst[120..122].copy_from_slice(&self.duration.to_le_bytes()); + dst[122] = self.bump; + Ok(()) + } + + /// Reads a fundraiser from `src`, which must be at least [`Fundraiser::LEN`] bytes. + pub fn deserialize(src: &[u8]) -> Result { + let src: &[u8; Self::LEN] = src + .get(..Self::LEN) + .and_then(|s| s.try_into().ok()) + .ok_or(ProgramError::InvalidAccountData)?; + Ok(Self { + maker: src[0..32].try_into().unwrap(), + mint_to_raise: src[32..64].try_into().unwrap(), + vault: src[64..96].try_into().unwrap(), + amount_to_raise: u64::from_le_bytes(src[96..104].try_into().unwrap()), + current_amount: u64::from_le_bytes(src[104..112].try_into().unwrap()), + time_started: i64::from_le_bytes(src[112..120].try_into().unwrap()), + duration: u16::from_le_bytes(src[120..122].try_into().unwrap()), + bump: src[122], + }) + } +} + +/// Per-contributor record, stored in the contributor PDA derived from +/// `[b"contributor", fundraiser, contributor]`. Tracks how much the contributor +/// has put in so it can be refunded if the campaign fails. +/// +/// Serialized byte layout (little-endian): `[amount: u64]`. +pub struct Contributor { + /// Total amount this contributor has supplied. + pub amount: u64, +} + +impl Contributor { + /// Seed prefix for the contributor PDA: `[SEED_PREFIX, fundraiser, contributor]`. + pub const SEED_PREFIX: &'static [u8] = b"contributor"; + + /// Serialized size of a `Contributor` in bytes. + pub const LEN: usize = 8; + + /// Writes the contributor into `dst` using the layout documented above. + pub fn serialize(&self, dst: &mut [u8]) -> Result<(), ProgramError> { + let dst = dst + .get_mut(..Self::LEN) + .ok_or(ProgramError::AccountDataTooSmall)?; + dst[0..8].copy_from_slice(&self.amount.to_le_bytes()); + Ok(()) + } + + /// Reads a contributor from `src`, which must be at least [`Contributor::LEN`] bytes. + pub fn deserialize(src: &[u8]) -> Result { + let src: &[u8; Self::LEN] = src + .get(..Self::LEN) + .and_then(|s| s.try_into().ok()) + .ok_or(ProgramError::InvalidAccountData)?; + Ok(Self { + amount: u64::from_le_bytes(src[0..8].try_into().unwrap()), + }) + } +} diff --git a/tokens/token-fundraiser/pinocchio/tests/account.ts b/tokens/token-fundraiser/pinocchio/tests/account.ts new file mode 100644 index 000000000..a465a94e6 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/account.ts @@ -0,0 +1,37 @@ +import * as borsh from "borsh"; + +// Matches the on-chain `Fundraiser` byte layout (see program/src/state.rs). +export const FundraiserSchema = { + struct: { + maker: { array: { type: "u8", len: 32 } }, + mint_to_raise: { array: { type: "u8", len: 32 } }, + vault: { array: { type: "u8", len: 32 } }, + amount_to_raise: "u64", + current_amount: "u64", + time_started: "i64", + duration: "u16", + bump: "u8", + }, +}; + +export type FundraiserRaw = { + maker: Uint8Array; + mint_to_raise: Uint8Array; + vault: Uint8Array; + amount_to_raise: bigint; + current_amount: bigint; + time_started: bigint; + duration: number; + bump: number; +}; + +// Matches the on-chain `Contributor` byte layout (see program/src/state.rs). +export const ContributorSchema = { + struct: { + amount: "u64", + }, +}; + +export type ContributorRaw = { + amount: bigint; +}; diff --git a/tokens/token-fundraiser/pinocchio/tests/instruction.ts b/tokens/token-fundraiser/pinocchio/tests/instruction.ts new file mode 100644 index 000000000..b506e2824 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/instruction.ts @@ -0,0 +1,173 @@ +import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { type PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; +import type BN from "bn.js"; +import * as borsh from "borsh"; + +enum FundraiserInstruction { + Initialize = 0, + Contribute = 1, + CheckContributions = 2, + Refund = 3, +} + +// The Pinocchio program receives PDA bumps in the instruction data (and stores +// the fundraiser bump) instead of deriving them on-chain. +const InitializeSchema = { + struct: { + instruction: "u8", + amount: "u64", + duration: "u16", + bump: "u8", + }, +}; + +const ContributeSchema = { + struct: { + instruction: "u8", + amount: "u64", + contributor_bump: "u8", + }, +}; + +const CheckContributionsSchema = { + struct: { + instruction: "u8", + }, +}; + +const RefundSchema = { + struct: { + instruction: "u8", + contributor_bump: "u8", + }, +}; + +function borshSerialize(schema: borsh.Schema, data: object): Buffer { + return Buffer.from(borsh.serialize(schema, data)); +} + +export function buildInitialize(props: { + amount: BN; + duration: number; + bump: number; + maker: PublicKey; + mint: PublicKey; + fundraiser: PublicKey; + vault: PublicKey; + programId: PublicKey; +}) { + const data = borshSerialize(InitializeSchema, { + instruction: FundraiserInstruction.Initialize, + amount: props.amount, + duration: props.duration, + bump: props.bump, + }); + + return new TransactionInstruction({ + keys: [ + { pubkey: props.maker, isSigner: true, isWritable: true }, + { pubkey: props.mint, isSigner: false, isWritable: false }, + { pubkey: props.fundraiser, isSigner: false, isWritable: true }, + { pubkey: props.vault, isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + programId: props.programId, + data, + }); +} + +export function buildContribute(props: { + amount: BN; + contributor_bump: number; + contributor: PublicKey; + mint: PublicKey; + fundraiser: PublicKey; + contributorAccount: PublicKey; + contributorAta: PublicKey; + vault: PublicKey; + programId: PublicKey; +}) { + const data = borshSerialize(ContributeSchema, { + instruction: FundraiserInstruction.Contribute, + amount: props.amount, + contributor_bump: props.contributor_bump, + }); + + return new TransactionInstruction({ + keys: [ + { pubkey: props.contributor, isSigner: true, isWritable: true }, + { pubkey: props.mint, isSigner: false, isWritable: false }, + { pubkey: props.fundraiser, isSigner: false, isWritable: true }, + { pubkey: props.contributorAccount, isSigner: false, isWritable: true }, + { pubkey: props.contributorAta, isSigner: false, isWritable: true }, + { pubkey: props.vault, isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + programId: props.programId, + data, + }); +} + +export function buildCheckContributions(props: { + maker: PublicKey; + mint: PublicKey; + fundraiser: PublicKey; + vault: PublicKey; + makerAta: PublicKey; + programId: PublicKey; +}) { + const data = borshSerialize(CheckContributionsSchema, { + instruction: FundraiserInstruction.CheckContributions, + }); + + return new TransactionInstruction({ + keys: [ + { pubkey: props.maker, isSigner: true, isWritable: true }, + { pubkey: props.mint, isSigner: false, isWritable: false }, + { pubkey: props.fundraiser, isSigner: false, isWritable: true }, + { pubkey: props.vault, isSigner: false, isWritable: true }, + { pubkey: props.makerAta, isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + programId: props.programId, + data, + }); +} + +export function buildRefund(props: { + contributor_bump: number; + contributor: PublicKey; + maker: PublicKey; + mint: PublicKey; + fundraiser: PublicKey; + contributorAccount: PublicKey; + contributorAta: PublicKey; + vault: PublicKey; + programId: PublicKey; +}) { + const data = borshSerialize(RefundSchema, { + instruction: FundraiserInstruction.Refund, + contributor_bump: props.contributor_bump, + }); + + return new TransactionInstruction({ + keys: [ + { pubkey: props.contributor, isSigner: true, isWritable: true }, + { pubkey: props.maker, isSigner: false, isWritable: false }, + { pubkey: props.mint, isSigner: false, isWritable: false }, + { pubkey: props.fundraiser, isSigner: false, isWritable: true }, + { pubkey: props.contributorAccount, isSigner: false, isWritable: true }, + { pubkey: props.contributorAta, isSigner: false, isWritable: true }, + { pubkey: props.vault, isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + programId: props.programId, + data, + }); +} diff --git a/tokens/token-fundraiser/pinocchio/tests/test.ts b/tokens/token-fundraiser/pinocchio/tests/test.ts new file mode 100644 index 000000000..251bb00a6 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/test.ts @@ -0,0 +1,369 @@ +import { + ACCOUNT_SIZE, + AccountLayout, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + createInitializeAccount3Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction } from "@solana/web3.js"; +import BN from "bn.js"; +import * as borsh from "borsh"; +import { assert } from "chai"; +import { start } from "solana-bankrun"; +import { type ContributorRaw, ContributorSchema, type FundraiserRaw, FundraiserSchema } from "./account"; +import { buildCheckContributions, buildContribute, buildInitialize, buildRefund } from "./instruction"; +import { expectRevert, fundAccount, mintingTokens } from "./utils"; + +describe("Token Fundraiser (Pinocchio)", async () => { + const programId = PublicKey.unique(); + const context = await start([{ name: "token_fundraiser_pinocchio_program", programId }], []); + const client = context.banksClient; + const payer = context.payer; + // The bankrun payer plays the role of the contributor. + const contributor = payer; + + const maker = Keypair.generate(); + const mintKeypair = Keypair.generate(); + + const [fundraiser, fundraiserBump] = PublicKey.findProgramAddressSync( + [Buffer.from("fundraiser"), maker.publicKey.toBuffer()], + programId, + ); + const [contributorAccount, contributorBump] = PublicKey.findProgramAddressSync( + [Buffer.from("contributor"), fundraiser.toBuffer(), contributor.publicKey.toBuffer()], + programId, + ); + + const vault = getAssociatedTokenAddressSync(mintKeypair.publicKey, fundraiser, true); + const contributorAta = getAssociatedTokenAddressSync(mintKeypair.publicKey, contributor.publicKey); + const makerAta = getAssociatedTokenAddressSync(mintKeypair.publicKey, maker.publicKey); + + const decimals = 6; + const amountToRaise = new BN(30_000_000); // 30 tokens + const duration = 5; // an open, 5-day campaign + + // Fund the maker so it can pay for the fundraiser + vault, and mint tokens to + // the contributor. + await fundAccount(context, maker.publicKey, 5 * LAMPORTS_PER_SOL); + await mintingTokens({ context, holder: contributor, mintKeypair, mintedAmount: 10, decimals }); + + async function sendInstruction(ix, signers: Keypair[]) { + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(...signers); + await client.processTransaction(tx); + } + + async function readTokenAmount(account: PublicKey): Promise { + const info = await client.getAccount(account); + if (info === null) throw new Error("Token account not found"); + return AccountLayout.decode(info.data).amount; + } + + it("Initializes a fundraiser", async () => { + const ix = buildInitialize({ + amount: amountToRaise, + duration, + bump: fundraiserBump, + maker: maker.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + vault, + programId, + }); + + await sendInstruction(ix, [payer, maker]); + + const info = await client.getAccount(fundraiser); + if (info === null) throw new Error("Fundraiser account not found"); + const state = borsh.deserialize(FundraiserSchema, Buffer.from(info.data)) as FundraiserRaw; + + assert.equal(new PublicKey(state.maker).toBase58(), maker.publicKey.toBase58(), "wrong maker"); + assert.equal( + new PublicKey(state.mint_to_raise).toBase58(), + mintKeypair.publicKey.toBase58(), + "wrong mint", + ); + assert.equal(state.amount_to_raise.toString(), amountToRaise.toString(), "wrong target"); + assert.equal(state.current_amount.toString(), "0", "current amount should start at zero"); + assert.equal(state.duration, duration, "wrong duration"); + assert.equal(state.bump, fundraiserBump, "wrong bump"); + + // The vault exists and starts empty. + assert.equal((await readTokenAmount(vault)).toString(), "0", "vault should start empty"); + }); + + it("Accepts contributions while the campaign is open", async () => { + for (let i = 0; i < 2; i++) { + const ix = buildContribute({ + amount: new BN(1_000_000), + contributor_bump: contributorBump, + contributor: contributor.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + contributorAccount, + contributorAta, + vault, + programId, + }); + await sendInstruction(ix, [payer]); + } + + assert.equal((await readTokenAmount(vault)).toString(), "2000000", "vault should hold both contributions"); + + const info = await client.getAccount(contributorAccount); + if (info === null) throw new Error("Contributor account not found"); + const state = borsh.deserialize(ContributorSchema, Buffer.from(info.data)) as ContributorRaw; + assert.equal(state.amount.toString(), "2000000", "contributor total should be tracked"); + + const fundraiserInfo = await client.getAccount(fundraiser); + if (fundraiserInfo === null) throw new Error("Fundraiser account not found"); + const fundraiserState = borsh.deserialize(FundraiserSchema, Buffer.from(fundraiserInfo.data)) as FundraiserRaw; + assert.equal(fundraiserState.current_amount.toString(), "2000000", "fundraiser total should be tracked"); + }); + + it("Rejects a contribution whose vault is not owned by the fundraiser", async () => { + // Passing the contributor's own token account as the vault must be rejected; + // otherwise the contributor could keep their tokens while still inflating the + // recorded total and later drain the real vault via a refund. + const ix = buildContribute({ + amount: new BN(1_000_000), + contributor_bump: contributorBump, + contributor: contributor.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + contributorAccount, + contributorAta, + vault: contributorAta, // not the fundraiser's vault + programId, + }); + + await expectRevert(sendInstruction(ix, [payer])); + }); + + it("Rejects a contribution above the per-contributor maximum", async () => { + // The contributor is already at 2_000_000; the cap is 10% of 30_000_000 = + // 3_000_000, so another 2_000_000 must be rejected. + const ix = buildContribute({ + amount: new BN(2_000_000), + contributor_bump: contributorBump, + contributor: contributor.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + contributorAccount, + contributorAta, + vault, + programId, + }); + + await expectRevert(sendInstruction(ix, [payer])); + }); + + it("Rejects settling before the target is met", async () => { + const ix = buildCheckContributions({ + maker: maker.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + vault, + makerAta, + programId, + }); + + await expectRevert(sendInstruction(ix, [payer, maker])); + }); + + it("Rejects settling against a non-canonical fundraiser-owned vault", async () => { + // A malicious maker could create a token account they pre-fund and assign to + // the fundraiser PDA as its owner. It must not be accepted in place of the + // canonical vault recorded at initialization — otherwise the maker could + // settle with the fake vault, close the fundraiser, and strand the real + // contributions. + const fakeVault = Keypair.generate(); + const rent = await client.getRent(); + const lamports = Number(rent.minimumBalance(BigInt(ACCOUNT_SIZE))); + + const setupTx = new Transaction(); + setupTx.recentBlockhash = context.lastBlockhash; + setupTx + .add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: fakeVault.publicKey, + space: ACCOUNT_SIZE, + lamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeAccount3Instruction(fakeVault.publicKey, mintKeypair.publicKey, fundraiser, TOKEN_PROGRAM_ID), + createMintToInstruction(mintKeypair.publicKey, fakeVault.publicKey, payer.publicKey, 30_000_000, [], TOKEN_PROGRAM_ID), + ) + .sign(payer, fakeVault); + await client.processTransaction(setupTx); + + const ix = buildCheckContributions({ + maker: maker.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + vault: fakeVault.publicKey, + makerAta, + programId, + }); + + await expectRevert(sendInstruction(ix, [payer, maker])); + }); + + it("Rejects a refund while the campaign is still open", async () => { + // The 5-day campaign has just started, so a refund must be rejected. + const ix = buildRefund({ + contributor_bump: contributorBump, + contributor: contributor.publicKey, + maker: maker.publicKey, + mint: mintKeypair.publicKey, + fundraiser, + contributorAccount, + contributorAta, + vault, + programId, + }); + + await expectRevert(sendInstruction(ix, [payer])); + }); + + it("Rejects a contribution after the campaign has ended", async () => { + // A second fundraiser created with duration = 0 is already over the moment + // it is initialized, so any contribution must be rejected. + const maker2 = Keypair.generate(); + await fundAccount(context, maker2.publicKey, 5 * LAMPORTS_PER_SOL); + + const [endedFundraiser, endedBump] = PublicKey.findProgramAddressSync( + [Buffer.from("fundraiser"), maker2.publicKey.toBuffer()], + programId, + ); + const endedVault = getAssociatedTokenAddressSync(mintKeypair.publicKey, endedFundraiser, true); + const [endedContributor, endedContributorBump] = PublicKey.findProgramAddressSync( + [Buffer.from("contributor"), endedFundraiser.toBuffer(), contributor.publicKey.toBuffer()], + programId, + ); + + await sendInstruction( + buildInitialize({ + amount: amountToRaise, + duration: 0, + bump: endedBump, + maker: maker2.publicKey, + mint: mintKeypair.publicKey, + fundraiser: endedFundraiser, + vault: endedVault, + programId, + }), + [payer, maker2], + ); + + const ix = buildContribute({ + amount: new BN(1_000_000), + contributor_bump: endedContributorBump, + contributor: contributor.publicKey, + mint: mintKeypair.publicKey, + fundraiser: endedFundraiser, + contributorAccount: endedContributor, + contributorAta, + vault: endedVault, + programId, + }); + + await expectRevert(sendInstruction(ix, [payer])); + }); + + it("Settles a fully-funded campaign and reclaims rent", async () => { + // A fresh campaign funded to its target by 10 contributors (each capped at + // 10% of the target). Settling transfers everything to the maker and closes + // both the vault and the fundraiser account. + const settleMaker = Keypair.generate(); + await fundAccount(context, settleMaker.publicKey, 5 * LAMPORTS_PER_SOL); + + const [settleFundraiser, settleBump] = PublicKey.findProgramAddressSync( + [Buffer.from("fundraiser"), settleMaker.publicKey.toBuffer()], + programId, + ); + const settleVault = getAssociatedTokenAddressSync(mintKeypair.publicKey, settleFundraiser, true); + const settleMakerAta = getAssociatedTokenAddressSync(mintKeypair.publicKey, settleMaker.publicKey); + + await sendInstruction( + buildInitialize({ + amount: amountToRaise, // 30_000_000 + duration: 5, + bump: settleBump, + maker: settleMaker.publicKey, + mint: mintKeypair.publicKey, + fundraiser: settleFundraiser, + vault: settleVault, + programId, + }), + [payer, settleMaker], + ); + + // 10 contributors x 3_000_000 (the per-contributor cap) == the 30_000_000 target. + for (let i = 0; i < 10; i++) { + const c = Keypair.generate(); + await fundAccount(context, c.publicKey, 1 * LAMPORTS_PER_SOL); + const cAta = getAssociatedTokenAddressSync(mintKeypair.publicKey, c.publicKey); + + const fundTx = new Transaction(); + fundTx.recentBlockhash = context.lastBlockhash; + fundTx + .add( + createAssociatedTokenAccountIdempotentInstruction( + payer.publicKey, + cAta, + c.publicKey, + mintKeypair.publicKey, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + createMintToInstruction(mintKeypair.publicKey, cAta, payer.publicKey, 3_000_000, [], TOKEN_PROGRAM_ID), + ) + .sign(payer); + await client.processTransaction(fundTx); + + const [cAccount, cBump] = PublicKey.findProgramAddressSync( + [Buffer.from("contributor"), settleFundraiser.toBuffer(), c.publicKey.toBuffer()], + programId, + ); + await sendInstruction( + buildContribute({ + amount: new BN(3_000_000), + contributor_bump: cBump, + contributor: c.publicKey, + mint: mintKeypair.publicKey, + fundraiser: settleFundraiser, + contributorAccount: cAccount, + contributorAta: cAta, + vault: settleVault, + programId, + }), + [payer, c], + ); + } + + assert.equal((await readTokenAmount(settleVault)).toString(), "30000000", "vault should hold the full target"); + + await sendInstruction( + buildCheckContributions({ + maker: settleMaker.publicKey, + mint: mintKeypair.publicKey, + fundraiser: settleFundraiser, + vault: settleVault, + makerAta: settleMakerAta, + programId, + }), + [payer, settleMaker], + ); + + // The maker received the full raised amount, and the vault + fundraiser are closed. + assert.equal((await readTokenAmount(settleMakerAta)).toString(), "30000000", "maker should receive the raised funds"); + assert.equal(await client.getAccount(settleVault), null, "vault should be closed after settlement"); + assert.equal(await client.getAccount(settleFundraiser), null, "fundraiser should be closed after settlement"); + }); +}); diff --git a/tokens/token-fundraiser/pinocchio/tests/utils.ts b/tokens/token-fundraiser/pinocchio/tests/utils.ts new file mode 100644 index 000000000..dd14e0972 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/utils.ts @@ -0,0 +1,102 @@ +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { Keypair, PublicKey, type Signer, SystemProgram, Transaction } from "@solana/web3.js"; +import BN from "bn.js"; +import type { ProgramTestContext } from "solana-bankrun"; + +export const expectRevert = async (promise: Promise) => { + let reverted = false; + try { + await promise; + } catch { + reverted = true; + } + if (!reverted) { + throw new Error("Expected a revert, but the transaction succeeded"); + } +}; + +// Transfers SOL from the bankrun payer to another account so it can fund account +// creation and pay fees. +export const fundAccount = async (context: ProgramTestContext, recipient: PublicKey, lamports: number) => { + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: context.payer.publicKey, + toPubkey: recipient, + lamports, + }), + ); + transaction.recentBlockhash = context.lastBlockhash; + transaction.sign(context.payer); + await context.banksClient.processTransaction(transaction); +}; + +// Creates a mint, gives `holder` an associated token account, and mints +// `mintedAmount` whole tokens into it. +export const mintingTokens = async ({ + context, + holder, + mintKeypair, + mintedAmount = 100, + decimals = 6, +}: { + context: ProgramTestContext; + holder: Signer; + mintKeypair: Keypair; + mintedAmount?: number; + decimals?: number; +}) => { + const rent = await context.banksClient.getRent(); + const lamports = rent.minimumBalance(BigInt(MINT_SIZE)); + + const createMintTx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: context.payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: MINT_SIZE, + lamports: new BN(lamports.toString()).toNumber(), + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + mintKeypair.publicKey, + decimals, + context.payer.publicKey, + context.payer.publicKey, + TOKEN_PROGRAM_ID, + ), + ); + createMintTx.recentBlockhash = context.lastBlockhash; + createMintTx.sign(context.payer, mintKeypair); + await context.banksClient.processTransaction(createMintTx); + + const holderAta = getAssociatedTokenAddressSync(mintKeypair.publicKey, holder.publicKey, true); + + const mintToTx = new Transaction().add( + createAssociatedTokenAccountIdempotentInstruction( + context.payer.publicKey, + holderAta, + holder.publicKey, + mintKeypair.publicKey, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + createMintToInstruction( + mintKeypair.publicKey, + holderAta, + context.payer.publicKey, + mintedAmount * 10 ** decimals, + [], + TOKEN_PROGRAM_ID, + ), + ); + mintToTx.recentBlockhash = context.lastBlockhash; + mintToTx.sign(context.payer); + await context.banksClient.processTransaction(mintToTx); +}; diff --git a/tokens/token-fundraiser/pinocchio/tsconfig.json b/tokens/token-fundraiser/pinocchio/tsconfig.json new file mode 100644 index 000000000..8c20b2236 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai", "node"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +}