diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..6d855c9b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(git:*)", + "Bash(just:*)", + "Bash(ls:*)", + "Bash(mkdir:*)", + "Bash(ni:*)", + "mcp__http-server__*", + "WebFetch(domain:anthropic.com)", + "WebFetch(domain:github.com)", + "WebFetch(domain:npmjs.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:sablier.com)" + ], + "deny": ["Bash(git checkout:*)", "Bash(git reset:*)", "Bash(git unstage:*)", "Bash(rm -rf:*)"] + } +} diff --git a/.editorconfig b/.editorconfig index 36191bf3..bc0ea62a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,4 +10,7 @@ end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file +trim_trailing_whitespace = true + +[justfile] +indent_size = 4 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 4e648d76..00000000 --- a/.eslintignore +++ /dev/null @@ -1,20 +0,0 @@ -# directories -dist -**/dist -node_modules -**/node_modules -programs -target - -# files -*.env -*.log -*.tsbuildinfo -.DS_Store -.pnp.* -bun.lock -bun.lockb -npm-debug.log -package-lock.json -pnpm-lock.yaml -yarn.lock \ No newline at end of file diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 9ec47827..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,31 +0,0 @@ -env: - shared-node-browser: true -extends: - - "eslint:recommended" - - "plugin:@typescript-eslint/eslint-recommended" - - "plugin:@typescript-eslint/recommended" - - "prettier" -ignorePatterns: - - "node_modules" -parser: "@typescript-eslint/parser" -parserOptions: - project: "tsconfig.json" -plugins: - - "@typescript-eslint" -root: true -rules: - "no-empty": "off" - "@typescript-eslint/no-empty-block": "off" - "@typescript-eslint/no-empty-function": "off" - "@typescript-eslint/no-empty-interface": "off" - "@typescript-eslint/no-explicit-any": "off" - "@typescript-eslint/no-floating-promises": - - error - - ignoreIIFE: true - ignoreVoid: true - "@typescript-eslint/no-inferrable-types": "off" - "@typescript-eslint/no-non-null-assertion": "off" - "@typescript-eslint/no-unused-vars": - - error - - argsIgnorePattern: ^_ - varsIgnorePattern: ^_ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 6423f349..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Build Solana Program - -# Run this workflow on pushes and pull requests to the main branch -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - - - name: Cache Cargo dependencies - uses: Swatinem/rust-cache@v2 - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true - - - name: Get the required dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential pkg-config libudev-dev llvm libclang-dev protobuf-compiler libssl-dev - shell: bash - - # - name: Cache Solana CLI - # uses: actions/cache@v4 - # id: cache-solana-cli - # with: - # path: | - # ~/.local/share/solana - # ~/.cache/solana - # key: ${{ runner.os }}-solana-stable - - - name: Install Solana CLI if not cached - # if: steps.cache-solana-cli.outputs.cache-hit != 'true' - run: | - curl -sSfL https://release.anza.xyz/stable/install | sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - shell: bash - - - name: Cache Anchor - uses: actions/cache@v4 - id: cache-anchor - with: - path: | - ~/.cargo/bin/anchor - key: ${{ runner.os }}-anchor-latest - - - name: Install Anchor if not cached - if: steps.cache-anchor.outputs.cache-hit != 'true' - run: cargo install --git https://github.com/coral-xyz/anchor --tag v0.31.1 anchor-cli - - - name: Check that Anchor has been installed successfully - run: anchor --version - shell: bash - - - name: Build the programs via Anchor - run: anchor build - shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4789a17b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: "CI" + +concurrency: + cancel-in-progress: true + group: ${{github.workflow}}-${{github.ref}} + +on: + pull_request: + push: + branches: ["main"] + +jobs: + ci: + runs-on: "macos-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Set up Sablier devkit and install Node.js dependencies" + uses: "sablier-labs/devkit/actions/setup@main" + with: + package-manager: "bun" + + - name: "Set up Rust, Solana, and Anchor" + uses: "sablier-labs/gha-utils/.github/actions/anchor-toolchain@main" + with: + anchor-version: "0.31.1" + rust-version: "nightly" + solana-version: "2.1.21" + + - name: "Cache Anchor build artifacts" + id: "cache-build" + uses: "sablier-labs/gha-utils/.github/actions/solana-cache@main" + with: + cache-path: target + + - name: "Build the programs" + if: steps.cache-build.outputs.cache-status != 'primary' + run: "just build" + + - name: "Run the Rust code checks" + if: steps.cache-build.outputs.cache-status != 'primary' + run: "just rust-check" + + - name: "Run the other code checks" + run: | # shell + just prettier-check + just biome-check + just tsc-check + + - name: "Run the tests" + if: steps.cache-build.outputs.cache-status != 'primary' + run: "just test-lite" + + - name: "Add summary" + run: | # shell + echo "## CI result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test.yml.disabled b/.github/workflows/test.yml.disabled deleted file mode 100644 index 8755c032..00000000 --- a/.github/workflows/test.yml.disabled +++ /dev/null @@ -1,84 +0,0 @@ -name: Test Solana Program - -# Run this workflow on pushes and pull requests to any branch -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: "npm" - - - name: Cache Cargo dependencies - uses: Swatinem/rust-cache@v2 - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true - - - name: Get the required dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential pkg-config llvm libclang-dev libssl-dev - shell: bash - - # - name: Cache Solana CLI - # uses: actions/cache@v4 - # id: cache-solana-cli - # with: - # path: | - # ~/.local/share/solana - # ~/.cache/solana - # key: ${{ runner.os }}-solana-stable - - - name: Install Solana CLI if not cached - # if: steps.cache-solana-cli.outputs.cache-hit != 'true' - run: | - curl -sSfL https://release.anza.xyz/stable/install | sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - shell: bash - - # - name: Cache Anchor CLI - # uses: actions/cache@v4 - # id: cache-anchor-cli - # with: - # path: | - # ~/.cargo/bin/anchor - # ~/.cargo/bin/avm - # key: ${{ runner.os }}-anchor-latest - - - name: Install Anchor CLI if not cached - # if: steps.cache-anchor-cli.outputs.cache-hit != 'true' - run: | - cargo install --git https://github.com/coral-xyz/anchor avm --force - avm install latest - shell: bash - - - run: avm use latest - shell: bash - - - run: anchor build - shell: bash - - # - name: Create keypair - # run: solana-keygen new --no-bip39-passphrase - # shell: bash - - # - name: Install - # run: npm i -g @project-serum/anchor-cli@$ANCHOR_VERSION ts-mocha typescript - # shell: bash - - # - name: Run tests - # run: anchor test --skip-build - # shell: bash diff --git a/.gitignore b/.gitignore index 2c9df7ca..0f2adef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,9 @@ # Anchor .anchor docker-target -**/*.rs.bk target test-ledger - -# Misc -*.env -.DS_Store +**/*.rs.bk # Node.js .npm @@ -16,4 +12,10 @@ bun.lockb node_modules package-lock.json pnpm-lock.yaml -yarn.lock \ No newline at end of file +yarn.lock + +# Misc +repomix +*.env +*.local.json +.DS_Store diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..c27d8893 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +lint-staged diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 00000000..b2381eaa --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,8 @@ +/** + * @type {import("lint-staged").Configuration} + */ +module.exports = { + "*.{js,json,jsonc,ts}": "na biome check --no-errors-on-unmatched --write", + "*.{js,ts}": "na biome lint --no-errors-on-unmatched --unsafe --write --only correctness/noUnusedImports", + "*.{md,yml}": "na prettier --cache --write", +}; diff --git a/.prettierignore b/.prettierignore index 943dcd77..cb2ac1b7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,20 +1,9 @@ # directories .anchor -build -dist node_modules +repomix target test-ledger # files -*.env -*.log -*.tsbuildinfo -.DS_Store -.pnp.* -bun.lock -bun.lockb -npm-debug.log package-lock.json -pnpm-lock.yaml -yarn.lock \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..9779cd65 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,9 @@ +const baseConfig = require("@sablier/devkit/prettier"); + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = baseConfig; + +module.exports = config; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..eba3c422 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "amasfe.even-better-toml", + "biomejs.biome", + "esbenp.prettier-vscode", + "nefrob.vscode-just-syntax", + "stackbreak.comment-divider", + "rust-lang.rust-analyzer" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..fc231250 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "biome.enabled": true, + "editor.formatOnSave": true, + "eslint.enable": false, + "evenBetterToml.formatter.indentEntries": true, + "typescript.tsdk": "node_modules/typescript/lib", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "[json][ts]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[md][yml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + } +} diff --git a/Anchor.toml b/Anchor.toml index d3ca002d..b6f07556 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,20 +1,22 @@ -[toolchain] - [features] -resolution = true -skip-lint = false + resolution = true + skip-lint = false [programs.devnet] -sablier_lockup = "DczGzCFQ5kHRBGVkH22HDdtduE4qaqopdc5HGLrAzfQD" -sablier_merkle_instant = "GthZ6aQHJonsia3jpdrSBukxipyRfo9TR5ZrepGXLTQR" + sablier_lockup = "DczGzCFQ5kHRBGVkH22HDdtduE4qaqopdc5HGLrAzfQD" + sablier_merkle_instant = "GthZ6aQHJonsia3jpdrSBukxipyRfo9TR5ZrepGXLTQR" [programs.localnet] -sablier_lockup = "DczGzCFQ5kHRBGVkH22HDdtduE4qaqopdc5HGLrAzfQD" -sablier_merkle_instant = "Cmo3bW9vDNiESzNVgqUGzbmJZ68ncJkBMgLACaRRjXyx" + sablier_lockup = "DczGzCFQ5kHRBGVkH22HDdtduE4qaqopdc5HGLrAzfQD" + sablier_merkle_instant = "Cmo3bW9vDNiESzNVgqUGzbmJZ68ncJkBMgLACaRRjXyx" + +[provider] + cluster = "devnet" + wallet = "~/.config/solana/id.json" [registry] -url = "https://api.apr.dev" + url = "https://api.apr.dev" -[provider] -cluster = "devnet" -wallet = "~/.config/solana/id.json" +[toolchain] + anchor_version = "0.31.1" + solana_version = "2.1.21" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..053ff4d0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing to SolSab + +Thank you for your interest in contributing to SolSab! This guide will help you get set up for development. + +## Pre-Requisites + +Ensure you have the following software installed and configured on your machine: + +- [Rust (Nightly)](https://rust-lang.org/tools/install) +- [Solana CLI](https://solana.com/docs/intro/installation#install-the-solana-cli) +- [Anchor CLI](https://solana.com/docs/intro/installation#install-anchor-cli) (Solana development framework) +- [Node.js v23+](https://nodejs.org/en) +- [Just](https://github.com/casey/just) (command runner) +- [Bun](https://bun.sh/docs/installation) (package manager) +- [Ni](https://github.com/antfu-collective/ni) (package manager resolver) + +> [!NOTE] Consider running this one-time script to install all Sablier dependencies. +> +> ```sh +> curl -fsSL https://raw.githubusercontent.com/sablier-labs/team-setup/main/sablier.sh | sh +> ``` + +## Set Up + +### Wallet + +Make sure to configure your local [Solana wallet](https://anchor-lang.com/docs/installation#solana-cli-basics). + +### Clone the repository + +```shell +$ git clone git@github.com:sablier-labs/solsab.git && cd solsab +``` + +### Install dependencies + +```shell +$ just install +``` + +### List available scripts + +To see a list of all available scripts, run this command: + +```shell +$ just --list +``` + +### Build the programs + +```bash +just build +``` + +### Testing + +```bash +just test +``` + +## VSCode Extensions + +See the recommended VSCode extensions in [`.vscode/extensions.json`](./.vscode/extensions.json). + +## Other useful information + +Solana Cluster RPC URLs: + +- **Mainnet Beta**: https://api.mainnet-beta.solana.com +- **Devnet**: https://api.devnet.solana.com +- **Testnet**: https://api.testnet.solana.com +- **Localnet**: http://127.0.0.1:8899 diff --git a/Cargo.toml b/Cargo.toml index 68da9ddd..54ed2827 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [workspace] -members = ["programs/*"] -resolver = "2" + members = ["programs/*"] + resolver = "2" [profile.release] -overflow-checks = true -lto = "fat" -codegen-units = 1 + codegen-units = 1 + lto = "fat" + overflow-checks = true [profile.release.build-override] -opt-level = 3 -incremental = false -codegen-units = 1 + codegen-units = 1 + incremental = false + opt-level = 3 diff --git a/LICENSE-GPL.md b/LICENSE-GPL.md deleted file mode 100644 index c83358d4..00000000 --- a/LICENSE-GPL.md +++ /dev/null @@ -1,466 +0,0 @@ -# GNU General Public License - -_Version 3, 29 June 2007_ _Copyright © 2007 Free Software Foundation, Inc. <>_ - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -## Preamble - -The GNU General Public License is a free, copyleft license for software and other kinds of works. - -The licenses for most software and other practical works are designed to take away your freedom to share and change the -works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all -versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use -the GNU General Public License for most of our software; it applies also to any other work released this way by its -authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make -sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive -source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and -that you know you can do these things. - -To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. -Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: -responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients -the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must -show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: **(1)** assert copyright on the software, and -**(2)** offer you this License giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. -For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems -will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified versions of the software inside them, although -the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the -software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely -where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we stand ready to extend this provision to those -domains in future versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States should not allow patents to restrict -development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger -that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and modification follow. - -## TERMS AND CONDITIONS - -### 0. Definitions - -“This License” refers to version 3 of the GNU General Public License. - -“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. - -“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. -“Licensees” and “recipients” may be individuals or organizations. - -To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, -other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work -“based on” the earlier work. - -A “covered work” means either the unmodified Program or a work based on the Program. - -To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily -liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. -Propagation includes copying, distribution (with or without modification), making available to the public, and in some -countries other activities as well. - -To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction -with a user through a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and -prominently visible feature that **(1)** displays an appropriate copyright notice, and **(2)** tells the user that there -is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work -under this License, and how to view a copy of this License. If the interface presents a list of user commands or -options, such as a menu, a prominent item in the list meets this criterion. - -### 1. Source Code - -The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means -any non-source form of a work. - -A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, -or, in the case of interfaces specified for a particular programming language, one that is widely used among developers -working in that language. - -The “System Libraries” of an executable work include anything, other than the work as a whole, that **(a)** is included -in the normal form of packaging a Major Component, but which is not part of that Major Component, and **(b)** serves -only to enable use of the work with that Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A “Major Component”, in this context, means a major -essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable -work runs, or a compiler used to produce the work, or an object code interpreter used to run it. - -The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and -(for an executable work) run the object code and to modify the work, including scripts to control those activities. -However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs -which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding -Source includes interface definition files associated with source files for the work, and the source code for shared -libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data -communication or control flow between those subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from other parts of the -Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -### 2. Basic Permissions - -All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided -the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. -The output from running a covered work is covered by this License only if the output, given its content, constitutes a -covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as your license -otherwise remains in force. You may convey covered works to others for the sole purpose of having them make -modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do not control copyright. Those thus making or running -the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that -prohibit them from making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not -allowed; section 10 makes it unnecessary. - -### 3. Protecting Users' Legal Rights From Anti-Circumvention Law - -No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling -obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or -restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the -extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you -disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, -your or third parties' legal rights to forbid circumvention of technological measures. - -### 4. Conveying Verbatim Copies - -You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating -that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices -of the absence of any warranty; and give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for -a fee. - -### 5. Conveying Modified Source Versions - -You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source -code under the terms of section 4, provided that you also meet all of these conditions: - -- **a)** The work must carry prominent notices stating that you modified it, and giving a relevant date. -- **b)** The work must carry prominent notices stating that it is released under this License and any conditions added - under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. -- **c)** You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. - This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and - all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other - way, but it does not invalidate such permission if you have separately received it. -- **d)** If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the - Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their nature extensions of -the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or -distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the -access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other parts of the aggregate. - -### 6. Conveying Non-Source Forms - -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, in one of these ways: - -- **a)** Convey the object code in, or embodied in, a physical product (including a physical distribution medium), - accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. -- **b)** Convey the object code in, or embodied in, a physical product (including a physical distribution medium), - accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or - customer support for that product model, to give anyone who possesses the object code either **(1)** a copy of the - Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium - customarily used for software interchange, for a price no more than your reasonable cost of physically performing this - conveying of source, or **(2)** access to copy the Corresponding Source from a network server at no charge. -- **c)** Convey individual copies of the object code with a copy of the written offer to provide the Corresponding - Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code - with such an offer, in accord with subsection 6b. -- **d)** Convey the object code by offering access from a designated place (gratis or for a charge), and offer - equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need - not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object - code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying - where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated - to ensure that it is available for as long as needed to satisfy these requirements. -- **e)** Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code - and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, -need not be included in conveying the object code work. - -A “User Product” is either **(1)** a “consumer product”, which means any tangible personal property which is normally -used for personal, family, or household purposes, or **(2)** anything designed or sold for incorporation into a -dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. -For a particular product received by a particular user, “normally used” refers to a typical or common use of that class -of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or -expects or is expected to use, the product. A product is a consumer product regardless of whether the product has -substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of -the product. - -“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information -required to install and execute modified versions of a covered work in that User Product from a modified version of its -Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code -is in no case prevented or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the -conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to -the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding -Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not -apply if neither you nor any third party retains the ability to install modified object code on the User Product (for -example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to provide support -service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product -in which it has been modified or installed. Access to a network may be denied when the modification itself materially -and adversely affects the operation of the network or violates the rules and protocols for communication across the -network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format -that is publicly documented (and with an implementation available to the public in source code form), and must require -no special password or key for unpacking, reading or copying. - -### 7. Additional Terms - -“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of -its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were -included in this License, to the extent that they are valid under applicable law. If additional permissions apply only -to part of the Program, that part may be used separately under those permissions, but the entire Program remains -governed by this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or -from any part of it. (Additional permissions may be written to require their own removal in certain cases when you -modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have -or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by -the copyright holders of that material) supplement the terms of this License with terms: - -- **a)** Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or -- **b)** Requiring preservation of specified reasonable legal notices or author attributions in that material or in the - Appropriate Legal Notices displayed by works containing it; or -- **c)** Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such - material be marked in reasonable ways as different from the original version; or -- **d)** Limiting the use for publicity purposes of names of licensors or authors of the material; or -- **e)** Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or -- **f)** Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or - modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these - contractual assumptions directly impose on those licensors and authors. - -All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the -Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with -a term that is a further restriction, you may remove that term. If a license document contains a further restriction but -permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of -that license document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a -statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as -exceptions; the above requirements apply either way. - -### 8. Termination - -You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to -propagate or modify it is void, and will automatically terminate your rights under this License (including any patent -licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated -**(a)** provisionally, unless and until the copyright holder explicitly and finally terminates your license, and **(b)** -permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days -after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you -of the violation by some reasonable means, this is the first time you have received notice of violation of this License -(for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have received copies or -rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not -qualify to receive new licenses for the same material under section 10. - -### 9. Acceptance Not Required for Having Copies - -You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a -covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not -require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered -work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -### 10. Automatic Licensing of Downstream Recipients - -Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, -modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third -parties with this License. - -An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or -subdividing an organization, or merging organizations. If propagation of a covered work results from an entity -transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work -the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with -reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For -example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, -and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent -claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. - -### 11. Patents - -A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the -Program is based. The work thus licensed is called the contributor's “contributor version”. - -A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already -acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or -selling its contributor version, but do not include claims that would be infringed only as a consequence of further -modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent -sublicenses in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential -patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its -contributor version. - -In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not -to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). -To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent -against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not -available for anyone to copy, free of charge and under the terms of this License, through a publicly available network -server or other readily accessible means, then you must either **(1)** cause the Corresponding Source to be so -available, or **(2)** arrange to deprive yourself of the benefit of the patent license for this particular work, or -**(3)** arrange, in a manner consistent with the requirements of this License, to extend the patent license to -downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your -conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or -more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring -conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing -them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is -automatically extended to all recipients of the covered work and works based on it. - -A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, -or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You -may not convey a covered work if you are a party to an arrangement with a third party that is in the business of -distributing software, under which you make payment to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a -discriminatory patent license **(a)** in connection with copies of the covered work conveyed by you (or copies made from -those copies), or **(b)** primarily for and in connection with specific products or compilations that contain the -covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to -infringement that may otherwise be available to you under applicable patent law. - -### 12. No Surrender of Others' Freedom - -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this -License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to -satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence -you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further -conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License -would be to refrain entirely from conveying the Program. - -### 13. Use with the GNU Affero General Public License - -Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work -licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the -resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special -requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply -to the combination as such. - -### 14. Revised Versions of this License - -The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to -time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new -problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the -GNU General Public License “or any later version” applies to it, you have the option of following the terms and -conditions either of that numbered version or of any later version published by the Free Software Foundation. If the -Program does not specify a version number of the GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, -that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the -Program. - -Later license versions may give you additional or different permissions. However, no additional obligations are imposed -on any author or copyright holder as a result of your choosing to follow a later version. - -### 15. Disclaimer of Warranty - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING -THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR -IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU -ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -### 16. Limitation of Liability - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO -MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, -INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO -LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM -TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - -### 17. Interpretation of Sections 15 and 16 - -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to -their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil -liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program -in return for a fee. - -_END OF TERMS AND CONDITIONS_ - -## How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve -this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to -most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer -to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type 'show c' for details. - -The hypothetical commands `show w` and `show c` should show the appropriate parts of the General Public License. Of -course, your program's commands might be different; for a GUI interface, you would use an “about box”. - -You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for -the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see -<>. - -The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is -a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If -this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -<>. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..1dc4ffcd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,82 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of +MariaDB Corporation Ab. + +--- + +Parameters + +Licensor: Sablier Labs Ltd + +Licensed Work: Sablier Solana Programs The Licensed Work is (C) 2025 Sablier Labs Ltd + +Additional Use Grant: Any uses listed and defined at +[`license-grants.sablier.eth`](https://app.ens.domains/license-grants.sablier.eth) + +Change Date: The earlier of 2029-07-01 or a date specified at +[`license-dates.sablier.eth`](https://app.ens.domains/license-dates.sablier.eth) + +Change License: GNU General Public License v3.0 or later + +--- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production +use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific +version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the +terms of the Change License, and the rights granted in the paragraph above terminate. + +If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, +you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must +refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this +License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each +version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the +Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply +to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License +for the current and all other versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may +use a trademark or logo of Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS +ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. + +MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the +trademark "Business Source License", as long as you comply with the Covenants of Licensor below. + +--- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business Source License" name and trademark, Licensor +covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 3.0 or any later version, or a license that is compatible with GPL + Version 3.0 or a later version, where "compatible" means that software provided under the Change License can be + included in a program with software provided under GPL Version 3.0 or a later version. Licensor may specify + additional Change Licenses without limitation. + +2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the + right granted in this License, as the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +--- + +Notice + +The Business Source License (this document, or the "License") is not an Open Source license. However, the Licensed Work +will eventually be made available under an Open Source License, as stated in this License. diff --git a/README.md b/README.md index 33f53ab8..59e0856b 100644 --- a/README.md +++ b/README.md @@ -2,85 +2,35 @@ Sablier programs on Solana -## Pre-Requisites - -Ensure you have the following software installed and configured on your machine: - -- **[Git](https://git-scm.com/downloads)** -- **[Rust](https://rust-lang.org/tools/install)** -- **[Bun](https://bun.sh/docs/installation)** -- **[Solana CLI](https://solana.com/docs/intro/installation#quick-installation)** -- **[Anchor CLI](https://www.anchor-lang.com/docs/installation#install-anchor-cli)** - -## Set Up - -### Solana config - -Make sure to configure your local [Solana wallet](https://www.anchor-lang.com/docs/installation#solana-cli-basics). - -### Clone the SolSab repository: - -```bash -git clone https://github.com/sablier-labs/solsab.git -``` - -### Navigate to the project’s directory: - -```bash -cd solsab -``` - -### Install dependencies - -```bash -bun install -``` - -## Building & Testing - -Build the project with: - -```bash -bun run build -``` - -Test it with: - -```bash -bun run t -``` - ## Architecture SolSab uses a monorepo structure with two main Solana programs. ### Lockup -Sablier Lockup is a token distribution protocol that enables onchain vesting and payments. Our flagship model is the linear stream, which distributes tokens on a continuous, by-the-second basis. +Sablier Lockup is a token distribution protocol that enables onchain vesting and payments. Our flagship model is the +linear stream, which distributes tokens on a continuous, by-the-second basis. -The way it works is that the sender of a payment stream first deposits a specific amount of SPL, or Token2022, tokens in a program. Then, the program progressively allocates the funds to the recipient, who can access them as they become available over time. The payment rate is influenced by various factors, including the start and end times, as well as the total amount of tokens deposited. +The way it works is that the sender of a payment stream first deposits a specific amount of SPL, or Token2022, tokens in +a program. Then, the program progressively allocates the funds to the recipient, who can access them as they become +available over time. The payment rate is influenced by various factors, including the start and end times, as well as +the total amount of tokens deposited. ### Merkle Instant -Merkle Instant is a program that enables the creation of token airdrop campaigns using Merkle trees, allowing users to instantly claim and receive their allocation through a single transaction. - -## Recommended VS Code Extensions - -To improve your development experience, consider installing the following Visual Studio Code extensions: +Merkle Instant is a program that enables the creation of token airdrop campaigns using Merkle trees, allowing users to +instantly claim and receive their allocation through a single transaction. -1. **[rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)** -2. **[Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)** -3. **[Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)** +## Contributing 🤝 -## Other useful information +We welcome contributions! -Solana Cluster RPC URLs: +- 🐛 [Bug reports](../../issues/new) +- 💬 [Discussions](../../discussions/new) +- 💬 [Discord](https://discord.sablier.com) -- **Mainnet Beta**: [https://api.mainnet-beta.solana.com](https://api.mainnet-beta.solana.com) -- **Devnet**: [https://api.devnet.solana.com](https://api.devnet.solana.com) -- **Testnet**: [https://api.testnet.solana.com](https://api.testnet.solana.com) -- **Localnet**: [http://127.0.0.1:8899](http://127.0.0.1:8899) +For guidance on how to make PRs, see the [CONTRIBUTING](./CONTRIBUTING.md) guide. ---- +## License -Congrats, you’re, now, all set for SolSab! +See [LICENSE.md](./LICENSE.md). diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 00000000..4b61d2e4 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,14 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["@sablier/devkit/biome"], + "files": { + "includes": ["**/*.{js,json,jsonc,ts}", "!node_modules", "!target"] + }, + "linter": { + "rules": { + "complexity": { + "noExcessiveNestedTestSuites": "off" + } + } + } +} diff --git a/bun.lock b/bun.lock index 4e17eb03..f2f63ea6 100644 --- a/bun.lock +++ b/bun.lock @@ -3,63 +3,171 @@ "workspaces": { "": { "name": "@sablier/solsab", - "dependencies": { - "@coral-xyz/anchor": "^0.31.1", - "@solana/web3.js": "^1.98.2", - "anchor-bankrun": "^0.5.0", - "solana-bankrun": "^0.4.0", - }, "devDependencies": { + "@biomejs/biome": "^2.1.2", + "@coral-xyz/anchor": "^0.31.1", + "@coral-xyz/anchor-errors": "^0.31.1", + "@sablier/devkit": "github:sablier-labs/devkit#main", "@solana/spl-token": "^0.4.13", - "@types/chai": "^4.3.20", - "@types/mocha": "^9.1.1", + "@solana/web3.js": "^1.98.2", + "@types/bn.js": "^5.2.0", + "@types/lodash": "^4.17.20", "@types/node": "^24.0.1", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", - "chai": "^4.5.0", - "eslint": "^8.57.1", - "eslint-config-prettier": "^8.10.0", + "@vitest/ui": "^3.2.4", + "anchor-bankrun": "^0.5.0", + "bn.js": "^5.2.2", + "dayjs": "^1.11.13", + "husky": "^9.1.7", "keccak256": "^1.0.6", + "lint-staged": "^16.1.2", + "lodash": "^4.17.21", "merkletreejs": "^0.5.2", - "mocha": "^9.2.2", "prettier": "^2.8.8", - "ts-mocha": "^10.1.0", - "typescript": "^5.8.3", + "solana-bankrun": "^0.4.0", + "typescript": "5.8.3", + "viem": "^2.33.0", + "vitest": "^3.2.4", }, }, }, "packages": { + "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.0", "", {}, "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg=="], + "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], + "@biomejs/biome": ["@biomejs/biome@2.1.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.2", "@biomejs/cli-darwin-x64": "2.1.2", "@biomejs/cli-linux-arm64": "2.1.2", "@biomejs/cli-linux-arm64-musl": "2.1.2", "@biomejs/cli-linux-x64": "2.1.2", "@biomejs/cli-linux-x64-musl": "2.1.2", "@biomejs/cli-win32-arm64": "2.1.2", "@biomejs/cli-win32-x64": "2.1.2" }, "bin": { "biome": "bin/biome" } }, "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA=="], + "@coral-xyz/anchor": ["@coral-xyz/anchor@0.31.1", "", { "dependencies": { "@coral-xyz/anchor-errors": "^0.31.1", "@coral-xyz/borsh": "^0.31.1", "@noble/hashes": "^1.3.1", "@solana/web3.js": "^1.69.0", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.2", "camelcase": "^6.3.0", "cross-fetch": "^3.1.5", "eventemitter3": "^4.0.7", "pako": "^2.0.3", "superstruct": "^0.15.4", "toml": "^3.0.0" } }, "sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA=="], "@coral-xyz/anchor-errors": ["@coral-xyz/anchor-errors@0.31.1", "", {}, "sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ=="], "@coral-xyz/borsh": ["@coral-xyz/borsh@0.31.1", "", { "dependencies": { "bn.js": "^5.1.2", "buffer-layout": "^1.2.0" }, "peerDependencies": { "@solana/web3.js": "^1.69.0" } }, "sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.8", "", { "os": "aix", "cpu": "ppc64" }, "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.8", "", { "os": "android", "cpu": "arm" }, "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.8", "", { "os": "android", "cpu": "arm64" }, "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.8", "", { "os": "android", "cpu": "x64" }, "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw=="], - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg=="], - "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA=="], - "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw=="], - "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.8", "", { "os": "linux", "cpu": "arm" }, "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg=="], - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w=="], - "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.8", "", { "os": "linux", "cpu": "ia32" }, "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.8", "", { "os": "linux", "cpu": "x64" }, "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.8", "", { "os": "none", "cpu": "x64" }, "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.8", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.8", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.8", "", { "os": "sunos", "cpu": "x64" }, "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@noble/curves": ["@noble/curves@1.9.2", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.1", "", { "os": "android", "cpu": "arm64" }, "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g=="], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="], + + "@sablier/devkit": ["@sablier/devkit@github:sablier-labs/devkit#60c10da", {}, "sablier-labs-devkit-60c10da"], + + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], @@ -89,13 +197,17 @@ "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@types/chai": ["@types/chai@4.3.20", "", {}, "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ=="], + "@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="], + + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/mocha": ["@types/mocha@9.1.1", "", {}, "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw=="], + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], "@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], @@ -103,55 +215,35 @@ "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.36.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/type-utils": "8.36.0", "@typescript-eslint/utils": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.36.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.36.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.36.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.36.0", "", { "dependencies": { "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0" } }, "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA=="], + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.36.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA=="], + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.36.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/utils": "8.36.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg=="], + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.36.0", "", {}, "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ=="], + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.36.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg=="], + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/typescript-estree": "8.36.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g=="], + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.36.0", "", { "dependencies": { "@typescript-eslint/types": "8.36.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA=="], + "@vitest/ui": ["@vitest/ui@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.1", "tinyglobby": "^0.2.14", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "vitest": "3.2.4" } }, "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA=="], - "@ungap/promise-all-settled": ["@ungap/promise-all-settled@1.1.2", "", {}, "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="], + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "anchor-bankrun": ["anchor-bankrun@0.5.0", "", { "peerDependencies": { "@coral-xyz/anchor": "^0.30.0", "@solana/web3.js": ">1.92.0", "solana-bankrun": ">=0.2.0 <0.5.0" } }, "sha512-cNTRv7pN9dy+kiyJ3UlNVTg9hAXhY2HtNVNXJbP/2BkS9nOdLV0qKWhgW8UR9Go0gYuEOLKuPzrGL4HFAZPsVw=="], - "ansi-colors": ["ansi-colors@4.1.1", "", {}, "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], @@ -161,241 +253,145 @@ "bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="], - "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "buffer-layout": ["buffer-layout@1.2.2", "", {}, "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA=="], "buffer-reverse": ["buffer-reverse@1.0.1", "", {}, "sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg=="], "bufferutil": ["bufferutil@4.0.9", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], + "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - "deep-eql": ["deep-eql@4.1.4", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], - "diff": ["diff@5.0.0", "", {}, "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="], + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], - - "eslint-config-prettier": ["eslint-config-prettier@8.10.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg=="], - - "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="], - "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], "fastestsmallesttextencoderdecoder": ["fastestsmallesttextencoderdecoder@1.0.22", "", {}, "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], - - "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-func-name": ["get-func-name@2.0.2", "", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], - - "glob": ["glob@7.2.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], - - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - - "growl": ["growl@1.10.5", "", {}, "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], - - "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], - - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], - "jayson": ["jayson@4.2.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "jayson": ["jayson@4.2.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "keccak": ["keccak@3.0.4", "", { "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", "readable-stream": "^3.6.0" } }, "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q=="], "keccak256": ["keccak256@1.0.6", "", { "dependencies": { "bn.js": "^5.2.0", "buffer": "^6.0.3", "keccak": "^3.0.2" } }, "sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lint-staged": ["lint-staged@16.1.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "merkletreejs": ["merkletreejs@0.5.2", "", { "dependencies": { "buffer-reverse": "^1.0.1", "crypto-js": "^4.2.0", "treeify": "^1.1.0" } }, "sha512-MHqclSWRSQQbYciUMALC3PZmE23NPf5IIYo+Z7qAz5jVcqgCB95L1T9jGcr+FtOj2Pa2/X26uG2Xzxs7FJccUg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - - "mocha": ["mocha@9.2.2", "", { "dependencies": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.2.0", "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", "minimatch": "4.2.1", "ms": "2.1.3", "nanoid": "3.3.1", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" }, "bin": { "mocha": "bin/mocha", "_mocha": "bin/_mocha" } }, "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.1", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="], + "nano-spawn": ["nano-spawn@1.0.2", "", {}, "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "node-addon-api": ["node-addon-api@2.0.2", "", {}, "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="], @@ -403,65 +399,45 @@ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "ox": ["ox@0.8.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.8", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-e+z5epnzV+Zuz91YYujecW8cF01mzmrUtWotJ0oEPym/G82uccs7q0WDHTYL3eiONbTUEvcZrptAKLgTBD3u2A=="], "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "rollup": ["rollup@4.45.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.1", "@rollup/rollup-android-arm64": "4.45.1", "@rollup/rollup-darwin-arm64": "4.45.1", "@rollup/rollup-darwin-x64": "4.45.1", "@rollup/rollup-freebsd-arm64": "4.45.1", "@rollup/rollup-freebsd-x64": "4.45.1", "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", "@rollup/rollup-linux-arm-musleabihf": "4.45.1", "@rollup/rollup-linux-arm64-gnu": "4.45.1", "@rollup/rollup-linux-arm64-musl": "4.45.1", "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-musl": "4.45.1", "@rollup/rollup-linux-s390x-gnu": "4.45.1", "@rollup/rollup-linux-x64-gnu": "4.45.1", "@rollup/rollup-linux-x64-musl": "4.45.1", "@rollup/rollup-win32-arm64-msvc": "4.45.1", "@rollup/rollup-win32-ia32-msvc": "4.45.1", "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw=="], "rpc-websockets": ["rpc-websockets@9.1.1", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "serialize-javascript": ["serialize-javascript@6.0.0", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "solana-bankrun": ["solana-bankrun@0.4.0", "", { "dependencies": { "@solana/web3.js": "^1.68.0", "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" } }, "sha512-NMmXUipPBkt8NgnyNO3SCnPERP6xT/AMNMBooljGA3+rG6NN8lmXJsKeLqQTiFsDeWD74U++QM/DgcueSWvrIg=="], @@ -475,97 +451,83 @@ "solana-bankrun-linux-x64-musl": ["solana-bankrun-linux-x64-musl@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Nv328ZanmURdYfcLL+jwB1oMzX4ZzK57NwIcuJjGlf0XSNLq96EoaO5buEiUTo4Ls7MqqMyLbClHcrPE7/aKyA=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], "superstruct": ["superstruct@0.15.5", "", {}, "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - "treeify": ["treeify@1.1.0", "", {}, "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A=="], + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - "ts-mocha": ["ts-mocha@10.1.0", "", { "dependencies": { "ts-node": "7.0.1" }, "optionalDependencies": { "tsconfig-paths": "^3.5.0" }, "peerDependencies": { "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X || ^11.X.X" }, "bin": { "ts-mocha": "bin/ts-mocha" } }, "sha512-T0C0Xm3/WqCuF2tpa0GNGESTBoKZaiqdUP8guNv4ZY316AFXlyidnrzQ1LUrCT0Wb1i3J0zFTgOh/55Un44WdA=="], + "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "ts-node": ["ts-node@7.0.1", "", { "dependencies": { "arrify": "^1.0.0", "buffer-from": "^1.1.0", "diff": "^3.1.0", "make-error": "^1.1.1", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "source-map-support": "^0.5.6", "yn": "^2.0.0" }, "bin": { "ts-node": "dist/bin.js" } }, "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + "treeify": ["treeify@1.1.0", "", {}, "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A=="], - "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "viem": ["viem@2.33.0", "", { "dependencies": { "@noble/curves": "1.9.2", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.0.8", "isows": "1.0.7", "ox": "0.8.1", "ws": "8.18.2" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-SxBM3CmeU+LWLlBclV9MPdbuFV8mQEl0NeRc9iyYU4a7Xb5sr5oku3s/bRGTPpEP+1hCAHYpM09/ui3/dQ6EsA=="], - "workerpool": ["workerpool@6.2.0", "", {}, "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A=="], + "vite": ["vite@7.0.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "yargs-parser": ["yargs-parser@20.2.4", "", {}, "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "yargs-unparser": ["yargs-unparser@2.0.0", "", { "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" } }, "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "yn": ["yn@2.0.0", "", {}, "sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ=="], + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], - "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], "@solana/codecs/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], @@ -583,8 +545,6 @@ "@solana/codecs-strings/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/errors/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "@solana/errors/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], "@solana/options/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], @@ -595,23 +555,19 @@ "@solana/web3.js/superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "jayson/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "listr2/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], - "mocha/debug": ["debug@4.3.3", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "mocha/minimatch": ["minimatch@4.2.1", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g=="], + "ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -619,34 +575,20 @@ "rpc-websockets/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "ts-node/diff": ["diff@3.5.0", "", {}, "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="], - - "@solana/codecs-data-structures/@solana/errors/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "@solana/codecs-data-structures/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@solana/codecs-strings/@solana/errors/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "@solana/codecs-strings/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@solana/codecs/@solana/codecs-core/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], "@solana/codecs/@solana/codecs-numbers/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/options/@solana/errors/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "@solana/options/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "mocha/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - - "@solana/codecs/@solana/codecs-core/@solana/errors/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], "@solana/codecs/@solana/codecs-core/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@solana/codecs/@solana/codecs-numbers/@solana/errors/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "@solana/codecs/@solana/codecs-numbers/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], } } diff --git a/justfile b/justfile new file mode 100644 index 00000000..1de07d97 --- /dev/null +++ b/justfile @@ -0,0 +1,127 @@ +# See https://github.com/sablier-labs/devkit/blob/main/just/base.just +import "./node_modules/@sablier/devkit/just/base.just" + +# ---------------------------------------------------------------------------- # +# DEPENDENCIES # +# ---------------------------------------------------------------------------- # + +# Anchor: https://solana.com/docs/intro/installation#install-anchor-cli +anchor := require("anchor") +# Solana: https://solana.com/docs/intro/installation#install-the-solana-cli +solana := require("solana") + +# ---------------------------------------------------------------------------- # +# ENVIRONMENT # +# ---------------------------------------------------------------------------- # + +export RUST_LOG := env("RUST_LOG", "off") +# See https://github.com/sablier-labs/solsab/issues/180 +export RUSTUP_TOOLCHAIN := "nightly" + +# ---------------------------------------------------------------------------- # +# CONSTANTS # +# ---------------------------------------------------------------------------- # + +GLOBS_CLEAN := "{.anchor,target}" +GLOBS_PRETTIER := "**/*.{md,yml}" + +# ---------------------------------------------------------------------------- # +# RECIPES # +# ---------------------------------------------------------------------------- # + +# Default recipe - show available commands +default: + just --list + +# Build programs using Anchor +[group("build")] +build program_name="all": + #!/usr/bin/env sh + if [ "{{ program_name }}" = "all" ]; then + cmd="anchor build" + else + cmd="anchor build --program-name {{ program_name }}" + fi + # Suppressing the annoying "Compiling" and "Downloaded" messages + # Remove this once this gets implemented: https://github.com/solana-foundation/anchor/issues/3788 + $cmd 2>&1 | grep -v "Compiling\|Downloaded" + echo "✅ Successful build\n" + just codegen {{ program_name }} +alias b := build + +# Build Lockup program only using Anchor +[group("build")] +build-lockup: (build "sablier_lockup") +alias blk := build-lockup + +# Build Merkle Instant program only using Anchor +[group("build")] +build-merkle-instant: (build "sablier_merkle_instant") +alias bmi := build-merkle-instant + +# Codegen errors and struct types +@codegen program_name="all": + bun run ./scripts/ts/codegen-errors.ts {{ program_name }} + bun run ./scripts/ts/codegen-structs.ts {{ program_name }} + +# Clean build artifacts +clean globs=GLOBS_CLEAN: + nlx del-cli "{{ globs }}" + +# Run verification script +verify: + bash ./scripts/bash/verify.sh +alias v := verify + +# ---------------------------------------------------------------------------- # +# CODE CHECKS # +# ---------------------------------------------------------------------------- # + +# Run all code checks +full-check: prettier-check biome-check tsc-check rust-check + +# Run all code fixes +full-write: prettier-write biome-write rust-write + +# Check Rust formatting +rust-check: + cargo fmt --check +alias rc := rust-check + +# Format Rust code +rust-write: + cargo fmt +alias rw := rust-write + +# ============================================================================ # +# TESTING # +# ============================================================================ # + +# Run all tests +# To debug the Solana logs, run this as `RUST_LOG=debug just test` +[group("test")] +test *args: build + na vitest run --hideSkippedTests {{args}} +alias t := test + +# Run all tests without building +test-lite *args: + na vitest run --hideSkippedTests {{args}} +alias tl := test-lite + +# Run tests with UI +test-ui *args: build + na vitest --hideSkippedTests --ui {{args}} +alias tui := test-ui + +# Run Lockup tests only +[group("test")] +test-lockup *args="tests/lockup/**/*.test.ts": + just test {{ args }} +alias tlk := test-lockup + +# Run Merkle Instant tests only +[group("test")] +test-merkle-instant *args="tests/merkle-instant/**/*.test.ts": + just test {{ args }} +alias tmi := test-merkle-instant diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 00000000..6e298a67 --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1,22 @@ +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { PublicKey, LAMPORTS_PER_SOL as raw_LAMPORTS_PER_SOL } from "@solana/web3.js"; +import BN from "bn.js"; + +export const BN_1 = new BN(1); +export const BN_1000 = new BN(1000); +export const LAMPORTS_PER_SOL = new BN(raw_LAMPORTS_PER_SOL); +export const REDUNDANCY_BUFFER = new BN(1_000_000); // 0.001 SOL +export const SCALING_FACTOR = new BN("1000000000000000000"); // 1e18 +export const ZERO = new BN(0); + +export namespace Decimals { + export const DAI = 9; + export const SOL = 9; + export const USDC = 6; +} + +export namespace ProgramId { + export const TOKEN_2022 = TOKEN_2022_PROGRAM_ID; + export const TOKEN_METADATA = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); + export const TOKEN = TOKEN_PROGRAM_ID; +} diff --git a/lib/convertors.ts b/lib/convertors.ts new file mode 100644 index 00000000..085641e5 --- /dev/null +++ b/lib/convertors.ts @@ -0,0 +1,44 @@ +/** + * @file Convert a floating point number to a token amount in its decimal system. + * + * @param amount - The amount to convert, as either a number or a string. + * @returns The number of tokens in their smallest unit. + * + * @example + * ```ts + * sol("1.02"); // 1_020_000_000 + * sol(10_000); // 10_000_000_000_000 + * usdc("100.5"); // 100_500_000 + * ``` + * + * @todo: Make this work with scientific notation inputs. + */ +import { BN } from "@coral-xyz/anchor"; +import _ from "lodash"; +import { parseUnits } from "viem"; +import { Decimals } from "./constants"; + +function convertTokenAmount(amount: number | string, decimals: number): BN { + if (typeof amount === "number") { + if (!_.isInteger(amount)) { + throw new Error( + `Amount must be an integer, got: ${amount}. If you want to use floating points, pass it as a string.`, + ); + } + return new BN(amount).mul(new BN(10 ** decimals)); + } + + return new BN(parseUnits(amount, decimals)); +} + +export function dai(amount: number | string): BN { + return convertTokenAmount(amount, Decimals.DAI); +} + +export function sol(amount: number | string): BN { + return convertTokenAmount(amount, Decimals.SOL); +} + +export function usdc(amount: number | string): BN { + return convertTokenAmount(amount, Decimals.USDC); +} diff --git a/lib/enums.ts b/lib/enums.ts new file mode 100644 index 00000000..68f6163b --- /dev/null +++ b/lib/enums.ts @@ -0,0 +1,4 @@ +export enum ProgramName { + Lockup = "sablier_lockup", + MerkleInstant = "sablier_merkle_instant", +} diff --git a/lib/helpers.ts b/lib/helpers.ts new file mode 100644 index 00000000..e241aee2 --- /dev/null +++ b/lib/helpers.ts @@ -0,0 +1,18 @@ +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +export function getPDAAddress(seeds: Array, programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync(seeds, programId)[0]; +} + +export async function sleepFor(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function toBigInt(number: number | BN): bigint { + return BigInt(number.toString()); +} + +export function toBn(number: number | bigint): BN { + return new BN(number.toString()); +} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..7a4565f0 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,3 @@ +import { type ProgramName as ProgramNameEnum } from "./enums"; + +export type ProgramName = `${ProgramNameEnum}` | ProgramNameEnum; diff --git a/migrations/deploy.ts b/migrations/deploy.ts index 17efabf0..31a5a26b 100644 --- a/migrations/deploy.ts +++ b/migrations/deploy.ts @@ -1,10 +1,9 @@ -// Migrations are an early feature. Currently, they're nothing more than this -// single deploy script that's invoked from the CLI, injecting a provider -// configured from the workspace's Anchor.toml. +// Migrations are an early feature. Currently, they're nothing more than this single deploy script that's invoked +// from the CLI, injecting a provider configured from the workspace's Anchor.toml. import * as anchor from "@coral-xyz/anchor"; -module.exports = async function (provider: anchor.Provider) { +module.exports = async (provider: anchor.Provider) => { // Configure client to use the provider. anchor.setProvider(provider); diff --git a/package.json b/package.json index 2105e56a..35385bea 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sablier/solsab", - "description": "Programs of the Sablier token distribution protocol for the Solana chain", - "license": "GPL-3.0-or-later", + "description": "Solana programs of the Sablier token distribution protocol", + "license": "SEE LICENSE.md", "version": "1.0.0", "author": { "name": "Sablier Labs Ltd", @@ -10,50 +10,40 @@ "bugs": { "url": "https://github.com/sablier-labs/solsab/issues" }, - "dependencies": { - "@coral-xyz/anchor": "^0.31.1", - "@solana/web3.js": "^1.98.2", - "anchor-bankrun": "^0.5.0", - "solana-bankrun": "^0.4.0" - }, "devDependencies": { + "@biomejs/biome": "^2.1.2", + "@coral-xyz/anchor": "^0.31.1", + "@coral-xyz/anchor-errors": "^0.31.1", + "@sablier/devkit": "github:sablier-labs/devkit#main", "@solana/spl-token": "^0.4.13", - "@types/chai": "^4.3.20", - "@types/mocha": "^9.1.1", + "@solana/web3.js": "^1.98.2", + "@types/bn.js": "^5.2.0", + "@types/lodash": "^4.17.20", "@types/node": "^24.0.1", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", - "chai": "^4.5.0", - "eslint": "^8.57.1", - "eslint-config-prettier": "^8.10.0", + "@vitest/ui": "^3.2.4", + "anchor-bankrun": "^0.5.0", + "bn.js": "^5.2.2", + "dayjs": "^1.11.13", + "husky": "^9.1.7", "keccak256": "^1.0.6", + "lint-staged": "^16.1.2", + "lodash": "^4.17.21", "merkletreejs": "^0.5.2", - "mocha": "^9.2.2", "prettier": "^2.8.8", - "ts-mocha": "^10.1.0", - "typescript": "^5.8.3" + "solana-bankrun": "^0.4.0", + "typescript": "5.8.3", + "viem": "^2.33.0", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=23" + }, + "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/sablier-labs/solsab.git" }, "scripts": { - "b": "bun run build", - "b:lk": "RUSTUP_TOOLCHAIN=nightly anchor build -p sablier_lockup", - "b:mi": "RUSTUP_TOOLCHAIN=nightly anchor build -p sablier_merkle_instant", - "build": "RUSTUP_TOOLCHAIN=nightly anchor build", - "clean": "rm -rf .anchor target", - "fmt:rust": "RUSTUP_TOOLCHAIN=nightly cargo fmt", - "fmt:rust:check": "RUSTUP_TOOLCHAIN=nightly cargo fmt --check", - "lint": "bun run fmt:rust:check && bun run lint:ts && bun run prettier:check", - "lint:fix": "bun run fmt:rust && bun run lint:ts:fix && bun run prettier:write", - "lint:ts": "eslint \"migrations/**/*.ts\" \"tests/**/*.ts\"", - "lint:ts:fix": "eslint --ext \"migrations/**/*.ts\" \"tests/**/*.ts\" --fix", - "prettier:check": "prettier --check \"**/*.{json,md,svg,ts,yml}\"", - "prettier:write": "prettier --write \"**/*.{json,md,svg,ts,yml}\"", - "t": "bun run build && RUST_LOG=off bun run ts-mocha -t 1000000 --parallel tests/**/**/*.ts", - "t:debug": "bun run build && RUST_LOG=debug bun run ts-mocha -t 1000000 --parallel tests/**/**/*.ts", - "t:match": "bun run t --grep", - "t:only": "bun run build && RUST_LOG=off bun run ts-mocha -t 1000000 tests/**/**/*.ts", - "t:only:debug": "bun run build && RUST_LOG=debug bun run ts-mocha -t 1000000 tests/**/**/*.ts", - "t:lk": "bun run build && RUST_LOG=off bun run ts-mocha -t 1000000 --parallel tests/lockup/**/*.ts", - "t:mi": "bun run build && RUST_LOG=off bun run ts-mocha -t 1000000 --parallel tests/merkle_instant/**/*.ts", - "verify": "bash ./verify.sh" + "prepare": "bun husky" } } diff --git a/programs/lockup/Cargo.toml b/programs/lockup/Cargo.toml index 08f34a99..f9dd531a 100644 --- a/programs/lockup/Cargo.toml +++ b/programs/lockup/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "sablier_lockup" -version = "0.1.0" -description = "Created with Anchor" -edition = "2021" + name = "sablier_lockup" + version = "0.1.0" + description = "Created with Anchor" + edition = "2021" [lib] -crate-type = ["cdylib", "lib"] -name = "sablier_lockup" + crate-type = ["cdylib", "lib"] + name = "sablier_lockup" [features] -default = [] -cpi = ["no-entrypoint"] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] + default = [] + cpi = ["no-entrypoint"] + no-entrypoint = [] + no-idl = [] + no-log-ix-name = [] + idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] -anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } -anchor-spl = { version = "0.31.1", features = ["metadata"] } -mpl-token-metadata = "5.1.0" + anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } + anchor-spl = { version = "0.31.1", features = ["metadata"] } + mpl-token-metadata = "5.1.0" diff --git a/programs/lockup/Xargo.toml b/programs/lockup/Xargo.toml index 475fb71e..de170be4 100644 --- a/programs/lockup/Xargo.toml +++ b/programs/lockup/Xargo.toml @@ -1,2 +1,2 @@ [target.bpfel-unknown-unknown.dependencies.std] -features = [] + features = [] diff --git a/programs/lockup/src/lib.rs b/programs/lockup/src/lib.rs index 8b93aa70..57a90368 100644 --- a/programs/lockup/src/lib.rs +++ b/programs/lockup/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(unexpected_cfgs)] use anchor_lang::prelude::*; pub mod instructions; diff --git a/programs/merkle_instant/Cargo.toml b/programs/merkle_instant/Cargo.toml index 754c33bf..3b1126c8 100644 --- a/programs/merkle_instant/Cargo.toml +++ b/programs/merkle_instant/Cargo.toml @@ -1,25 +1,25 @@ [package] -name = "sablier_merkle_instant" -version = "0.1.0" -description = "Created with Anchor" -edition = "2021" + name = "sablier_merkle_instant" + version = "0.1.0" + description = "Created with Anchor" + edition = "2021" [lib] -crate-type = ["cdylib", "lib"] -name = "sablier_merkle_instant" + crate-type = ["cdylib", "lib"] + name = "sablier_merkle_instant" [features] -default = [] -cpi = ["no-entrypoint"] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] + default = [] + cpi = ["no-entrypoint"] + no-entrypoint = [] + no-idl = [] + no-log-ix-name = [] + idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] -anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } -anchor-spl = { version = "0.31.1" } + anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } + anchor-spl = { version = "0.31.1" } -bitvec = "1.0.1" -# svm-merkle-tree = { git = "https://github.com/deanmlittle/svm-merkle-tree", version = "0.1.1" } -solana-merkle-tree = { version = "2.2.12" } + bitvec = "1.0.1" + # svm-merkle-tree = { git = "https://github.com/deanmlittle/svm-merkle-tree", version = "0.1.1" } + solana-merkle-tree = { version = "2.2.12" } diff --git a/programs/merkle_instant/Xargo.toml b/programs/merkle_instant/Xargo.toml index 475fb71e..de170be4 100644 --- a/programs/merkle_instant/Xargo.toml +++ b/programs/merkle_instant/Xargo.toml @@ -1,2 +1,2 @@ [target.bpfel-unknown-unknown.dependencies.std] -features = [] + features = [] diff --git a/programs/merkle_instant/src/lib.rs b/programs/merkle_instant/src/lib.rs index b7b333b7..9cf953ad 100644 --- a/programs/merkle_instant/src/lib.rs +++ b/programs/merkle_instant/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(unexpected_cfgs)] use anchor_lang::prelude::*; pub mod instructions; diff --git a/repomix.config.jsonc b/repomix.config.jsonc new file mode 100644 index 00000000..b087a9be --- /dev/null +++ b/repomix.config.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "https://repomix.com/schemas/latest/schema.json", + "ignore": { + "useDefaultPatterns": true, + "useGitignore": true + }, + "include": ["."], + "output": { + "filePath": "repomix/output.md", + "style": "markdown" + } +} diff --git a/rustfmt.toml b/rustfmt.toml index 65daa87c..a36318f1 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,9 +2,4 @@ binop_separator = "Back" comment_width = 120 imports_granularity = "Crate" max_width = 120 -reorder_imports = true -tab_spaces = 4 -trailing_semicolon = true -use_field_init_shorthand = true use_small_heuristics = "Max" -wrap_comments = true \ No newline at end of file diff --git a/scripts/bash/deploy_programs.sh b/scripts/bash/deploy-programs.sh similarity index 95% rename from scripts/bash/deploy_programs.sh rename to scripts/bash/deploy-programs.sh index a71b2ef2..814379d5 100755 --- a/scripts/bash/deploy_programs.sh +++ b/scripts/bash/deploy-programs.sh @@ -4,7 +4,7 @@ # It must be run from the root of the SolSab repo. # # USAGE: -# ./scripts/bash/deploy_programs.sh [OPTIONS] +# ./scripts/bash/deploy-programs.sh [OPTIONS] # # OPTIONS: # --program PROGRAM [PROGRAM...] Specify which program(s) to deploy @@ -13,11 +13,11 @@ # --no-init Do not run the post-deployment initialization script # # EXAMPLES: -# ./scripts/bash/deploy_programs.sh --program sablier_lockup # Deploy & initialize just the lockup program -# ./scripts/bash/deploy_programs.sh --program sablier_merkle_instant # Deploy & initialize just the merkle_instant program -# ./scripts/bash/deploy_programs.sh --program lk mi # Deploy & initialize both programs -# ./scripts/bash/deploy_programs.sh --no-init --program sablier_lockup # Deploy the lockup program without the initialization -# ./scripts/bash/deploy_programs.sh --no-init --program lk mi # Deploy both programs without the initialization +# ./scripts/bash/deploy-programs.sh --program sablier_lockup # Deploy & initialize just the lockup program +# ./scripts/bash/deploy-programs.sh --program sablier_merkle_instant # Deploy & initialize just the merkle_instant program +# ./scripts/bash/deploy-programs.sh --program lk mi # Deploy & initialize both programs +# ./scripts/bash/deploy-programs.sh --no-init --program sablier_lockup # Deploy the lockup program without the initialization +# ./scripts/bash/deploy-programs.sh --no-init --program lk mi # Deploy both programs without the initialization # # WHAT THIS SCRIPT DOES: # 1. Switches to main branch and pulls latest changes diff --git a/scripts/bash/verify.sh b/scripts/bash/verify.sh index 09320bb2..acc18886 100755 --- a/scripts/bash/verify.sh +++ b/scripts/bash/verify.sh @@ -5,6 +5,9 @@ # --commit Verify against a specific commit # --cluster Specify the Solana cluster URL +# Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca +set -euo pipefail + PROGRAM_ID="uwuJk35aCL3z2FzfPr8fQE1U19A8N18qdA5YfdfUbPt" CLUSTER=$(solana config get | grep 'RPC URL' | awk '{print $3}') SKIP_BUILD=false diff --git a/scripts/ts/codegen-errors.ts b/scripts/ts/codegen-errors.ts new file mode 100644 index 00000000..9418ec77 --- /dev/null +++ b/scripts/ts/codegen-errors.ts @@ -0,0 +1,79 @@ +/** + * @file This script generates TypeScript bindings for the Solana program errors. + * + * Purpose: + * - Reads Anchor IDL JSON files from target/idl/*.json + * - Extracts error definitions from the "errors" field + * - Generates TypeScript error code constants and types + * - Outputs to target/types/ as *_errors.ts files + * + * Usage: + * - bun run codegen:errors all (generates for all programs) + * - bun run codegen:errors sablier_lockup (generates for a specific program) + */ + +import type { IdlErrorCode } from "@coral-xyz/anchor/dist/cjs/idl"; +import { createMainFunction, extractIdlField, generateFileHeader, writeGeneratedFile } from "./common/codegen-utils"; + +/** + * Main function that generates TypeScript error definitions for a single program + * + * Process: + * 1. Read the JSON IDL file from target/idl/{programName}.json + * 2. Extract and validate the "errors" array + * 3. Generate TypeScript error code constants and types + * 4. Write the result to target/types/{programName}_errors.ts + * + * @param programName - The name of the program (e.g., "sablier_lockup") + * @throws Error if the IDL file is malformed or missing errors + */ +function genErrors(programName: string): void { + const errors = extractIdlField(programName, "errors", validateErrors); + const content = generateErrorCode(errors); + writeGeneratedFile(content, programName, "_errors"); +} + +/** + * Generates TypeScript error code definitions from IDL error data + * + * Creates: + * 1. ProgramErrorCode constant object mapping error names to codes + * 2. ProgramErrorName type for type-safe error name usage + * + * @param errors - Array of error definitions from the IDL + * @returns Generated TypeScript content + */ +function generateErrorCode(errors: IdlErrorCode[]): string { + const errorEntries = errors.map(({ name, code }) => ` ${name}: ${code},`); + + const content = [ + ...generateFileHeader(), + "export const ProgramErrorCode = {", + ...errorEntries, + "} as const;", + "", + "export type ProgramErrorName = keyof typeof ProgramErrorCode;", + ]; + + return content.join("\n"); +} + +/** + * Validates that error definitions have the required fields + * + * @param errors - Array of error definitions from the IDL + * @returns True if all errors are valid + */ +function validateErrors(errors: IdlErrorCode[]): boolean { + return !errors.some((e) => !e.name || !e.code); +} + +/* -------------------------------------------------------------------------- */ +/* MAIN FUNCTION */ +/* -------------------------------------------------------------------------- */ + +const main = createMainFunction(genErrors, "TypeScript error bindings"); + +if (require.main === module) { + main(); +} diff --git a/scripts/ts/codegen-structs.ts b/scripts/ts/codegen-structs.ts new file mode 100644 index 00000000..478c9eef --- /dev/null +++ b/scripts/ts/codegen-structs.ts @@ -0,0 +1,237 @@ +/** + * @file This script generates TypeScript bindings for the Solana program struct types. + * + * Purpose: + * - Reads Anchor IDL JSON files from target/idl/*.json + * - Extracts type definitions (structs and enums) from the "types" field + * - Converts Rust/Solana types to TypeScript equivalents + * - Generates clean TypeScript type definitions with camelCase field names + * - Outputs to target/types/ as *_structs.ts files + * + * Usage: + * - bun run codegen:structs all (generates for all programs) + * - bun run codegen:structs sablier_lockup (generates for specific program) + */ + +import _ from "lodash"; +import { createMainFunction, extractIdlField, generateFileHeader, writeGeneratedFile } from "./common/codegen-utils"; + +/* -------------------------------------------------------------------------- */ +/* TYPES */ +/* -------------------------------------------------------------------------- */ + +/** + * Represents a field in an Anchor IDL type definition + */ +type IdlField = { + name: string; + type: IdlTypeDefinition; +}; + +/** + * Represents a complete type definition from the Anchor IDL + */ +type IdlType = { + name: string; + type: { + kind: "struct" | "enum"; + fields?: IdlField[]; + variants?: { name: string }[]; + }; +}; + +/** + * Represents the possible type definitions in an Anchor IDL + * + * Can be one of: + * - string: Primitive type like "u64", "bool", "pubkey" + * - { defined: { name: string } }: Reference to another type in the same IDL + * - { array: [string, number] }: Array type with element type and size + */ +type IdlTypeDefinition = string | { defined: { name: string } } | { array: [string, number] }; + +/** + * Mapping from Rust/Solana primitive types to TypeScript equivalents + * + * Type conversion strategy: + * - Integer types with less than 64 bits → number + * - Integer types with 64 bits or more → BN (BigNumber) + * - bool → boolean + * - pubkey → PublicKey (from @solana/web3.js) + * - string → string (unchanged) + * + * Note: We use BN for all integers to handle Solana's large numbers safely + */ +const RUST_TYPES = { + bool: "boolean", + i64: "BN", + pubkey: "PublicKey", + u8: "number", + u32: "number", + u64: "BN", + u128: "BN", +} as const; + +type RustType = keyof typeof RUST_TYPES; + +/* -------------------------------------------------------------------------- */ +/* HELPERS */ +/* -------------------------------------------------------------------------- */ + +/** + * Analyzes all types and generates the necessary import statements + * + * Scans through all type definitions to determine which external types are needed: + * - BN: Required for any u64, u128, i64 fields (imported from "bn.js") + * - PublicKey: Required for any pubkey fields (imported from "@solana/web3.js") + * + * This ensures we only import what we actually use, keeping the generated files clean. + */ +function generateImports(types: IdlType[]): string[] { + const imports: string[] = []; + let needsBN = false; + let needsPublicKey = false; + + // Scan through all struct fields to see what types we need to import + _.forEach(types, (type) => { + if (type.type.kind !== "struct" || !type.type.fields) { + return; + } + + _.forEach(type.type.fields, (field) => { + const mappedType = mapSolanaTypeToTypeScript(field.type); + + // Check if this field requires external type imports + if (mappedType.includes("BN")) { + needsBN = true; + } else if (mappedType.includes("PublicKey")) { + needsPublicKey = true; + } + }); + }); + + // Generate import statements only for types we actually use + if (needsBN) { + imports.push('import BN from "bn.js";'); + } + if (needsPublicKey) { + imports.push('import { type PublicKey } from "@solana/web3.js";'); + } + + return imports; +} + +/** + * Generates a complete TypeScript type definition from an IDL type + * + * Handles two main cases: + * 1. Enums: Creates union types with string literals + * Example: export type StreamStatus = "Pending" | "Streaming" | "Settled"; + * + * 2. Structs: Creates object types with typed properties + * Example: export type Amounts = { + * startUnlock: BN; + * cliffUnlock: BN; + * }; + */ +function generateStructType(idlType: IdlType): string { + if (idlType.type.kind === "enum") { + // Handle enum types - convert to TypeScript union types with string literals + // Rust: enum StreamStatus { Pending, Streaming, Settled } + // TS: type StreamStatus = "Pending" | "Streaming" | "Settled" + const variants = idlType.type.variants?.map((variant) => `"${variant.name}"`).join(" | "); + return `export type ${idlType.name} = ${variants};\n`; + } + + if (!idlType.type.fields || idlType.type.fields.length === 0) { + // Skip empty structs (like ClaimReceipt which has no fields) + return ""; + } + + // Handle struct types - convert each field and create object type + const fields = idlType.type.fields + .map((field) => { + const fieldName = _.camelCase(field.name); // snake_case → camelCase + const fieldType = mapSolanaTypeToTypeScript(field.type); // Rust type → TS type + return ` ${fieldName}: ${fieldType};`; + }) + .join("\n"); + + return `export type ${idlType.name} = {\n${fields}\n};\n`; +} + +/** + * Converts Rust/Solana types from the IDL to their TypeScript equivalents + * + * Handles three main cases: + * 1. Primitive types (string): Maps using RUST_TYPES lookup table + * 2. Custom defined types (object with 'defined' key): References another type in the same file + * 3. Arrays (object with 'array' key): Converts element type and adds [] + * + * @param type - The type definition from the IDL + * @returns The equivalent TypeScript type string + * @throws Error if an unknown type is encountered + */ +function mapSolanaTypeToTypeScript(type: IdlTypeDefinition): string { + if (typeof type === "string") { + // Handle primitive types using RUST_TYPES lookup table + // If type is not in table, return as-is (assume it's already a valid TS type) + return RUST_TYPES[type as RustType] || type; + } + + if (type && _.isObject(type)) { + if ("defined" in type) { + // Handle custom defined types (references to other structs/enums in the same IDL) + // These will be converted to PascalCase when we generate the type definitions + return type.defined.name; + } else if ("array" in type) { + // Handle arrays - convert the element type and add array notation + const [elementType] = type.array; + const mappedElementType = mapSolanaTypeToTypeScript(elementType); + return `${mappedElementType}[]`; + } + } + + // If we reach here, it's an unknown type that needs to be added to RUST_TYPES + throw new Error(`Unknown type: ${JSON.stringify(type)}. Add it to the RUST_TYPES object.`); +} + +/* -------------------------------------------------------------------------- */ +/* MAIN FUNCTION */ +/* -------------------------------------------------------------------------- */ + +/** + * Main function that generates TypeScript struct definitions for a single program + * + * Process: + * 1. Read the JSON IDL file from target/idl/{programName}.json + * 2. Extract the "types" array containing struct and enum definitions + * 3. Analyze types to determine required imports (BN, PublicKey) + * 4. Generate TypeScript type definitions for each struct/enum + * 5. Combine imports and type definitions into a complete file + * 6. Write the result to target/types/{programName}_structs.ts + */ +function genStructs(programName: string): void { + // Extract and validate type definitions + const types = extractIdlField(programName, "types"); + + // Generate the necessary imports and type definitions + const imports = generateImports(types); + const typeDefinitions = types.map(generateStructType).filter(Boolean); // Filter out empty strings + + // Combine everything into a complete TypeScript file + const content = [...generateFileHeader(imports), ...typeDefinitions].join("\n"); + + // Write the generated TypeScript file + writeGeneratedFile(content, programName, "_structs"); +} + +/* -------------------------------------------------------------------------- */ +/* MAIN */ +/* -------------------------------------------------------------------------- */ + +const main = createMainFunction(genStructs, "TypeScript struct bindings"); + +if (require.main === module) { + main(); +} diff --git a/scripts/ts/common/codegen-utils.ts b/scripts/ts/common/codegen-utils.ts new file mode 100644 index 00000000..27036f24 --- /dev/null +++ b/scripts/ts/common/codegen-utils.ts @@ -0,0 +1,180 @@ +/** + * @file Shared utilities for code generation scripts + * + * This module contains common functionality used by both codegen-errors.ts and codegen-structs.ts + * to avoid duplication and ensure consistency across code generation scripts. + */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { Idl } from "@coral-xyz/anchor"; +import _ from "lodash"; +import { ProgramName as ProgramNameEnum } from "../../../lib/enums"; +import type { ProgramName } from "../../../lib/types"; + +/* -------------------------------------------------------------------------- */ +/* CONSTANTS */ +/* -------------------------------------------------------------------------- */ + +/** Root directory of the project */ +export const ROOT_DIR = join(__dirname, "..", "..", ".."); + +/** Directory containing Anchor-generated IDL JSON files */ +export const IDL_DIR = join(ROOT_DIR, "target", "idl"); + +/** Directory for generated TypeScript files */ +export const TYPES_DIR = join(ROOT_DIR, "target", "types"); + +/** Valid program names that can be processed by codegen scripts */ +export const VALID_PROGRAMS = ["all", ..._.values(ProgramNameEnum)]; + +/* -------------------------------------------------------------------------- */ +/* CORE UTILITIES */ +/* -------------------------------------------------------------------------- */ + +/** + * Extracts a specific field from an IDL and validates it with type safety + * + * @template T - The expected type of the array elements + * @param programName - The name of the Solana program (e.g., "sablier_lockup") + * @param fieldName - The field to extract (e.g., "errors", "types") + * @param validator - Optional validation function for the extracted data + * @returns The extracted and validated data with proper typing + * @throws Error if the field is missing, not an array, or validation fails + */ +export function extractIdlField( + programName: string, + fieldName: keyof Idl, + validator?: (data: T[]) => boolean, +): T[] { + // Read and parse the IDL file + const idl = readIdlFile(programName); + + // Extract the specified field + const data = idl[fieldName]; + + if (!_.isArray(data)) { + throw new Error(`IDL incorrectly formatted - ${fieldName} field missing or not an array`); + } + + // Type assertion is safe here because we've validated it's an array + const typedData = data as T[]; + + if (validator && !validator(typedData)) { + throw new Error(`IDL validation failed for ${fieldName} field`); + } + + return typedData; +} + +/** + * Generates the standard file header for auto-generated files + * + * @param additionalImports - Optional additional import statements + * @returns The file header as an array of lines + */ +export function generateFileHeader(additionalImports: string[] = []): string[] { + return [ + "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + ...additionalImports, + ...(additionalImports.length > 0 ? [""] : []), + ]; +} + +/** + * Reads and parses an IDL JSON file for a given program + * + * @param programName - The name of the program (e.g., "sablier_lockup") + * @returns The parsed IDL object + * @throws Error if the file cannot be read or parsed + */ +export function readIdlFile(programName: string): Idl { + const idlPath = join(IDL_DIR, `${programName}.json`); + + try { + const content = readFileSync(idlPath, { encoding: "utf-8" }); + const parsed = JSON.parse(content) as Idl; + + // Basic validation of IDL structure + if (!parsed.address || !parsed.metadata || !parsed.metadata.name) { + throw new Error("Invalid IDL structure - missing required fields"); + } + + return parsed; + } catch (error) { + throw new Error(`Failed to read IDL file for ${programName}: ${error}`); + } +} + +/** + * Writes generated TypeScript content to a file + * + * @param content - The TypeScript content to write + * @param programName - The program name for the output file + * @param suffix - The file suffix (e.g., "_errors", "_structs") + */ +export function writeGeneratedFile(content: string, programName: string, suffix: string): void { + const outputPath = join(TYPES_DIR, `${programName}${suffix}.ts`); + writeFileSync(outputPath, content); +} + +/* -------------------------------------------------------------------------- */ +/* COMMAND LINE HANDLING */ +/* -------------------------------------------------------------------------- */ + +/** + * Standard main function for codegen scripts + * + * @param generatorFn - Function that generates code for a single program + * @param successMessage - Message to display on success + */ +export function createMainFunction(generatorFn: (programName: string) => void, successMessage: string) { + return function main() { + // Parse command line arguments + const programName = process.argv[2] as ProgramName | "all" | undefined; + + // Validate program name + validateProgramName(programName); + + // Execute code generation + executeCodegen(programName, generatorFn, successMessage); + }; +} + +/** + * Executes a code generation function for the specified program(s) + * + * @param programName - The program name or "all" + * @param generatorFn - Function that generates code for a single program + * @param successMessage - Message to display on success + */ +function executeCodegen( + programName: ProgramName | "all", + generatorFn: (programName: string) => void, + successMessage: string, +): void { + if (programName === "all") { + // Generate for all supported programs + generatorFn(ProgramNameEnum.Lockup); + generatorFn(ProgramNameEnum.MerkleInstant); + console.log(`✅ Successfully generated ${successMessage} for all programs\n`); + } else { + // Generate for a specific program + generatorFn(programName); + console.log(`✅ Successfully generated ${successMessage} for ${programName}\n`); + } +} + +/** + * Validates command line arguments for codegen scripts + * + * @param programName - The program name from command line arguments + * @throws Process exit if validation fails + */ +export function validateProgramName(programName: string | undefined): asserts programName is ProgramName | "all" { + if (!programName || !VALID_PROGRAMS.includes(programName)) { + console.error(`❌ Missing or Invalid program name: ${programName}`); + console.error(`📋 Valid options: ${VALID_PROGRAMS.join(", ")}`); + process.exit(1); + } +} diff --git a/scripts/ts/lockup-initialization.ts b/scripts/ts/init-lockup.ts similarity index 67% rename from scripts/ts/lockup-initialization.ts rename to scripts/ts/init-lockup.ts index ad34ddbf..c6abd4d7 100644 --- a/scripts/ts/lockup-initialization.ts +++ b/scripts/ts/init-lockup.ts @@ -1,76 +1,74 @@ -import { PublicKey, Keypair, ComputeBudgetProgram } from "@solana/web3.js"; import * as anchor from "@coral-xyz/anchor"; -import { BN } from "@coral-xyz/anchor"; - -import { - createMint, - getOrCreateAssociatedTokenAccount, - mintTo, - TOKEN_PROGRAM_ID, -} from "@solana/spl-token"; - -import { SablierLockup } from "../../target/types/sablier_lockup"; - -let anchorProvider: any; +import { createMint, getOrCreateAssociatedTokenAccount, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { ComputeBudgetProgram, Keypair, type PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { beforeEach, describe, it } from "vitest"; +import { BN_1, Decimals, ZERO } from "../../lib/constants"; +import { sol } from "../../lib/convertors"; +import { type SablierLockup } from "../../target/types/sablier_lockup"; + +let anchorProvider: anchor.AnchorProvider; let lockupProgram: anchor.Program; let senderKeys: Keypair; describe("SablierLockup post-deployment initialization", () => { beforeEach(async () => { await configureTestingEnvironment(); - await initializeSablierLockup(); + await initSablierLockup(); }); - it("Creates 3 different SPL Token LL Streams", async () => { + it("Creates three different SPL Token LL Streams", async () => { // Create a token mint and mint some tokens to the sender const depositTokenMint = await createTokenAndMintToSender(); // Create 3 unique streams await createStream({ - salt: new BN(1), + cliffDuration: ZERO, + depositAmount: sol(1000), depositTokenMint, - depositAmount: new BN(1_000e9), - cliffDuration: new BN(0), - totalDuration: new BN(3600), // 1 hour - unlockStartAmount: new BN(0), - unlockCliffAmount: new BN(0), isCancelable: true, + salt: BN_1, + totalDuration: new BN(3600), // 1 hour + unlockCliffAmount: ZERO, + unlockStartAmount: ZERO, }); await createStream({ - salt: new BN(2), - depositTokenMint, - depositAmount: new BN(10_000e9), cliffDuration: new BN(3600), // 1 hour + depositAmount: sol(10_000), + depositTokenMint, + isCancelable: true, + salt: new BN(2), totalDuration: new BN(3600 * 3), // 3 hours + unlockCliffAmount: sol(2000), unlockStartAmount: new BN(0), - unlockCliffAmount: new BN(2000e9), - isCancelable: true, }); await createStream({ - salt: new BN(3), - depositTokenMint, - depositAmount: new BN(30_000e9), cliffDuration: new BN(3600 * 24), // 1 day - totalDuration: new BN(3 * 3600 * 24), // 3 days - unlockStartAmount: new BN(2000e9), - unlockCliffAmount: new BN(10_000e9), + depositAmount: sol(30_000), + depositTokenMint, isCancelable: false, + salt: new BN(3), + totalDuration: new BN(3 * 3600 * 24), // 3 days + unlockCliffAmount: sol(10_000), + unlockStartAmount: sol(2000), }); }); }); -// HELPER FUNCTIONS AND DATA STRUCTS +/* -------------------------------------------------------------------------- */ +/* HELPERS */ +/* -------------------------------------------------------------------------- */ -interface CreateParams { - salt: BN; - depositTokenMint: PublicKey; - depositAmount: BN; +type CreateParams = { cliffDuration: BN; + depositAmount: BN; + depositTokenMint: PublicKey; + isCancelable: boolean; + salt: BN; totalDuration: BN; unlockStartAmount: BN; unlockCliffAmount: BN; - isCancelable: boolean; -} +}; async function createStream(params: CreateParams) { const { @@ -96,14 +94,14 @@ async function createStream(params: CreateParams) { totalDuration, unlockStartAmount, unlockCliffAmount, - isCancelable + isCancelable, ) .accounts({ - sender: senderKeys.publicKey, depositTokenMint, - recipient: senderKeys.publicKey, - nftTokenProgram: TOKEN_PROGRAM_ID, depositTokenProgram: TOKEN_PROGRAM_ID, + nftTokenProgram: TOKEN_PROGRAM_ID, + recipient: senderKeys.publicKey, + sender: senderKeys.publicKey, }) .preInstructions([increaseCULimitIx]) .rpc(); @@ -112,13 +110,12 @@ async function createStream(params: CreateParams) { async function configureTestingEnvironment() { anchorProvider = anchor.AnchorProvider.env(); anchor.setProvider(anchorProvider); - lockupProgram = anchor.workspace - .SablierLockup as anchor.Program; + lockupProgram = anchor.workspace.SablierLockup as anchor.Program; // Initialize the accounts involved in the tests senderKeys = (anchorProvider.wallet as anchor.Wallet).payer; } -async function initializeSablierLockup() { +async function initSablierLockup() { await lockupProgram.methods .initialize(senderKeys.publicKey) .signers([senderKeys]) @@ -130,7 +127,6 @@ async function initializeSablierLockup() { } async function createTokenAndMintToSender(): Promise { - const TOKEN_DECIMALS = 9; const freezeAuthority = null; const depositTokenMint = await createMint( @@ -138,25 +134,25 @@ async function createTokenAndMintToSender(): Promise { senderKeys, senderKeys.publicKey, freezeAuthority, - TOKEN_DECIMALS, - Keypair.generate() + Decimals.SOL, + Keypair.generate(), ); const senderATA = await getOrCreateAssociatedTokenAccount( anchorProvider.connection, senderKeys, depositTokenMint, - senderKeys.publicKey + senderKeys.publicKey, ); - const mintedAmount = new BN(100_000e9); // sufficient amount + const mintedAmount = sol(1_000_000); await mintTo( anchorProvider.connection, senderKeys, depositTokenMint, senderATA.address, senderKeys, - Number(mintedAmount) + Number(mintedAmount), ); return depositTokenMint; diff --git a/scripts/ts/merkle-instant-initialization.ts b/scripts/ts/init-merkle-instant.ts similarity index 52% rename from scripts/ts/merkle-instant-initialization.ts rename to scripts/ts/init-merkle-instant.ts index 22a6e306..5466728d 100644 --- a/scripts/ts/merkle-instant-initialization.ts +++ b/scripts/ts/init-merkle-instant.ts @@ -1,31 +1,35 @@ import * as anchor from "@coral-xyz/anchor"; -import { Keypair } from "@solana/web3.js"; +import type { Keypair } from "@solana/web3.js"; +import { beforeEach, describe } from "vitest"; -import { SablierMerkleInstant } from "../../target/types/sablier_merkle_instant"; +import type { SablierMerkleInstant } from "../../target/types/sablier_merkle_instant"; -let anchorProvider: any; +let anchorProvider: anchor.AnchorProvider; let merkleInstantProgram: anchor.Program; let senderKeys: Keypair; describe("SablierLockup post-deployment initialization", () => { beforeEach(async () => { await configureTestingEnvironment(); - await initializeSablierMerkleInstant(); + await initSablierMerkleInstant(); }); }); +/* -------------------------------------------------------------------------- */ +/* HELPERS */ +/* -------------------------------------------------------------------------- */ + async function configureTestingEnvironment() { anchorProvider = anchor.AnchorProvider.env(); anchor.setProvider(anchorProvider); - merkleInstantProgram = anchor.workspace - .SablierMerkleInstant as anchor.Program; + merkleInstantProgram = anchor.workspace.SablierMerkleInstant as anchor.Program; // Initialize the accounts involved in the tests senderKeys = (anchorProvider.wallet as anchor.Wallet).payer; } -async function initializeSablierMerkleInstant() { +async function initSablierMerkleInstant() { await merkleInstantProgram.methods .initialize(senderKeys.publicKey) .signers([senderKeys]) diff --git a/tests/common-base.ts b/tests/common-base.ts deleted file mode 100644 index c85ca802..00000000 --- a/tests/common-base.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import * as token from "@solana/spl-token"; -import { PublicKey, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"; -import { BankrunProvider } from "anchor-bankrun"; -import { - BanksClient, - Clock, - ProgramTestContext, - startAnchor, -} from "solana-bankrun"; - -import { createATAAndFund, createMint } from "./anchor-bankrun-adapter"; - -// Config variables -export let banksClient: BanksClient; -export let bankrunProvider: BankrunProvider; -export let context: ProgramTestContext; -export let defaultBankrunPayer: Keypair; - -// Users -export let eve: User; -export let feeCollector: User; -export let recipient: User; - -// Tokens -export let usdc: PublicKey; -export let dai: PublicKey; -export let randomToken: PublicKey; - -export async function setUp( - programName: string, - programId: PublicKey, - additionalPrograms: { name: string; programId: PublicKey }[] = [] -) { - const programs = [{ name: programName, programId }, ...additionalPrograms]; - - // Start Anchor context with the provided programs - context = await startAnchor("", programs, []); - banksClient = context.banksClient; - bankrunProvider = new BankrunProvider(context); - defaultBankrunPayer = bankrunProvider.wallet.payer; - - // Initialize the tokens - await createTokens(); - - // Create the users - eve = await createUser(); - feeCollector = await createUser(); - recipient = await createUser(); -} - -async function createATAsAndFund( - user: PublicKey -): Promise<{ usdcATA: PublicKey; daiATA: PublicKey }> { - const USDC_USER_BALANCE = 1_000_000e6; // 1M tokens - const DAI_USER_BALANCE = 1_000_000e9; // 1M tokens - // Create ATAs for the user - const usdcATA = await createATAAndFund( - banksClient, - defaultBankrunPayer, - usdc, - USDC_USER_BALANCE, - token.TOKEN_PROGRAM_ID, - user - ); - const daiATA = await createATAAndFund( - banksClient, - defaultBankrunPayer, - dai, - DAI_USER_BALANCE, - token.TOKEN_2022_PROGRAM_ID, - user - ); - - return { usdcATA, daiATA }; -} - -async function createTokens(): Promise { - const mintAndFreezeAuthority = defaultBankrunPayer.publicKey; - - dai = await createMint( - banksClient, - defaultBankrunPayer, - mintAndFreezeAuthority, - mintAndFreezeAuthority, - 9, - Keypair.generate(), - token.TOKEN_2022_PROGRAM_ID - ); - - randomToken = await createMint( - banksClient, - defaultBankrunPayer, - mintAndFreezeAuthority, - mintAndFreezeAuthority, - 6, - Keypair.generate(), - token.TOKEN_PROGRAM_ID - ); - - usdc = await createMint( - banksClient, - defaultBankrunPayer, - mintAndFreezeAuthority, - mintAndFreezeAuthority, - 6, - Keypair.generate(), - token.TOKEN_PROGRAM_ID - ); -} - -export async function createUser(): Promise { - // Create the keypair for the user - const acc = Keypair.generate(); - - // Set up the account info for the new keypair - const accInfo = { - lamports: 100 * LAMPORTS_PER_SOL, // Default balance (100 SOL) - owner: new PublicKey("11111111111111111111111111111111"), // Default owner (System Program) - executable: false, // Not a program account - rentEpoch: 0, // Default rent epoch - data: new Uint8Array(), // Empty data - }; - - // Add account to the BanksClient context - context.setAccount(acc.publicKey, accInfo); - - // Create ATAs and mint tokens for the user - const { usdcATA, daiATA } = await createATAsAndFund(acc.publicKey); - - const user: User = { - keys: acc, - usdcATA, - daiATA, - }; - - return user; -} - -/*////////////////////////////////////////////////////////////////////////// - HELPERS -//////////////////////////////////////////////////////////////////////////*/ - -export async function accountExists(address: PublicKey): Promise { - return (await banksClient.getAccount(address)) != null; -} - -/** - * Get the hex code for a given error name from an error code object - * @param errorCodeObj The error code object/enum - * @param errorName The name of the error - * @returns The hex code for the error - */ -export function getErrorCode>( - errorCodeObj: T, - errorName: string -): string { - const errorCode = errorCodeObj[errorName as keyof T]; - return `0x${errorCode.toString(16)}`; -} - -/** - * Get the error name for a given hex code from an error code object - * @param errorCodeObj The error code object/enum - * @param hexCode The hex code of the error (e.g., "0x177e") - * @returns The error name - */ -export function getErrorName>( - errorCodeObj: T, - hexCode: string -): string { - // Convert the hex string to a number - const numericCode = parseInt(hexCode, 16); - - // Find the error name by value - for (const [key, value] of Object.entries(errorCodeObj)) { - // Skip numeric keys (TypeScript enums have reverse mappings) - if (!isNaN(Number(key))) continue; - - if (value === numericCode) { - return key; - } - } - - return "not found"; -} - -export async function getLamportsOf(user: PublicKey): Promise { - return await banksClient.getBalance(user); -} - -export function getPDAAddress( - seeds: Array, - programId: PublicKey -): PublicKey { - return PublicKey.findProgramAddressSync(seeds, programId)[0]; -} - -export async function sleepFor(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -export async function timeTravelTo(timestamp: bigint | BN) { - const currentClock = await banksClient.getClock(); - const timestampAsBigInt = - timestamp instanceof BN ? BigInt(timestamp.toString()) : timestamp; - - context.setClock( - new Clock( - currentClock.slot, - currentClock.epochStartTimestamp, - currentClock.epoch, - currentClock.leaderScheduleEpoch, - timestampAsBigInt - ) - ); -} - -export interface User { - keys: Keypair; - daiATA: PublicKey; - usdcATA: PublicKey; -} diff --git a/tests/anchor-bankrun-adapter.ts b/tests/common/anchor-bankrun.ts similarity index 58% rename from tests/anchor-bankrun-adapter.ts rename to tests/common/anchor-bankrun.ts index 9fa49b28..ac105278 100644 --- a/tests/anchor-bankrun-adapter.ts +++ b/tests/common/anchor-bankrun.ts @@ -1,29 +1,28 @@ -import { BN } from "@coral-xyz/anchor"; import * as token from "@solana/spl-token"; import { + type Blockhash, ComputeBudgetProgram, Keypair, - PublicKey, - Signer, + type PublicKey, + type Signer, SystemProgram, Transaction, - TransactionInstruction as TxIx, + type TransactionInstruction as TxIx, } from "@solana/web3.js"; -import { BanksClient, BanksTransactionMeta } from "solana-bankrun"; +import type BN from "bn.js"; +import type { BanksClient, BanksTransactionMeta } from "solana-bankrun"; +import { toBigInt, toBn } from "../../lib/helpers"; export async function buildSignAndProcessTx( banksClient: BanksClient, ixs: TxIx | TxIx[], signerKeys: Keypair | Keypair[], - cuLimit: number = 1_400_000 // The maximum Compute Unit limit for a tx + cuLimit: number = 1_400_000, // The maximum Compute Unit limit for a tx ) { // Get the latest blockhash - const res = await banksClient.getLatestBlockhash(); - if (!res) throw new Error("Couldn't get the latest blockhash"); - // Initialize the transaction const tx = new Transaction(); - tx.recentBlockhash = res[0]; + tx.recentBlockhash = await getLatestBlockhash(banksClient); // Add compute unit limit instruction if specified if (cuLimit !== undefined) { @@ -55,7 +54,7 @@ export async function createMint( freezeAuthority: PublicKey | null, decimals: number, mintKeypair = Keypair.generate(), - programId = token.TOKEN_PROGRAM_ID + programId = token.TOKEN_PROGRAM_ID, ): Promise { const rent = await banksClient.getRent(); @@ -63,21 +62,15 @@ export async function createMint( const tx = new Transaction().add( SystemProgram.createAccount({ fromPubkey: payer.publicKey, + lamports: Number(rent.minimumBalance(toBigInt(token.MINT_SIZE))), newAccountPubkey: mint, - space: token.MINT_SIZE, - lamports: Number(rent.minimumBalance(BigInt(token.MINT_SIZE))), programId, + space: token.MINT_SIZE, }), - token.createInitializeMint2Instruction( - mint, - decimals, - mintAuthority, - freezeAuthority, - programId - ) + token.createInitializeMint2Instruction(mint, decimals, mintAuthority, freezeAuthority, programId), ); - [tx.recentBlockhash] = (await banksClient.getLatestBlockhash())!; + tx.recentBlockhash = await getLatestBlockhash(banksClient); tx.sign(payer, mintKeypair); await banksClient.processTransaction(tx); @@ -89,21 +82,15 @@ export async function createATA( payer: Signer, mint: PublicKey, owner: PublicKey, - programId: PublicKey + programId: PublicKey, ): Promise { const ata = deriveATAAddress(mint, owner, programId); const tx = new Transaction().add( - token.createAssociatedTokenAccountInstruction( - payer.publicKey, - ata, - owner, - mint, - programId - ) + token.createAssociatedTokenAccountInstruction(payer.publicKey, ata, owner, mint, programId), ); - [tx.recentBlockhash] = (await banksClient.getLatestBlockhash())!; + tx.recentBlockhash = await getLatestBlockhash(banksClient); tx.sign(payer); await banksClient.processTransaction(tx); @@ -115,54 +102,33 @@ export async function createATAAndFund( banksClient: BanksClient, payer: Signer, mint: PublicKey, - amount: number, + amount: BN, tokenProgram: PublicKey, - user: PublicKey + user: PublicKey, ): Promise { // Create ATA for the user const userATA = await createATA(banksClient, payer, mint, user, tokenProgram); // Mint the amount to the user's ATA - await mintTo( - banksClient, - payer, - mint, - userATA, - payer.publicKey, - amount, - [], - tokenProgram - ); + await mintTo(banksClient, payer, mint, userATA, payer.publicKey, amount, [], tokenProgram); return userATA; } -export function deriveATAAddress( - mint: PublicKey, - owner: PublicKey, - programId: PublicKey -): PublicKey { - return token.getAssociatedTokenAddressSync(mint, owner, true, programId); +export function deriveATAAddress(mint: PublicKey, owner: PublicKey, programId: PublicKey): PublicKey { + const allowOwnerOffCurve = true; + return token.getAssociatedTokenAddressSync(mint, owner, allowOwnerOffCurve, programId); } -export async function getATABalanceMint( - banksClient: BanksClient, - owner: PublicKey, - mint: PublicKey -): Promise { +export async function getATABalanceMint(banksClient: BanksClient, owner: PublicKey, mint: PublicKey): Promise { const mintAccount = await banksClient.getAccount(mint); - if (!mintAccount) { throw new Error("Mint account does not exist!"); } // Derive the ATA address from owner and mint - const ataAddress = await token.getAssociatedTokenAddressSync( - mint, - owner, - true, - mintAccount.owner - ); + const allowOwnerOffCurve = true; + const ataAddress = await token.getAssociatedTokenAddressSync(mint, owner, allowOwnerOffCurve, mintAccount.owner); // Get the ATA account data const ataAccount = await banksClient.getAccount(ataAddress); @@ -171,83 +137,75 @@ export async function getATABalanceMint( } const accountData = token.AccountLayout.decode(ataAccount.data); - return new BN(accountData.amount.toString()); + return toBn(accountData.amount); } -export async function getATABalance( - banksClient: BanksClient, - ataAddress: PublicKey -): Promise { +export async function getATABalance(banksClient: BanksClient, ataAddress: PublicKey): Promise { const ataAccount = await banksClient.getAccount(ataAddress); if (!ataAccount) { throw new Error("The queried ATA account does not exist!"); } const accountData = token.AccountLayout.decode(ataAccount.data); - return new BN(accountData.amount.toString()); + return toBn(accountData.amount); } -export async function getMintTotalSupplyOf( - banksClient: BanksClient, - mintAddress: PublicKey -): Promise { +export async function getMintTotalSupplyOf(banksClient: BanksClient, mintAddress: PublicKey): Promise { const mintAccount = await banksClient.getAccount(mintAddress); if (!mintAccount) { throw new Error("The queried mint account does not exist!"); } const mintData = token.MintLayout.decode(mintAccount.data); - return new BN(mintData.supply.toString()); + return toBn(mintData.supply); } -export async function mintTo( +export async function transfer( banksClient: BanksClient, payer: Signer, - mint: PublicKey, + source: PublicKey, destination: PublicKey, - authority: PublicKey, - amount: number | bigint, + owner: PublicKey, + amount: BN, multiSigners: Signer[] = [], - programId = token.TOKEN_PROGRAM_ID + programId = token.TOKEN_PROGRAM_ID, ): Promise { const tx = new Transaction().add( - token.createMintToInstruction( - mint, - destination, - authority, - amount, - multiSigners, - programId - ) + token.createTransferInstruction(source, destination, owner, toBigInt(amount), multiSigners, programId), ); - - [tx.recentBlockhash] = (await banksClient.getLatestBlockhash())!; + tx.recentBlockhash = await getLatestBlockhash(banksClient); tx.sign(payer, ...multiSigners); return await banksClient.processTransaction(tx); } -export async function transfer( +/* -------------------------------------------------------------------------- */ +/* INTERNAL LOGIC */ +/* -------------------------------------------------------------------------- */ + +async function getLatestBlockhash(banksClient: BanksClient): Promise { + const result = await banksClient.getLatestBlockhash(); + if (!result) { + throw new Error("Couldn't get the latest blockhash"); + } + return result[0]; +} + +async function mintTo( banksClient: BanksClient, payer: Signer, - source: PublicKey, + mint: PublicKey, destination: PublicKey, - owner: PublicKey, - amount: number | bigint, + authority: PublicKey, + amount: BN, multiSigners: Signer[] = [], - programId = token.TOKEN_PROGRAM_ID + programId = token.TOKEN_PROGRAM_ID, ): Promise { const tx = new Transaction().add( - token.createTransferInstruction( - source, - destination, - owner, - amount, - multiSigners, - programId - ) + token.createMintToInstruction(mint, destination, authority, toBigInt(amount), multiSigners, programId), ); - [tx.recentBlockhash] = (await banksClient.getLatestBlockhash())!; + + tx.recentBlockhash = await getLatestBlockhash(banksClient); tx.sign(payer, ...multiSigners); return await banksClient.processTransaction(tx); diff --git a/tests/common/assertions.ts b/tests/common/assertions.ts new file mode 100644 index 00000000..04117c86 --- /dev/null +++ b/tests/common/assertions.ts @@ -0,0 +1,54 @@ +import { type PublicKey } from "@solana/web3.js"; +import type BN from "bn.js"; +import { assert, expect } from "vitest"; +import { LAMPORTS_PER_SOL } from "../../lib/constants"; +import { type TestContext } from "./context"; + +export async function assertAccountExists(ctx: TestContext, account: PublicKey, accountName: string) { + assert.isTrue(await ctx.accountExists(account), `${accountName} account does not exist when it should`); +} + +export async function assertAccountNotExists(ctx: TestContext, account: PublicKey, accountName: string) { + assert.isFalse(await ctx.accountExists(account), `${accountName} account exists when it should not`); +} + +export function assertEqualSOLBalance(left: BN, right: BN, message?: string) { + const actualSol = left.div(LAMPORTS_PER_SOL).toString(); + const expectedSol = right.div(LAMPORTS_PER_SOL).toString(); + const defaultMessage = `Balance mismatch: ${actualSol} SOL !== ${expectedSol} SOL`; + assertEqualBn(left, right, message ?? defaultMessage); +} + +export function assertEqualBn(left: BN, right: BN, message?: string) { + const defaultMessage = `BN values mismatch: ${left.toString()} !== ${right.toString()}`; + assert.isTrue(left.eq(right), message ?? defaultMessage); +} + +export function assertEqualPublicKey(left: PublicKey, right: PublicKey, message?: string) { + const defaultMessage = `PublicKey mismatch: ${left.toBase58()} !== ${right.toBase58()}`; + assert.isTrue(left.equals(right), message ?? defaultMessage); +} + +export function assertLteBn(left: BN, right: BN, message?: string) { + const defaultMessage = `Expected ${left.toString()} to be <= to ${right.toString()}`; + assert.isTrue(left.lte(right), message ?? defaultMessage); +} + +export function assertZeroBn(left: BN, message?: string) { + const defaultMessage = `Expected ${left.toString()} to be zero`; + assert.isTrue(left.isZero(), message ?? defaultMessage); +} + +export function expectToThrow>( + promise: Promise, + errorMap: T, + errorNameOrCode: keyof T | number, +) { + if (typeof errorNameOrCode === "number") { + const hexErrorCode = `0x${errorNameOrCode.toString(16)}`; + return expect(promise).rejects.toThrow(hexErrorCode); + } + + const hexErrorCode = `0x${errorMap[errorNameOrCode].toString(16)}`; + return expect(promise).rejects.toThrow(hexErrorCode); +} diff --git a/tests/common/context.ts b/tests/common/context.ts new file mode 100644 index 00000000..8cabd399 --- /dev/null +++ b/tests/common/context.ts @@ -0,0 +1,165 @@ +import * as token from "@solana/spl-token"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { BankrunProvider } from "anchor-bankrun"; +import type BN from "bn.js"; +import { type AccountInfoBytes, type BanksClient, Clock, type ProgramTestContext, startAnchor } from "solana-bankrun"; +import { Decimals } from "../../lib/constants"; +import { dai, sol, usdc } from "../../lib/convertors"; +import { toBigInt, toBn } from "../../lib/helpers"; +import { type ProgramName } from "../../lib/types"; +import { createATAAndFund, createMint } from "./anchor-bankrun"; +import { type User } from "./types"; + +export class TestContext { + // Core Bankrun components + private context!: ProgramTestContext; + public banksClient!: BanksClient; + public bankrunProvider!: BankrunProvider; + public defaultBankrunPayer!: Keypair; + + // Users - encapsulated within the context + public eve!: User; + public feeCollector!: User; + public recipient!: User; + + // Tokens + public dai!: PublicKey; // Token 2022 + public randomToken!: PublicKey; // Token standard + public usdc!: PublicKey; // Token standard + + async setUp( + programName: ProgramName, + programId: PublicKey, + additionalPrograms: { name: string; programId: PublicKey }[] = [], + ) { + const programs = [{ name: programName, programId }, ...additionalPrograms]; + + // Start Anchor context with the provided programs + this.context = await startAnchor("", programs, []); + this.banksClient = this.context.banksClient; + this.bankrunProvider = new BankrunProvider(this.context); + this.defaultBankrunPayer = this.bankrunProvider.wallet.payer; + + // Initialize the tokens + await this.createTokens(); + + // Create the users + this.eve = await this.createUser(); + this.feeCollector = await this.createUser(); + this.recipient = await this.createUser(); + } + + async createUser(): Promise { + // Create the keypair for the user + const acc = Keypair.generate(); + + // Set up the account info for the new keypair + const accInfo: AccountInfoBytes = { + data: new Uint8Array(), // Empty data + executable: false, // Not a program account + lamports: sol(100).toNumber(), // Default balance (100 SOL) + owner: new PublicKey("11111111111111111111111111111111"), // Default owner (System Program) + rentEpoch: 0, // Default rent epoch + }; + + // Add account to the BanksClient context + this.context.setAccount(acc.publicKey, accInfo); + + // Create ATAs and mint tokens for the user + const { usdcATA, daiATA } = await this.createATAsAndFund(acc.publicKey); + + return { + daiATA, + keys: acc, + usdcATA, + }; + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + async accountExists(address: PublicKey): Promise { + return (await this.banksClient.getAccount(address)) !== null; + } + + async getLamportsOf(user: PublicKey): Promise { + const balance = await this.banksClient.getBalance(user); + return toBn(balance); + } + + async timeTravelTo(timestamp: BN) { + const currentClock = await this.banksClient.getClock(); + + this.context.setClock( + new Clock( + currentClock.slot, + currentClock.epochStartTimestamp, + currentClock.epoch, + currentClock.leaderScheduleEpoch, + toBigInt(timestamp), + ), + ); + } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE METHODS + //////////////////////////////////////////////////////////////////////////*/ + + private async createATAsAndFund(user: PublicKey): Promise<{ usdcATA: PublicKey; daiATA: PublicKey }> { + // Create ATAs for the user + const usdcATA = await createATAAndFund( + this.banksClient, + this.defaultBankrunPayer, + this.usdc, + usdc(1_000_000), + token.TOKEN_PROGRAM_ID, + user, + ); + const daiATA = await createATAAndFund( + this.banksClient, + this.defaultBankrunPayer, + this.dai, + dai(1_000_000), + token.TOKEN_2022_PROGRAM_ID, + user, + ); + + return { daiATA, usdcATA }; + } + + private async createTokens(): Promise { + const mintAndFreezeAuthority = this.defaultBankrunPayer.publicKey; + + this.dai = await createMint( + this.banksClient, + this.defaultBankrunPayer, + mintAndFreezeAuthority, + mintAndFreezeAuthority, + Decimals.DAI, + Keypair.generate(), + token.TOKEN_2022_PROGRAM_ID, + ); + + const randomTokenDecimals = 6; + this.randomToken = await createMint( + this.banksClient, + this.defaultBankrunPayer, + mintAndFreezeAuthority, + mintAndFreezeAuthority, + randomTokenDecimals, + Keypair.generate(), + token.TOKEN_PROGRAM_ID, + ); + + this.usdc = await createMint( + this.banksClient, + this.defaultBankrunPayer, + mintAndFreezeAuthority, + mintAndFreezeAuthority, + Decimals.USDC, + Keypair.generate(), + token.TOKEN_PROGRAM_ID, + ); + } +} diff --git a/tests/common/types.ts b/tests/common/types.ts new file mode 100644 index 00000000..28a0c252 --- /dev/null +++ b/tests/common/types.ts @@ -0,0 +1,7 @@ +import type { Keypair, PublicKey } from "@solana/web3.js"; + +export type User = { + keys: Keypair; + daiATA: PublicKey; + usdcATA: PublicKey; +}; diff --git a/tests/lockup/base.ts b/tests/lockup/base.ts deleted file mode 100644 index 76dff63e..00000000 --- a/tests/lockup/base.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { BN, Program } from "@coral-xyz/anchor"; -import * as token from "@solana/spl-token"; -import { PublicKey, Keypair } from "@solana/web3.js"; - -import { SablierLockup } from "../../target/types/sablier_lockup"; -import IDL from "../../target/idl/sablier_lockup.json"; - -import * as defaults from "./utils/defaults"; -import { Stream, StreamData, Salts } from "./utils/types"; - -import { - buildSignAndProcessTx, - deriveATAAddress, - getATABalance, -} from "../anchor-bankrun-adapter"; - -import { - banksClient, - bankrunProvider, - createUser, - dai, - feeCollector, - getLamportsOf, - getPDAAddress, - recipient, - setUp as commonSetUp, - timeTravelTo, - usdc, - User, -} from "../common-base"; - -export { - deriveATAAddress, - getATABalance, - getATABalanceMint, - getMintTotalSupplyOf, -} from "../anchor-bankrun-adapter"; - -// Programs and addresses -export let nftCollectionDataAddress: PublicKey; -export let lockup: Program; -export let treasuryAddress: PublicKey; - -// Users -export let sender: User; - -// Streams -export let salts: Salts; - -/*////////////////////////////////////////////////////////////////////////// - SET-UP -//////////////////////////////////////////////////////////////////////////*/ - -export async function setUp(initOrNot = true) { - // Call common setup with lockup specific programs - await commonSetUp("sablier_lockup", new PublicKey(IDL.address), [ - { - name: "token_metadata_program", - programId: defaults.TOKEN_METADATA_PROGRAM_ID, - }, - ]); - - // Deploy the program being tested - lockup = new Program(IDL, bankrunProvider); - - // Create the sender user. - sender = await createUser(); - - // Pre-calculate the address of the NFT Collection Data - nftCollectionDataAddress = getPDAAddress( - [Buffer.from(defaults.NFT_COLLECTION_DATA_SEED)], - lockup.programId - ); - - // Pre-calculate the address of the Treasury - treasuryAddress = getPDAAddress( - [Buffer.from(defaults.TREASURY_SEED)], - lockup.programId - ); - - // Set the block time to APR 1, 2025 - await timeTravelTo(defaults.APR_1_2025); - - if (initOrNot) { - // Initialize the SablierLockup program - await initializeLockup(); - - // Create the default streams - salts = { - default: await createWithTimestamps(), - nonCancelable: await createWithTimestamps({ - isCancelable: false, - }), - nonExisting: new BN(1729), - }; - } -} - -/*////////////////////////////////////////////////////////////////////////// - TX-IX -//////////////////////////////////////////////////////////////////////////*/ - -export async function cancel({ - salt = salts.default, - signer = sender.keys, - depositedTokenMint = usdc, - depositedTokenProgram = token.TOKEN_PROGRAM_ID, -} = {}): Promise { - const streamNftMint = getStreamNftMintAddress(salt); - const cancelStreamIx = await lockup.methods - .cancel() - .accounts({ - sender: signer.publicKey, - depositedTokenMint, - streamNftMint, - depositedTokenProgram, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, cancelStreamIx, signer); -} - -export async function cancelToken2022(salt: BN): Promise { - await cancel({ - salt, - depositedTokenMint: dai, - depositedTokenProgram: token.TOKEN_2022_PROGRAM_ID, - }); -} - -export async function collectFees(signer: Keypair = feeCollector.keys) { - const collectFeesIx = await lockup.methods - .collectFees() - .accounts({ - feeCollector: signer.publicKey, - feeRecipient: sender.keys.publicKey, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, collectFeesIx, signer); -} - -export async function createWithDurations( - cliffDuration = defaults.CLIFF_DURATION -): Promise { - // Use the total supply as the salt for the stream - const salt = await getTotalSupply(); - - const createWithDurationsIx = await lockup.methods - .createWithDurations( - salt, - defaults.DEPOSIT_AMOUNT, - cliffDuration, - defaults.TOTAL_DURATION, - defaults.START_AMOUNT, - cliffDuration.eq(defaults.ZERO_BN) - ? defaults.ZERO_BN - : defaults.CLIFF_AMOUNT, - true - ) - .accounts({ - creator: sender.keys.publicKey, - sender: sender.keys.publicKey, - depositTokenMint: usdc, - recipient: recipient.keys.publicKey, - depositTokenProgram: token.TOKEN_PROGRAM_ID, - nftTokenProgram: token.TOKEN_PROGRAM_ID, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, createWithDurationsIx, sender.keys); - - return salt; -} - -export async function createWithTimestamps({ - creator = sender.keys, - senderPubKey = sender.keys.publicKey, - recipientPubKey = recipient.keys.publicKey, - depositTokenMint = usdc, - depositTokenProgram = token.TOKEN_PROGRAM_ID, - timestamps = defaults.timestamps(), - depositAmount = defaults.DEPOSIT_AMOUNT, - unlockAmounts = defaults.unlockAmounts(), - isCancelable = true, -} = {}): Promise { - // Use the total supply as the salt for the stream - const salt = await getTotalSupply(); - - const txIx = await lockup.methods - .createWithTimestamps( - salt, - depositAmount, - timestamps.start, - timestamps.cliff, - timestamps.end, - unlockAmounts.start, - unlockAmounts.cliff, - isCancelable - ) - .accounts({ - creator: creator.publicKey, - sender: senderPubKey, - depositTokenMint, - recipient: recipientPubKey, - depositTokenProgram, - nftTokenProgram: token.TOKEN_PROGRAM_ID, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, txIx, sender.keys); - - return salt; -} - -export async function createWithTimestampsToken2022(): Promise { - return await createWithTimestamps({ - depositTokenMint: dai, - depositTokenProgram: token.TOKEN_2022_PROGRAM_ID, - }); -} - -export async function initializeLockup(): Promise { - const initializeIx = await lockup.methods - .initialize(feeCollector.keys.publicKey) - .accounts({ - initializer: sender.keys.publicKey, - nftTokenProgram: token.TOKEN_PROGRAM_ID, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, initializeIx, sender.keys); -} - -export async function renounce({ - salt = salts.default, - signer = sender.keys, -} = {}): Promise { - const streamNftMint = getStreamNftMintAddress(salt); - const renounceIx = await lockup.methods - .renounce() - .accounts({ - sender: signer.publicKey, - streamNftMint, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, renounceIx, signer); -} - -export async function withdraw({ - salt = salts.default, - withdrawAmount = defaults.WITHDRAW_AMOUNT, - signer = recipient.keys, - withdrawalRecipient = recipient.keys.publicKey, - depositedTokenMint = usdc, - depositedTokenProgram = token.TOKEN_PROGRAM_ID, -} = {}): Promise { - const streamNftMint = getStreamNftMintAddress(salt); - const withdrawIx = await lockup.methods - .withdraw(withdrawAmount) - .accounts({ - signer: signer.publicKey, - depositedTokenMint, - streamNftMint, - streamRecipient: recipient.keys.publicKey, - withdrawalRecipient, - depositedTokenProgram, - nftTokenProgram: token.TOKEN_PROGRAM_ID, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, withdrawIx, signer); -} - -export async function withdrawToken2022( - salt: BN, - signer: Keypair -): Promise { - await withdraw({ - salt, - depositedTokenMint: dai, - signer, - depositedTokenProgram: token.TOKEN_2022_PROGRAM_ID, - }); -} - -export async function withdrawMax({ - salt = salts.default, - signer = sender.keys.publicKey, - withdrawalRecipient = recipient.keys.publicKey, - depositedTokenMint = usdc, - depositedTokenProgram = token.TOKEN_PROGRAM_ID, -} = {}): Promise { - const streamNftMint = getStreamNftMintAddress(salt); - - const withdrawMaxIx = await lockup.methods - .withdrawMax() - .accounts({ - signer, - depositedTokenMint, - streamRecipient: recipient.keys.publicKey, - streamNftMint, - withdrawalRecipient, - depositedTokenProgram, - nftTokenProgram: token.TOKEN_PROGRAM_ID, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, withdrawMaxIx, sender.keys); -} - -/*////////////////////////////////////////////////////////////////////////// - HELPERS -//////////////////////////////////////////////////////////////////////////*/ - -export async function getSenderLamports(): Promise { - return await getLamportsOf(sender.keys.publicKey); -} - -export async function getStreamDataAtaBalance( - salt = salts.default -): Promise { - return getATABalance(banksClient, getStreamDataAddress(salt)); -} - -export async function getTreasuryLamports(): Promise { - return await getLamportsOf(treasuryAddress); -} - -export function defaultStream({ - salt = salts.default, - depositedTokenMint = usdc, - tokenProgram = token.TOKEN_PROGRAM_ID, - isCancelable = true, - isDepleted = false, - wasCanceled = false, -} = {}): Stream { - const data: StreamData = { - amounts: defaults.amountsAfterCreate(), - depositedTokenMint, - salt, - isCancelable, - isDepleted, - timestamps: defaults.timestamps(), - sender: sender.keys.publicKey, - wasCanceled, - }; - const streamDataAddress = getStreamDataAddress(salt); - const streamDataAta = deriveATAAddress( - depositedTokenMint, - streamDataAddress, - tokenProgram - ); - const streamNftMint = getStreamNftMintAddress(salt); - const recipientStreamNftAta = deriveATAAddress( - streamNftMint, - recipient.keys.publicKey, - token.TOKEN_PROGRAM_ID - ); - const streamNftMetadata = getPDAAddress( - [ - Buffer.from(defaults.METADATA_SEED), - defaults.TOKEN_METADATA_PROGRAM_ID.toBuffer(), - streamNftMint.toBuffer(), - ], - defaults.TOKEN_METADATA_PROGRAM_ID - ); - const streamNftMasterEdition = getPDAAddress( - [ - Buffer.from(defaults.METADATA_SEED), - defaults.TOKEN_METADATA_PROGRAM_ID.toBuffer(), - streamNftMint.toBuffer(), - Buffer.from(defaults.EDITION_SEED), - ], - defaults.TOKEN_METADATA_PROGRAM_ID - ); - - // Return the Stream object - return { - data, - dataAddress: streamDataAddress, - dataAta: streamDataAta, - nftMasterEdition: streamNftMasterEdition, - nftMetadataAddress: streamNftMetadata, - nftMintAddress: streamNftMint, - recipientStreamNftAta: recipientStreamNftAta, - }; -} - -export function defaultStreamToken2022({ - salt = salts.default, - isCancelable = true, - isDepleted = false, - wasCanceled = false, -} = {}): Stream { - return defaultStream({ - depositedTokenMint: dai, - salt, - isCancelable, - isDepleted, - wasCanceled, - tokenProgram: token.TOKEN_2022_PROGRAM_ID, - }); -} - -export async function fetchStreamData(salt = salts.default): Promise { - const streamDataAddress = getStreamDataAddress(salt); - const streamDataAcc = await banksClient.getAccount(streamDataAddress); - if (!streamDataAcc) { - throw new Error("Stream Data account is undefined"); - } - - // Return the Stream data decoded via the Anchor account layout - const streamLayout = lockup.account.streamData; - - return streamLayout.coder.accounts.decode( - "streamData", - Buffer.from(streamDataAcc.data) - ); -} - -export async function getSenderTokenBalance(tokenMint = usdc): Promise { - const senderAta = tokenMint === usdc ? sender.usdcATA : sender.daiATA; - return await getATABalance(banksClient, senderAta); -} - -function getStreamDataAddress(salt: BN): PublicKey { - const streamNftMint = getStreamNftMintAddress(salt); - const streamDataSeeds = [ - Buffer.from(defaults.STREAM_DATA_SEED), - streamNftMint.toBuffer(), - ]; - return getPDAAddress(streamDataSeeds, lockup.programId); -} - -function getStreamNftMintAddress( - salt: BN, - signer: PublicKey = sender.keys.publicKey -): PublicKey { - // The seeds used when creating the Stream NFT Mint - const streamNftMintSeeds = [ - Buffer.from(defaults.STREAM_NFT_MINT_SEED), - signer.toBuffer(), - salt.toBuffer("le", 16), - ]; - - return getPDAAddress(streamNftMintSeeds, lockup.programId); -} - -async function getTotalSupply(): Promise { - const nftCollectionDataAcc = await banksClient.getAccount( - nftCollectionDataAddress - ); - - if (!nftCollectionDataAcc) { - throw new Error("NFT Collection Data account is undefined"); - } - - // Get the NFT Collection Data - const nftCollectionData = - lockup.account.nftCollectionData.coder.accounts.decode( - "nftCollectionData", - Buffer.from(nftCollectionDataAcc.data) - ); - - const totalSupply = new BN(nftCollectionData.totalSupply.toString(), 10); - return totalSupply; -} diff --git a/tests/lockup/context.ts b/tests/lockup/context.ts new file mode 100644 index 00000000..a82a9a79 --- /dev/null +++ b/tests/lockup/context.ts @@ -0,0 +1,399 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as token from "@solana/spl-token"; +import { type Keypair, PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { ProgramId, ZERO } from "../../lib/constants"; +import { ProgramName } from "../../lib/enums"; +import { getPDAAddress } from "../../lib/helpers"; +import IDL from "../../target/idl/sablier_lockup.json"; +import { type SablierLockup as SablierLockupProgram } from "../../target/types/sablier_lockup"; +import type { NftCollectionData, StreamData } from "../../target/types/sablier_lockup_structs"; +import { buildSignAndProcessTx, deriveATAAddress, getATABalance } from "../common/anchor-bankrun"; +import { TestContext } from "../common/context"; +import type { User } from "../common/types"; +import { AMOUNTS, Amount, Seed, TIMESTAMPS, Time, UNLOCK_AMOUNTS } from "./utils/defaults"; +import type { Salts, Stream } from "./utils/types"; + +export class LockupTestContext extends TestContext { + // Programs and addresses + public nftCollectionDataAddress!: PublicKey; + public lockup!: anchor.Program; + public treasuryAddress!: PublicKey; + + // Users + public sender!: User; + + // Stream Salts + public salts!: Salts; + + async setUpLockup({ initProgram = true } = {}) { + // Call parent setup with lockup specific programs + await super.setUp(ProgramName.Lockup, new PublicKey(IDL.address), [ + { + name: "token_metadata_program", + programId: ProgramId.TOKEN_METADATA, + }, + ]); + + // Deploy the program being tested + this.lockup = new anchor.Program(IDL, this.bankrunProvider); + + // Create the sender user + this.sender = await this.createUser(); + + // Compute addresses + this.nftCollectionDataAddress = getPDAAddress([Seed.NFT_COLLECTION_DATA], this.lockup.programId); + this.treasuryAddress = getPDAAddress([Seed.TREASURY], this.lockup.programId); + + // Set the block time to the genesis time + await this.timeTravelTo(Time.GENESIS); + + if (initProgram) { + // Initialize the SablierLockup program + await this.initializeLockup(); + + // Create the default streams + this.salts = { + default: await this.createWithTimestamps(), + nonCancelable: await this.createWithTimestamps({ + isCancelable: false, + }), + nonExisting: new BN(1729), + }; + } + } + + /*////////////////////////////////////////////////////////////////////////// + TX-IX + //////////////////////////////////////////////////////////////////////////*/ + + async cancel({ + salt = this.salts.default, + signer = this.sender.keys, + depositedTokenMint = this.usdc, + depositedTokenProgram = token.TOKEN_PROGRAM_ID, + } = {}): Promise { + const streamNftMint = this.getStreamNftMintAddress(salt); + const cancelStreamIx = await this.lockup.methods + .cancel() + .accounts({ + depositedTokenMint, + depositedTokenProgram, + sender: signer.publicKey, + streamNftMint, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, cancelStreamIx, signer); + } + + async cancelToken2022(salt: BN): Promise { + await this.cancel({ + depositedTokenMint: this.dai, + depositedTokenProgram: token.TOKEN_2022_PROGRAM_ID, + salt, + }); + } + + async collectFees(signer: Keypair = this.feeCollector.keys) { + const collectFeesIx = await this.lockup.methods + .collectFees() + .accounts({ + feeCollector: signer.publicKey, + feeRecipient: this.sender.keys.publicKey, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, collectFeesIx, signer); + } + + async createWithDurations({ + cliffDuration = Time.CLIFF_DURATION, + salt, + }: { + cliffDuration?: BN; + salt?: BN; + } = {}): Promise { + // Use the total supply as the salt for the stream + salt = salt ?? (await this.getTotalSupply()); + + const createWithDurationsIx = await this.lockup.methods + .createWithDurations( + salt, + Amount.DEPOSIT, + cliffDuration, + Time.TOTAL_DURATION, + Amount.START, + cliffDuration.isZero() ? ZERO : Amount.CLIFF, + true, + ) + .accounts({ + creator: this.sender.keys.publicKey, + depositTokenMint: this.usdc, + depositTokenProgram: token.TOKEN_PROGRAM_ID, + nftTokenProgram: token.TOKEN_PROGRAM_ID, + recipient: this.recipient.keys.publicKey, + sender: this.sender.keys.publicKey, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, createWithDurationsIx, this.sender.keys); + + return salt; + } + + async createWithTimestamps({ + creator = this.sender.keys, + senderPubKey = this.sender.keys.publicKey, + recipientPubKey = this.recipient.keys.publicKey, + depositTokenMint = this.usdc, + depositTokenProgram = token.TOKEN_PROGRAM_ID, + timestamps = TIMESTAMPS(), + depositAmount = Amount.DEPOSIT, + unlockAmounts = UNLOCK_AMOUNTS(), + isCancelable = true, + salt = new BN(-1), + } = {}): Promise { + // Use the total supply as the salt for the stream + salt = salt.isNeg() ? await this.getTotalSupply() : salt; + + const txIx = await this.lockup.methods + .createWithTimestamps( + salt, + depositAmount, + timestamps.start, + timestamps.cliff, + timestamps.end, + unlockAmounts.start, + unlockAmounts.cliff, + isCancelable, + ) + .accounts({ + creator: creator.publicKey, + depositTokenMint, + depositTokenProgram, + nftTokenProgram: token.TOKEN_PROGRAM_ID, + recipient: recipientPubKey, + sender: senderPubKey, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, txIx, this.sender.keys); + + return salt; + } + + async createWithTimestampsToken2022(): Promise { + return await this.createWithTimestamps({ + depositTokenMint: this.dai, + depositTokenProgram: token.TOKEN_2022_PROGRAM_ID, + }); + } + + async initializeLockup(): Promise { + const initializeIx = await this.lockup.methods + .initialize(this.feeCollector.keys.publicKey) + .accounts({ + initializer: this.sender.keys.publicKey, + nftTokenProgram: token.TOKEN_PROGRAM_ID, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, initializeIx, this.sender.keys); + } + + async renounce({ salt = this.salts.default, signer = this.sender.keys } = {}): Promise { + const streamNftMint = this.getStreamNftMintAddress(salt); + const renounceIx = await this.lockup.methods + .renounce() + .accounts({ + sender: signer.publicKey, + streamNftMint, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, renounceIx, signer); + } + + async withdraw({ + salt = this.salts.default, + withdrawAmount = Amount.WITHDRAW, + signer = this.recipient.keys, + withdrawalRecipient = this.recipient.keys.publicKey, + depositedTokenMint = this.usdc, + depositedTokenProgram = token.TOKEN_PROGRAM_ID, + } = {}): Promise { + const streamNftMint = this.getStreamNftMintAddress(salt); + const withdrawIx = await this.lockup.methods + .withdraw(withdrawAmount) + .accounts({ + depositedTokenMint, + depositedTokenProgram, + nftTokenProgram: token.TOKEN_PROGRAM_ID, + signer: signer.publicKey, + streamNftMint, + streamRecipient: this.recipient.keys.publicKey, + withdrawalRecipient, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, withdrawIx, signer); + } + + async withdrawToken2022(salt: BN, signer: Keypair): Promise { + await this.withdraw({ + depositedTokenMint: this.dai, + depositedTokenProgram: token.TOKEN_2022_PROGRAM_ID, + salt, + signer, + }); + } + + async withdrawMax({ + salt = this.salts.default, + signer = this.sender.keys.publicKey, + withdrawalRecipient = this.recipient.keys.publicKey, + depositedTokenMint = this.usdc, + depositedTokenProgram = token.TOKEN_PROGRAM_ID, + } = {}): Promise { + const streamNftMint = this.getStreamNftMintAddress(salt); + + const withdrawMaxIx = await this.lockup.methods + .withdrawMax() + .accounts({ + depositedTokenMint, + depositedTokenProgram, + nftTokenProgram: token.TOKEN_PROGRAM_ID, + signer, + streamNftMint, + streamRecipient: this.recipient.keys.publicKey, + withdrawalRecipient, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, withdrawMaxIx, this.sender.keys); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + async getSenderLamports(): Promise { + return await this.getLamportsOf(this.sender.keys.publicKey); + } + + async getTreasuryLamports(): Promise { + return await this.getLamportsOf(this.treasuryAddress); + } + + defaultStream({ + salt = this.salts.default, + depositedTokenMint = this.usdc, + tokenProgram = ProgramId.TOKEN, + isCancelable = true, + isDepleted = false, + wasCanceled = false, + } = {}): Stream { + const data: StreamData = { + amounts: AMOUNTS(), + bump: 0, + depositedTokenMint, + isCancelable, + isDepleted, + salt, + sender: this.sender.keys.publicKey, + timestamps: TIMESTAMPS(), + wasCanceled, + }; + const streamDataAddress = this.getStreamDataAddress(salt); + const streamDataAta = deriveATAAddress(depositedTokenMint, streamDataAddress, tokenProgram); + const streamNftMint = this.getStreamNftMintAddress(salt); + const recipientStreamNftAta = deriveATAAddress(streamNftMint, this.recipient.keys.publicKey, ProgramId.TOKEN); + const streamNftMetadata = getPDAAddress( + [Seed.METADATA, ProgramId.TOKEN_METADATA.toBuffer(), streamNftMint.toBuffer()], + ProgramId.TOKEN_METADATA, + ); + const streamNftMasterEdition = getPDAAddress( + [Seed.METADATA, ProgramId.TOKEN_METADATA.toBuffer(), streamNftMint.toBuffer(), Seed.EDITION], + ProgramId.TOKEN_METADATA, + ); + + // Return the Stream object + return { + data, + dataAddress: streamDataAddress, + dataAta: streamDataAta, + nftMasterEdition: streamNftMasterEdition, + nftMetadataAddress: streamNftMetadata, + nftMintAddress: streamNftMint, + recipientStreamNftAta: recipientStreamNftAta, + }; + } + + defaultStreamToken2022({ + salt = this.salts.default, + isCancelable = true, + isDepleted = false, + wasCanceled = false, + } = {}): Stream { + return this.defaultStream({ + depositedTokenMint: this.dai, + isCancelable, + isDepleted, + salt, + tokenProgram: ProgramId.TOKEN_2022, + wasCanceled, + }); + } + + async fetchStreamData(salt = this.salts.default): Promise { + const streamDataAddress = this.getStreamDataAddress(salt); + const streamDataAcc = await this.banksClient.getAccount(streamDataAddress); + if (!streamDataAcc) { + throw new Error("Stream Data account is undefined"); + } + + // Return the Stream data decoded via the Anchor account layout + const streamLayout = this.lockup.account.streamData; + + return streamLayout.coder.accounts.decode("streamData", Buffer.from(streamDataAcc.data)); + } + + async getSenderTokenBalance(tokenMint = this.usdc): Promise { + const senderAta = tokenMint === this.usdc ? this.sender.usdcATA : this.sender.daiATA; + return await getATABalance(this.banksClient, senderAta); + } + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE METHODS + //////////////////////////////////////////////////////////////////////////*/ + + private getStreamDataAddress(salt: BN): PublicKey { + const streamNftMint = this.getStreamNftMintAddress(salt); + const streamDataSeeds = [Seed.STREAM_DATA, streamNftMint.toBuffer()]; + return getPDAAddress(streamDataSeeds, this.lockup.programId); + } + + private getStreamNftMintAddress(salt: BN, signer: PublicKey = this.sender.keys.publicKey): PublicKey { + // The seeds used when creating the Stream NFT Mint + const streamNftMintSeeds = [Seed.STREAM_NFT_MINT, signer.toBuffer(), salt.toBuffer("le", 16)]; + + return getPDAAddress(streamNftMintSeeds, this.lockup.programId); + } + + private async getTotalSupply(): Promise { + const nftCollectionDataAcc = await this.banksClient.getAccount(this.nftCollectionDataAddress); + + if (!nftCollectionDataAcc) { + throw new Error("NFT Collection Data account is undefined"); + } + + // Get the NFT Collection Data + const nftCollectionData = this.lockup.account.nftCollectionData.coder.accounts.decode( + "nftCollectionData", + Buffer.from(nftCollectionDataAcc.data), + ); + + return nftCollectionData.totalSupply; + } +} diff --git a/tests/lockup/unit/cancel.test.ts b/tests/lockup/unit/cancel.test.ts new file mode 100644 index 00000000..969e2740 --- /dev/null +++ b/tests/lockup/unit/cancel.test.ts @@ -0,0 +1,236 @@ +import { + ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, + ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, +} from "@coral-xyz/anchor-errors"; +import type BN from "bn.js"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { BN_1, ProgramId, ZERO } from "../../../lib/constants"; +import { sleepFor } from "../../../lib/helpers"; +import { createATAAndFund, deriveATAAddress, getATABalance, getATABalanceMint } from "../../common/anchor-bankrun"; +import { assertAccountNotExists, assertEqualBn } from "../../common/assertions"; +import { LockupTestContext } from "../context"; +import { assertEqStreamData, expectToThrow } from "../utils/assertions"; +import { Amount, Time } from "../utils/defaults"; +import { type Stream } from "../utils/types"; + +describe("cancel", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + // Set the time to 26% of the stream duration + await ctx.timeTravelTo(Time.MID_26_PERCENT); + }); + + it("should revert", async () => { + await expectToThrow(ctx.cancel({ salt: BN_1 }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + // Set the time to 26% of the stream duration + await ctx.timeTravelTo(Time.MID_26_PERCENT); + }); + + describe("given a null stream", () => { + it("should revert", async () => { + await expectToThrow(ctx.cancel({ salt: ctx.salts.nonExisting }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("given a valid stream", () => { + describe("given an invalid deposited token mint", () => { + it("should revert", async () => { + await expectToThrow(ctx.cancel({ depositedTokenMint: ctx.randomToken }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("given a valid deposited token mint", () => { + describe("given cold stream", () => { + describe("given DEPLETED status", () => { + it("should revert", async () => { + await ctx.timeTravelTo(Time.END); + await ctx.withdrawMax(); + await expectToThrow(ctx.cancel(), "StreamDepleted"); + }); + }); + + describe("given CANCELED status", () => { + it("should revert", async () => { + await ctx.cancel(); + await sleepFor(7); + await expectToThrow(ctx.cancel(), "StreamCanceled"); + }); + }); + + describe("given SETTLED status", () => { + it("should revert", async () => { + await ctx.timeTravelTo(Time.END); + await expectToThrow(ctx.cancel(), "StreamSettled"); + }); + }); + }); + + describe("given warm stream", () => { + describe("when signer not sender", () => { + it("should revert", async () => { + await expectToThrow(ctx.cancel({ signer: ctx.recipient.keys }), CONSTRAINT_ADDRESS); + }); + }); + + describe("when signer sender", () => { + describe("given non cancelable stream", () => { + it("should revert", async () => { + await expectToThrow(ctx.cancel({ salt: ctx.salts.nonCancelable }), "StreamIsNotCancelable"); + }); + }); + + describe("given cancelable stream", () => { + describe("when the sender does not have ATA", () => { + it("should cancel the stream", async () => { + // Derive the sender's ATA for the random token + const senderATA = deriveATAAddress(ctx.randomToken, ctx.sender.keys.publicKey, ProgramId.TOKEN); + + // Assert the sender's ATA doesn't exist + await assertAccountNotExists(ctx, senderATA, "Sender's ATA"); + + // Create ATA for & mint random token to the stream creator + await createATAAndFund( + ctx.banksClient, + ctx.defaultBankrunPayer, + ctx.randomToken, + Amount.DEPOSIT, + ProgramId.TOKEN, + ctx.sender.keys.publicKey, + ); + + // Create a stream with a random token + const salt = await ctx.createWithTimestamps({ + creator: ctx.sender.keys, + depositTokenMint: ctx.randomToken, + depositTokenProgram: ProgramId.TOKEN, + }); + + // Cancel the stream + await ctx.cancel({ + depositedTokenMint: ctx.randomToken, + depositedTokenProgram: ProgramId.TOKEN, + salt, + }); + + // Assert the cancelation + const expectedStream = ctx.defaultStream({ + depositedTokenMint: ctx.randomToken, + isCancelable: false, + salt: salt, + tokenProgram: ProgramId.TOKEN, + wasCanceled: true, + }); + expectedStream.data.amounts.refunded = Amount.REFUND; + + // Assert the cancelation + await postCancelAssertions(ctx, salt, expectedStream, ZERO); + }); + }); + + describe("given PENDING status", () => { + it("should cancel the stream", async () => { + // Go back in time so that the stream is PENDING + await ctx.timeTravelTo(Time.GENESIS); + + const beforeSenderBalance = await getATABalance(ctx.banksClient, ctx.sender.usdcATA); + + // Cancel the stream + await ctx.cancel(); + + // Assert the cancelation + const expectedStream = ctx.defaultStream({ + isCancelable: false, + isDepleted: true, + wasCanceled: true, + }); + expectedStream.data.amounts.refunded = Amount.DEPOSIT; + + // Assert the cancelation + await postCancelAssertions(ctx, ctx.salts.default, expectedStream, beforeSenderBalance); + }); + }); + + describe("given STREAMING status", () => { + describe("given token SPL standard", () => { + it("should cancel the stream", async () => { + const beforeSenderBalance = await getATABalance(ctx.banksClient, ctx.sender.usdcATA); + + // Cancel the stream + await ctx.cancel(); + + const expectedStream = ctx.defaultStream({ + isCancelable: false, + wasCanceled: true, + }); + expectedStream.data.amounts.refunded = Amount.REFUND; + + // Assert the cancelation + await postCancelAssertions(ctx, ctx.salts.default, expectedStream, beforeSenderBalance); + }); + }); + + describe("given token 2022 standard", () => { + it("should cancel the stream", async () => { + // Create a stream with a Token2022 mint + const salt = await ctx.createWithTimestampsToken2022(); + + const beforeSenderBalance = await getATABalance(ctx.banksClient, ctx.sender.daiATA); + + // Cancel the stream + await ctx.cancelToken2022(salt); + + const expectedStream = ctx.defaultStreamToken2022({ + isCancelable: false, + salt: salt, + wasCanceled: true, + }); + expectedStream.data.amounts.refunded = Amount.REFUND; + + // Assert the cancelation + await postCancelAssertions(ctx, salt, expectedStream, beforeSenderBalance); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); + +async function postCancelAssertions(ctx: LockupTestContext, salt: BN, expectedStream: Stream, beforeSenderBalance: BN) { + // Assert that the Stream state has been updated correctly + const actualStreamData = await ctx.fetchStreamData(salt); + assertEqStreamData(actualStreamData, expectedStream.data); + + // Assert the Sender's ATA balance + const afterSenderBalance = await getATABalanceMint( + ctx.banksClient, + expectedStream.data.sender, + expectedStream.data.depositedTokenMint, + ); + + const actualBalanceRefunded = afterSenderBalance.sub(beforeSenderBalance); + assertEqualBn(actualBalanceRefunded, expectedStream.data.amounts.refunded); + + // Assert the StreamData ATA balance + const actualStreamDataBalance = await getATABalanceMint( + ctx.banksClient, + expectedStream.dataAddress, + expectedStream.data.depositedTokenMint, + ); + const expectedStreamDataBalance = expectedStream.data.amounts.deposited.sub(expectedStream.data.amounts.refunded); + assertEqualBn(actualStreamDataBalance, expectedStreamDataBalance); +} diff --git a/tests/lockup/unit/cancel.ts b/tests/lockup/unit/cancel.ts deleted file mode 100644 index 769c58de..00000000 --- a/tests/lockup/unit/cancel.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import { assert } from "chai"; - -import { - createATAAndFund, - deriveATAAddress, -} from "../../anchor-bankrun-adapter"; -import { - cancel, - cancelToken2022, - createWithTimestamps, - createWithTimestampsToken2022, - defaultStream, - defaultStreamToken2022, - fetchStreamData, - getATABalanceMint, - getATABalance, - salts, - sender, - setUp, - withdrawMax, -} from "../base"; -import { - accountExists, - banksClient, - defaultBankrunPayer, - randomToken, - recipient, - sleepFor, - timeTravelTo, -} from "../../common-base"; -import { - assertErrorHexCode, - assertEqStreamDatas, - assertFail, -} from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; -import { Stream } from "../utils/types"; - -describe("cancel", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - // Set the time to 26% of the stream duration - await timeTravelTo(defaults.PASS_26_PERCENT); - }); - - it("should revert", async () => { - try { - await cancel({ salt: new BN(1) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - // Set the time to 26% of the stream duration - await timeTravelTo(defaults.PASS_26_PERCENT); - }); - - context("given a null stream", () => { - it("should revert", async () => { - try { - await cancel({ salt: salts.nonExisting }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("given a valid stream", () => { - context("given an invalid deposited token mint", () => { - it("should revert", async () => { - try { - await cancel({ depositedTokenMint: randomToken }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("given a valid deposited token mint", () => { - context("given cold stream", () => { - context("given DEPLETED status", () => { - it("should revert", async () => { - await timeTravelTo(defaults.END_TIME); - await withdrawMax(); - try { - await cancel(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("StreamDepleted")); - } - }); - }); - - context("given CANCELED status", () => { - it("should revert", async () => { - await cancel(); - // Sleep for 5 ms to allow the tx to be processed - await sleepFor(7); - try { - await cancel(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("StreamCanceled")); - } - }); - }); - - context("given SETTLED status", () => { - it("should revert", async () => { - await timeTravelTo(defaults.END_TIME); - try { - await cancel(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("StreamSettled")); - } - }); - }); - }); - - context("given warm stream", () => { - context("when signer not sender", () => { - it("should revert", async () => { - try { - await cancel({ signer: recipient.keys }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("ConstraintAddress")); - } - }); - }); - - context("when signer sender", () => { - context("given non cancelable stream", () => { - it("should revert", async () => { - try { - await cancel({ salt: salts.nonCancelable }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StreamIsNotCancelable") - ); - } - }); - }); - - context("given cancelable stream", () => { - context("when the sender does not have ATA", () => { - it("should cancel the stream", async () => { - // Derive the sender's ATA for the random token - const senderATA = deriveATAAddress( - randomToken, - sender.keys.publicKey, - defaults.TOKEN_PROGRAM_ID - ); - - // Assert the sender's ATA doesn't exist - assert.isFalse(await accountExists(senderATA)); - - // Create ATA for & mint random token to the stream creator - await createATAAndFund( - banksClient, - defaultBankrunPayer, - randomToken, - defaults.DEPOSIT_AMOUNT, - defaults.TOKEN_PROGRAM_ID, - sender.keys.publicKey - ); - - // Create a stream with a random token - const salt = await createWithTimestamps({ - creator: sender.keys, - depositTokenMint: randomToken, - depositTokenProgram: defaults.TOKEN_PROGRAM_ID, - }); - - // Cancel the stream - await cancel({ - salt, - depositedTokenMint: randomToken, - depositedTokenProgram: defaults.TOKEN_PROGRAM_ID, - }); - - // Assert the cancelation - const expectedStream = defaultStream({ - salt: salt, - isCancelable: false, - wasCanceled: true, - depositedTokenMint: randomToken, - tokenProgram: defaults.TOKEN_PROGRAM_ID, - }); - expectedStream.data.amounts.refunded = defaults.REFUND_AMOUNT; - - // Assert the cancelation - await postCancelAssertions(salt, expectedStream, new BN(0)); - }); - }); - - context("given PENDING status", () => { - it("should cancel the stream", async () => { - // Go back in time so that the stream is PENDING - await timeTravelTo(defaults.APR_1_2025); - - const beforeSenderBalance = await getATABalance( - banksClient, - sender.usdcATA - ); - - // Cancel the stream - await cancel(); - - // Assert the cancelation - const expectedStream = defaultStream({ - isCancelable: false, - isDepleted: true, - wasCanceled: true, - }); - expectedStream.data.amounts.refunded = - defaults.DEPOSIT_AMOUNT; - - // Assert the cancelation - await postCancelAssertions( - salts.default, - expectedStream, - beforeSenderBalance - ); - }); - }); - - context("given STREAMING status", () => { - context("given token SPL standard", () => { - it("should cancel the stream", async () => { - const beforeSenderBalance = await getATABalance( - banksClient, - sender.usdcATA - ); - - // Cancel the stream - await cancel(); - - const expectedStream = defaultStream({ - isCancelable: false, - wasCanceled: true, - }); - expectedStream.data.amounts.refunded = - defaults.REFUND_AMOUNT; - - // Assert the cancelation - await postCancelAssertions( - salts.default, - expectedStream, - beforeSenderBalance - ); - }); - }); - - context("given token 2022 standard", () => { - it("should cancel the stream", async () => { - // Create a stream with a Token2022 mint - const salt = await createWithTimestampsToken2022(); - - const beforeSenderBalance = await getATABalance( - banksClient, - sender.daiATA - ); - - // Cancel the stream - await cancelToken2022(salt); - - const expectedStream = defaultStreamToken2022({ - salt: salt, - isCancelable: false, - wasCanceled: true, - }); - expectedStream.data.amounts.refunded = - defaults.REFUND_AMOUNT; - - // Assert the cancelation - await postCancelAssertions( - salt, - expectedStream, - beforeSenderBalance - ); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -async function postCancelAssertions( - salt: BN, - expectedStream: Stream, - beforeSenderBalance: BN -) { - // Assert that the Stream state has been updated correctly - const actualStreamData = await fetchStreamData(salt); - assertEqStreamDatas(actualStreamData, expectedStream.data); - - // Assert the Sender's ATA balance - const afterSenderBalance = await getATABalanceMint( - banksClient, - expectedStream.data.sender, - expectedStream.data.depositedTokenMint - ); - - const actualBalanceRefunded = afterSenderBalance.sub(beforeSenderBalance); - assert( - actualBalanceRefunded.eq(expectedStream.data.amounts.refunded), - "Sender's ATA balance mismatch" - ); - - // Assert the StreamData ATA balance - const actualStreamDataBalance = await getATABalanceMint( - banksClient, - expectedStream.dataAddress, - expectedStream.data.depositedTokenMint - ); - const expectedStreamDataBalance = expectedStream.data.amounts.deposited.sub( - expectedStream.data.amounts.refunded - ); - assert( - actualStreamDataBalance.eq(expectedStreamDataBalance), - "StreamData's ATA balance mismatch" - ); -} diff --git a/tests/lockup/unit/collectFees.test.ts b/tests/lockup/unit/collectFees.test.ts new file mode 100644 index 00000000..2bd83a24 --- /dev/null +++ b/tests/lockup/unit/collectFees.test.ts @@ -0,0 +1,86 @@ +import { + ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, + ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, +} from "@coral-xyz/anchor-errors"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { REDUNDANCY_BUFFER } from "../../../lib/constants"; +import { sleepFor } from "../../../lib/helpers"; +import { assertEqualSOLBalance } from "../../common/assertions"; +import { LockupTestContext } from "../context"; +import { expectToThrow } from "../utils/assertions"; +import { Amount, Time } from "../utils/defaults"; + +describe("collectFees", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + }); + + it("should revert", async () => { + await expectToThrow(ctx.collectFees(), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + }); + + describe("when signer is not the authorized fee collector", () => { + it("should revert", async () => { + await withdrawMultipleTimes(ctx); + await expectToThrow(ctx.collectFees(ctx.eve.keys), CONSTRAINT_ADDRESS); + }); + }); + + describe("when signer is the authorized fee collector", () => { + describe("given no fees accumulated", () => { + it("should revert", async () => { + await expectToThrow(ctx.collectFees(), "CantCollectZeroFees"); + }); + }); + + describe("given accumulated fees", () => { + it("should collect the fees", async () => { + await withdrawMultipleTimes(ctx); + + const beforeLamports = { + feeRecipient: await getFeeRecipientLamports(ctx), + treasury: await ctx.getTreasuryLamports(), + }; + + // Collect fees + await ctx.collectFees(); + + const afterLamports = { + feeRecipient: await getFeeRecipientLamports(ctx), + treasury: await ctx.getTreasuryLamports(), + }; + + // 2 withdrawals worth of fees minus the minimum lamports balance (a buffer on top of the redundancy buffer). + const expectedFeesCollected = Amount.WITHDRAW_FEE.muln(2).sub(REDUNDANCY_BUFFER); + + assertEqualSOLBalance(afterLamports.treasury, beforeLamports.treasury.sub(expectedFeesCollected)); + assertEqualSOLBalance(afterLamports.feeRecipient, beforeLamports.feeRecipient.add(expectedFeesCollected)); + }); + }); + }); + }); +}); + +async function getFeeRecipientLamports(ctx: LockupTestContext) { + return await ctx.getSenderLamports(); +} + +/// Helper function to withdraw multiple times so that there are fees collected +async function withdrawMultipleTimes(ctx: LockupTestContext) { + await ctx.timeTravelTo(Time.MID_26_PERCENT); + await ctx.withdrawMax(); + await ctx.timeTravelTo(Time.END); + await sleepFor(7); + await ctx.withdrawMax(); +} diff --git a/tests/lockup/unit/collectFees.ts b/tests/lockup/unit/collectFees.ts deleted file mode 100644 index 8634f93a..00000000 --- a/tests/lockup/unit/collectFees.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - collectFees, - getSenderLamports, - getTreasuryLamports, - setUp, - withdrawMax, -} from "../base"; -import { eve, sleepFor, timeTravelTo } from "../../common-base"; -import { assert, assertErrorHexCode, assertFail } from "../utils/assertions"; -import { getErrorCode } from "../utils/errors"; -import * as defaults from "../utils/defaults"; - -describe("collectFees", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - }); - - it("should revert", async () => { - try { - await collectFees(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("when signer is not the authorized fee collector", () => { - it("should revert", async () => { - await withdrawMultipleTimes(); - - try { - await collectFees(eve.keys); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("ConstraintAddress")); - } - }); - }); - - context("when signer is the authorized fee collector", () => { - context("given no fees accumulated", () => { - it("should revert", async () => { - try { - await collectFees(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("CantCollectZeroFees")); - } - }); - }); - - context("given accumulated fees", () => { - it("should collect the fees", async () => { - await withdrawMultipleTimes(); - - const treasuryLamportsBefore = await getTreasuryLamports(); - const feeRecipientLamportsBefore = await getFeeRecipientLamports(); - // Collect fees - await collectFees(); - - const treasuryLamportsAfter = await getTreasuryLamports(); - const feeRecipientLamportsAfter = await getFeeRecipientLamports(); - - const expectedFeesCollected = - 2 * defaults.WITHDRAWAL_FEE_AMOUNT - 1_000_000; // 2 withdrawals worth of fees minus the safety buffer - - assert( - treasuryLamportsAfter === - treasuryLamportsBefore - BigInt(expectedFeesCollected) - ); - assert( - feeRecipientLamportsAfter === - feeRecipientLamportsBefore + BigInt(expectedFeesCollected) - ); - }); - }); - }); - }); -}); - -async function getFeeRecipientLamports() { - return await getSenderLamports(); -} - -/// Helper function to withdraw multiple times so that there are fees collected -async function withdrawMultipleTimes() { - await timeTravelTo(defaults.PASS_26_PERCENT); - await withdrawMax(); - await timeTravelTo(defaults.END_TIME); - await sleepFor(7); - await withdrawMax(); -} diff --git a/tests/lockup/unit/createWithDurations.test.ts b/tests/lockup/unit/createWithDurations.test.ts new file mode 100644 index 00000000..f2bbb5b7 --- /dev/null +++ b/tests/lockup/unit/createWithDurations.test.ts @@ -0,0 +1,52 @@ +import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { ZERO } from "../../../lib/constants"; +import { LockupTestContext } from "../context"; +import { assertEqStreamData, expectToThrow } from "../utils/assertions"; +import { Time } from "../utils/defaults"; + +describe("createWithDurations", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + await ctx.timeTravelTo(Time.START); + }); + + it("should revert", async () => { + await expectToThrow(ctx.createWithDurations({ salt: ZERO }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + await ctx.timeTravelTo(Time.START); + }); + + describe("when cliff duration not zero", () => { + it("it should create the stream", async () => { + const salt = await ctx.createWithDurations(); + + const actualStreamData = await ctx.fetchStreamData(salt); + const expectedStreamData = ctx.defaultStream({ salt: salt }).data; + assertEqStreamData(actualStreamData, expectedStreamData); + }); + }); + + describe("when cliff duration zero", () => { + it("it should create the stream", async () => { + const salt = await ctx.createWithDurations({ cliffDuration: ZERO }); + + const actualStreamData = await ctx.fetchStreamData(salt); + const expectedStreamData = ctx.defaultStream({ salt: salt }).data; + expectedStreamData.amounts.cliffUnlock = ZERO; + expectedStreamData.timestamps.cliff = ZERO; + assertEqStreamData(actualStreamData, expectedStreamData); + }); + }); + }); +}); diff --git a/tests/lockup/unit/createWithDurations.ts b/tests/lockup/unit/createWithDurations.ts deleted file mode 100644 index 5d91d0ea..00000000 --- a/tests/lockup/unit/createWithDurations.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - createWithDurations, - defaultStream, - fetchStreamData, - setUp, -} from "../base"; -import { timeTravelTo } from "../../common-base"; -import { - assertEqStreamDatas, - assertErrorContains, - assertFail, -} from "../utils/assertions"; -import * as defaults from "../utils/defaults"; - -describe("createWithDurations", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - await timeTravelTo(defaults.START_TIME); - }); - - it("should revert", async () => { - try { - await createWithDurations(); - assertFail(); - } catch (error) { - assertErrorContains( - error, - defaults.PROGRAM_NOT_INITIALIZED_ERR.CreateWithTimestamps - ); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - await timeTravelTo(defaults.START_TIME); - }); - - context("when cliff duration not zero", () => { - it("it should create the stream", async () => { - const salt = await createWithDurations(); - - const actualStreamData = await fetchStreamData(salt); - const expectedStreamData = defaultStream({ salt: salt }).data; - assertEqStreamDatas(actualStreamData, expectedStreamData); - }); - }); - - context("when cliff duration zero", () => { - it("it should create the stream", async () => { - const salt = await createWithDurations(defaults.ZERO_BN); - - const actualStreamData = await fetchStreamData(salt); - const expectedStreamData = defaultStream({ salt: salt }).data; - expectedStreamData.amounts.cliffUnlock = defaults.ZERO_BN; - expectedStreamData.timestamps.cliff = defaults.ZERO_BN; - assertEqStreamDatas(actualStreamData, expectedStreamData); - }); - }); - }); -}); diff --git a/tests/lockup/unit/createWithTimestamps.test.ts b/tests/lockup/unit/createWithTimestamps.test.ts new file mode 100644 index 00000000..be4ef666 --- /dev/null +++ b/tests/lockup/unit/createWithTimestamps.test.ts @@ -0,0 +1,237 @@ +import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors"; +import BN from "bn.js"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { BN_1, BN_1000, ZERO } from "../../../lib/constants"; +import { usdc } from "../../../lib/convertors"; +import { getATABalance, getMintTotalSupplyOf } from "../../common/anchor-bankrun"; +import { assertAccountExists, assertEqualBn } from "../../common/assertions"; +import { LockupTestContext } from "../context"; +import { assertEqStreamData, expectToThrow } from "../utils/assertions"; +import { AMOUNTS, Amount, TIMESTAMPS, Time, UNLOCK_AMOUNTS } from "../utils/defaults"; + +describe("createWithTimestamps", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + }); + + it("should revert", async () => { + await expectToThrow(ctx.createWithTimestamps({ salt: ZERO }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + }); + + describe("when deposit amount zero", () => { + it("should revert", async () => { + await expectToThrow(ctx.createWithTimestamps({ depositAmount: ZERO }), "DepositAmountZero"); + }); + }); + + describe("when deposit amount not zero", () => { + describe("when start time is zero", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ start: ZERO }), + }), + "StartTimeNotPositive", + ); + }); + }); + + describe("when start time is not zero", () => { + describe("when start time is not positive", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ start: new BN(-1) }), + }), + "StartTimeNotPositive", + ); + }); + }); + + describe("when start time is positive", () => { + describe("when sender lacks an ATA for deposited token", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + depositTokenMint: ctx.randomToken, + }), + ACCOUNT_NOT_INITIALIZED, + ); + }); + }); + + describe("when sender has an ATA for deposited token", () => { + describe("when sender has an insufficient token balance", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + depositAmount: usdc(1_000_000).addn(1), + }), + 0x1, + ); + }); + }); + + describe("when sender has a sufficient token balance", () => { + describe("when cliff time zero", () => { + describe("when cliff unlock amount not zero", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ cliff: ZERO }), + }), + "CliffTimeZeroUnlockAmountNotZero", + ); + }); + }); + + describe("when start time not less than end time", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ cliff: ZERO, start: Time.END }), + }), + "StartTimeNotLessThanEndTime", + ); + }); + }); + + describe("when start time less than end time", () => { + it("should create the stream", async () => { + const beforeSenderTokenBalance = await getATABalance(ctx.banksClient, ctx.sender.usdcATA); + + const salt = await ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ cliff: ZERO }), + unlockAmounts: UNLOCK_AMOUNTS({ cliff: ZERO, start: ZERO }), + }); + + const expectedStream = ctx.defaultStream({ + salt: salt, + }); + expectedStream.data.timestamps.cliff = ZERO; + expectedStream.data.amounts = AMOUNTS({ cliffUnlock: ZERO, startUnlock: ZERO }); + + await assertStreamCreation(ctx, salt, beforeSenderTokenBalance, expectedStream); + }); + }); + }); + + describe("when cliff time not zero", () => { + describe("when start time not less than cliff time", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ start: Time.CLIFF }), + }), + "StartTimeNotLessThanCliffTime", + ); + }); + }); + + describe("when start time less than cliff time", () => { + describe("when cliff time not less than end time", () => { + it("should revert", async () => { + await expectToThrow( + ctx.createWithTimestamps({ + timestamps: TIMESTAMPS({ cliff: Time.END }), + }), + "CliffTimeNotLessThanEndTime", + ); + }); + }); + + describe("when cliff time less than end time", () => { + describe("when unlock amounts sum exceeds deposit amount", () => { + it("should revert", async () => { + const depositAmount = BN_1000; + await expectToThrow( + ctx.createWithTimestamps({ + depositAmount, + unlockAmounts: { + cliff: depositAmount, + start: depositAmount, + }, + }), + "UnlockAmountsSumTooHigh", + ); + }); + }); + + describe("when unlock amounts sum not exceed deposit amount", () => { + describe("when token SPL standard", () => { + it("should create the stream", async () => { + const beforeSenderTokenBalance = await getATABalance(ctx.banksClient, ctx.sender.usdcATA); + const salt = await ctx.createWithTimestamps(); + + await assertStreamCreation(ctx, salt, beforeSenderTokenBalance); + }); + }); + + describe("when token 2022 standard", () => { + it("should create the stream", async () => { + const beforeSenderTokenBalance = await ctx.getSenderTokenBalance(ctx.dai); + const salt = await ctx.createWithTimestampsToken2022(); + + await assertStreamCreation( + ctx, + salt, + beforeSenderTokenBalance, + ctx.defaultStreamToken2022({ salt: salt }), + ); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); + +async function assertStreamCreation( + ctx: LockupTestContext, + salt: BN, + beforeSenderTokenBalance: BN, + expectedStream = ctx.defaultStream({ salt: salt }), +) { + await assertAccountExists(ctx, expectedStream.nftMintAddress, "Stream NFT Mint"); + await assertAccountExists(ctx, expectedStream.dataAddress, "Stream Data"); + await assertAccountExists(ctx, expectedStream.dataAta, "Stream Data ATA"); + await assertAccountExists(ctx, expectedStream.nftMasterEdition, "Stream NFT Master Edition"); + await assertAccountExists(ctx, expectedStream.nftMetadataAddress, "Stream NFT Metadata"); + await assertAccountExists(ctx, expectedStream.recipientStreamNftAta, "Recipient Stream NFT ATA"); + + // Assert the contents of the Stream Data account + const actualStreamData = await ctx.fetchStreamData(salt); + assertEqStreamData(actualStreamData, expectedStream.data); + + // Assert that the Stream NFT Mint has the correct total supply + const streamNftMintTotalSupply = await getMintTotalSupplyOf(ctx.banksClient, expectedStream.nftMintAddress); + assertEqualBn(streamNftMintTotalSupply, BN_1, "Stream NFT Mint total supply not 1"); + + // Assert that the Recipient's Stream NFT ATA has the correct balance + const recipientStreamNftBalance = await getATABalance(ctx.banksClient, expectedStream.recipientStreamNftAta); + assertEqualBn(recipientStreamNftBalance, BN_1, "Stream NFT not minted"); + + // TODO: test that the Stream NFT has been properly added to the LL NFT collection + + // Assert that the Sender's balance has changed correctly + const expectedTokenBalance = beforeSenderTokenBalance.sub(Amount.DEPOSIT); + const afterSenderTokenBalance = await ctx.getSenderTokenBalance(expectedStream.data.depositedTokenMint); + assertEqualBn(expectedTokenBalance, afterSenderTokenBalance, "sender balance not updated correctly"); +} diff --git a/tests/lockup/unit/createWithTimestamps.ts b/tests/lockup/unit/createWithTimestamps.ts deleted file mode 100644 index 5976bba0..00000000 --- a/tests/lockup/unit/createWithTimestamps.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; - -import { - assert, - assertErrorHexCode, - assertErrorContains, - assertEqStreamDatas, - assertFail, -} from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; -import { - createWithTimestamps, - createWithTimestampsToken2022, - defaultStream, - defaultStreamToken2022, - fetchStreamData, - getATABalance, - getMintTotalSupplyOf, - getSenderTokenBalance, - sender, - setUp, -} from "../base"; - -import { - accountExists, - banksClient, - dai, - randomToken, -} from "../../common-base"; - -describe("createWithTimestamps", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - }); - - it("should revert", async () => { - try { - await createWithTimestamps(); - assertFail(); - } catch (error) { - assertErrorContains( - error, - defaults.PROGRAM_NOT_INITIALIZED_ERR.CreateWithTimestamps - ); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("when deposit amount zero", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ depositAmount: defaults.ZERO_BN }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("DepositAmountZero")); - } - }); - }); - - context("when deposit amount not zero", () => { - context("when start time is zero", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - timestamps: { ...defaults.timestamps(), start: defaults.ZERO_BN }, - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("StartTimeNotPositive")); - } - }); - }); - - context("when start time is not zero", () => { - context("when start time is not positive", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - timestamps: { - ...defaults.timestamps(), - start: defaults.ZERO_BN.sub(new BN(1)), - }, - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("StartTimeNotPositive")); - } - }); - }); - - context("when start time is positive", () => { - context("when sender lacks an ATA for deposited token", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - depositTokenMint: randomToken, - }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("AccountNotInitialized") - ); - } - }); - }); - - context("when sender has an ATA for deposited token", () => { - context("when sender has an insufficient token balance", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - depositAmount: new BN(1_000_000e6 + 1), - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, "0x1"); - } - }); - }); - - context("when sender has a sufficient token balance", () => { - context("when cliff time zero", () => { - context("when cliff unlock amount not zero", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - timestamps: { - ...defaults.timestamps(), - cliff: new BN(0), - }, - unlockAmounts: { - ...defaults.unlockAmounts(), - cliff: new BN(1000), - }, - }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("CliffTimeZeroUnlockAmountNotZero") - ); - } - }); - }); - - context("when start time not less than end time", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - timestamps: { - ...defaults.timestamps(), - start: defaults.timestamps().end, - cliff: defaults.ZERO_BN, - }, - }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StartTimeNotLessThanEndTime") - ); - } - }); - }); - - context("when start time less than end time", () => { - it("should create the stream", async () => { - const beforeSenderTokenBalance = await getATABalance( - banksClient, - sender.usdcATA - ); - - const salt = await createWithTimestamps({ - timestamps: { - ...defaults.timestamps(), - cliff: defaults.ZERO_BN, - }, - unlockAmounts: defaults.unlockAmountsZero(), - }); - - const expectedStream = defaultStream({ - salt: salt, - }); - expectedStream.data.timestamps.cliff = new BN(0); - expectedStream.data.amounts = - defaults.amountsAfterCreateWithZeroUnlocks(); - - await assertStreamCreation( - salt, - beforeSenderTokenBalance, - expectedStream - ); - }); - }); - }); - - context("when cliff time not zero", () => { - context("when start time not less than cliff time", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - timestamps: { - ...defaults.timestamps(), - start: defaults.timestamps().cliff, - }, - }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StartTimeNotLessThanCliffTime") - ); - } - }); - }); - - context("when start time less than cliff time", () => { - context("when cliff time not less than end time", () => { - it("should revert", async () => { - try { - await createWithTimestamps({ - timestamps: { - ...defaults.timestamps(), - cliff: defaults.timestamps().end, - }, - }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("CliffTimeNotLessThanEndTime") - ); - } - }); - }); - - context("when cliff time less than end time", () => { - context( - "when unlock amounts sum exceeds deposit amount", - () => { - it("should revert", async () => { - try { - const depositAmount = new BN(1000); - await createWithTimestamps({ - unlockAmounts: { - start: depositAmount, - cliff: depositAmount, - }, - depositAmount, - }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("UnlockAmountsSumTooHigh") - ); - } - }); - } - ); - - context( - "when unlock amounts sum not exceed deposit amount", - () => { - context("when token SPL standard", () => { - it("should create the stream", async () => { - const beforeSenderTokenBalance = - await getATABalance(banksClient, sender.usdcATA); - - const salt = await createWithTimestamps(); - - await assertStreamCreation( - salt, - beforeSenderTokenBalance - ); - }); - }); - - context("when token 2022 standard", () => { - it("should create the stream", async () => { - const beforeSenderTokenBalance = - await getSenderTokenBalance(dai); - const salt = await createWithTimestampsToken2022(); - - await assertStreamCreation( - salt, - beforeSenderTokenBalance, - defaultStreamToken2022({ salt: salt }) - ); - }); - }); - } - ); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -async function assertStreamCreation( - salt: BN, - beforeSenderTokenBalance: BN, - expectedStream = defaultStream({ salt: salt }) -) { - // Assert that the Stream NFT Mint has been created - assert( - await accountExists(expectedStream.nftMintAddress), - "Stream NFT Mint address not initialized" - ); - // Assert that the Stream Data has been created - assert( - await accountExists(expectedStream.dataAddress), - "Stream Data address not initialized" - ); - // Assert that the Stream Data ATA has been created - assert( - await accountExists(expectedStream.dataAta), - "Stream Data ATA address not initialized" - ); - // Assert that the Stream NFT Master Edition has been created - assert( - await accountExists(expectedStream.nftMasterEdition), - "Stream NFT Master Edition address not initialized" - ); - // Assert that the Stream NFT Metadata has been created - assert( - await accountExists(expectedStream.nftMetadataAddress), - "Stream NFT Metadata address not initialized" - ); - // Assert that the Recipient's Stream NFT ATA has been created - assert( - await accountExists(expectedStream.recipientStreamNftAta), - "Recipient Stream NFT ATA address not initialized" - ); - - // Assert the contents of the Stream Data account - const actualStreamData = await fetchStreamData(salt); - assertEqStreamDatas(actualStreamData, expectedStream.data); - - // Assert that the Stream NFT Mint has the correct total supply - const streamNftMintTotalSupply = await getMintTotalSupplyOf( - banksClient, - expectedStream.nftMintAddress - ); - assert( - streamNftMintTotalSupply.eq(new BN(1)), - "Stream NFT Mint total supply not 1" - ); - - // Assert that the Recipient's Stream NFT ATA has the correct balance - const recipientStreamNftBalance = await getATABalance( - banksClient, - expectedStream.recipientStreamNftAta - ); - assert(recipientStreamNftBalance.eq(new BN(1)), "Stream NFT not minted"); - - // TODO: assert that the Stream NFT has been properly added to the LL NFT collection - - // Assert that the Sender's balance has changed correctly - const expectedTokenBalance = beforeSenderTokenBalance.sub( - defaults.DEPOSIT_AMOUNT - ); - const afterSenderTokenBalance = await getSenderTokenBalance( - expectedStream.data.depositedTokenMint - ); - assert( - expectedTokenBalance.eq(afterSenderTokenBalance), - "sender balance not updated correctly" - ); -} diff --git a/tests/lockup/unit/initialize.test.ts b/tests/lockup/unit/initialize.test.ts new file mode 100644 index 00000000..ded942e0 --- /dev/null +++ b/tests/lockup/unit/initialize.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { BN_1, ProgramId } from "../../../lib/constants"; +import { getPDAAddress, sleepFor } from "../../../lib/helpers"; +import { deriveATAAddress, getATABalance, getMintTotalSupplyOf } from "../../common/anchor-bankrun"; +import { assertAccountExists, assertEqualBn } from "../../common/assertions"; +import { LockupTestContext } from "../context"; +import { Seed } from "../utils/defaults"; + +describe("initialize", () => { + let ctx: LockupTestContext; + + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + }); + + describe("given initialized", () => { + it("should revert", async () => { + await ctx.initializeLockup(); + await sleepFor(7); + await expect(ctx.initializeLockup(), "Tx succeeded when it should have reverted").rejects.toThrow( + "Instruction 1: custom program error: 0x0", + ); + }); + }); + + describe("given not initialized", () => { + it("should initialize the program", async () => { + await ctx.initializeLockup(); + + await assertAccountExists(ctx, ctx.nftCollectionDataAddress, "NFT Collection Data"); + await assertAccountExists(ctx, ctx.treasuryAddress, "Treasury"); + + const nftCollectionMint = getPDAAddress([Seed.NFT_COLLECTION_MINT], ctx.lockup.programId); + await assertAccountExists(ctx, nftCollectionMint, "NFT Collection Mint"); + + // Assert that the Total Supply of the NFT Collection Mint is 1 + const totalSupply = await getMintTotalSupplyOf(ctx.banksClient, nftCollectionMint); + assertEqualBn(totalSupply, BN_1); + + const nftCollectionATA = deriveATAAddress(nftCollectionMint, ctx.treasuryAddress, ProgramId.TOKEN); + await assertAccountExists(ctx, nftCollectionATA, "NFT Collection ATA"); + + // Assert that the NFT Collection ATA has a balance of 1 + const nftCollectionATABalance = await getATABalance(ctx.banksClient, nftCollectionATA); + assertEqualBn(nftCollectionATABalance, BN_1); + + const nftCollectionMintAsBuffer = nftCollectionMint.toBuffer(); + + const nftCollectionMetadata = getPDAAddress( + [Seed.METADATA, ProgramId.TOKEN_METADATA.toBuffer(), nftCollectionMintAsBuffer], + ProgramId.TOKEN_METADATA, + ); + await assertAccountExists(ctx, nftCollectionMetadata, "NFT Collection Metadata"); + + const nftCollectionMasterEdition = getPDAAddress( + [Seed.METADATA, ProgramId.TOKEN_METADATA.toBuffer(), nftCollectionMintAsBuffer, Seed.EDITION], + ProgramId.TOKEN_METADATA, + ); + await assertAccountExists(ctx, nftCollectionMasterEdition, "NFT Collection Master Edition"); + }); + }); +}); diff --git a/tests/lockup/unit/initialize.ts b/tests/lockup/unit/initialize.ts deleted file mode 100644 index c7fe125c..00000000 --- a/tests/lockup/unit/initialize.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; - -import { - deriveATAAddress, - getATABalance, - getMintTotalSupplyOf, - initializeLockup, - lockup, - nftCollectionDataAddress, - setUp, - treasuryAddress, -} from "../base"; - -import { - accountExists, - banksClient, - getPDAAddress, - sleepFor, -} from "../../common-base"; - -import { assert, assertErrorHexCode, assertFail } from "../utils/assertions"; -import * as defaults from "../utils/defaults"; - -describe("initialize", () => { - beforeEach(async () => { - await setUp(false); - }); - - context("given initialized", () => { - it("should revert", async () => { - await initializeLockup(); - await sleepFor(7); - try { - await initializeLockup(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, "0x0"); - } - }); - }); - - context("given not initialized", () => { - it("should initialize the program", async () => { - await initializeLockup(); - - assert( - await accountExists(nftCollectionDataAddress), - "nftCollectionDataAddress is null" - ); - - assert(await accountExists(treasuryAddress), "Treasury not initialized"); - - const nftCollectionMint = getPDAAddress( - [Buffer.from(defaults.NFT_COLLECTION_MINT_SEED)], - lockup.programId - ); - - assert( - await accountExists(nftCollectionMint), - "NFT Collection Mint not initialized" - ); - - // Assert that the Total Supply of the NFT Collection Mint is 1 - const totalSupply = await getMintTotalSupplyOf( - banksClient, - nftCollectionMint - ); - assert(totalSupply.eq(new BN(1))); - - const nftCollectionATA = deriveATAAddress( - nftCollectionMint, - treasuryAddress, - defaults.TOKEN_PROGRAM_ID - ); - - assert( - await accountExists(nftCollectionATA), - "NFT Collection ATA not initialized" - ); - - // Assert that the NFT Collection ATA has a balance of 1 - const nftCollectionATABalance = await getATABalance( - banksClient, - nftCollectionATA - ); - assert(nftCollectionATABalance.eq(new BN(1))); - - const nftCollectionMintAsBuffer = nftCollectionMint.toBuffer(); - - const nftCollectionMetadata = getPDAAddress( - [ - Buffer.from(defaults.METADATA_SEED), - defaults.TOKEN_METADATA_PROGRAM_ID.toBuffer(), - nftCollectionMintAsBuffer, - ], - defaults.TOKEN_METADATA_PROGRAM_ID - ); - - assert( - await accountExists(nftCollectionMetadata), - "NFT Collection Metadata not initialized" - ); - - const nftCollectionMasterEdition = getPDAAddress( - [ - Buffer.from("metadata"), - defaults.TOKEN_METADATA_PROGRAM_ID.toBuffer(), - nftCollectionMintAsBuffer, - Buffer.from("edition"), - ], - defaults.TOKEN_METADATA_PROGRAM_ID - ); - - assert( - await accountExists(nftCollectionMasterEdition), - "NFT Collection Master Edition not initialized" - ); - }); - }); -}); diff --git a/tests/lockup/unit/renounce.test.ts b/tests/lockup/unit/renounce.test.ts new file mode 100644 index 00000000..b2649c2c --- /dev/null +++ b/tests/lockup/unit/renounce.test.ts @@ -0,0 +1,91 @@ +import { + ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, + ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, +} from "@coral-xyz/anchor-errors"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { BN_1 } from "../../../lib/constants"; +import { LockupTestContext } from "../context"; +import { assertEqStreamData, expectToThrow } from "../utils/assertions"; +import { Time } from "../utils/defaults"; + +describe("renounce", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + }); + + it("should revert", async () => { + await expectToThrow(ctx.renounce({ salt: BN_1 }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + }); + + describe("given a null stream", () => { + it("should revert", async () => { + await expectToThrow(ctx.renounce({ salt: ctx.salts.nonExisting }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("given a valid stream", () => { + describe("given cold stream", () => { + describe("given DEPLETED status", () => { + it("should revert", async () => { + await ctx.timeTravelTo(Time.END); + await ctx.withdrawMax(); + await expectToThrow(ctx.renounce(), "StreamAlreadyNonCancelable"); + }); + }); + + describe("given CANCELED status", () => { + it("should revert", async () => { + await ctx.cancel(); + await expectToThrow(ctx.renounce(), "StreamAlreadyNonCancelable"); + }); + }); + + describe("given SETTLED status", () => { + it("should revert", async () => { + await ctx.timeTravelTo(Time.END); + await expectToThrow(ctx.renounce(), "StreamAlreadyNonCancelable"); + }); + }); + }); + + describe("given warm stream", () => { + describe("when signer not sender", () => { + it("should revert", async () => { + await expectToThrow(ctx.renounce({ signer: ctx.eve.keys }), CONSTRAINT_ADDRESS); + }); + }); + + describe("when signer sender", () => { + describe("given non cancelable stream", () => { + it("should revert", async () => { + await expectToThrow(ctx.renounce({ salt: ctx.salts.nonCancelable }), "StreamAlreadyNonCancelable"); + }); + }); + + describe("given cancelable stream", () => { + it("should make stream non cancelable", async () => { + await ctx.renounce(); + + const actualStreamData = await ctx.fetchStreamData(); + const expectedStreamData = ctx.defaultStream().data; + expectedStreamData.isCancelable = false; + + assertEqStreamData(actualStreamData, expectedStreamData); + }); + }); + }); + }); + }); + }); +}); diff --git a/tests/lockup/unit/renounce.ts b/tests/lockup/unit/renounce.ts deleted file mode 100644 index 994d26f1..00000000 --- a/tests/lockup/unit/renounce.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import { - cancel, - defaultStream, - fetchStreamData, - salts, - renounce, - setUp, - withdrawMax, -} from "../base"; -import { eve, timeTravelTo } from "../../common-base"; -import { - assertErrorHexCode, - assertEqStreamDatas, - assertFail, -} from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; - -describe("renounce", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - }); - - it("should revert", async () => { - try { - await renounce({ salt: new BN(1) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("given a null stream", () => { - it("should revert", async () => { - try { - await renounce({ salt: salts.nonExisting }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("given a valid stream", () => { - context("given cold stream", () => { - context("given DEPLETED status", () => { - it("should revert", async () => { - await timeTravelTo(defaults.END_TIME); - await withdrawMax(); - try { - await renounce(); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StreamAlreadyNonCancelable") - ); - } - }); - }); - - context("given CANCELED status", () => { - it("should revert", async () => { - await cancel(); - try { - await renounce(); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StreamAlreadyNonCancelable") - ); - } - }); - }); - - context("given SETTLED status", () => { - it("should revert", async () => { - await timeTravelTo(defaults.END_TIME); - try { - await renounce(); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StreamAlreadyNonCancelable") - ); - } - }); - }); - }); - - context("given warm stream", () => { - context("when signer not sender", () => { - it("should revert", async () => { - try { - await renounce({ signer: eve.keys }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("ConstraintAddress")); - } - }); - }); - - context("when signer sender", () => { - context("given non cancelable stream", () => { - it("should revert", async () => { - try { - await renounce({ salt: salts.nonCancelable }); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("StreamAlreadyNonCancelable") - ); - } - }); - }); - - context("given cancelable stream", () => { - it("should make stream non cancelable", async () => { - await renounce(); - - const actualStreamData = await fetchStreamData(); - const expectedStreamData = defaultStream().data; - expectedStreamData.isCancelable = false; - - assertEqStreamDatas(actualStreamData, expectedStreamData); - }); - }); - }); - }); - }); - }); -}); diff --git a/tests/lockup/unit/withdraw.test.ts b/tests/lockup/unit/withdraw.test.ts new file mode 100644 index 00000000..4e287022 --- /dev/null +++ b/tests/lockup/unit/withdraw.test.ts @@ -0,0 +1,366 @@ +import { + ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, + ANCHOR_ERROR__CONSTRAINT_RAW as CONSTRAINT_RAW, +} from "@coral-xyz/anchor-errors"; +import { type PublicKey } from "@solana/web3.js"; +import type BN from "bn.js"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { BN_1, ProgramId, ZERO } from "../../../lib/constants"; +import type { StreamData } from "../../../target/types/sablier_lockup_structs"; +import { createATAAndFund, deriveATAAddress, getATABalance } from "../../common/anchor-bankrun"; +import { + assertAccountExists, + assertAccountNotExists, + assertEqualBn, + assertEqualSOLBalance, +} from "../../common/assertions"; +import { LockupTestContext } from "../context"; +import { assertEqStreamData, expectToThrow } from "../utils/assertions"; +import { Amount, Time } from "../utils/defaults"; + +describe("withdraw", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + // Set the time to 26% of the stream duration + await ctx.timeTravelTo(Time.MID_26_PERCENT); + }); + + it("should revert", async () => { + await expectToThrow(ctx.withdraw({ salt: BN_1 }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + // Set the time to 26% of the stream duration + await ctx.timeTravelTo(Time.MID_26_PERCENT); + }); + + describe("given a null stream", () => { + it("should revert", async () => { + await expectToThrow(ctx.withdraw({ salt: ctx.salts.nonExisting }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("given a valid stream", () => { + describe("given an invalid deposited token mint", () => { + it("should revert", async () => { + await expectToThrow(ctx.withdraw({ depositedTokenMint: ctx.randomToken }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("given a valid deposited token mint", () => { + describe("when stream status is DEPLETED", () => { + it("should revert", async () => { + await ctx.timeTravelTo(Time.END); + await ctx.withdrawMax(); + await expectToThrow(ctx.withdraw(), "StreamDepleted"); + }); + }); + + describe("when stream status is not DEPLETED", () => { + describe("when zero withdraw amount", () => { + it("should revert", async () => { + await expectToThrow( + ctx.withdraw({ + withdrawAmount: ZERO, + }), + "WithdrawAmountZero", + ); + }); + }); + + describe("when non zero withdraw amount", () => { + describe("when withdraw amount overdraws", () => { + it("should revert", async () => { + await expectToThrow( + ctx.withdraw({ + withdrawAmount: Amount.WITHDRAW.add(BN_1), + }), + "Overdraw", + ); + }); + }); + + describe("when withdraw amount does not overdraw", () => { + describe("when withdrawal address not recipient", () => { + describe("when signer not recipient", () => { + it("should revert", async () => { + await expectToThrow( + ctx.withdraw({ + signer: ctx.sender.keys, + withdrawalRecipient: ctx.sender.keys.publicKey, + }), + CONSTRAINT_RAW, + ); + }); + }); + + describe("when recipient doesn't have an ATA for the Stream's asset", () => { + it("should create the ATA", async () => { + // Set up the sender for the test + await createATAAndFund( + ctx.banksClient, + ctx.defaultBankrunPayer, + ctx.randomToken, + Amount.DEPOSIT, + ProgramId.TOKEN, + ctx.sender.keys.publicKey, + ); + + // Create a new stream with a random token + const salt = await ctx.createWithTimestamps({ + depositAmount: Amount.DEPOSIT, + depositTokenMint: ctx.randomToken, + }); + + // Derive the recipient's ATA address + const recipientATA = deriveATAAddress( + ctx.randomToken, + ctx.recipient.keys.publicKey, + ProgramId.TOKEN, + ); + + // Assert that the recipient's ATA does not exist + await assertAccountNotExists(ctx, recipientATA, "Recipient's ATA"); + + // Perform the withdrawal + await ctx.withdraw({ + depositedTokenMint: ctx.randomToken, + salt, + }); + + // Assert that the recipient's ATA was created + await assertAccountExists(ctx, recipientATA, "Recipient's ATA"); + }); + }); + + describe("when recipient has an ATA for the Stream's asset", () => { + describe("when signer recipient", () => { + it("should make the withdrawal", async () => { + // Get the Lamports balance of the Treasury before the withdrawal + const treasuryLamportsBefore = await ctx.getTreasuryLamports(); + + // Get the withdrawal recipient's token balance before the withdrawal + const withdrawalRecipientATABalanceBefore = await getATABalance( + ctx.banksClient, + ctx.sender.usdcATA, + ); + + await ctx.withdraw({ + withdrawalRecipient: ctx.sender.keys.publicKey, + }); + + const expectedStreamData = ctx.defaultStream().data; + expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; + + await postWithdrawAssertions( + ctx, + ctx.salts.default, + treasuryLamportsBefore, + ctx.sender.usdcATA, + withdrawalRecipientATABalanceBefore, + expectedStreamData, + ); + }); + }); + }); + }); + + describe("when withdrawal address recipient", () => { + describe("when signer recipient", () => { + it("should make the withdrawal", async () => { + // Get the Lamports balance of the Treasury before the withdrawal + const treasuryLamportsBefore = await ctx.getTreasuryLamports(); + + // Get the withdrawal recipient's token balance before the withdrawal + const withdrawalRecipientATABalanceBefore = await getATABalance( + ctx.banksClient, + ctx.recipient.usdcATA, + ); + + await ctx.withdraw(); + + const expectedStreamData = ctx.defaultStream().data; + expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; + + await postWithdrawAssertions( + ctx, + ctx.salts.default, + treasuryLamportsBefore, + ctx.recipient.usdcATA, + withdrawalRecipientATABalanceBefore, + expectedStreamData, + ); + }); + }); + + describe("when signer not recipient", () => { + describe("when stream status is SETTLED", () => { + it("should make the withdrawal", async () => { + await ctx.timeTravelTo(Time.END); + + // Get the Lamports balance of the Treasury before the withdrawal + const treasuryLamportsBefore = await ctx.getTreasuryLamports(); + + // Get the withdrawal recipient's token balance before the withdrawal + const withdrawalRecipientATABalanceBefore = await getATABalance( + ctx.banksClient, + ctx.recipient.usdcATA, + ); + + await ctx.withdraw({ + signer: ctx.sender.keys, + withdrawAmount: Amount.DEPOSIT, + }); + + const expectedStreamData = ctx.defaultStream().data; + expectedStreamData.amounts.withdrawn = Amount.DEPOSIT; + expectedStreamData.isCancelable = false; + expectedStreamData.isDepleted = true; + + await postWithdrawAssertions( + ctx, + ctx.salts.default, + treasuryLamportsBefore, + ctx.recipient.usdcATA, + withdrawalRecipientATABalanceBefore, + expectedStreamData, + ); + }); + }); + + describe("when stream status is not SETTLED", () => { + describe("when stream status is CANCELED", () => { + it("should make the withdrawal", async () => { + await ctx.cancel(); + + // Get the Lamports balance of the Treasury before the withdrawal + const treasuryLamportsBefore = await ctx.getTreasuryLamports(); + + // Get the withdrawal recipient's token balance before the withdrawal + const withdrawalRecipientATABalanceBefore = await getATABalance( + ctx.banksClient, + ctx.recipient.usdcATA, + ); + + await ctx.withdraw({ signer: ctx.sender.keys }); + const expectedStreamData = ctx.defaultStream({ + isCancelable: false, + isDepleted: true, + wasCanceled: true, + }).data; + expectedStreamData.amounts.refunded = Amount.REFUND; + expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; + + await postWithdrawAssertions( + ctx, + ctx.salts.default, + treasuryLamportsBefore, + ctx.recipient.usdcATA, + withdrawalRecipientATABalanceBefore, + expectedStreamData, + ); + }); + }); + + describe("when stream status is STREAMING", () => { + describe("given token SPL standard", () => { + it("should make the withdrawal", async () => { + // Get the Lamports balance of the Treasury before the withdrawal + const treasuryLamportsBefore = await ctx.getTreasuryLamports(); + + // Get the withdrawal recipient's token balance before the withdrawal + const withdrawalRecipientATABalanceBefore = await getATABalance( + ctx.banksClient, + ctx.recipient.usdcATA, + ); + + await ctx.withdraw({ signer: ctx.sender.keys }); + const expectedStreamData = ctx.defaultStream().data; + expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; + await postWithdrawAssertions( + ctx, + ctx.salts.default, + treasuryLamportsBefore, + ctx.recipient.usdcATA, + withdrawalRecipientATABalanceBefore, + expectedStreamData, + ); + }); + }); + + describe("given token 2022 standard", () => { + it("should make the withdrawal", async () => { + const salt = await ctx.createWithTimestampsToken2022(); + + // Get the Lamports balance of the Treasury before the withdrawal + const treasuryLamportsBefore = await ctx.getTreasuryLamports(); + + // Get the withdrawal recipient's token balance before the withdrawal + const withdrawalRecipientATABalanceBefore = await getATABalance( + ctx.banksClient, + ctx.recipient.daiATA, + ); + + await ctx.withdrawToken2022(salt, ctx.sender.keys); + + const expectedStreamData = ctx.defaultStreamToken2022({ + salt: salt, + }).data; + expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; + await postWithdrawAssertions( + ctx, + salt, + treasuryLamportsBefore, + ctx.recipient.daiATA, + withdrawalRecipientATABalanceBefore, + expectedStreamData, + ); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); + +async function postWithdrawAssertions( + ctx: LockupTestContext, + salt: BN, + treasuryLamportsBefore: BN, + withdrawalRecipientATA: PublicKey, + withdrawalRecipientATABalanceBefore: BN, + expectedStreamData: StreamData, +) { + // Assert that the Stream state has been updated correctly + const actualStreamData = await ctx.fetchStreamData(salt); + assertEqStreamData(actualStreamData, expectedStreamData); + + // Get the Lamports balance of the Treasury after the withdrawal + const treasuryLamportsAfter = await ctx.getTreasuryLamports(); + + // Assert that the Treasury's balance has been credited with the withdrawal fee + assertEqualSOLBalance(treasuryLamportsAfter, treasuryLamportsBefore.add(Amount.WITHDRAW_FEE)); + + // Get the withdrawal recipient's token balance + const withdrawalRecipientTokenBalance = await getATABalance(ctx.banksClient, withdrawalRecipientATA); + + // Assert that the withdrawal recipient's token balance has been changed correctly + const expectedWithdrawnAmount = expectedStreamData.amounts.withdrawn; + assertEqualBn(withdrawalRecipientTokenBalance, withdrawalRecipientATABalanceBefore.add(expectedWithdrawnAmount)); + + // TODO: Assert that the StreamData ATA has been changed correctly +} diff --git a/tests/lockup/unit/withdraw.ts b/tests/lockup/unit/withdraw.ts deleted file mode 100644 index fb7c1c89..00000000 --- a/tests/lockup/unit/withdraw.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; -import { BN } from "@coral-xyz/anchor"; - -import { createATAAndFund } from "../../anchor-bankrun-adapter"; -import { - cancel, - createWithTimestampsToken2022, - defaultStream, - defaultStreamToken2022, - deriveATAAddress, - fetchStreamData, - getATABalance, - getTreasuryLamports, - salts, - sender, - setUp, - withdrawMax, - withdraw, - withdrawToken2022, - createWithTimestamps, -} from "../base"; -import { - accountExists, - banksClient, - defaultBankrunPayer, - randomToken, - recipient, - timeTravelTo, -} from "../../common-base"; -import { - assert, - assertErrorHexCode, - assertEqStreamDatas, - assertFail, -} from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; - -describe("withdraw", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - // Set the time to 26% of the stream duration - await timeTravelTo(defaults.PASS_26_PERCENT); - }); - - it("should revert", async () => { - try { - await withdraw({ salt: new BN(1) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - // Set the time to 26% of the stream duration - await timeTravelTo(defaults.PASS_26_PERCENT); - }); - - context("given a null stream", () => { - it("should revert", async () => { - try { - await withdraw({ salt: salts.nonExisting }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("given a valid stream", () => { - context("given an invalid deposited token mint", () => { - it("should revert", async () => { - try { - await withdraw({ depositedTokenMint: randomToken }); - - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("given a valid deposited token mint", () => { - context("when stream status is DEPLETED", () => { - it("should revert", async () => { - await timeTravelTo(defaults.END_TIME); - await withdrawMax(); - try { - await withdraw(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("StreamDepleted")); - } - }); - }); - - context("when stream status is not DEPLETED", () => { - context("when zero withdraw amount", () => { - it("should revert", async () => { - try { - await withdraw({ - withdrawAmount: defaults.ZERO_BN, - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("WithdrawAmountZero")); - } - }); - }); - - context("when non zero withdraw amount", () => { - context("when withdraw amount overdraws", () => { - it("should revert", async () => { - try { - await withdraw({ - withdrawAmount: defaults.WITHDRAW_AMOUNT.add(new BN(1)), - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("Overdraw")); - } - }); - }); - - context("when withdraw amount does not overdraw", () => { - context("when withdrawal address not recipient", () => { - context("when signer not recipient", () => { - it("should revert", async () => { - try { - await withdraw({ - signer: sender.keys, - withdrawalRecipient: sender.keys.publicKey, - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("ConstraintRaw")); - } - }); - }); - - context( - "when recipient doesn't have an ATA for the Stream's asset", - () => { - it("should create the ATA", async () => { - // Set up the sender for the test - await createATAAndFund( - banksClient, - defaultBankrunPayer, - randomToken, - defaults.DEPOSIT_AMOUNT.toNumber(), - defaults.TOKEN_PROGRAM_ID, - sender.keys.publicKey - ); - - // Create a new stream with a random token - const salt = await createWithTimestamps({ - depositTokenMint: randomToken, - depositAmount: defaults.DEPOSIT_AMOUNT, - }); - - // Derive the recipient's ATA address - const recipientATA = deriveATAAddress( - randomToken, - recipient.keys.publicKey, - defaults.TOKEN_PROGRAM_ID - ); - - // Assert that the recipient's ATA does not exist - assert( - !(await accountExists(recipientATA)), - "Recipient's ATA shouldn't exist before the withdrawal" - ); - - // Perform the withdrawal - await withdraw({ - salt, - depositedTokenMint: randomToken, - }); - - // Assert that the recipient's ATA was created - assert( - await accountExists(recipientATA), - "Recipient's ATA should exist after the withdrawal" - ); - }); - } - ); - - context( - "when recipient has an ATA for the Stream's asset", - () => { - context("when signer recipient", () => { - it("should make the withdrawal", async () => { - // Get the Lamports balance of the Treasury before the withdrawal - const treasuryLamportsBefore = - await getTreasuryLamports(); - - // Get the withdrawal recipient's token balance before the withdrawal - const withdrawalRecipientATABalanceBefore = - await getATABalance(banksClient, sender.usdcATA); - - await withdraw({ - withdrawalRecipient: sender.keys.publicKey, - }); - - const expectedStreamData = defaultStream().data; - expectedStreamData.amounts.withdrawn = - defaults.WITHDRAW_AMOUNT; - - await postWithdrawAssertions( - salts.default, - treasuryLamportsBefore, - sender.usdcATA, - withdrawalRecipientATABalanceBefore, - expectedStreamData - ); - }); - }); - } - ); - }); - - context("when withdrawal address recipient", () => { - context("when signer recipient", () => { - it("should make the withdrawal", async () => { - // Get the Lamports balance of the Treasury before the withdrawal - const treasuryLamportsBefore = await getTreasuryLamports(); - - // Get the withdrawal recipient's token balance before the withdrawal - const withdrawalRecipientATABalanceBefore = - await getATABalance(banksClient, recipient.usdcATA); - - await withdraw(); - - const expectedStreamData = defaultStream().data; - expectedStreamData.amounts.withdrawn = - defaults.WITHDRAW_AMOUNT; - - await postWithdrawAssertions( - salts.default, - treasuryLamportsBefore, - recipient.usdcATA, - withdrawalRecipientATABalanceBefore, - expectedStreamData - ); - }); - }); - - context("when signer not recipient", () => { - context("when stream status is SETTLED", () => { - it("should make the withdrawal", async () => { - await timeTravelTo(defaults.END_TIME); - - // Get the Lamports balance of the Treasury before the withdrawal - const treasuryLamportsBefore = - await getTreasuryLamports(); - - // Get the withdrawal recipient's token balance before the withdrawal - const withdrawalRecipientATABalanceBefore = - await getATABalance(banksClient, recipient.usdcATA); - - await withdraw({ - withdrawAmount: defaults.DEPOSIT_AMOUNT, - signer: sender.keys, - }); - - const expectedStreamData = defaultStream().data; - expectedStreamData.amounts.withdrawn = - defaults.DEPOSIT_AMOUNT; - expectedStreamData.isCancelable = false; - expectedStreamData.isDepleted = true; - - await postWithdrawAssertions( - salts.default, - treasuryLamportsBefore, - recipient.usdcATA, - withdrawalRecipientATABalanceBefore, - expectedStreamData - ); - }); - }); - - context("when stream status is not SETTLED", () => { - context("when stream status is CANCELED", () => { - it("should make the withdrawal", async () => { - await cancel(); - - // Get the Lamports balance of the Treasury before the withdrawal - const treasuryLamportsBefore = - await getTreasuryLamports(); - - // Get the withdrawal recipient's token balance before the withdrawal - const withdrawalRecipientATABalanceBefore = - await getATABalance(banksClient, recipient.usdcATA); - - await withdraw({ signer: sender.keys }); - const expectedStreamData = defaultStream({ - isCancelable: false, - isDepleted: true, - wasCanceled: true, - }).data; - expectedStreamData.amounts.refunded = - defaults.REFUND_AMOUNT; - expectedStreamData.amounts.withdrawn = - defaults.WITHDRAW_AMOUNT; - - await postWithdrawAssertions( - salts.default, - treasuryLamportsBefore, - recipient.usdcATA, - withdrawalRecipientATABalanceBefore, - expectedStreamData - ); - }); - }); - - context("when stream status is STREAMING", () => { - context("given token SPL standard", () => { - it("should make the withdrawal", async () => { - // Get the Lamports balance of the Treasury before the withdrawal - const treasuryLamportsBefore = - await getTreasuryLamports(); - - // Get the withdrawal recipient's token balance before the withdrawal - const withdrawalRecipientATABalanceBefore = - await getATABalance(banksClient, recipient.usdcATA); - - await withdraw({ signer: sender.keys }); - const expectedStreamData = defaultStream().data; - expectedStreamData.amounts.withdrawn = - defaults.WITHDRAW_AMOUNT; - await postWithdrawAssertions( - salts.default, - treasuryLamportsBefore, - recipient.usdcATA, - withdrawalRecipientATABalanceBefore, - expectedStreamData - ); - }); - }); - - context("given token 2022 standard", () => { - it("should make the withdrawal", async () => { - const salt = await createWithTimestampsToken2022(); - - // Get the Lamports balance of the Treasury before the withdrawal - const treasuryLamportsBefore = - await getTreasuryLamports(); - - // Get the withdrawal recipient's token balance before the withdrawal - const withdrawalRecipientATABalanceBefore = - await getATABalance(banksClient, recipient.daiATA); - - await withdrawToken2022(salt, sender.keys); - - const expectedStreamData = defaultStreamToken2022({ - salt: salt, - }).data; - expectedStreamData.amounts.withdrawn = - defaults.WITHDRAW_AMOUNT; - await postWithdrawAssertions( - salt, - treasuryLamportsBefore, - recipient.daiATA, - withdrawalRecipientATABalanceBefore, - expectedStreamData - ); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -async function postWithdrawAssertions( - salt: BN, - treasuryLamportsBefore: bigint, - withdrawalRecipientATA: PublicKey, - withdrawalRecipientATABalanceBefore: BN, - expectedStreamData: any -) { - // Assert that the Stream state has been updated correctly - const actualStreamData = await fetchStreamData(salt); - assertEqStreamDatas(actualStreamData, expectedStreamData); - - // Get the Lamports balance of the Treasury after the withdrawal - const treasuryLamportsAfter = await getTreasuryLamports(); - - // Assert that the Treasury's balance has been credited with the withdrawal fee - assert( - treasuryLamportsAfter === - treasuryLamportsBefore + BigInt(defaults.WITHDRAWAL_FEE_AMOUNT), - "The Treasury's Lamports balance hasn't been credited correctly" - ); - - // Get the withdrawal recipient's token balance - const withdrawalRecipientTokenBalance = await getATABalance( - banksClient, - withdrawalRecipientATA - ); - - // Assert that the withdrawal recipient's token balance has been changed correctly - const expectedWithdrawnAmount = expectedStreamData.amounts.withdrawn; - assert( - withdrawalRecipientTokenBalance.eq( - withdrawalRecipientATABalanceBefore.add(expectedWithdrawnAmount) - ), - "The amount withdrawn to the withdrawal recipient is incorrect" - ); - - // TODO: Assert that the StreamData ATA has been changed correctly -} diff --git a/tests/lockup/unit/withdrawMax.test.ts b/tests/lockup/unit/withdrawMax.test.ts new file mode 100644 index 00000000..03c64d43 --- /dev/null +++ b/tests/lockup/unit/withdrawMax.test.ts @@ -0,0 +1,61 @@ +import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { BN_1 } from "../../../lib/constants"; +import { LockupTestContext } from "../context"; +import { assertEqStreamData, expectToThrow } from "../utils/assertions"; +import { Amount, Time } from "../utils/defaults"; + +describe("withdrawMax", () => { + let ctx: LockupTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup({ initProgram: false }); + }); + + it("should revert", async () => { + await expectToThrow(ctx.withdrawMax({ salt: BN_1 }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new LockupTestContext(); + await ctx.setUpLockup(); + }); + + describe("given a null stream", () => { + it("should revert", async () => { + await expectToThrow(ctx.withdrawMax({ salt: ctx.salts.nonExisting }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("given a valid stream", () => { + describe("given end time not in the future", () => { + it("should make the max withdrawal", async () => { + await ctx.timeTravelTo(Time.END); + await ctx.withdrawMax(); + const actualStreamData = await ctx.fetchStreamData(); + const expectedStreamData = ctx.defaultStream({ + isCancelable: false, + isDepleted: true, + }).data; + expectedStreamData.amounts.withdrawn = Amount.DEPOSIT; + assertEqStreamData(actualStreamData, expectedStreamData); + }); + }); + + describe("given end time in the future", () => { + it("should make the max withdrawal", async () => { + await ctx.timeTravelTo(Time.MID_26_PERCENT); + await ctx.withdrawMax(); + const actualStreamData = await ctx.fetchStreamData(); + const expectedStreamData = ctx.defaultStream().data; + expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; + assertEqStreamData(actualStreamData, expectedStreamData); + }); + }); + }); + }); +}); diff --git a/tests/lockup/unit/withdrawMax.ts b/tests/lockup/unit/withdrawMax.ts deleted file mode 100644 index dc6a6bef..00000000 --- a/tests/lockup/unit/withdrawMax.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import { - defaultStream, - fetchStreamData, - salts, - setUp, - withdrawMax, -} from "../base"; -import { timeTravelTo } from "../../common-base"; -import { - assertEqStreamDatas, - assertErrorHexCode, - assertFail, -} from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; - -describe("withdrawMax", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp(false); - }); - - it("should revert", async () => { - try { - await withdrawMax({ salt: new BN(1) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("given a null stream", () => { - it("should revert", async () => { - try { - await withdrawMax({ salt: salts.nonExisting }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("given a valid stream", () => { - context("given end time not in future", () => { - it("should make the max withdrawal", async () => { - await timeTravelTo(defaults.END_TIME); - await withdrawMax(); - const actualStreamData = await fetchStreamData(); - const expectedStreamData = defaultStream({ - isCancelable: false, - isDepleted: true, - }).data; - expectedStreamData.amounts.withdrawn = defaults.DEPOSIT_AMOUNT; - assertEqStreamDatas(actualStreamData, expectedStreamData); - }); - }); - - context("given end time in future", () => { - it("should make the max withdrawal", async () => { - await timeTravelTo(defaults.PASS_26_PERCENT); - await withdrawMax(); - const actualStreamData = await fetchStreamData(); - const expectedStreamData = defaultStream().data; - expectedStreamData.amounts.withdrawn = - defaults.STREAMED_AMOUNT_26_PERCENT; - assertEqStreamDatas(actualStreamData, expectedStreamData); - }); - }); - }); - }); -}); diff --git a/tests/lockup/utils/assertions.ts b/tests/lockup/utils/assertions.ts index fe23fe25..5a3245f2 100644 --- a/tests/lockup/utils/assertions.ts +++ b/tests/lockup/utils/assertions.ts @@ -1,95 +1,54 @@ -import { assert } from "chai"; -export { assert }; +import { assert } from "vitest"; +import { ProgramErrorCode, type ProgramErrorName } from "../../../target/types/sablier_lockup_errors"; +import type { Amounts, StreamData, Timestamps } from "../../../target/types/sablier_lockup_structs"; +import { assertEqualBn, assertEqualPublicKey, expectToThrow as baseExpectToThrow } from "../../common/assertions"; +import type { UnlockAmounts } from "./types"; -import { Amounts, StreamData, Timestamps, UnlockAmounts } from "./types"; - -export function assertEqAmounts(a: Amounts, b: Amounts) { - assert( - a.cliffUnlock.eq(b.cliffUnlock), - `Cliff unlock amounts mismatch: ${a.cliffUnlock} !== ${b.cliffUnlock}` - ); - assert( - a.deposited.eq(b.deposited), - `Deposited amounts mismatch: ${a.deposited} !== ${b.deposited}` - ); - assert( - a.refunded.eq(b.refunded), - `Refunded amounts mismatch: ${a.refunded} !== ${b.refunded}` - ); - assert( - a.startUnlock.eq(b.startUnlock), - `Start unlock amounts mismatch: ${a.startUnlock} !== ${b.startUnlock}` - ); - assert( - a.withdrawn.eq(b.withdrawn), - `Withdrawn amounts mismatch: ${a.withdrawn} !== ${b.withdrawn}` - ); -} - -export function assertEqStreamDatas(a: StreamData, b: StreamData) { +export function assertEqStreamData(a: StreamData, b: StreamData) { assertEqAmounts(a.amounts, b.amounts); assertEqTimestamps(a.timestamps, b.timestamps); - assert( - a.depositedTokenMint.equals(b.depositedTokenMint), - `Asset mint addresses mismatch: ${a.depositedTokenMint.toBase58()} !== ${b.depositedTokenMint.toBase58()}` + assertEqualPublicKey( + a.depositedTokenMint, + b.depositedTokenMint, + `Asset mint addresses mismatch: ${a.depositedTokenMint.toBase58()} !== ${b.depositedTokenMint.toBase58()}`, ); - assert(a.salt.eq(b.salt), `Salt values mismatch: ${a.salt} !== ${b.salt}`); - assert( - a.isCancelable === b.isCancelable, - `Cancelable flag mismatch: ${a.isCancelable} !== ${b.isCancelable}` - ); - assert( - a.isDepleted === b.isDepleted, - `Depleted flag mismatch: ${a.isDepleted} !== ${b.isDepleted}` - ); - assert( - a.sender.equals(b.sender), - `Sender address mismatch: ${a.sender.toBase58()} !== ${b.sender.toBase58()}` - ); - assert( - a.wasCanceled === b.wasCanceled, - `Was canceled flag mismatch: ${a.wasCanceled} !== ${b.wasCanceled}` + assertEqualBn(a.salt, b.salt); + assert.equal(a.isCancelable, b.isCancelable); + assert.equal(a.isDepleted, b.isDepleted); + assertEqualPublicKey( + a.sender, + b.sender, + `Sender address mismatch: ${a.sender.toBase58()} !== ${b.sender.toBase58()}`, ); + assert.equal(a.wasCanceled, b.wasCanceled); } export function assertEqTimestamps(a: Timestamps, b: Timestamps) { - assert( - a.cliff.eq(b.cliff), - `Cliff timestamps mismatch: ${a.cliff} !== ${b.cliff}` - ); - assert(a.end.eq(b.end), `End timestamps mismatch: ${a.end} !== ${b.end}`); - assert( - a.start.eq(b.start), - `Start timestamps mismatch: ${a.start} !== ${b.start}` - ); + assertEqualBn(a.cliff, b.cliff); + assertEqualBn(a.end, b.end); + assertEqualBn(a.start, b.start); } export function assertEqUnlockAmounts(a: UnlockAmounts, b: UnlockAmounts) { - assert(a.cliff.eq(b.cliff), "Cliff unlock amounts mismatch"); - assert(a.start.eq(b.start), "Start unlock amounts mismatch"); + assertEqualBn(a.cliff, b.cliff); + assertEqualBn(a.start, b.start); } -export function assertErrorHexCode(error: unknown, hexErrorCode: string) { - assertErrorContains( - error, - `custom program error: ${hexErrorCode}`, - `The expected error code ${hexErrorCode} not found in "${error}"` - ); +export function expectToThrow(promise: Promise, errorNameOrCode: ProgramErrorName | number) { + return baseExpectToThrow(promise, ProgramErrorCode, errorNameOrCode); } -export function assertErrorContains( - error: unknown, - expectedText: string, - message?: string -) { - assert(errorToMessage(error).includes(expectedText), message); -} +/* -------------------------------------------------------------------------- */ +/* INTERNAL LOGIC */ +/* -------------------------------------------------------------------------- */ -export function assertFail() { - assert.fail("Expected the tx to revert, but it succeeded."); -} +function assertEqAmounts(a: Amounts, b: Amounts) { + assertEqualBn(a.deposited, b.deposited); + assertEqualBn(a.refunded, b.refunded); + assertEqualBn(a.withdrawn, b.withdrawn); -function errorToMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); + // Unlock amounts + assertEqualBn(a.cliffUnlock, b.cliffUnlock); + assertEqualBn(a.startUnlock, b.startUnlock); } diff --git a/tests/lockup/utils/calculations.ts b/tests/lockup/utils/calculations.ts index 6f605dd0..1832230f 100644 --- a/tests/lockup/utils/calculations.ts +++ b/tests/lockup/utils/calculations.ts @@ -1,17 +1,15 @@ -import { BN } from "@coral-xyz/anchor"; -import { Amounts, Timestamps } from "./types"; - -/// Replicates the logic of {lockup_math::get_streamed_amount} in the Solana program. -export function getStreamedAmount( - amounts: Amounts, - blockTimestamp: number | BN, - timestamps: Timestamps -): BN { - const now = - blockTimestamp instanceof BN ? blockTimestamp : new BN(blockTimestamp); +import type BN from "bn.js"; +import { SCALING_FACTOR, ZERO } from "../../../lib/constants"; +import type { Amounts, Timestamps } from "../../../target/types/sablier_lockup_structs"; +/** + * Replicates the logic of the `get_streamed_amount` function in the Solana program. + * This is unused at the moment, but we keep it because it will be used in the future when we add fuzzing. + * @see {@link file://./../../../programs/lockup/src/utils/lockup_math.rs} + */ +export function getStreamedAmount(amounts: Amounts, now: BN, timestamps: Timestamps): BN { if (timestamps.start.gt(now)) { - return new BN(0); + return ZERO; } if (timestamps.cliff.gt(now)) { return amounts.startUnlock; @@ -25,19 +23,14 @@ export function getStreamedAmount( return amounts.deposited; } - const streamingStartTime = amounts.cliffUnlock.isZero() - ? timestamps.start - : timestamps.cliff; + const streamingStartTime = amounts.cliffUnlock.isZero() ? timestamps.start : timestamps.cliff; - const SCALING_FACTOR = new BN("1000000000000000000"); // 1e18 const elapsedTime = now.sub(streamingStartTime).mul(SCALING_FACTOR); const streamableTimeRange = timestamps.end.sub(streamingStartTime); const streamedPercentage = elapsedTime.div(streamableTimeRange); const streamableAmount = amounts.deposited.sub(unlockAmountsSum); - const streamedPortion = streamedPercentage - .mul(streamableAmount) - .div(SCALING_FACTOR); + const streamedPortion = streamedPercentage.mul(streamableAmount).div(SCALING_FACTOR); const streamedAmount = unlockAmountsSum.add(streamedPortion); if (streamedAmount.gt(amounts.deposited)) { return amounts.deposited; diff --git a/tests/lockup/utils/defaults.ts b/tests/lockup/utils/defaults.ts index c1dc8658..75f27423 100644 --- a/tests/lockup/utils/defaults.ts +++ b/tests/lockup/utils/defaults.ts @@ -1,85 +1,82 @@ -import { BN } from "@coral-xyz/anchor"; -import { Amounts, PublicKey, Timestamps, UnlockAmounts } from "./types"; +import BN from "bn.js"; +import dayjs from "dayjs"; +import { ZERO } from "../../../lib/constants"; +import { sol, usdc } from "../../../lib/convertors"; +import type { Amounts, Timestamps } from "../../../target/types/sablier_lockup_structs"; +import type { UnlockAmounts } from "./types"; -/*////////////////////////////////////////////////////////////////////////// - CONSTANTS -//////////////////////////////////////////////////////////////////////////*/ -// Addresses -export const TOKEN_METADATA_PROGRAM_ID = new PublicKey( - "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" -); -export { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; +export namespace Amount { + export const CLIFF = usdc("2500.000001"); + export const DEPOSIT = usdc(10_000); + export const START = ZERO; + export const WITHDRAW_FEE = sol("0.01"); -// Amounts -export const DEPOSIT_AMOUNT = new BN(10_000e6); -export const STREAMED_AMOUNT_26_PERCENT = new BN(2600e6); -export const WITHDRAW_AMOUNT = STREAMED_AMOUNT_26_PERCENT; -export const REFUND_AMOUNT = DEPOSIT_AMOUNT.sub(WITHDRAW_AMOUNT); -export const CLIFF_AMOUNT = new BN(2500e6 + 1); -export const WITHDRAWAL_FEE_AMOUNT = 10_000_000; // 0.01 SOL -export const ZERO_BN = new BN(0); -export const START_AMOUNT = ZERO_BN; + export const WITHDRAW = usdc(2600); + export const REFUND = DEPOSIT.sub(WITHDRAW); +} -// Timestamps -export const APR_1_2025 = new BN(1_743_454_800); -export const START_TIME = APR_1_2025.add(new BN(1000)); -export const CLIFF_DURATION = new BN(2500); -export const CLIFF_TIME = START_TIME.add(CLIFF_DURATION); -export const TOTAL_DURATION = new BN(10_000); -export const END_TIME = START_TIME.add(TOTAL_DURATION); -export const PASS_26_PERCENT = START_TIME.add(new BN(2600)); +export namespace Seed { + export const EDITION = Buffer.from("edition"); + export const METADATA = Buffer.from("metadata"); + export const NFT_COLLECTION_DATA = Buffer.from("nft_collection_data"); + export const NFT_COLLECTION_MINT = Buffer.from("nft_collection_mint"); + export const STREAM_DATA = Buffer.from("stream_data"); + export const STREAM_NFT_MINT = Buffer.from("stream_nft_mint"); + export const TREASURY = Buffer.from("treasury"); +} -// Seeds -export const EDITION_SEED = "edition"; -export const FEE_COLLECTOR_DATA_SEED = "fee_collector_data"; -export const METADATA_SEED = "metadata"; -export const NFT_COLLECTION_DATA_SEED = "nft_collection_data"; -export const NFT_COLLECTION_MINT_SEED = "nft_collection_mint"; -export const STREAM_NFT_MINT_SEED = "stream_nft_mint"; -export const STREAM_DATA_SEED = "stream_data"; -export const TREASURY_SEED = "treasury"; +/** + * All timestamps and durations are in seconds. + */ +export namespace Time { + export const CLIFF_DURATION = new BN(2500); + export const GENESIS = new BN(dayjs().add(1, "day").unix()); // tomorrow + export const START = GENESIS.add(new BN(1000)); + export const TOTAL_DURATION = new BN(10_000); -// Miscellaneous -export const PROGRAM_NOT_INITIALIZED_ERR = { - CreateWithTimestamps: "NFT Collection Data account is undefined", -}; + export const CLIFF = START.add(CLIFF_DURATION); + export const END = START.add(TOTAL_DURATION); + export const MID_26_PERCENT = START.add(new BN(2600)); +} -/*////////////////////////////////////////////////////////////////////////// - PARAMETERS -//////////////////////////////////////////////////////////////////////////*/ +/** + * These are written as functions so that the fields can be updated in each test. + */ -export function amountsAfterCreate(): Amounts { +export function AMOUNTS({ + cliffUnlock = Amount.CLIFF, + deposited = Amount.DEPOSIT, + refunded = ZERO, + startUnlock = Amount.START, + withdrawn = ZERO, +}: Partial = {}): Amounts { return { - cliffUnlock: CLIFF_AMOUNT, - deposited: DEPOSIT_AMOUNT, - refunded: ZERO_BN, - startUnlock: START_AMOUNT, - withdrawn: ZERO_BN, + cliffUnlock, + deposited, + refunded, + startUnlock, + withdrawn, }; } -export function amountsAfterCreateWithZeroUnlocks(): Amounts { +export function TIMESTAMPS({ + cliff = Time.CLIFF, + end = Time.END, + start = Time.START, +}: Partial = {}): Timestamps { return { - cliffUnlock: ZERO_BN, - deposited: DEPOSIT_AMOUNT, - refunded: ZERO_BN, - startUnlock: ZERO_BN, - withdrawn: ZERO_BN, + cliff, + end, + start, }; } -export function timestamps(): Timestamps { +export function UNLOCK_AMOUNTS({ + cliff = Amount.CLIFF, + start = Amount.START, +}: Partial = {}): UnlockAmounts { return { - start: START_TIME, - cliff: CLIFF_TIME, - end: END_TIME, + cliff, + start, }; } - -export function unlockAmounts(): UnlockAmounts { - return { cliff: CLIFF_AMOUNT, start: START_AMOUNT }; -} - -export function unlockAmountsZero(): UnlockAmounts { - return { cliff: ZERO_BN, start: ZERO_BN }; -} diff --git a/tests/lockup/utils/errors.ts b/tests/lockup/utils/errors.ts deleted file mode 100644 index 5c8f9c5f..00000000 --- a/tests/lockup/utils/errors.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - getErrorCode as getErrorCodeBase, - getErrorName as getErrorNameBase, -} from "../../common-base"; - -const ErrorCode = { - // Cancel Stream - StreamCanceled: 0x1770, - StreamIsNotCancelable: 0x1771, - StreamSettled: 0x1772, - - // Collect Fees - CantCollectZeroFees: 0x1773, - - // Create Stream - CliffTimeZeroUnlockAmountNotZero: 0x1774, - CliffTimeNotLessThanEndTime: 0x1775, - DepositAmountZero: 0x1776, - StartTimeNotPositive: 0x1777, - StartTimeNotLessThanCliffTime: 0x1778, - StartTimeNotLessThanEndTime: 0x1779, - UnlockAmountsSumTooHigh: 0x177a, - - // Renounce - StreamAlreadyNonCancelable: 0x177b, - - // Withdraw - Overdraw: 0x177c, - WithdrawAmountZero: 0x177d, - - // Common - StreamDepleted: 0x177e, - - // Anchor Errors - AccountNotInitialized: 0xbc4, - ConstraintAddress: 0x7dc, - ConstraintRaw: 0x7d3, -} as const; -type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode]; - -/** - * Get the hex code for a given error name - * @param errorName The name of the error - * @returns The hex code for the error - */ -export function getErrorCode(errorName: string): string { - return getErrorCodeBase(ErrorCode, errorName); -} - -/** - * Get the error name for a given hex code - * @param hexCode The hex code of the error (e.g., "0x177e") - * @returns The error name - */ -export function getErrorName(hexCode: string): string { - return getErrorNameBase(ErrorCode, hexCode); -} diff --git a/tests/lockup/utils/types.ts b/tests/lockup/utils/types.ts index 42700414..0d871afb 100644 --- a/tests/lockup/utils/types.ts +++ b/tests/lockup/utils/types.ts @@ -1,42 +1,22 @@ -import { BN } from "@coral-xyz/anchor"; -import { PublicKey } from "@solana/web3.js"; -export { PublicKey } from "@solana/web3.js"; +/** + * @file Note that this is only a type helper and is not the actual IDL. The original + * IDL types can be found at @see {@link file://./../../../target/types/sablier_lockup_structs.ts}. + */ -export interface Amounts { - cliffUnlock: BN; - deposited: BN; - refunded: BN; - startUnlock: BN; - withdrawn: BN; -} +import { type PublicKey } from "@solana/web3.js"; +import type BN from "bn.js"; +import type { StreamData } from "../../../target/types/sablier_lockup_structs"; -export interface Timestamps { - cliff: BN; - end: BN; - start: BN; -} - -export interface StreamData { - amounts: Amounts; - depositedTokenMint: PublicKey; - salt: BN; - isCancelable: boolean; - isDepleted: boolean; - timestamps: Timestamps; - sender: PublicKey; - wasCanceled: boolean; -} - -export interface Salts { - // Default stream salt. +export type Salts = { + /* Default stream salt. */ default: BN; - // A non-cancelable stream salt. + /* The salt of a non-cancelable stream. */ nonCancelable: BN; - // A stream salt that does not exist. + /* The salt of a stream that does not exist. */ nonExisting: BN; -} +}; -export interface Stream { +export type Stream = { data: StreamData; dataAddress: PublicKey; dataAta: PublicKey; @@ -44,9 +24,9 @@ export interface Stream { nftMetadataAddress: PublicKey; nftMintAddress: PublicKey; recipientStreamNftAta: PublicKey; -} +}; -export interface UnlockAmounts { +export type UnlockAmounts = { cliff: BN; start: BN; -} +}; diff --git a/tests/merkle-instant/context.ts b/tests/merkle-instant/context.ts new file mode 100644 index 00000000..a015ef62 --- /dev/null +++ b/tests/merkle-instant/context.ts @@ -0,0 +1,243 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; +import type BN from "bn.js"; +import { ProgramId, ZERO } from "../../lib/constants"; +import { ProgramName } from "../../lib/enums"; +import { getPDAAddress } from "../../lib/helpers"; +import IDL from "../../target/idl/sablier_merkle_instant.json"; +import { type SablierMerkleInstant as SablierMerkleInstantProgram } from "../../target/types/sablier_merkle_instant"; +import type { Campaign as CampaignData } from "../../target/types/sablier_merkle_instant_structs"; +import { buildSignAndProcessTx, deriveATAAddress, transfer } from "../common/anchor-bankrun"; +import { TestContext } from "../common/context"; +import type { User } from "../common/types"; +import { Amount, Campaign, Seed, Time } from "./utils/defaults"; +import { getProof, getRoot, type LeafData } from "./utils/merkle"; + +export class MerkleInstantTestContext extends TestContext { + // Programs and addresses + public merkleInstant!: anchor.Program; + public treasuryAddress!: PublicKey; + + // Users + public campaignCreator!: User; + + // Campaigns + public defaultCampaign!: PublicKey; + public defaultCampaignToken2022!: PublicKey; + + /** For the recipient declared in the base TestContext */ + public readonly defaultIndex = 0; + private defaultMerkleProof!: number[][]; + + // Merkle Tree + private leaves!: LeafData[]; + private merkleRoot!: number[]; + private recipient1!: PublicKey; + private recipient2!: PublicKey; + private recipient3!: PublicKey; + + async setUpMerkleInstant({ initProgram = true } = {}): Promise { + // Call parent setup with merkle-instant specific programs + await super.setUp(ProgramName.MerkleInstant, new PublicKey(IDL.address)); + + // Deploy the program being tested + this.merkleInstant = new anchor.Program(IDL, this.bankrunProvider); + + // Create the Campaign Creator user + this.campaignCreator = await this.createUser(); + + // Pre-calculate the address of the Treasury + this.treasuryAddress = getPDAAddress([Seed.TREASURY], this.merkleInstant.programId); + + // Create the recipients to be included in the Merkle Tree + this.recipient1 = (await this.createUser()).keys.publicKey; + this.recipient2 = (await this.createUser()).keys.publicKey; + this.recipient3 = (await this.createUser()).keys.publicKey; + + // Declare the leaves of the Merkle Tree before hashing and sorting + this.leaves = [ + { + amount: Amount.CLAIM, + index: this.defaultIndex, + // Use the default recipient's public key for the first leaf + recipient: this.recipient.keys.publicKey, + }, + { amount: Amount.CLAIM, index: 1, recipient: this.recipient1 }, + { amount: Amount.CLAIM, index: 2, recipient: this.recipient2 }, + { amount: Amount.CLAIM, index: 3, recipient: this.recipient3 }, + ]; + + this.merkleRoot = getRoot(this.leaves); + this.defaultMerkleProof = getProof(this.leaves, this.leaves[0]); + + // Set the block time to the genesis time. + await this.timeTravelTo(Time.GENESIS); + + if (initProgram) { + // Initialize the Merkle Instant program + await this.initializeMerkleInstant(); + + // Create the default campaigns + this.defaultCampaign = await this.createCampaign(); + this.defaultCampaignToken2022 = await this.createCampaign({ + airdropTokenMint: this.dai, + airdropTokenProgram: ProgramId.TOKEN_2022, + }); + } + } + + /*////////////////////////////////////////////////////////////////////////// + TX-IX + //////////////////////////////////////////////////////////////////////////*/ + + async claim({ + campaign = this.defaultCampaign, + claimerKeys = this.recipient.keys, + amount = Amount.CLAIM, + recipientAddress = this.recipient.keys.publicKey, + airdropTokenMint = this.usdc, + airdropTokenProgram = ProgramId.TOKEN, + } = {}): Promise { + const txIx = await this.merkleInstant.methods + .claim(this.defaultIndex, amount, this.defaultMerkleProof) + .accounts({ + airdropTokenMint, + airdropTokenProgram, + campaign: campaign, + claimer: claimerKeys.publicKey, + recipient: recipientAddress, + }) + .instruction(); + + // Build and sign the transaction + await buildSignAndProcessTx(this.banksClient, txIx, claimerKeys); + } + + async clawback({ + signer = this.campaignCreator.keys, + campaign = this.defaultCampaign, + amount = Amount.CLAWBACK, + airdropTokenMint = this.usdc, + airdropTokenProgram = ProgramId.TOKEN, + } = {}): Promise { + const txIx = await this.merkleInstant.methods + .clawback(amount) + .accounts({ + airdropTokenMint, + airdropTokenProgram, + campaign, + campaignCreator: signer.publicKey, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, txIx, signer); + } + + async collectFees({ + signer = this.feeCollector.keys, + feeRecipient = this.recipient.keys.publicKey, + } = {}): Promise { + const txIx = await this.merkleInstant.methods + .collectFees() + .accounts({ + feeCollector: signer.publicKey, + feeRecipient, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, txIx, signer); + } + + async createCampaign({ + creator = this.campaignCreator, + name = Campaign.CAMPAIGN_NAME, + campaignFunder = this.campaignCreator.keys, + airdropTokenMint = this.usdc, + airdropTokenProgram = ProgramId.TOKEN, + } = {}): Promise { + // Derive the address of the campaign + const campaign = getPDAAddress( + [ + Seed.CAMPAIGN, + creator.keys.publicKey.toBuffer(), + Buffer.from(this.merkleRoot), + Campaign.EXPIRATION.toArrayLike(Buffer, "le", 8), + Buffer.from(name), + airdropTokenMint.toBuffer(), + ], + this.merkleInstant.programId, + ); + + const txIx = await this.merkleInstant.methods + .createCampaign( + this.merkleRoot, + Campaign.EXPIRATION, + name, + Campaign.IPFS_CID, + Amount.AGGREGATE, + this.leaves.length, + ) + .accounts({ + airdropTokenMint, + airdropTokenProgram, + creator: creator.keys.publicKey, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, txIx, creator.keys); + + const campaignAta = deriveATAAddress(airdropTokenMint, campaign, airdropTokenProgram); + + const campaignFunderAta = deriveATAAddress(airdropTokenMint, campaignFunder.publicKey, airdropTokenProgram); + + // Transfer the aggregate amount from the campaign funder to the campaign + await transfer( + this.banksClient, + campaignFunder, + campaignFunderAta, + campaignAta, + campaignFunder.publicKey, + Amount.AGGREGATE, + [], + airdropTokenProgram, + ); + + return campaign; + } + + async initializeMerkleInstant(): Promise { + const initializeIx = await this.merkleInstant.methods + .initialize(this.feeCollector.keys.publicKey) + .accounts({ + initializer: this.campaignCreator.keys.publicKey, + }) + .instruction(); + + await buildSignAndProcessTx(this.banksClient, initializeIx, this.campaignCreator.keys); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + defaultCampaignData(): CampaignData { + return { + airdropTokenMint: this.usdc, + bump: 0, + creator: this.campaignCreator.keys.publicKey, + expirationTime: Campaign.EXPIRATION, + firstClaimTime: ZERO, + ipfsCid: Campaign.IPFS_CID, + merkleRoot: Array.from(this.merkleRoot), + name: Campaign.CAMPAIGN_NAME, + }; + } + + async getTreasuryLamports(): Promise { + return await this.getLamportsOf(this.treasuryAddress); + } + + async fetchCampaignData(campaign = this.defaultCampaign): Promise { + return await this.merkleInstant.account.campaign.fetch(campaign); + } +} diff --git a/tests/merkle-instant/unit/claim.test.ts b/tests/merkle-instant/unit/claim.test.ts new file mode 100644 index 00000000..c096c5b0 --- /dev/null +++ b/tests/merkle-instant/unit/claim.test.ts @@ -0,0 +1,225 @@ +import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED } from "@coral-xyz/anchor-errors"; +import { PublicKey } from "@solana/web3.js"; +import { assert, beforeAll, beforeEach, describe, it } from "vitest"; +import { BN_1, ProgramId, ZERO } from "../../../lib/constants"; +import { sleepFor } from "../../../lib/helpers"; +import { createATAAndFund, getATABalanceMint } from "../../common/anchor-bankrun"; +import { assertEqualBn, assertEqualSOLBalance, assertLteBn, assertZeroBn } from "../../common/assertions"; +import { MerkleInstantTestContext } from "../context"; +import { expectToThrow } from "../utils/assertions"; +import { Amount, Campaign, Time } from "../utils/defaults"; + +describe("claim", () => { + let ctx: MerkleInstantTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant({ + initProgram: false, + }); + }); + + describe("when the campaign doesn't exist", () => { + it("should revert", async () => { + // Passing a non-Campaign account since no Campaigns exist yet + await expectToThrow(ctx.claim({ campaign: new PublicKey(12345) }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the campaign exists", () => { + it("should revert", async () => { + const campaign = await ctx.createCampaign({ name: "Test Campaign" }); + await expectToThrow(ctx.claim({ campaign: campaign }), ACCOUNT_NOT_INITIALIZED); + }); + }); + }); + + describe("when the program is initialized", () => { + describe("when the campaign doesn't exist", () => { + it("should revert", async () => { + // Claim from a non-existent Campaign + await expectToThrow(ctx.claim({ campaign: new PublicKey(12345) }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the campaign exists", () => { + beforeEach(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant(); + }); + + describe("when the token mint is invalid", () => { + it("should revert", async () => { + // Claim from the Campaign with an invalid token mint + await expectToThrow(ctx.claim({ airdropTokenMint: ctx.dai }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the token mint is valid", () => { + describe("when the airdrop has already been claimed", () => { + it("should revert", async () => { + await ctx.claim(); + await sleepFor(7); + + // Claim from the Campaign again + await expectToThrow(ctx.claim(), 0x0); + }); + }); + + describe("when the airdrop has not been claimed", () => { + describe("when the merkle proof is invalid", () => { + it("should revert", async () => { + await expectToThrow( + ctx.claim({ + amount: Amount.CLAIM.sub(BN_1), + }), + "InvalidMerkleProof", + ); + }); + }); + + describe("when the merkle proof is valid", () => { + describe("when the campaign expired", () => { + it("should revert", async () => { + // Time travel to when the campaign has expired + await ctx.timeTravelTo(Campaign.EXPIRATION); + await expectToThrow(ctx.claim(), "CampaignExpired"); + }); + }); + + describe("when the campaign has not expired", () => { + describe("when the recipient doesn't have an ATA for the token", () => { + it("should claim the airdrop", async () => { + // Mint the random token to the campaign creator + await createATAAndFund( + ctx.banksClient, + ctx.defaultBankrunPayer, + ctx.randomToken, + Amount.AGGREGATE, + ProgramId.TOKEN, + ctx.campaignCreator.keys.publicKey, + ); + + // Create a Campaign with the random token + const campaign = await ctx.createCampaign({ + airdropTokenMint: ctx.randomToken, + }); + + // Test the campaign + await testClaim(ctx, campaign, ctx.recipient.keys, ctx.randomToken, ProgramId.TOKEN, false); + }); + }); + + describe("when the recipient has an ATA for the token", () => { + describe("when the claimer is not the recipient", () => { + it("should claim the airdrop", async () => { + // Test the claim. + await testClaim(ctx, ctx.defaultCampaign, ctx.campaignCreator.keys); + }); + }); + + describe("when the claimer is the recipient", () => { + describe("given token SPL standard", () => { + it("should claim the airdrop", async () => { + // Claim from the Campaign + await testClaim(ctx); + }); + }); + + describe("given token 2022 standard", () => { + it("should claim the airdrop", async () => { + // Test the claim. + await testClaim( + ctx, + ctx.defaultCampaignToken2022, + ctx.recipient.keys, + ctx.dai, + ProgramId.TOKEN_2022, + ); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); + +/// Common test function to test the claim functionality +async function testClaim( + ctx: MerkleInstantTestContext, + campaign = ctx.defaultCampaign, + claimer = ctx.recipient.keys, + tokenMint = ctx.usdc, + tokenProgram = ProgramId.TOKEN, + recipientAtaExists = true, +): Promise { + // Assert that the claim was not made yet. + assert.isFalse(await hasClaimed(ctx)); + + // Get the Campaign's data before claiming + const campaignDataBefore = await ctx.fetchCampaignData(campaign); + + // Assert that the Campaign's firstClaimTime is zero before claiming + assertZeroBn(campaignDataBefore.firstClaimTime); + + // Get the campaign and recipient ATA balances before claiming. + const campaignAtaBalanceBefore = await getATABalanceMint(ctx.banksClient, campaign, tokenMint); + const recipientAtaBalanceBefore = recipientAtaExists + ? await getATABalanceMint(ctx.banksClient, ctx.recipient.keys.publicKey, tokenMint) + : ZERO; + + // Get the claimer and treasury lamports balance before claiming + const claimerLamportsBefore = await ctx.getLamportsOf(claimer.publicKey); + const treasuryLamportsBefore = await ctx.getLamportsOf(ctx.treasuryAddress); + + // Claim from the Campaign + await ctx.claim({ + airdropTokenMint: tokenMint, + airdropTokenProgram: tokenProgram, + campaign: campaign, + claimerKeys: claimer, + }); + + const campaignDataAfter = await ctx.fetchCampaignData(campaign); + assertEqualBn(campaignDataAfter.firstClaimTime, Time.GENESIS); + + // Assert that the claim has been made + assert.isTrue(await hasClaimed(ctx, campaign)); + + const campaignAtaBalanceAfter = await getATABalanceMint(ctx.banksClient, campaign, tokenMint); + + // Assert that the Campaign's ATA balance decreased by the claim amount + assertEqualBn(campaignAtaBalanceAfter, campaignAtaBalanceBefore.sub(Amount.CLAIM)); + + const recipientAtaBalanceAfter = await getATABalanceMint(ctx.banksClient, ctx.recipient.keys.publicKey, tokenMint); + + // Assert that the recipient's ATA balance increased by the claim amount + assertEqualBn(recipientAtaBalanceAfter, recipientAtaBalanceBefore.add(Amount.CLAIM)); + + const claimerLamportsAfter = await ctx.getLamportsOf(claimer.publicKey); + + // Assert that the claimer's lamports balance has changed at least by the claim fee amount. + // We use `<=` because we don't know in advance the gas cost. + assertLteBn(claimerLamportsAfter, claimerLamportsBefore.sub(Amount.CLAIM_FEE)); + + const treasuryLamportsAfter = await ctx.getLamportsOf(ctx.treasuryAddress); + + // Assert that the treasury's balance has increased by the claim fee amount + assertEqualSOLBalance(treasuryLamportsAfter, treasuryLamportsBefore.add(Amount.CLAIM_FEE)); +} + +// Implicitly tests the `has_claimed` Ix works. +async function hasClaimed(ctx: MerkleInstantTestContext, campaign = ctx.defaultCampaign): Promise { + return await ctx.merkleInstant.methods + .hasClaimed(ctx.defaultIndex) + .accounts({ + campaign: campaign, + }) + .signers([ctx.defaultBankrunPayer]) + .view(); +} diff --git a/tests/merkle-instant/unit/clawback.test.ts b/tests/merkle-instant/unit/clawback.test.ts new file mode 100644 index 00000000..93eb1e15 --- /dev/null +++ b/tests/merkle-instant/unit/clawback.test.ts @@ -0,0 +1,197 @@ +import { + ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, + ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, +} from "@coral-xyz/anchor-errors"; +import { PublicKey } from "@solana/web3.js"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { ProgramId, ZERO } from "../../../lib/constants"; +import { createATAAndFund, deriveATAAddress, getATABalanceMint } from "../../common/anchor-bankrun"; +import { assertAccountExists, assertAccountNotExists, assertEqualBn } from "../../common/assertions"; +import { MerkleInstantTestContext } from "../context"; +import { expectToThrow } from "../utils/assertions"; +import { Amount, Campaign } from "../utils/defaults"; + +describe("clawback", () => { + let ctx: MerkleInstantTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant({ + initProgram: false, + }); + }); + + it("should revert", async () => { + // Passing a non-Campaign account since no Campaigns exist yet + await expectToThrow(ctx.clawback({ campaign: new PublicKey(12345) }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant(); + }); + + describe("when the passed campaign account is invalid", () => { + it("should revert", async () => { + await expectToThrow(ctx.clawback({ campaign: new PublicKey(12345) }), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the passed campaign account is valid", () => { + describe("when the passed mint is invalid", () => { + it("should revert", async () => { + await expectToThrow( + ctx.clawback({ + airdropTokenMint: ctx.dai, + }), + ACCOUNT_NOT_INITIALIZED, + ); + }); + }); + + describe("when the passed mint is valid", () => { + describe("when the signer is not the campaign creator", () => { + it("should revert", async () => { + await expectToThrow( + ctx.clawback({ + signer: ctx.eve.keys, + }), + CONSTRAINT_ADDRESS, + ); + }); + }); + + describe("when the signer is the campaign creator", () => { + describe("when first claim not made", () => { + it("should clawback", async () => { + await testClawback(ctx); + }); + }); + + describe("when first claim made", () => { + beforeEach(async () => { + await ctx.claim(); + }); + + describe("given grace period not passed", () => { + it("should clawback", async () => { + await testClawback(ctx); + }); + }); + + describe("given grace period passed", () => { + beforeEach(async () => { + // Time travel to the end of the grace period + await ctx.timeTravelTo(Campaign.POST_GRACE_PERIOD); + }); + + describe("given campaign not expired", () => { + it("should revert", async () => { + await expectToThrow(ctx.clawback(), "ClawbackNotAllowed"); + }); + }); + + describe("given campaign expired", () => { + beforeEach(async () => { + // Time travel to the end of the campaign + await ctx.timeTravelTo(Campaign.EXPIRATION); + }); + + describe("when campaign creator does not have ATA", () => { + it("should clawback", async () => { + await createATAAndFund( + ctx.banksClient, + ctx.defaultBankrunPayer, + ctx.randomToken, + Amount.AGGREGATE, + ProgramId.TOKEN, + ctx.recipient.keys.publicKey, + ); + + const campaign = await ctx.createCampaign({ + airdropTokenMint: ctx.randomToken, + campaignFunder: ctx.recipient.keys, + }); + + const campaignCreatorAta = deriveATAAddress( + ctx.randomToken, + ctx.campaignCreator.keys.publicKey, + ProgramId.TOKEN, + ); + await assertAccountNotExists(ctx, campaignCreatorAta, "Campaign Creator's ATA"); + + // Claim from the Campaign + await testClawback(ctx, { + airdropTokenMint: ctx.randomToken, + campaign: campaign, + campaignCreatorAtaExists: false, + }); + + await assertAccountExists(ctx, campaignCreatorAta, "Campaign Creator's ATA"); + }); + }); + + describe("given token SPL standard", () => { + it("should clawback", async () => { + // Claim from the Campaign + await testClawback(ctx); + }); + }); + + describe("given token 2022 standard", () => { + it("should clawback", async () => { + // Test the claim. + await testClawback(ctx, { + airdropTokenMint: ctx.dai, + airdropTokenProgram: ProgramId.TOKEN_2022, + campaign: ctx.defaultCampaignToken2022, + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); + +async function testClawback( + ctx: MerkleInstantTestContext, + { + campaign = ctx.defaultCampaign, + airdropTokenMint = ctx.usdc, + airdropTokenProgram = ProgramId.TOKEN, + campaignCreatorAtaExists = true, + } = {}, +) { + const campaignAtaBalanceBefore = await getATABalanceMint(ctx.banksClient, campaign, airdropTokenMint); + const creatorAtaBalanceBefore = campaignCreatorAtaExists + ? await getATABalanceMint(ctx.banksClient, ctx.campaignCreator.keys.publicKey, airdropTokenMint) + : ZERO; + + await ctx.clawback({ + airdropTokenMint, + airdropTokenProgram, + amount: Amount.CLAWBACK, + campaign, + }); + + const campaignAtaBalanceAfter = await getATABalanceMint(ctx.banksClient, campaign, airdropTokenMint); + + // Assert that the campaign token balance has decreased as expected + assertEqualBn(campaignAtaBalanceBefore, campaignAtaBalanceAfter.add(Amount.CLAWBACK)); + + const creatorAtaBalanceAfter = await getATABalanceMint( + ctx.banksClient, + ctx.campaignCreator.keys.publicKey, + airdropTokenMint, + ); + + // Assert that the campaign creator's token balance has increased as expected + assertEqualBn(creatorAtaBalanceBefore, creatorAtaBalanceAfter.sub(Amount.CLAWBACK)); +} diff --git a/tests/merkle-instant/unit/collectFees.test.ts b/tests/merkle-instant/unit/collectFees.test.ts new file mode 100644 index 00000000..69aa96af --- /dev/null +++ b/tests/merkle-instant/unit/collectFees.test.ts @@ -0,0 +1,81 @@ +import { + ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, + ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, +} from "@coral-xyz/anchor-errors"; +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { REDUNDANCY_BUFFER } from "../../../lib/constants"; +import { assertEqualSOLBalance } from "../../common/assertions"; +import { MerkleInstantTestContext } from "../context"; +import { expectToThrow } from "../utils/assertions"; +import { Amount } from "../utils/defaults"; + +describe("collectFees", () => { + let ctx: MerkleInstantTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant({ + initProgram: false, + }); + }); + + it("should revert", async () => { + await expectToThrow(ctx.collectFees(), ACCOUNT_NOT_INITIALIZED); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant(); + }); + + describe("when signer is not the authorized fee collector", () => { + it("should revert", async () => { + // Perform a claim, generating fees + await ctx.claim(); + + await expectToThrow(ctx.collectFees({ signer: ctx.eve.keys }), CONSTRAINT_ADDRESS); + }); + }); + + describe("when signer is the authorized fee collector", () => { + describe("given no fees accumulated", () => { + it("should revert", async () => { + await expectToThrow(ctx.collectFees(), "CantCollectZeroFees"); + }); + }); + + describe("given accumulated fees", () => { + it("should collect the fees", async () => { + // Perform a claim, generating fees + await ctx.claim({ claimerKeys: ctx.recipient.keys }); + + const beforeLamports = { + feeRecipient: await getFeeRecipientLamports(ctx), + treasury: await ctx.getTreasuryLamports(), + }; + + // Collect the fees + await ctx.collectFees(); + + const afterLamports = { + feeRecipient: await getFeeRecipientLamports(ctx), + treasury: await ctx.getTreasuryLamports(), + }; + + // 1 claim worth of fees minus the minimum lamports balance (a buffer on top of the redundancy buffer). + const expectedFeesCollected = Amount.CLAIM_FEE.sub(REDUNDANCY_BUFFER); + + assertEqualSOLBalance(afterLamports.treasury, beforeLamports.treasury.sub(expectedFeesCollected)); + assertEqualSOLBalance(afterLamports.feeRecipient, beforeLamports.feeRecipient.add(expectedFeesCollected)); + }); + }); + }); + }); +}); + +async function getFeeRecipientLamports(ctx: MerkleInstantTestContext) { + return await ctx.getLamportsOf(ctx.recipient.keys.publicKey); +} diff --git a/tests/merkle-instant/unit/createCampaign.test.ts b/tests/merkle-instant/unit/createCampaign.test.ts new file mode 100644 index 00000000..37b55b34 --- /dev/null +++ b/tests/merkle-instant/unit/createCampaign.test.ts @@ -0,0 +1,51 @@ +import { beforeAll, beforeEach, describe, it } from "vitest"; +import { MerkleInstantTestContext } from "../context"; +import { assertEqCampaignData, expectToThrow } from "../utils/assertions"; + +describe("createCampaign", () => { + let ctx: MerkleInstantTestContext; + + describe("when the program is not initialized", () => { + beforeAll(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant({ + initProgram: false, + }); + }); + + it("should create the campaign", async () => { + await testCreateCampaign(ctx); + }); + }); + + describe("when the program is initialized", () => { + beforeEach(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant(); + }); + + describe("when the campaign already exists", () => { + it("should revert", async () => { + await expectToThrow(ctx.createCampaign(), 0x0); + }); + }); + + describe("when the campaign does not exist", () => { + it("should create the campaign", async () => { + await testCreateCampaign(ctx); + }); + }); + }); +}); + +async function testCreateCampaign(ctx: MerkleInstantTestContext) { + const name = "Test Campaign"; + const campaign = await ctx.createCampaign({ name: name }); + // Assert that the campaign was created successfully + const expectedCampaignData = { + ...ctx.defaultCampaignData(), + name: name, + }; + const actualCampaignData = await ctx.fetchCampaignData(campaign); + assertEqCampaignData(actualCampaignData, expectedCampaignData); +} diff --git a/tests/merkle-instant/unit/initialize.test.ts b/tests/merkle-instant/unit/initialize.test.ts new file mode 100644 index 00000000..032ad3ab --- /dev/null +++ b/tests/merkle-instant/unit/initialize.test.ts @@ -0,0 +1,33 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { sleepFor } from "../../../lib/helpers"; +import { assertAccountExists } from "../../common/assertions"; +import { MerkleInstantTestContext } from "../context"; + +describe("initialize", () => { + let ctx: MerkleInstantTestContext; + + beforeEach(async () => { + ctx = new MerkleInstantTestContext(); + await ctx.setUpMerkleInstant({ + initProgram: false, + }); + }); + + describe("given initialized", () => { + it("should revert", async () => { + await ctx.initializeMerkleInstant(); + await sleepFor(7); + await expect(ctx.initializeMerkleInstant(), "Tx succeeded when it should have reverted").rejects.toThrow( + "Instruction 1: custom program error: 0x0", + ); + }); + }); + + describe("given not initialized", () => { + it("should initialize the program", async () => { + await ctx.initializeMerkleInstant(); + + await assertAccountExists(ctx, ctx.treasuryAddress, "Treasury"); + }); + }); +}); diff --git a/tests/merkle-instant/utils/assertions.ts b/tests/merkle-instant/utils/assertions.ts new file mode 100644 index 00000000..27ca97ae --- /dev/null +++ b/tests/merkle-instant/utils/assertions.ts @@ -0,0 +1,19 @@ +import { assert } from "vitest"; +import { ProgramErrorCode, type ProgramErrorName } from "../../../target/types/sablier_merkle_instant_errors"; +import type { Campaign as CampaignData } from "../../../target/types/sablier_merkle_instant_structs"; +import { assertEqualBn, assertEqualPublicKey, expectToThrow as baseExpectToThrow } from "../../common/assertions"; + +export function expectToThrow(promise: Promise, errorNameOrCode: ProgramErrorName | number) { + return baseExpectToThrow(promise, ProgramErrorCode, errorNameOrCode); +} + +export function assertEqCampaignData(a: CampaignData, b: CampaignData) { + assertEqualPublicKey(a.airdropTokenMint, b.airdropTokenMint); + assertEqualPublicKey(a.creator, b.creator); + assertEqualBn(a.expirationTime, b.expirationTime); + assertEqualBn(a.firstClaimTime, b.firstClaimTime); + assert.equal(a.ipfsCid, b.ipfsCid); + assert.equal(a.merkleRoot.length, b.merkleRoot.length); + assert.deepEqual(a.merkleRoot, b.merkleRoot); + assert.equal(a.name, b.name); +} diff --git a/tests/merkle-instant/utils/defaults.ts b/tests/merkle-instant/utils/defaults.ts new file mode 100644 index 00000000..b2a3132f --- /dev/null +++ b/tests/merkle-instant/utils/defaults.ts @@ -0,0 +1,27 @@ +import BN from "bn.js"; +import dayjs from "dayjs"; +import { sol, usdc } from "../../../lib/convertors"; + +export namespace Amount { + export const AGGREGATE = usdc(10_000); + export const CLAIM_FEE = sol("0.03"); + export const CLAIM = usdc(100); + export const CLAWBACK = usdc(1000); +} + +export namespace Time { + export const GENESIS_DAY = dayjs().add(1, "day"); + export const GENESIS = new BN(GENESIS_DAY.unix()); // tomorrow +} + +export namespace Campaign { + export const CAMPAIGN_NAME = "HODL or Nothing"; + export const EXPIRATION = new BN(dayjs().add(10, "days").unix()); + export const IPFS_CID = "bafkreiecpwdhvkmw4y6iihfndk7jhwjas3m5htm7nczovt6m37mucwgsrq"; + export const POST_GRACE_PERIOD = new BN(Time.GENESIS_DAY.add(7, "days").add(1, "second").unix()); +} + +export namespace Seed { + export const CAMPAIGN = Buffer.from("campaign"); + export const TREASURY = Buffer.from("treasury"); +} diff --git a/tests/merkle_instant/utils/merkle.ts b/tests/merkle-instant/utils/merkle.ts similarity index 72% rename from tests/merkle_instant/utils/merkle.ts rename to tests/merkle-instant/utils/merkle.ts index 19f5fc8b..9d1a6281 100644 --- a/tests/merkle_instant/utils/merkle.ts +++ b/tests/merkle-instant/utils/merkle.ts @@ -1,16 +1,38 @@ -import { BN } from "@coral-xyz/anchor"; -import { PublicKey } from "@solana/web3.js"; +import { type PublicKey } from "@solana/web3.js"; +import type BN from "bn.js"; import keccak256 from "keccak256"; import { MerkleTree } from "merkletreejs"; +import { toBigInt } from "../../../lib/helpers"; -// ---- LeafData interface ---- -export interface LeafData { +export type LeafData = { index: number; recipient: PublicKey; amount: BN; +}; + +export function getProof(leaves: LeafData[], targetLeaf: LeafData): number[][] { + const tree = buildTree(leaves); + + const targetHash = computeLeaf(targetLeaf); + const proofBuffers = tree.getProof(targetHash).map((p) => p.data); + + return proofBuffers.map((buf) => Array.from(buf)); +} + +export function getRoot(leaves: LeafData[]): number[] { + const tree = buildTree(leaves); + return Array.from(tree.getRoot()); +} + +/* -------------------------------------------------------------------------- */ +/* INTERNAL LOGIC */ +/* -------------------------------------------------------------------------- */ + +function buildTree(leaves: LeafData[]): MerkleTree { + const hashedLeaves = leaves.map(computeLeaf); + return new MerkleTree(hashedLeaves, keccak256, { sortPairs: true }); } -// ---- Compute the leaf ---- function computeLeaf(leafData: LeafData): Buffer { const indexBytes = Buffer.alloc(4); indexBytes.writeUInt32LE(leafData.index); @@ -18,11 +40,7 @@ function computeLeaf(leafData: LeafData): Buffer { const recipientBytes = leafData.recipient.toBuffer(); // 32 bytes const amountBytes = Buffer.alloc(8); - const amount = - typeof leafData.amount === "bigint" - ? leafData.amount - : BigInt(leafData.amount.toString()); - amountBytes.writeBigUInt64LE(amount); + amountBytes.writeBigUInt64LE(toBigInt(leafData.amount)); const leafBytes = Buffer.concat([indexBytes, recipientBytes, amountBytes]); @@ -31,24 +49,3 @@ function computeLeaf(leafData: LeafData): Buffer { return finalHash; } - -function buildTree(leaves: LeafData[]): MerkleTree { - const hashedLeaves = leaves.map(computeLeaf); - return new MerkleTree(hashedLeaves, keccak256, { sortPairs: true }); -} - -// ---- Get root as number[] ---- -export function getRoot(leaves: LeafData[]): number[] { - const tree = buildTree(leaves); - return Array.from(tree.getRoot()); -} - -// ---- Get proof for a specific leaf as number[][] ---- -export function getProof(leaves: LeafData[], targetLeaf: LeafData): number[][] { - const tree = buildTree(leaves); - - const targetHash = computeLeaf(targetLeaf); - const proofBuffers = tree.getProof(targetHash).map((p) => p.data); - - return proofBuffers.map((buf) => Array.from(buf)); -} diff --git a/tests/merkle_instant/base.ts b/tests/merkle_instant/base.ts deleted file mode 100644 index d8ff734c..00000000 --- a/tests/merkle_instant/base.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { BN, Program } from "@coral-xyz/anchor"; -import { PublicKey } from "@solana/web3.js"; -import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; - -import { - buildSignAndProcessTx, - deriveATAAddress, - transfer, -} from "../anchor-bankrun-adapter"; -import { - banksClient, - bankrunProvider, - createUser, - dai, - feeCollector, - getLamportsOf, - getPDAAddress, - recipient, - setUp as commonSetUp, - timeTravelTo, - usdc, - User, -} from "../common-base"; - -import * as defaults from "./utils/defaults"; -import { getProof, getRoot, LeafData } from "./utils/merkle"; -import { CampaignData } from "./utils/types"; - -import { SablierMerkleInstant } from "../../target/types/sablier_merkle_instant"; -import IDL from "../../target/idl/sablier_merkle_instant.json"; - -// Programs and addresses -export let merkleInstant: Program; -export let treasuryAddress: PublicKey; - -// Users -export let campaignCreator: User; - -// Campaigns -export let defaultCampaign: PublicKey; -export let defaultCampaignToken2022: PublicKey; - -// For the recipient declared in `common-base.ts` -export const defaultIndex = 0; -let defaultMerkleProof: number[][]; - -// Merkle Tree -let leaves: LeafData[]; -let merkleRoot: number[]; -let recipient1: PublicKey; -let recipient2: PublicKey; -let recipient3: PublicKey; - -/*////////////////////////////////////////////////////////////////////////// - SET-UP -//////////////////////////////////////////////////////////////////////////*/ - -export async function setUp({ initProgram = true } = {}) { - // Call common setup with merkle-instant specific programs - await commonSetUp("sablier_merkle_instant", new PublicKey(IDL.address)); - - // Deploy the program being tested - merkleInstant = new Program(IDL, bankrunProvider); - - // Create the Campaign Creator user - campaignCreator = await createUser(); - - // Pre-calculate the address of the Treasury - treasuryAddress = getPDAAddress( - [Buffer.from(defaults.TREASURY_SEED)], - merkleInstant.programId - ); - - // Create the recipients to be included in the Merkle Tree - recipient1 = (await createUser()).keys.publicKey; - recipient2 = (await createUser()).keys.publicKey; - recipient3 = (await createUser()).keys.publicKey; - - // Declare the leaves of the Merkle Tree before hashing and sorting - leaves = [ - { - index: defaultIndex, - // Use the default recipient's public key for the first leaf - recipient: recipient.keys.publicKey, - amount: defaults.CLAIM_AMOUNT, - }, - { index: 1, recipient: recipient1, amount: defaults.CLAIM_AMOUNT }, - { index: 2, recipient: recipient2, amount: defaults.CLAIM_AMOUNT }, - { index: 3, recipient: recipient3, amount: defaults.CLAIM_AMOUNT }, - ]; - - merkleRoot = getRoot(leaves); - defaultMerkleProof = getProof(leaves, leaves[0]); - - // Set the block time to APR 1, 2025 - await timeTravelTo(defaults.APR_1_2025); - - if (initProgram) { - // Initialize the Merkle Instant program - await initializeMerkleInstant(); - - // Create the default campaigns - defaultCampaign = await createCampaign(); - defaultCampaignToken2022 = await createCampaign({ - airdropTokenMint: dai, - airdropTokenProgram: TOKEN_2022_PROGRAM_ID, - }); - } -} - -/*////////////////////////////////////////////////////////////////////////// - TX-IX -//////////////////////////////////////////////////////////////////////////*/ - -export async function claim({ - campaign = defaultCampaign, - claimerKeys = recipient.keys, - amount = defaults.CLAIM_AMOUNT, - recipientAddress = recipient.keys.publicKey, - airdropTokenMint = usdc, - airdropTokenProgram = TOKEN_PROGRAM_ID, -} = {}): Promise { - const txIx = await merkleInstant.methods - .claim(defaultIndex, amount, defaultMerkleProof) - .accounts({ - claimer: claimerKeys.publicKey, - campaign: campaign, - recipient: recipientAddress, - airdropTokenMint, - airdropTokenProgram, - }) - .instruction(); - - // Build and sign the transaction - await buildSignAndProcessTx(banksClient, txIx, claimerKeys); -} - -export async function clawback({ - signer = campaignCreator.keys, - campaign = defaultCampaign, - amount = defaults.CLAWBACK_AMOUNT, - airdropTokenMint = usdc, - airdropTokenProgram = TOKEN_PROGRAM_ID, -} = {}): Promise { - const txIx = await merkleInstant.methods - .clawback(amount) - .accounts({ - campaign, - campaignCreator: signer.publicKey, - airdropTokenMint, - airdropTokenProgram, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, txIx, signer); -} - -export async function collectFees({ - signer = feeCollector.keys, - feeRecipient = recipient.keys.publicKey, -} = {}): Promise { - const txIx = await merkleInstant.methods - .collectFees() - .accounts({ - feeCollector: signer.publicKey, - feeRecipient, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, txIx, signer); -} - -export async function createCampaign({ - creator = campaignCreator, - name = defaults.CAMPAIGN_NAME, - campaignFunder = campaignCreator.keys, - airdropTokenMint = usdc, - airdropTokenProgram = TOKEN_PROGRAM_ID, -} = {}): Promise { - // Derive the address of the campaign - const campaign = getPDAAddress( - [ - Buffer.from(defaults.CAMPAIGN_SEED), - creator.keys.publicKey.toBuffer(), - Buffer.from(merkleRoot), - defaults.EXPIRATION_TIME.toArrayLike(Buffer, "le", 8), - Buffer.from(name), - airdropTokenMint.toBuffer(), - ], - merkleInstant.programId - ); - - const txIx = await merkleInstant.methods - .createCampaign( - merkleRoot, - defaults.EXPIRATION_TIME, - name, - defaults.IPFS_CID, - defaults.AGGREGATE_AMOUNT, - leaves.length - ) - .accounts({ - creator: creator.keys.publicKey, - airdropTokenMint, - airdropTokenProgram, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, txIx, creator.keys); - - const campaignAta = deriveATAAddress( - airdropTokenMint, - campaign, - airdropTokenProgram - ); - - const campaignFunderAta = deriveATAAddress( - airdropTokenMint, - campaignFunder.publicKey, - airdropTokenProgram - ); - - // Transfer the aggregate amount from the campaign funder to the campaign - await transfer( - banksClient, - campaignFunder, - campaignFunderAta, - campaignAta, - campaignFunder.publicKey, - defaults.AGGREGATE_AMOUNT, - [], - airdropTokenProgram - ); - - return campaign; -} - -export async function initializeMerkleInstant(): Promise { - const initializeIx = await merkleInstant.methods - .initialize(feeCollector.keys.publicKey) - .accounts({ - initializer: campaignCreator.keys.publicKey, - }) - .instruction(); - - await buildSignAndProcessTx(banksClient, initializeIx, campaignCreator.keys); -} - -/*////////////////////////////////////////////////////////////////////////// - HELPERS -//////////////////////////////////////////////////////////////////////////*/ - -export function defaultCampaignData(): CampaignData { - return { - airdropTokenMint: usdc, - creator: campaignCreator.keys.publicKey, - expirationTime: defaults.EXPIRATION_TIME, - firstClaimTime: new BN(0), - ipfsCid: defaults.IPFS_CID, - merkleRoot: Array.from(merkleRoot), - name: defaults.CAMPAIGN_NAME, - }; -} - -export async function fetchCampaignData( - campaign = defaultCampaign -): Promise { - return await merkleInstant.account.campaign.fetch(campaign); -} - -export async function getTreasuryLamports(): Promise { - return await getLamportsOf(treasuryAddress); -} diff --git a/tests/merkle_instant/unit/claim.ts b/tests/merkle_instant/unit/claim.ts deleted file mode 100644 index 224802bc..00000000 --- a/tests/merkle_instant/unit/claim.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; - -import { - createATAAndFund, - getATABalanceMint, -} from "../../anchor-bankrun-adapter"; -import { - banksClient, - dai, - defaultBankrunPayer, - getLamportsOf, - randomToken, - recipient, - sleepFor, - timeTravelTo, - usdc, -} from "../../common-base"; - -import { - campaignCreator, - claim, - createCampaign, - defaultCampaign, - defaultCampaignToken2022, - defaultIndex, - fetchCampaignData, - merkleInstant, - setUp, - treasuryAddress, -} from "../base"; -import { assert, assertErrorHexCode, assertFail } from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; - -describe("claim", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp({ - initProgram: false, - }); - }); - context("when the campaign doesn't exist", () => { - it("should revert", async () => { - try { - // Passing a non-Campaign account since no Campaigns exist yet - await claim({ campaign: new PublicKey(12345) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the campaign exists", () => { - it("should revert", async () => { - const campaign = await createCampaign({ name: "Test Campaign" }); - try { - await claim({ campaign: campaign }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - }); - - context("when the program is initialized", () => { - context("when the campaign doesn't exist", () => { - it("should revert", async () => { - try { - // Claim from a non-existent Campaign - await claim({ campaign: new PublicKey(12345) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the campaign exists", () => { - beforeEach(async () => { - await setUp(); - }); - - context("when the token mint is invalid", () => { - it("should revert", async () => { - try { - // Claim from the Campaign with an invalid token mint - await claim({ airdropTokenMint: dai }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the token mint is valid", () => { - context("when the airdrop has already been claimed", () => { - it("should revert", async () => { - await claim(); - await sleepFor(7); - try { - // Claim from the Campaign again - await claim(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, "0x0"); - } - }); - }); - - context("when the airdrop has not been claimed", () => { - context("when the merkle proof is invalid", () => { - it("should revert", async () => { - try { - await claim({ - amount: defaults.CLAIM_AMOUNT.sub(new BN(1)), - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("InvalidMerkleProof")); - } - }); - }); - - context("when the merkle proof is valid", () => { - context("when the campaign expired", () => { - it("should revert", async () => { - // Time travel to when the campaign has expired - await timeTravelTo(defaults.EXPIRATION_TIME); - try { - await claim(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("CampaignExpired")); - } - }); - }); - - context("when the campaign has not expired", () => { - context( - "when the recipient doesn't have an ATA for the token", - () => { - it("should claim the airdrop", async () => { - // Mint the random token to the campaign creator - await createATAAndFund( - banksClient, - defaultBankrunPayer, - randomToken, - defaults.AGGREGATE_AMOUNT.toNumber(), - TOKEN_PROGRAM_ID, - campaignCreator.keys.publicKey - ); - - // Create a Campaign with the random token - const campaign = await createCampaign({ - airdropTokenMint: randomToken, - }); - - // Test the campaign - await testClaim( - campaign, - recipient.keys, - randomToken, - TOKEN_PROGRAM_ID, - false - ); - }); - } - ); - - context("when the recipient has an ATA for the token", () => { - context("when the claimer is not the recipient", () => { - it("should claim the airdrop", async () => { - // Test the claim. - await testClaim(defaultCampaign, campaignCreator.keys); - }); - }); - - context("when the claimer is the recipient", () => { - context("given token SPL standard", () => { - it("should claim the airdrop", async () => { - // Claim from the Campaign - await testClaim(); - }); - }); - - context("given token 2022 standard", () => { - it("should claim the airdrop", async () => { - // Test the claim. - await testClaim( - defaultCampaignToken2022, - recipient.keys, - dai, - TOKEN_2022_PROGRAM_ID - ); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -/// Common test function to test the claim functionality -async function testClaim( - campaign = defaultCampaign, - claimer = recipient.keys, - tokenMint = usdc, - tokenProgram = TOKEN_PROGRAM_ID, - recipientAtaExists = true -) { - // Assert that the claim has not been made - assert.isFalse(await hasClaimed()); - - // Get the Campaign's data before claiming - const campaignDataBefore = await fetchCampaignData(campaign); - - // Assert that the Campaign's firstClaimTime is zero before claiming - assert(campaignDataBefore.firstClaimTime.isZero()); - - // Get the campaign and recipient ATA balances before claiming. - const campaignAtaBalanceBefore = await getATABalanceMint( - banksClient, - campaign, - tokenMint - ); - const recipientAtaBalanceBefore = recipientAtaExists - ? await getATABalanceMint(banksClient, recipient.keys.publicKey, tokenMint) - : new BN(0); - - // Get the claimer and treasury lamports balance before claiming - const claimerLamportsBefore = await getLamportsOf(claimer.publicKey); - const treasuryLamportsBefore = await getLamportsOf(treasuryAddress); - - // Claim from the Campaign - await claim({ - campaign: campaign, - claimerKeys: claimer, - airdropTokenMint: tokenMint, - airdropTokenProgram: tokenProgram, - }); - - const campaignDataAfter = await fetchCampaignData(campaign); - assert(campaignDataAfter.firstClaimTime.eq(defaults.APR_1_2025)); - - // Assert that the claim has been made - assert(await hasClaimed(campaign)); - - const campaignAtaBalanceAfter = await getATABalanceMint( - banksClient, - campaign, - tokenMint - ); - - // Assert that the Campaign's ATA balance decreased by the claim amount - assert( - campaignAtaBalanceAfter.eq( - campaignAtaBalanceBefore.sub(defaults.CLAIM_AMOUNT) - ), - "campaign ATA balance" - ); - - const recipientAtaBalanceAfter = await getATABalanceMint( - banksClient, - recipient.keys.publicKey, - tokenMint - ); - - // Assert that the recipient's ATA balance increased by the claim amount - assert( - recipientAtaBalanceAfter.eq( - recipientAtaBalanceBefore.add(defaults.CLAIM_AMOUNT) - ), - "recipient ATA balance" - ); - - const claimerLamportsAfter = await getLamportsOf(claimer.publicKey); - - // Assert that the claimer's lamports balance has changed atleast by the claim fee amount. - // We use `<=` because we don't know in advance the gas cost. - assert( - claimerLamportsAfter <= - claimerLamportsBefore - BigInt(defaults.CLAIM_FEE_AMOUNT), - "claimer lamports balance" - ); - - const treasuryLamportsAfter = await getLamportsOf(treasuryAddress); - - // Assert that the treasury's balance has increased by the claim fee amount - assert( - treasuryLamportsAfter == - treasuryLamportsBefore + BigInt(defaults.CLAIM_FEE_AMOUNT), - "treasury balance" - ); -} - -// Implicitly tests the `has_claimed` Ix works. -async function hasClaimed(campaign = defaultCampaign): Promise { - return await merkleInstant.methods - .hasClaimed(defaultIndex) - .accounts({ - campaign: campaign, - }) - .signers([defaultBankrunPayer]) - .view(); -} diff --git a/tests/merkle_instant/unit/clawback.ts b/tests/merkle_instant/unit/clawback.ts deleted file mode 100644 index 73401234..00000000 --- a/tests/merkle_instant/unit/clawback.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; - -import { - createATAAndFund, - deriveATAAddress, - getATABalanceMint, -} from "../../anchor-bankrun-adapter"; -import { - accountExists, - banksClient, - dai, - defaultBankrunPayer, - eve, - randomToken, - recipient, - timeTravelTo, - usdc, -} from "../../common-base"; - -import { - campaignCreator, - claim, - clawback, - createCampaign, - defaultCampaign, - defaultCampaignToken2022, - setUp, -} from "../base"; - -import { assert, assertErrorHexCode, assertFail } from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; - -describe("clawback", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp({ - initProgram: false, - }); - }); - - it("should revert", async () => { - try { - // Passing a non-Campaign account since no Campaigns exist yet - await clawback({ campaign: new PublicKey(12345) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("when the passed campaign account is invalid", () => { - it("should revert", async () => { - try { - await clawback({ campaign: new PublicKey(12345) }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the passed campaign account is valid", () => { - context("when the passed mint is invalid", () => { - it("should revert", async () => { - try { - await clawback({ - airdropTokenMint: dai, - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the passed mint is valid", () => { - context("when the signer is not the campaign creator", () => { - it("should revert", async () => { - try { - await clawback({ - signer: eve.keys, - }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("ConstraintAddress")); - } - }); - }); - - context("when the signer is the campaign creator", () => { - context("when first claim not made", () => { - it("should clawback", async () => { - await testClawback(); - }); - }); - - context("when first claim made", () => { - beforeEach(async () => { - await claim(); - }); - - context("given grace period not passed", () => { - it("should clawback", async () => { - await testClawback(); - }); - }); - - context("given grace period passed", () => { - beforeEach(async () => { - // Time travel to the end of the grace period - await timeTravelTo(defaults.TIME_AFTER_GRACE_PERIOD); - }); - - context("given campaign not expired", () => { - it("should revert", async () => { - try { - await clawback(); - assertFail(); - } catch (error) { - assertErrorHexCode( - error, - getErrorCode("ClawbackNotAllowed") - ); - } - }); - }); - - context("given campaign expired", () => { - beforeEach(async () => { - // Time travel to the end of the campaign - await timeTravelTo(defaults.EXPIRATION_TIME); - }); - - context("when campaign creator does not have ATA", () => { - it("should clawback", async () => { - await createATAAndFund( - banksClient, - defaultBankrunPayer, - randomToken, - defaults.AGGREGATE_AMOUNT, - TOKEN_PROGRAM_ID, - recipient.keys.publicKey - ); - - const campaign = await createCampaign({ - campaignFunder: recipient.keys, - airdropTokenMint: randomToken, - }); - - const campaignCreatorAta = deriveATAAddress( - randomToken, - campaignCreator.keys.publicKey, - TOKEN_PROGRAM_ID - ); - assert.isFalse(await accountExists(campaignCreatorAta)); - - // Claim from the Campaign - await testClawback({ - campaign: campaign, - airdropTokenMint: randomToken, - campaignCreatorAtaExists: false, - }); - - assert(await accountExists(campaignCreatorAta)); - }); - }); - - context("given token SPL standard", () => { - it("should clawback", async () => { - // Claim from the Campaign - await testClawback(); - }); - }); - - context("given token 2022 standard", () => { - it("should clawback", async () => { - // Test the claim. - await testClawback({ - campaign: defaultCampaignToken2022, - airdropTokenMint: dai, - airdropTokenProgram: TOKEN_2022_PROGRAM_ID, - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -async function testClawback({ - campaign = defaultCampaign, - airdropTokenMint = usdc, - airdropTokenProgram = TOKEN_PROGRAM_ID, - campaignCreatorAtaExists = true, -} = {}) { - const campaignAtaBalanceBefore = await getATABalanceMint( - banksClient, - campaign, - airdropTokenMint - ); - const creatorAtaBalanceBefore = campaignCreatorAtaExists - ? await getATABalanceMint( - banksClient, - campaignCreator.keys.publicKey, - airdropTokenMint - ) - : new BN(0); - - await clawback({ - campaign, - amount: defaults.CLAWBACK_AMOUNT, - airdropTokenMint, - airdropTokenProgram, - }); - - const campaignAtaBalanceAfter = await getATABalanceMint( - banksClient, - campaign, - airdropTokenMint - ); - - // Assert that the campaign token balance has decreased as expected - assert( - campaignAtaBalanceBefore.eq( - campaignAtaBalanceAfter.add(defaults.CLAWBACK_AMOUNT) - ), - "Campaign ATA balance" - ); - - const creatorAtaBalanceAfter = await getATABalanceMint( - banksClient, - campaignCreator.keys.publicKey, - airdropTokenMint - ); - - // Assert that the campaign creator's token balance has increased as expected - assert( - creatorAtaBalanceBefore.eq( - creatorAtaBalanceAfter.sub(defaults.CLAWBACK_AMOUNT) - ), - "Campaign creator's ATA balance" - ); -} diff --git a/tests/merkle_instant/unit/collectFees.ts b/tests/merkle_instant/unit/collectFees.ts deleted file mode 100644 index 5acd20a4..00000000 --- a/tests/merkle_instant/unit/collectFees.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { eve, getLamportsOf, recipient } from "../../common-base"; - -import { claim, collectFees, getTreasuryLamports, setUp } from "../base"; -import { assert, assertErrorHexCode, assertFail } from "../utils/assertions"; -import * as defaults from "../utils/defaults"; -import { getErrorCode } from "../utils/errors"; - -describe("collectFees", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp({ - initProgram: false, - }); - }); - - it("should revert", async () => { - try { - await collectFees(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("AccountNotInitialized")); - } - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("when signer is not the authorized fee collector", () => { - it("should revert", async () => { - // Perform a claim, generating fees - await claim(); - - try { - await collectFees({ signer: eve.keys }); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("ConstraintAddress")); - } - }); - }); - - context("when signer is the authorized fee collector", () => { - context("given no fees accumulated", () => { - it("should revert", async () => { - try { - await collectFees(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, getErrorCode("CantCollectZeroFees")); - } - }); - }); - - context("given accumulated fees", () => { - it("should collect the fees", async () => { - // Perform a claim, generating fees - await claim({ claimerKeys: recipient.keys }); - - const treasuryLamportsBefore = await getTreasuryLamports(); - const feeRecipientLamportsBefore = await getFeeRecipientLamports(); - - // Collect the fees - await collectFees(); - - const treasuryLamportsAfter = await getTreasuryLamports(); - const feeRecipientLamportsAfter = await getFeeRecipientLamports(); - - const expectedFeesCollected = defaults.CLAIM_FEE_AMOUNT - 1_000_000; // 1 claim worth of fees minus the safety buffer - - assert( - treasuryLamportsAfter === - treasuryLamportsBefore - BigInt(expectedFeesCollected) - ); - assert( - feeRecipientLamportsAfter === - feeRecipientLamportsBefore + BigInt(expectedFeesCollected) - ); - }); - }); - }); - }); -}); - -async function getFeeRecipientLamports() { - return await getLamportsOf(recipient.keys.publicKey); -} diff --git a/tests/merkle_instant/unit/createCampaign.ts b/tests/merkle_instant/unit/createCampaign.ts deleted file mode 100644 index d4d00a78..00000000 --- a/tests/merkle_instant/unit/createCampaign.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - createCampaign, - defaultCampaignData, - fetchCampaignData, - setUp, -} from "../base"; -import { - assertEqCampaignDatas, - assertErrorHexCode, - assertFail, -} from "../utils/assertions"; - -describe("createCampaign", () => { - context("when the program is not initialized", () => { - before(async () => { - await setUp({ - initProgram: false, - }); - }); - - it("should create the campaign", async () => { - await testCreateCampaign(); - }); - }); - - context("when the program is initialized", () => { - beforeEach(async () => { - await setUp(); - }); - - context("when the campaign already exists", () => { - it("should revert", async () => { - try { - await createCampaign(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, "0x0"); - } - }); - }); - - context("when the campaign does not exist", () => { - it("should create the campaign", async () => { - await testCreateCampaign(); - }); - }); - }); -}); - -async function testCreateCampaign() { - const name = "Test Campaign"; - const campaign = await createCampaign({ name: name }); - // Assert that the campaign was created successfully - const expectedCampaignData = { - ...defaultCampaignData(), - name: name, - }; - const actualCampaignData = await fetchCampaignData(campaign); - assertEqCampaignDatas(actualCampaignData, expectedCampaignData); -} diff --git a/tests/merkle_instant/unit/initialize.ts b/tests/merkle_instant/unit/initialize.ts deleted file mode 100644 index 99a8c085..00000000 --- a/tests/merkle_instant/unit/initialize.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { accountExists, sleepFor } from "../../common-base"; - -import { initializeMerkleInstant, setUp, treasuryAddress } from "../base"; -import { assert, assertErrorHexCode, assertFail } from "../utils/assertions"; - -describe("initialize", () => { - beforeEach(async () => { - await setUp({ - initProgram: false, - }); - }); - - context("given initialized", () => { - it("should revert", async () => { - await initializeMerkleInstant(); - await sleepFor(7); - try { - await initializeMerkleInstant(); - assertFail(); - } catch (error) { - assertErrorHexCode(error, "0x0"); - } - }); - }); - - context("given not initialized", () => { - it("should initialize the program", async () => { - await initializeMerkleInstant(); - - assert(await accountExists(treasuryAddress), "Treasury not initialized"); - }); - }); -}); diff --git a/tests/merkle_instant/utils/assertions.ts b/tests/merkle_instant/utils/assertions.ts deleted file mode 100644 index 7a6bd002..00000000 --- a/tests/merkle_instant/utils/assertions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { assert } from "chai"; -export { assert }; - -import { CampaignData } from "./types"; - -export function assertErrorHexCode(error: unknown, hexErrorCode: string) { - assertErrorContains( - error, - `custom program error: ${hexErrorCode}`, - `The expected error code ${hexErrorCode} not found in "${error}"` - ); -} - -export function assertErrorContains( - error: unknown, - expectedText: string, - message?: string -) { - assert(errorToMessage(error).includes(expectedText), message); -} - -export function assertEqCampaignDatas(a: CampaignData, b: CampaignData) { - assert( - a.airdropTokenMint.equals(b.airdropTokenMint), - `Airdrop token mints mismatch: ${a.airdropTokenMint.toBase58()} !== ${b.airdropTokenMint.toBase58()}` - ); - assert( - a.creator.equals(b.creator), - `Creators mismatch: ${a.creator.toBase58()} !== ${b.creator.toBase58()}` - ); - assert( - a.expirationTime.eq(b.expirationTime), - `Expiration times mismatch: ${a.expirationTime.toString()} !== ${b.expirationTime.toString()}` - ); - assert( - a.firstClaimTime.eq(b.firstClaimTime), - `First claim times mismatch: ${a.firstClaimTime.toString()} !== ${b.firstClaimTime.toString()}` - ); - assert( - a.ipfsCid === b.ipfsCid, - `IPFS CIDs mismatch: ${a.ipfsCid} !== ${b.ipfsCid}` - ); - assert( - a.merkleRoot.length === b.merkleRoot.length && - a.merkleRoot.every((value, index) => value === b.merkleRoot[index]), - `Merkle roots mismatch: ${a.merkleRoot} !== ${b.merkleRoot}` - ); - assert(a.name === b.name, `Campaign names mismatch: ${a.name} !== ${b.name}`); -} - -export function assertFail() { - assert.fail("Expected the tx to revert, but it succeeded."); -} - -function errorToMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/tests/merkle_instant/utils/defaults.ts b/tests/merkle_instant/utils/defaults.ts deleted file mode 100644 index e5d40930..00000000 --- a/tests/merkle_instant/utils/defaults.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; - -/*////////////////////////////////////////////////////////////////////////// - CONSTANTS -//////////////////////////////////////////////////////////////////////////*/ - -// Amounts -export const AGGREGATE_AMOUNT = new BN(10_000e6); -export const CLAIM_AMOUNT = new BN(100e6); -export const CLAWBACK_AMOUNT = new BN(1_000e6); -export const CLAIM_FEE_AMOUNT = 30_000_000; // 0.01 SOL - -// Timestamps -export const APR_1_2025 = new BN(1_743_454_800); -const GRACE_PERIOD_DURATION = new BN(60 * 60 * 24 * 7); // 7 days in seconds -export const TIME_AFTER_GRACE_PERIOD = APR_1_2025.add( - GRACE_PERIOD_DURATION.add(new BN(1)) -); // 7 days + 1 sec past APR_1_2025 -const TEN_DAYS_IN_SECONDS = 60 * 60 * 24 * 10; -export const EXPIRATION_TIME = APR_1_2025.add(new BN(TEN_DAYS_IN_SECONDS)); - -// Seeds -export const TREASURY_SEED = "treasury"; -export const CAMPAIGN_SEED = "campaign"; -export const CLAIM_RECEIPT_SEED = "claim_receipt"; - -// Miscellaneous -export const CAMPAIGN_NAME = "Default Campaign Name"; -export const IPFS_CID = - "bafkreiecpwdhvkmw4y6iihfndk7jhwjas3m5htm7nczovt6m37mucwgsrq"; diff --git a/tests/merkle_instant/utils/errors.ts b/tests/merkle_instant/utils/errors.ts deleted file mode 100644 index ce2dbfea..00000000 --- a/tests/merkle_instant/utils/errors.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - getErrorCode as getErrorCodeBase, - getErrorName as getErrorNameBase, -} from "../../common-base"; - -const ErrorCode = { - // Collect Fees - CantCollectZeroFees: 0x1773, - - // Clawback - ClawbackNotAllowed: 0x1772, - - // Claim - CampaignExpired: 0x1770, - ClaimAmountZero: 0x177d, - InvalidMerkleProof: 0x1771, - - // Anchor Errors - AccountNotInitialized: 0xbc4, - ConstraintAddress: 0x7dc, -} as const; -type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode]; - -export function getErrorCode(errorName: string): string { - return getErrorCodeBase(ErrorCode, errorName); -} - -export function getErrorName(hexCode: string): string { - return getErrorNameBase(ErrorCode, hexCode); -} diff --git a/tests/merkle_instant/utils/types.ts b/tests/merkle_instant/utils/types.ts deleted file mode 100644 index c93f5fa4..00000000 --- a/tests/merkle_instant/utils/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BN } from "@coral-xyz/anchor"; -import { PublicKey } from "@solana/web3.js"; - -export interface CampaignData { - airdropTokenMint: PublicKey; - creator: PublicKey; - expirationTime: BN; - firstClaimTime: BN; - ipfsCid: string; - merkleRoot: number[]; - name: string; -} diff --git a/tsconfig.json b/tsconfig.json index 433e538a..edaa5f49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,6 @@ { - "compilerOptions": { - "types": ["mocha", "chai", "node"], - "typeRoots": ["./node_modules/@types"], - "lib": ["es2020"], - "module": "commonjs", - "target": "es2020", - "esModuleInterop": true, - "strict": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "noEmit": true - }, - "exclude": ["node_modules", "dist", "**/*.d.ts"], - "include": ["migrations", "tests", "scripts/ts"] + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./node_modules/@sablier/devkit/tsconfig/base.json", + "exclude": ["node_modules"], + "include": ["**/*.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..7536fac5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + hookTimeout: 1_000_000, // 1000 seconds + include: ["tests/**/*.test.ts"], + reporters: ["verbose"], + testTimeout: 1_000_000, // 1000 seconds + }, +});