From fb1330b11cfc2475d7b9e589c78c77c37103b9d8 Mon Sep 17 00:00:00 2001 From: MarkFeder <5670736+MarkFeder@users.noreply.github.com> Date: Sun, 28 Jun 2026 21:54:51 +0200 Subject: [PATCH 1/2] add pinocchio token-fundraiser example --- Cargo.lock | 12 + Cargo.toml | 1 + README.md | 2 +- tokens/token-fundraiser/pinocchio/cicd.sh | 8 + .../token-fundraiser/pinocchio/package.json | 25 + .../token-fundraiser/pinocchio/pnpm-lock.yaml | 1514 +++++++++++++++++ .../pinocchio/program/Cargo.toml | 22 + .../pinocchio/program/src/constants.rs | 14 + .../pinocchio/program/src/error.rs | 33 + .../src/instructions/check_contributions.rs | 106 ++ .../program/src/instructions/contribute.rs | 157 ++ .../program/src/instructions/initialize.rs | 114 ++ .../pinocchio/program/src/instructions/mod.rs | 31 + .../program/src/instructions/refund.rs | 124 ++ .../pinocchio/program/src/lib.rs | 12 + .../pinocchio/program/src/processor.rs | 41 + .../pinocchio/program/src/state.rs | 119 ++ .../pinocchio/tests/account.ts | 35 + .../pinocchio/tests/instruction.ts | 164 ++ .../token-fundraiser/pinocchio/tests/test.ts | 225 +++ .../token-fundraiser/pinocchio/tests/utils.ts | 143 ++ .../token-fundraiser/pinocchio/tsconfig.json | 10 + 22 files changed, 2911 insertions(+), 1 deletion(-) create mode 100644 tokens/token-fundraiser/pinocchio/cicd.sh create mode 100644 tokens/token-fundraiser/pinocchio/package.json create mode 100644 tokens/token-fundraiser/pinocchio/pnpm-lock.yaml create mode 100644 tokens/token-fundraiser/pinocchio/program/Cargo.toml create mode 100644 tokens/token-fundraiser/pinocchio/program/src/constants.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/error.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/lib.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/processor.rs create mode 100644 tokens/token-fundraiser/pinocchio/program/src/state.rs create mode 100644 tokens/token-fundraiser/pinocchio/tests/account.ts create mode 100644 tokens/token-fundraiser/pinocchio/tests/instruction.ts create mode 100644 tokens/token-fundraiser/pinocchio/tests/test.ts create mode 100644 tokens/token-fundraiser/pinocchio/tests/utils.ts create mode 100644 tokens/token-fundraiser/pinocchio/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 37971fded..52a990577 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,6 +1586,18 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "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 = "generic-array" version = "0.14.7" 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 2b1dc0458..671be16d5 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..9e82e241f --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/pnpm-lock.yaml @@ -0,0 +1,1514 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@solana/spl-token': + specifier: ^0.4.9 + version: 0.4.9(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10) + '@solana/web3.js': + specifier: ^1.98.4 + version: 1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + bn.js: + specifier: ^5.2.2 + version: 5.2.2 + devDependencies: + '@types/bn.js': + specifier: ^5.1.0 + version: 5.1.6 + '@types/chai': + specifier: ^4.3.1 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.9 + version: 10.0.9 + '@types/node': + specifier: ^22.8.6 + version: 22.8.6 + 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.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + ts-mocha: + specifier: ^10.0.0 + version: 10.0.0(mocha@10.8.2) + typescript: + specifier: ^5 + version: 5.6.3 + +packages: + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@noble/curves@1.6.0': + resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.5.0': + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + 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.9': + resolution: {integrity: sha512-g3wbj4F4gq82YQlwqhPB0gHFXfgsC6UmyGMxtSLf/BozT/oKd59465DbnlUK8L8EcimKMavxsVAMoLcEdeCicg==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + + '@types/bn.js@5.1.6': + resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} + + '@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.9': + resolution: {integrity: sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@22.8.6': + resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==} + + '@types/uuid@8.3.4': + resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + 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.10: + resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} + + 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.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + 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.2: + resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} + + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + + borsh@2.0.0: + resolution: {integrity: sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + 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.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} + 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.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + 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.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + 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.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + 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.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + 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.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + 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: Glob versions prior to v9 are no longer supported + + 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.1.2: + resolution: {integrity: sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA==} + engines: {node: '>=8'} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + 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 + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + 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.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + 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.2: + resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} + 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.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + 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'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rpc-websockets@9.0.4: + resolution: {integrity: sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ==} + + 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'} + + 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==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + 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.0.0: + resolution: {integrity: sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==} + 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 + + 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.0: + resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + 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.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + 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.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + 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.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@noble/curves@1.6.0': + dependencies: + '@noble/hashes': 1.5.0 + + '@noble/hashes@1.5.0': {} + + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + bigint-buffer: 1.1.5 + bignumber.js: 9.1.2 + 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.6.3)': + dependencies: + '@solana/errors': 2.0.0-rc.1(typescript@5.6.3) + typescript: 5.6.3 + + '@solana/codecs-core@2.3.0(typescript@5.6.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.6.3) + typescript: 5.6.3 + + '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.6.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.6.3) + typescript: 5.6.3 + + '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.6.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.6.3) + typescript: 5.6.3 + + '@solana/codecs-numbers@2.3.0(typescript@5.6.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.6.3) + '@solana/errors': 2.3.0(typescript@5.6.3) + typescript: 5.6.3 + + '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.6.3) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.6.3 + + '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/errors@2.0.0-rc.1(typescript@5.6.3)': + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + typescript: 5.6.3 + + '@solana/errors@2.3.0(typescript@5.6.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.6.3 + + '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token@0.4.9(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + + '@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.26.0 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.6.3) + agentkeepalive: 4.5.0 + bn.js: 5.2.2 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + node-fetch: 2.7.0 + rpc-websockets: 9.0.4 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@swc/helpers@0.5.13': + dependencies: + tslib: 2.8.0 + + '@types/bn.js@5.1.6': + dependencies: + '@types/node': 22.8.6 + + '@types/chai@4.3.20': {} + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.8.6 + + '@types/json5@0.0.29': + optional: true + + '@types/mocha@10.0.9': {} + + '@types/node@12.20.55': {} + + '@types/node@22.8.6': + dependencies: + undici-types: 6.19.8 + + '@types/uuid@8.3.4': {} + + '@types/ws@7.4.7': + dependencies: + '@types/node': 22.8.6 + + '@types/ws@8.5.12': + dependencies: + '@types/node': 22.8.6 + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + agentkeepalive@4.5.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.1 + + argparse@2.0.1: {} + + arrify@1.0.1: {} + + assertion-error@1.1.0: {} + + balanced-match@1.0.2: {} + + base-x@3.0.10: + dependencies: + safe-buffer: 5.2.1 + + base64-js@1.5.1: {} + + bigint-buffer@1.1.5: + dependencies: + bindings: 1.5.0 + + bignumber.js@9.1.2: {} + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bn.js@5.2.2: {} + + borsh@0.7.0: + dependencies: + bn.js: 5.2.2 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + + borsh@2.0.0: {} + + brace-expansion@2.0.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.10 + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.0.8: + dependencies: + node-gyp-build: 4.8.2 + 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.3.0: {} + + chalk@5.6.2: {} + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + chokidar@3.5.3: + 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.3.7(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.0: {} + + diff@5.2.0: {} + + 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.1: {} + + 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.6 + 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.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + dependencies: + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + + jayson@4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + JSONStream: 1.3.5 + 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.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + json-stringify-safe: 5.0.1 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-stringify-safe@5.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + optional: true + + jsonparse@1.3.1: {} + + 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.6: + dependencies: + brace-expansion: 2.0.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.5.3 + debug: 4.3.7(supports-color@8.1.1) + diff: 5.2.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.1.6 + 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.0 + 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.2: + 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.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regenerator-runtime@0.14.1: {} + + require-directory@2.1.1: {} + + rpc-websockets@9.0.4: + dependencies: + '@swc/helpers': 0.5.13 + '@types/uuid': 8.3.4 + '@types/ws': 8.5.12 + buffer: 6.0.3 + eventemitter3: 5.0.1 + uuid: 8.3.2 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + + 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.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10): + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10) + 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: {} + + 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: {} + + through@2.3.8: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-mocha@10.0.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.0 + 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.0: {} + + type-detect@4.1.0: {} + + typescript@5.6.3: {} + + undici-types@6.19.8: {} + + utf-8-validate@5.0.10: + dependencies: + node-gyp-build: 4.8.2 + optional: true + + 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.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + + ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + + 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.0: + 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..930794ec3 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "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/constants.rs b/tokens/token-fundraiser/pinocchio/program/src/constants.rs new file mode 100644 index 000000000..73daf3586 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/constants.rs @@ -0,0 +1,14 @@ +//! Shared fundraiser parameters, mirroring the native/Anchor example. + +/// Minimum target amount a fundraiser may set, scaled by the mint's decimals: +/// the effective minimum is `MIN_AMOUNT_TO_RAISE.pow(decimals)`. +pub const MIN_AMOUNT_TO_RAISE: u64 = 3; + +/// Seconds in a day, used to convert the elapsed time into whole days. +pub const SECONDS_TO_DAYS: i64 = 86400; + +/// Largest share of the target a single contributor may provide, as a percent. +pub const MAX_CONTRIBUTION_PERCENTAGE: u64 = 10; + +/// Denominator used together with [`MAX_CONTRIBUTION_PERCENTAGE`]. +pub const PERCENTAGE_SCALER: u64 = 100; 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..e51d8154c --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/error.rs @@ -0,0 +1,33 @@ +//! Program-specific errors, mirroring the Anchor example's `FundraiserError`. + +use pinocchio::error::ProgramError; + +/// Errors returned by the fundraiser program. +/// +/// Each variant maps to a [`ProgramError::Custom`] code equal to its position +/// in this enum, matching the order of the Anchor `#[error_code]` enum. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FundraiserError { + /// The target amount has not been met yet. + TargetNotMet, + /// The target amount has already been met. + 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. + FundraiserNotEnded, + /// The fundraiser has already ended. + FundraiserEnded, + /// The target amount is invalid (must be at least `3^decimals`). + InvalidAmount, +} + +impl From for ProgramError { + fn from(e: FundraiserError) -> Self { + ProgramError::Custom(e 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..d28728f51 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs @@ -0,0 +1,106 @@ +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::Transfer, state::TokenAccount}; + +use crate::{error::FundraiserError, state::Fundraiser}; + +/// Settles a successful fundraiser: once the target is met, the vault is drained +/// to the maker and the fundraiser account is closed. +/// +/// Accounts: +/// 0. `[signer, writable]` maker (receives the funds and the reclaimed rent) +/// 1. `[]` mint to raise +/// 2. `[writable]` fundraiser account (PDA `[b"fundraiser", maker]`, closed here) +/// 3. `[writable]` vault (fundraiser PDA's token account, drained) +/// 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 fundraiser and confirm it is the genuine PDA for this maker. + let fundraiser_state = Fundraiser::deserialize(&fundraiser.try_borrow()?)?; + 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(ProgramError::InvalidSeeds); + } + if &fundraiser_state.maker != maker.address().as_array() + || &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() + { + return Err(ProgramError::InvalidAccountData); + } + + // The target amount must have been reached. + let vault_amount = TokenAccount::from_account_view(vault)?.amount(); + 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 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 completed 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..06af53fc9 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs @@ -0,0 +1,157 @@ +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::{ + constants::{MAX_CONTRIBUTION_PERCENTAGE, PERCENTAGE_SCALER, SECONDS_TO_DAYS}, + error::FundraiserError, + instructions::read_u64, + state::{Contributor, Fundraiser}, +}; + +/// Contributes `amount` of the target token into the fundraiser vault, creating +/// the per-contributor record on first use. +/// +/// Accounts: +/// 0. `[signer, writable]` contributor (pays for the contributor account; source authority) +/// 1. `[]` mint to raise +/// 2. `[writable]` fundraiser account (PDA `[b"fundraiser", maker]`) +/// 3. `[writable]` contributor account (PDA `[b"contributor", fundraiser, contributor]`, created if needed) +/// 4. `[writable]` contributor's token account (source of the deposit) +/// 5. `[writable]` vault (fundraiser PDA's token account) +/// 6. `[]` token program +/// 7. `[]` system program +/// +/// Instruction data: `[amount: u64 (LE), bump: u8]` where `bump` is the +/// contributor PDA bump. +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 fundraiser and confirm it is the genuine PDA for the recorded maker. + let mut fundraiser_state = Fundraiser::deserialize(&fundraiser.try_borrow()?)?; + 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(ProgramError::InvalidSeeds); + } + if &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() { + return Err(ProgramError::InvalidAccountData); + } + + // A contribution must be at least one base unit. + if amount < 1 { + return Err(FundraiserError::ContributionTooSmall.into()); + } + + // No single contribution may exceed the per-contributor cap. + let max_contribution = fundraiser_state + .amount_to_raise + .checked_mul(MAX_CONTRIBUTION_PERCENTAGE) + .ok_or(ProgramError::ArithmeticOverflow)? + / PERCENTAGE_SCALER; + if amount > max_contribution { + return Err(FundraiserError::ContributionTooBig.into()); + } + + // The fundraiser must still be within its active window. + let current_time = Clock::get()?.unix_timestamp; + let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; + if fundraiser_state.duration > elapsed_days { + return Err(FundraiserError::FundraiserEnded.into()); + } + + // Verify the contributor PDA and load (or initialize) the running total. + 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(ProgramError::InvalidSeeds); + } + + 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 running total must not exceed the cap. + let new_amount = previous_amount + .checked_add(amount) + .ok_or(ProgramError::ArithmeticOverflow)?; + if previous_amount > max_contribution || new_amount > max_contribution { + return Err(FundraiserError::MaximumContributionsReached.into()); + } + + // Move the contributor's tokens into the vault. + log!("Transferring contribution into vault"); + Transfer { + from: contributor_ata, + to: vault, + authority: contributor, + amount, + } + .invoke()?; + + // Update the running totals. + fundraiser_state.current_amount = fundraiser_state + .current_amount + .checked_add(amount) + .ok_or(ProgramError::ArithmeticOverflow)?; + fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?; + + let contributor_state = Contributor { + amount: new_amount, + bump: contributor_bump, + }; + 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..c1f695210 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs @@ -0,0 +1,114 @@ +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::{ + constants::MIN_AMOUNT_TO_RAISE, + error::FundraiserError, + instructions::{read_u16, read_u64}, + state::Fundraiser, +}; + +/// Creates a fundraiser: records the target mint and amount and opens a vault +/// (the fundraiser PDA's associated token account) to collect contributions. +/// +/// Accounts: +/// 0. `[signer, writable]` maker (pays for the new accounts; the fundraiser authority) +/// 1. `[]` mint to raise +/// 2. `[writable]` fundraiser account (PDA `[b"fundraiser", maker]`, created here) +/// 3. `[writable]` vault (fundraiser PDA'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 = read_u16(data, 8)?; + let bump = *data.get(10).ok_or(ProgramError::InvalidInstructionData)?; + + // The target must clear the decimals-scaled minimum (`3^decimals`). + let decimals = Mint::from_account_view(mint_to_raise)?.decimals(); + let min_amount = MIN_AMOUNT_TO_RAISE + .checked_pow(decimals as u32) + .ok_or(FundraiserError::InvalidAmount)?; + if amount < min_amount { + return Err(FundraiserError::InvalidAmount.into()); + } + + // Verify the supplied fundraiser account is the canonical PDA. + 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(ProgramError::InvalidSeeds); + } + + // 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 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 fundraiser terms. + let fundraiser_state = Fundraiser { + maker: *maker.address().as_array(), + mint_to_raise: *mint_to_raise.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 created 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..a95717e33 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs @@ -0,0 +1,31 @@ +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::*; + +/// 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)) +} + +/// Reads a little-endian `u16` starting at `offset` within `data`. +pub(crate) fn read_u16(data: &[u8], offset: usize) -> Result { + let bytes: [u8; 2] = data + .get(offset..offset + 2) + .ok_or(ProgramError::InvalidInstructionData)? + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + Ok(u16::from_le_bytes(bytes)) +} 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..cba1c0f20 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs @@ -0,0 +1,124 @@ +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::{ + constants::SECONDS_TO_DAYS, + error::FundraiserError, + state::{Contributor, Fundraiser}, +}; + +/// Refunds a contributor after a fundraiser ends without meeting its target: +/// returns the contributor's deposit and closes the contributor account. +/// +/// 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 `[b"fundraiser", maker]`) +/// 4. `[writable]` contributor account (PDA `[b"contributor", fundraiser, contributor]`, closed here) +/// 5. `[writable]` contributor's token account (receives the refund) +/// 6. `[writable]` vault (fundraiser PDA's token account) +/// 7. `[]` token program +/// 8. `[]` system program +/// +/// Instruction data: none. +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); + } + + // Load the fundraiser and confirm it is the genuine PDA for this maker. + let mut fundraiser_state = Fundraiser::deserialize(&fundraiser.try_borrow()?)?; + 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(ProgramError::InvalidSeeds); + } + if &fundraiser_state.maker != maker.address().as_array() + || &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() + { + return Err(ProgramError::InvalidAccountData); + } + + // The fundraiser must have ended. + let current_time = Clock::get()?.unix_timestamp; + let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; + if fundraiser_state.duration < elapsed_days { + return Err(FundraiserError::FundraiserNotEnded.into()); + } + + // Refunds are only possible when the target was not met. + let vault_amount = TokenAccount::from_account_view(vault)?.amount(); + if vault_amount >= fundraiser_state.amount_to_raise { + return Err(FundraiserError::TargetMet.into()); + } + + // Load the contributor record and verify its PDA. + let contributor_state = Contributor::deserialize(&contributor_account.try_borrow()?)?; + let contributor_pda = derive_address( + &[ + Contributor::SEED_PREFIX, + fundraiser.address().as_ref(), + contributor.address().as_ref(), + ], + Some(contributor_state.bump), + program_id.as_array(), + ); + if contributor_account.address().as_array() != &contributor_pda { + return Err(ProgramError::InvalidSeeds); + } + + // Return the contributor's deposit, 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"); + Transfer { + from: vault, + to: contributor_ata, + authority: fundraiser, + amount: contributor_state.amount, + } + .invoke_signed(&signers)?; + + // Reduce the fundraiser's running total by the refunded amount. + fundraiser_state.current_amount = fundraiser_state + .current_amount + .checked_sub(contributor_state.amount) + .ok_or(ProgramError::ArithmeticOverflow)?; + fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?; + + // Close the contributor account, returning its rent to the contributor. + log!("Closing contributor account"); + let contributor_account_lamports = contributor_account.lamports(); + contributor_account.set_lamports(0); + contributor.set_lamports(contributor.lamports() + contributor_account_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..6966c4796 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/lib.rs @@ -0,0 +1,12 @@ +#![no_std] + +pub mod constants; +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..107e65bdc --- /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), bump: u8]`) +/// - `2` -> CheckContributions (no args) +/// - `3` -> Refund (no args) +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..489d771c4 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/program/src/state.rs @@ -0,0 +1,119 @@ +//! On-chain account layouts for the fundraiser program. + +use pinocchio::error::ProgramError; + +/// Persistent record of a fundraiser, stored in the fundraiser PDA. +/// +/// The fundraiser PDA is derived from `[b"fundraiser", maker]` and is the +/// authority of the vault token account that collects contributions. +/// +/// Serialized byte layout (little-endian), matching the field order below so a +/// Borsh client can deserialize it directly: +/// `[maker: 32][mint_to_raise: 32][amount_to_raise: u64][current_amount: u64] +/// [time_started: i64][duration: u16][bump: u8]` +pub struct Fundraiser { + /// The wallet that created the fundraiser; part of the PDA seeds. + pub maker: [u8; 32], + /// Mint of the token being raised. + pub mint_to_raise: [u8; 32], + /// Target amount to raise (in base units of `mint_to_raise`). + pub amount_to_raise: u64, + /// Amount contributed so far. + pub current_amount: u64, + /// Unix timestamp at which the fundraiser was created. + pub time_started: i64, + /// Duration of the fundraiser, 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 + 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..72].copy_from_slice(&self.amount_to_raise.to_le_bytes()); + dst[72..80].copy_from_slice(&self.current_amount.to_le_bytes()); + dst[80..88].copy_from_slice(&self.time_started.to_le_bytes()); + dst[88..90].copy_from_slice(&self.duration.to_le_bytes()); + dst[90] = 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(), + amount_to_raise: u64::from_le_bytes(src[64..72].try_into().unwrap()), + current_amount: u64::from_le_bytes(src[72..80].try_into().unwrap()), + time_started: i64::from_le_bytes(src[80..88].try_into().unwrap()), + duration: u16::from_le_bytes(src[88..90].try_into().unwrap()), + bump: src[90], + }) + } +} + +/// Per-contributor record, stored in the contributor PDA. +/// +/// The contributor PDA is derived from +/// `[b"contributor", fundraiser, contributor]`. +/// +/// Serialized byte layout (little-endian): `[amount: u64][bump: u8]`. +/// +/// Unlike the Anchor example (which stores only `amount` and re-derives the +/// bump), this port persists the bump so [`crate::instructions::refund`] can +/// verify and close the account without re-deriving it on-chain. +pub struct Contributor { + /// Total amount this contributor has deposited. + pub amount: u64, + /// Canonical bump for the contributor PDA. + pub bump: u8, +} + +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 + 1; + + /// Writes the contributor into `dst`. + 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()); + dst[8] = self.bump; + 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()), + bump: src[8], + }) + } +} diff --git a/tokens/token-fundraiser/pinocchio/tests/account.ts b/tokens/token-fundraiser/pinocchio/tests/account.ts new file mode 100644 index 000000000..5bdabfd4c --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/account.ts @@ -0,0 +1,35 @@ +// Mirrors the on-chain `Fundraiser` layout in `program/src/state.rs`. +export const FundraiserSchema = { + struct: { + maker: { array: { type: "u8", len: 32 } }, + mint_to_raise: { 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; + amount_to_raise: bigint; + current_amount: bigint; + time_started: bigint; + duration: number; + bump: number; +}; + +// Mirrors the on-chain `Contributor` layout in `program/src/state.rs`. +export const ContributorSchema = { + struct: { + amount: "u64", + bump: "u8", + }, +}; + +export type ContributorRaw = { + amount: bigint; + bump: number; +}; diff --git a/tokens/token-fundraiser/pinocchio/tests/instruction.ts b/tokens/token-fundraiser/pinocchio/tests/instruction.ts new file mode 100644 index 000000000..68b9b5385 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/instruction.ts @@ -0,0 +1,164 @@ +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, +} + +// Unlike the Anchor example, the Pinocchio program receives the PDA bumps in the +// instruction data (and stores them) instead of deriving them on-chain. +const InitializeSchema = { + struct: { + instruction: "u8", + amount: "u64", + duration: "u16", + bump: "u8", + }, +}; + +const ContributeSchema = { + struct: { + instruction: "u8", + amount: "u64", + bump: "u8", + }, +}; + +const NoArgsSchema = { + struct: { + instruction: "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; + 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, + bump: props.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(NoArgsSchema, { + 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: PublicKey; + maker: PublicKey; + mint: PublicKey; + fundraiser: PublicKey; + contributorAccount: PublicKey; + contributorAta: PublicKey; + vault: PublicKey; + programId: PublicKey; +}) { + const data = borshSerialize(NoArgsSchema, { + instruction: FundraiserInstruction.Refund, + }); + + 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..b0fe41c75 --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/test.ts @@ -0,0 +1,225 @@ +import { AccountLayout, getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { 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 { createValues, DECIMALS, expectRevert, mintingTokens } from "./utils"; + +describe("Fundraiser (Pinocchio)", async () => { + const values = createValues(); + + const context = await start([{ name: "fundraiser_pinocchio_program", programId: values.programId }], []); + const client = context.banksClient; + const payer = context.payer; + + // The fee payer doubles as the contributor. + const contributor = payer; + const contributorAta = getAssociatedTokenAddressSync(values.mintKeypair.publicKey, contributor.publicKey, true); + const [contributorAccount, contributorBump] = PublicKey.findProgramAddressSync( + [Buffer.from("contributor"), values.fundraiser.toBuffer(), contributor.publicKey.toBuffer()], + values.programId, + ); + + const oneToken = new BN(10 ** DECIMALS); + + // Give the maker some SOL to fund the fundraiser and vault accounts, then mint + // 100 tokens to the contributor. + { + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add( + SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: values.maker.publicKey, + lamports: LAMPORTS_PER_SOL, + }), + ).sign(payer); + await client.processTransaction(tx); + } + await mintingTokens({ context, holder: contributor, mintKeypair: values.mintKeypair }); + + it("Initializes a fundraiser", async () => { + const ix = buildInitialize({ + amount: values.amountToRaise, + duration: values.duration, + bump: values.fundraiserBump, + maker: values.maker.publicKey, + mint: values.mintKeypair.publicKey, + fundraiser: values.fundraiser, + vault: values.vault, + programId: values.programId, + }); + + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(payer, values.maker); + await client.processTransaction(tx); + + const fundraiserInfo = await client.getAccount(values.fundraiser); + if (fundraiserInfo === null) throw new Error("Fundraiser account not found"); + const fundraiser = borsh.deserialize(FundraiserSchema, Buffer.from(fundraiserInfo.data)) as FundraiserRaw; + + assert( + new PublicKey(fundraiser.maker).toBase58() === values.maker.publicKey.toBase58(), + "maker key does not match", + ); + assert( + new PublicKey(fundraiser.mint_to_raise).toBase58() === values.mintKeypair.publicKey.toBase58(), + "wrong mint", + ); + assert(fundraiser.amount_to_raise.toString() === values.amountToRaise.toString(), "wrong target amount"); + assert(fundraiser.current_amount.toString() === "0", "current amount should start at 0"); + assert(fundraiser.duration === values.duration, "wrong duration"); + assert(fundraiser.bump === values.fundraiserBump, "wrong bump"); + + const vaultInfo = await client.getAccount(values.vault); + if (vaultInfo === null) throw new Error("Vault account not found"); + const vault = AccountLayout.decode(vaultInfo.data); + assert(vault.amount.toString() === "0", "vault should start empty"); + }); + + it("Accepts a contribution", async () => { + const ix = buildContribute({ + amount: oneToken, + bump: contributorBump, + contributor: contributor.publicKey, + mint: values.mintKeypair.publicKey, + fundraiser: values.fundraiser, + contributorAccount, + contributorAta, + vault: values.vault, + programId: values.programId, + }); + + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(payer); + await client.processTransaction(tx); + + const vaultInfo = await client.getAccount(values.vault); + if (vaultInfo === null) throw new Error("Vault account not found"); + assert(AccountLayout.decode(vaultInfo.data).amount.toString() === oneToken.toString(), "wrong vault amount"); + + const contributorInfo = await client.getAccount(contributorAccount); + if (contributorInfo === null) throw new Error("Contributor account not found"); + const contributorState = borsh.deserialize(ContributorSchema, Buffer.from(contributorInfo.data)) as ContributorRaw; + assert(contributorState.amount.toString() === oneToken.toString(), "wrong contributor amount"); + assert(contributorState.bump === contributorBump, "wrong contributor bump"); + + const fundraiserInfo = await client.getAccount(values.fundraiser); + if (fundraiserInfo === null) throw new Error("Fundraiser account not found"); + const fundraiser = borsh.deserialize(FundraiserSchema, Buffer.from(fundraiserInfo.data)) as FundraiserRaw; + assert(fundraiser.current_amount.toString() === oneToken.toString(), "wrong current amount"); + }); + + it("Accepts a second contribution", async () => { + const ix = buildContribute({ + amount: oneToken, + bump: contributorBump, + contributor: contributor.publicKey, + mint: values.mintKeypair.publicKey, + fundraiser: values.fundraiser, + contributorAccount, + contributorAta, + vault: values.vault, + programId: values.programId, + }); + + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(payer); + await client.processTransaction(tx); + + const vaultInfo = await client.getAccount(values.vault); + if (vaultInfo === null) throw new Error("Vault account not found"); + const expected = oneToken.muln(2).toString(); + assert(AccountLayout.decode(vaultInfo.data).amount.toString() === expected, "wrong vault amount"); + + const contributorInfo = await client.getAccount(contributorAccount); + if (contributorInfo === null) throw new Error("Contributor account not found"); + const contributorState = borsh.deserialize(ContributorSchema, Buffer.from(contributorInfo.data)) as ContributorRaw; + assert(contributorState.amount.toString() === expected, "wrong contributor amount"); + }); + + it("Rejects a contribution above the per-contributor cap", async () => { + // The cap is 10% of the 30-token target = 3 tokens. Two tokens are already + // contributed, so a further two tokens (total 4) must be rejected. + const ix = buildContribute({ + amount: oneToken.muln(2), + bump: contributorBump, + contributor: contributor.publicKey, + mint: values.mintKeypair.publicKey, + fundraiser: values.fundraiser, + contributorAccount, + contributorAta, + vault: values.vault, + programId: values.programId, + }); + + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(payer); + await expectRevert(client.processTransaction(tx)); + }); + + it("Rejects checking contributions before the target is met", async () => { + const ix = buildCheckContributions({ + maker: values.maker.publicKey, + mint: values.mintKeypair.publicKey, + fundraiser: values.fundraiser, + vault: values.vault, + makerAta: values.makerAta, + programId: values.programId, + }); + + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(payer, values.maker); + await expectRevert(client.processTransaction(tx)); + }); + + it("Refunds the contributor", async () => { + const beforeInfo = await client.getAccount(contributorAta); + if (beforeInfo === null) throw new Error("Contributor token account not found"); + const before = AccountLayout.decode(beforeInfo.data).amount; + + const ix = buildRefund({ + contributor: contributor.publicKey, + maker: values.maker.publicKey, + mint: values.mintKeypair.publicKey, + fundraiser: values.fundraiser, + contributorAccount, + contributorAta, + vault: values.vault, + programId: values.programId, + }); + + const tx = new Transaction(); + tx.recentBlockhash = context.lastBlockhash; + tx.add(ix).sign(payer); + await client.processTransaction(tx); + + // The contributor account is closed. + const contributorInfo = await client.getAccount(contributorAccount); + assert(contributorInfo === null, "contributor account not closed"); + + // The vault is drained back to the contributor. + const vaultInfo = await client.getAccount(values.vault); + if (vaultInfo === null) throw new Error("Vault account not found"); + assert(AccountLayout.decode(vaultInfo.data).amount.toString() === "0", "vault should be empty after refund"); + + const afterInfo = await client.getAccount(contributorAta); + if (afterInfo === null) throw new Error("Contributor token account not found"); + const after = AccountLayout.decode(afterInfo.data).amount; + assert((after - before).toString() === oneToken.muln(2).toString(), "contributor not fully refunded"); + + // The fundraiser's running total is back to zero. + const fundraiserInfo = await client.getAccount(values.fundraiser); + if (fundraiserInfo === null) throw new Error("Fundraiser account not found"); + const fundraiser = borsh.deserialize(FundraiserSchema, Buffer.from(fundraiserInfo.data)) as FundraiserRaw; + assert(fundraiser.current_amount.toString() === "0", "current amount should be zero after refund"); + }); +}); diff --git a/tokens/token-fundraiser/pinocchio/tests/utils.ts b/tokens/token-fundraiser/pinocchio/tests/utils.ts new file mode 100644 index 000000000..8f0b0049e --- /dev/null +++ b/tokens/token-fundraiser/pinocchio/tests/utils.ts @@ -0,0 +1,143 @@ +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 DECIMALS = 6; + +export const expectRevert = async (promise: Promise) => { + try { + await promise; + throw new Error("Expected a revert"); + } catch { + return; + } +}; + +export const mintingTokens = async ({ + context, + holder, + mintKeypair, + mintedAmount = 100, + decimals = DECIMALS, +}: { + context: ProgramTestContext; + holder: Signer; + mintKeypair: Keypair; + mintedAmount?: number; + decimals?: number; +}) => { + async function createMint(context: ProgramTestContext, mint: Keypair, decimals: number) { + const rent = await context.banksClient.getRent(); + + const lamports = rent.minimumBalance(BigInt(MINT_SIZE)); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: context.payer.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: new BN(lamports.toString()).toNumber(), + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + mint.publicKey, + decimals, + context.payer.publicKey, + context.payer.publicKey, + TOKEN_PROGRAM_ID, + ), + ); + transaction.recentBlockhash = context.lastBlockhash; + transaction.sign(context.payer, mint); + + await context.banksClient.processTransaction(transaction); + } + + async function createAssociatedTokenAccountIfNeeded(context: ProgramTestContext, mint: PublicKey, owner: PublicKey) { + const associatedToken = getAssociatedTokenAddressSync(mint, owner, true); + + const transaction = new Transaction().add( + createAssociatedTokenAccountIdempotentInstruction( + context.payer.publicKey, + associatedToken, + owner, + mint, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + ); + transaction.recentBlockhash = context.lastBlockhash; + transaction.sign(context.payer); + + await context.banksClient.processTransaction(transaction); + } + + async function mintTo(context: ProgramTestContext, mint: PublicKey, destination: PublicKey, amount: number | bigint) { + const transaction = new Transaction().add( + createMintToInstruction(mint, destination, context.payer.publicKey, amount, [], TOKEN_PROGRAM_ID), + ); + transaction.recentBlockhash = context.lastBlockhash; + transaction.sign(context.payer); + + await context.banksClient.processTransaction(transaction); + } + + // creator creates the mint + await createMint(context, mintKeypair, decimals); + + // create holder token account + await createAssociatedTokenAccountIfNeeded(context, mintKeypair.publicKey, holder.publicKey); + + // mint to holders token account + await mintTo( + context, + mintKeypair.publicKey, + getAssociatedTokenAddressSync(mintKeypair.publicKey, holder.publicKey, true), + mintedAmount * 10 ** decimals, + ); +}; + +export interface TestValues { + programId: PublicKey; + maker: Keypair; + mintKeypair: Keypair; + amountToRaise: BN; + duration: number; + fundraiser: PublicKey; + fundraiserBump: number; + vault: PublicKey; + makerAta: PublicKey; +} + +export function createValues(): TestValues { + const programId = PublicKey.unique(); + const maker = Keypair.generate(); + const mintKeypair = Keypair.generate(); + + const [fundraiser, fundraiserBump] = PublicKey.findProgramAddressSync( + [Buffer.from("fundraiser"), maker.publicKey.toBuffer()], + programId, + ); + + return { + programId, + maker, + mintKeypair, + // 30 tokens target; well above the 3^decimals minimum. + amountToRaise: new BN(30 * 10 ** DECIMALS), + duration: 0, + fundraiser, + fundraiserBump, + vault: getAssociatedTokenAddressSync(mintKeypair.publicKey, fundraiser, true), + makerAta: getAssociatedTokenAddressSync(mintKeypair.publicKey, maker.publicKey, true), + }; +} 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 + } +} From 632307ac0b1b5254b3406ab50685a61eaef30610 Mon Sep 17 00:00:00 2001 From: MarkFeder <5670736+MarkFeder@users.noreply.github.com> Date: Sun, 28 Jun 2026 23:13:56 +0200 Subject: [PATCH 2/2] fix(token-fundraiser): correct time-window checks and verify vault account Addresses review feedback: - Flip the contribute/refund day-window comparisons so contributions are accepted during the active window and refunds after it ends. - Record the vault token account in fundraiser state at initialize and verify the caller-supplied vault against it in contribute, check, and refund, preventing a substituted-vault drain. --- .../program/src/instructions/check_contributions.rs | 5 +++++ .../pinocchio/program/src/instructions/contribute.rs | 7 ++++++- .../pinocchio/program/src/instructions/initialize.rs | 1 + .../pinocchio/program/src/instructions/refund.rs | 7 ++++++- tokens/token-fundraiser/pinocchio/program/src/state.rs | 10 ++++++++-- tokens/token-fundraiser/pinocchio/tests/account.ts | 2 ++ tokens/token-fundraiser/pinocchio/tests/test.ts | 1 + 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs index d28728f51..557c34c8c 100644 --- a/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs @@ -54,6 +54,11 @@ pub fn check_contributions( { return Err(ProgramError::InvalidAccountData); } + // The vault must be the fundraiser's recorded vault before we read its + // balance to decide the target has been met. + if &fundraiser_state.vault != vault.address().as_array() { + return Err(ProgramError::InvalidAccountData); + } // The target amount must have been reached. let vault_amount = TokenAccount::from_account_view(vault)?.amount(); diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs index 06af53fc9..e917efb53 100644 --- a/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs @@ -58,6 +58,11 @@ pub fn contribute(program_id: &Address, accounts: &[AccountView], data: &[u8]) - if &fundraiser_state.mint_to_raise != mint_to_raise.address().as_array() { return Err(ProgramError::InvalidAccountData); } + // The vault must be the fundraiser's recorded vault, otherwise a caller + // could record a contribution against an account they control. + if &fundraiser_state.vault != vault.address().as_array() { + return Err(ProgramError::InvalidAccountData); + } // A contribution must be at least one base unit. if amount < 1 { @@ -77,7 +82,7 @@ pub fn contribute(program_id: &Address, accounts: &[AccountView], data: &[u8]) - // The fundraiser must still be within its active window. let current_time = Clock::get()?.unix_timestamp; let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; - if fundraiser_state.duration > elapsed_days { + if elapsed_days > fundraiser_state.duration { return Err(FundraiserError::FundraiserEnded.into()); } diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs index c1f695210..ecac00205 100644 --- a/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs @@ -106,6 +106,7 @@ pub fn initialize(program_id: &Address, accounts: &[AccountView], data: &[u8]) - time_started: Clock::get()?.unix_timestamp, duration, bump, + vault: *vault.address().as_array(), }; fundraiser_state.serialize(&mut fundraiser.try_borrow_mut()?)?; diff --git a/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs b/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs index cba1c0f20..73d99b2ed 100644 --- a/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs +++ b/tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs @@ -55,11 +55,16 @@ pub fn refund(program_id: &Address, accounts: &[AccountView], _data: &[u8]) -> P { return Err(ProgramError::InvalidAccountData); } + // The vault must be the fundraiser's recorded vault, otherwise refund + // eligibility could be judged from an unrelated token account. + if &fundraiser_state.vault != vault.address().as_array() { + return Err(ProgramError::InvalidAccountData); + } // The fundraiser must have ended. let current_time = Clock::get()?.unix_timestamp; let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; - if fundraiser_state.duration < elapsed_days { + if elapsed_days < fundraiser_state.duration { return Err(FundraiserError::FundraiserNotEnded.into()); } diff --git a/tokens/token-fundraiser/pinocchio/program/src/state.rs b/tokens/token-fundraiser/pinocchio/program/src/state.rs index 489d771c4..5bfe14459 100644 --- a/tokens/token-fundraiser/pinocchio/program/src/state.rs +++ b/tokens/token-fundraiser/pinocchio/program/src/state.rs @@ -10,7 +10,7 @@ use pinocchio::error::ProgramError; /// Serialized byte layout (little-endian), matching the field order below so a /// Borsh client can deserialize it directly: /// `[maker: 32][mint_to_raise: 32][amount_to_raise: u64][current_amount: u64] -/// [time_started: i64][duration: u16][bump: u8]` +/// [time_started: i64][duration: u16][bump: u8][vault: 32]` pub struct Fundraiser { /// The wallet that created the fundraiser; part of the PDA seeds. pub maker: [u8; 32], @@ -26,6 +26,10 @@ pub struct Fundraiser { pub duration: u16, /// Canonical bump for the fundraiser PDA. pub bump: u8, + /// The fundraiser's vault token account (the PDA's associated token account + /// for `mint_to_raise`), recorded at creation. Later instructions check the + /// caller-supplied vault against this to reject a substituted account. + pub vault: [u8; 32], } impl Fundraiser { @@ -33,7 +37,7 @@ impl Fundraiser { pub const SEED_PREFIX: &'static [u8] = b"fundraiser"; /// Serialized size of a `Fundraiser` in bytes. - pub const LEN: usize = 32 + 32 + 8 + 8 + 8 + 2 + 1; + pub const LEN: usize = 32 + 32 + 8 + 8 + 8 + 2 + 1 + 32; /// Writes the fundraiser into `dst` using the layout documented above. pub fn serialize(&self, dst: &mut [u8]) -> Result<(), ProgramError> { @@ -47,6 +51,7 @@ impl Fundraiser { dst[80..88].copy_from_slice(&self.time_started.to_le_bytes()); dst[88..90].copy_from_slice(&self.duration.to_le_bytes()); dst[90] = self.bump; + dst[91..123].copy_from_slice(&self.vault); Ok(()) } @@ -65,6 +70,7 @@ impl Fundraiser { time_started: i64::from_le_bytes(src[80..88].try_into().unwrap()), duration: u16::from_le_bytes(src[88..90].try_into().unwrap()), bump: src[90], + vault: src[91..123].try_into().unwrap(), }) } } diff --git a/tokens/token-fundraiser/pinocchio/tests/account.ts b/tokens/token-fundraiser/pinocchio/tests/account.ts index 5bdabfd4c..79df8a49b 100644 --- a/tokens/token-fundraiser/pinocchio/tests/account.ts +++ b/tokens/token-fundraiser/pinocchio/tests/account.ts @@ -8,6 +8,7 @@ export const FundraiserSchema = { time_started: "i64", duration: "u16", bump: "u8", + vault: { array: { type: "u8", len: 32 } }, }, }; @@ -19,6 +20,7 @@ export type FundraiserRaw = { time_started: bigint; duration: number; bump: number; + vault: Uint8Array; }; // Mirrors the on-chain `Contributor` layout in `program/src/state.rs`. diff --git a/tokens/token-fundraiser/pinocchio/tests/test.ts b/tokens/token-fundraiser/pinocchio/tests/test.ts index b0fe41c75..0e175b595 100644 --- a/tokens/token-fundraiser/pinocchio/tests/test.ts +++ b/tokens/token-fundraiser/pinocchio/tests/test.ts @@ -74,6 +74,7 @@ describe("Fundraiser (Pinocchio)", async () => { assert(fundraiser.current_amount.toString() === "0", "current amount should start at 0"); assert(fundraiser.duration === values.duration, "wrong duration"); assert(fundraiser.bump === values.fundraiserBump, "wrong bump"); + assert(new PublicKey(fundraiser.vault).toBase58() === values.vault.toBase58(), "wrong vault recorded"); const vaultInfo = await client.getAccount(values.vault); if (vaultInfo === null) throw new Error("Vault account not found");