From ef686907e7c9403811911ec23e03e4bc6b4722cf Mon Sep 17 00:00:00 2001 From: Harsh Date: Mon, 7 Sep 2026 11:07:53 -0700 Subject: [PATCH 1/3] feat(nft-staking): add NFT staking example with checkpointed rewards Adds tokens/nft-staking/anchor: stake an NFT to earn reward tokens over time, and claim those rewards without unstaking. The NFT stays in the owner's wallet. The program takes delegate authority over the token account via `approve` and freezes it in place through Metaplex's FreezeDelegatedAccount, reversing it with ThawDelegatedAccount and `revoke` on unstake. That custody model, and `emit!` events, are not demonstrated anywhere else in the repo. Rewards accrue against a `last_claimed_at` checkpoint that each payout advances by exactly the span it paid for, so the same seconds can never be claimed twice and a part-day remainder is never forfeited. Both mistakes have dedicated tests that fail if reintroduced. Builds on the approach in #58 and #118, which stalled on an mpl-token-metadata/Borsh build failure and on inactivity. This version goes through anchor-spl 1.0.2's `metadata` feature with no direct mpl dependency, ships one example rather than three near-duplicate variants, and additionally: - rejects staking an NFT the signer does not hold. Both `approve` and the Metaplex freeze succeed on a zero-balance token account, so without this anyone could open an empty ATA for a collection NFT and farm rewards from an NFT they never owned - replaces an `.unwrap()` on optional collection metadata with distinct MissingCollection / UnverifiedCollection / InvalidCollection errors - uses checked arithmetic throughout - boxes the large Metaplex accounts in `unstake`, which otherwise overruns the instruction stack frame Tested with 17 LiteSVM cases covering the full lifecycle against the real Token Metadata program, using clock warping for accrual. Verified the suite fails when the checkpoint advance, the remainder handling, or the balance check is removed. Co-Authored-By: Claude Opus 5 --- .github/.workspace-ignore | 1 + README.md | 6 + tokens/nft-staking/README.md | 115 + tokens/nft-staking/anchor/.gitignore | 7 + tokens/nft-staking/anchor/.mocharc.json | 4 + tokens/nft-staking/anchor/.prettierignore | 7 + tokens/nft-staking/anchor/Anchor.toml | 24 + tokens/nft-staking/anchor/Cargo.toml | 15 + tokens/nft-staking/anchor/package.json | 32 + tokens/nft-staking/anchor/pnpm-lock.yaml | 2442 +++++++++++++++++ tokens/nft-staking/anchor/prepare.mjs | 34 + .../anchor/programs/nft-staking/Cargo.toml | 27 + .../anchor/programs/nft-staking/Xargo.toml | 2 + .../programs/nft-staking/src/constants.rs | 4 + .../anchor/programs/nft-staking/src/error.rs | 23 + .../anchor/programs/nft-staking/src/events.rs | 28 + .../nft-staking/src/instructions/claim.rs | 85 + .../src/instructions/initialize_config.rs | 60 + .../src/instructions/initialize_user.rs | 28 + .../nft-staking/src/instructions/mod.rs | 12 + .../nft-staking/src/instructions/shared.rs | 69 + .../nft-staking/src/instructions/stake.rs | 137 + .../nft-staking/src/instructions/unstake.rs | 140 + .../anchor/programs/nft-staking/src/lib.rs | 54 + .../programs/nft-staking/src/state/mod.rs | 7 + .../nft-staking/src/state/stake_account.rs | 20 + .../nft-staking/src/state/stake_config.rs | 18 + .../nft-staking/src/state/user_account.rs | 16 + .../nft-staking/anchor/tests/litesvm.test.ts | 540 ++++ tokens/nft-staking/anchor/tests/metaplex.ts | 180 ++ tokens/nft-staking/anchor/tests/utils.ts | 14 + tokens/nft-staking/anchor/tsconfig.json | 15 + 32 files changed, 4166 insertions(+) create mode 100644 tokens/nft-staking/README.md create mode 100644 tokens/nft-staking/anchor/.gitignore create mode 100644 tokens/nft-staking/anchor/.mocharc.json create mode 100644 tokens/nft-staking/anchor/.prettierignore create mode 100644 tokens/nft-staking/anchor/Anchor.toml create mode 100644 tokens/nft-staking/anchor/Cargo.toml create mode 100644 tokens/nft-staking/anchor/package.json create mode 100644 tokens/nft-staking/anchor/pnpm-lock.yaml create mode 100644 tokens/nft-staking/anchor/prepare.mjs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/Cargo.toml create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/Xargo.toml create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/error.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/events.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/mod.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/state/mod.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_account.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_config.rs create mode 100644 tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs create mode 100644 tokens/nft-staking/anchor/tests/litesvm.test.ts create mode 100644 tokens/nft-staking/anchor/tests/metaplex.ts create mode 100644 tokens/nft-staking/anchor/tests/utils.ts create mode 100644 tokens/nft-staking/anchor/tsconfig.json diff --git a/.github/.workspace-ignore b/.github/.workspace-ignore index 78234e002..8f8fadb2c 100644 --- a/.github/.workspace-ignore +++ b/.github/.workspace-ignore @@ -20,6 +20,7 @@ tokens/escrow/anchor/programs/escrow tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master tokens/merkle-tree-token-claimer/anchor/programs/merkle-tree-token-claimer tokens/nft-operations/anchor/programs/mint-nft +tokens/nft-staking/anchor/programs/nft-staking tokens/pda-mint-authority/anchor/programs/token-minter tokens/token-2022/basics/anchor/programs/basics tokens/token-2022/cpi-guard/anchor/programs/cpi-guard diff --git a/README.md b/README.md index afbd9cdca..b188cc29d 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,12 @@ Create an NFT collection, mint NFTs, and verify NFTs as part of a collection usi [anchor](./tokens/nft-operations/anchor) [pinocchio](./tokens/nft-operations/pinocchio) +### NFT staking + +[Stake an NFT to earn reward tokens over time, and claim those rewards without unstaking.](./tokens/nft-staking/README.md) The NFT is frozen in the owner's own wallet with a delegate authority rather than moved into a vault, and rewards accrue against a checkpoint so the same time can never be paid for twice. + +[anchor](./tokens/nft-staking/anchor) + ### Transferring Tokens [Create a token mint, mint tokens, and transfer tokens between accounts.](./tokens/transfer-tokens/README.md) diff --git a/tokens/nft-staking/README.md b/tokens/nft-staking/README.md new file mode 100644 index 000000000..de67de8db --- /dev/null +++ b/tokens/nft-staking/README.md @@ -0,0 +1,115 @@ +# NFT Staking + +Stake an NFT to earn reward tokens over time, then claim those rewards without +having to unstake. + +The NFT never leaves the owner's wallet. Instead of transferring it into a +vault, the program is made the **delegate** of the owner's token account and +then **freezes** that account through Metaplex Token Metadata. The NFT stays +visible in the owner's wallet and in marketplace views, but cannot be +transferred or sold until it is unstaked. This is how NFT staking generally +works in production, and it is a different custody model from the vault used in +[escrow](../escrow). + +Note that this is not Solana's built-in staking. Native staking delegates SOL to +a validator through the Stake program and earns protocol inflation on an epoch +schedule. This is an application-level staking pool: the rewards are a token +this program mints under rules it sets itself, and none of it touches consensus. + +## What this example demonstrates + +- **Delegate-and-freeze custody** — `approve` to hand a PDA delegate authority, + then a `FreezeDelegatedAccount` CPI into Metaplex to immobilise the NFT in + place. Unstaking reverses it with `ThawDelegatedAccount` and `revoke`. +- **Checkpointed reward accrual** — paying out a balance that grows with time, + without ever paying for the same span twice. +- **PDA-signed CPIs** — the stake account signs the freeze and thaw; the config + PDA signs as the reward mint's authority. +- **Events** — `emit!` on every state change so indexers can follow a pool + without polling. +- **Verified collection gating** — only NFTs from a specific, _verified_ + collection can be staked. + +## Instructions + +| Instruction | What it does | +| ------------------- | ----------------------------------------------------------------------------------------- | +| `initialize_config` | Creates the pool and its reward mint, whose mint authority is the config PDA | +| `initialize_user` | Creates the caller's per-user totals account | +| `stake` | Verifies collection membership, delegates the token account to a PDA, and freezes it | +| `claim` | Pays out rewards accrued since the last claim — callable while the NFT is still staked | +| `unstake` | Settles any outstanding rewards, thaws and un-delegates the NFT, closes the stake account | + +## Accounts + +| Account | Seeds | Holds | +| -------------- | ----------------------------- | -------------------------------------------------------------- | +| `StakeConfig` | `["config"]` | Collection, reward rate, stake cap, freeze period | +| `UserAccount` | `["user", user]` | Lifetime points earned, how many NFTs are currently staked | +| `StakeAccount` | `["stake", nft_mint, config]` | Owner, mint, `staked_at`, and the `last_claimed_at` checkpoint | + +`StakeAccount` doubles as the SPL delegate for the staked NFT's token account, +which is what lets the program freeze and thaw it. + +## The part worth reading closely: paying for time, once + +Rewards here are a function of elapsed time rather than of a balance somebody +deposited. That makes the payout path the dangerous one, and it is worth being +explicit about why. + +`claim` computes what is owed as `(now - last_claimed_at) → whole days → points` +and then **advances `last_claimed_at` in the same instruction**. If it paid out +but forgot to move the checkpoint, the very next call would read the same span +again and pay for it a second time — draining the reward mint one repeated +transaction at a time. + +There is no lock to forget on Solana, and no reentrancy guard to add. The +defence is simply that settling and checkpointing happen together, on one +account, in one instruction. The whole of it lives in +[`instructions/shared.rs`](./anchor/programs/nft-staking/src/instructions/shared.rs). + +There is a second, quieter bug in the same few lines. Only whole days pay out, +so a claim at 1.5 days owes one day. If the checkpoint were then snapped to +`now`, that leftover half day would vanish — and anyone claiming every 23 hours +would earn nothing, forever. Advancing by exactly `full_days * SECONDS_PER_DAY` +keeps the remainder banked for next time. + +Both mistakes are covered by tests that fail if you reintroduce them: +_"Refuses to pay the same day twice"_ and _"Banks the part-day remainder instead +of forfeiting it"_. + +## One more trap: freezing an NFT you do not own + +Both `approve` and the Metaplex freeze succeed happily on a **zero-balance** +token account. Anyone can open an associated token account for any mint, so +without an explicit balance check a user could open an empty account for some +NFT in the collection, "stake" it, and farm rewards from an NFT they never +owned. + +```rust +require!(self.nft_token_account.amount == 1, StakeError::NftNotHeld); +``` + +Covered by _"Rejects staking an NFT the signer does not actually hold"_ — delete +that one line and the test suite reports the stake succeeding. + +## Building and testing + +```bash +pnpm install # also dumps Metaplex Token Metadata into tests/fixtures/ +anchor test +``` + +Tests run on [LiteSVM](https://github.com/LiteSVM/litesvm), which can move its +own clock — necessary here, since every interesting behaviour in a staking +program only shows up after time passes. The suite mints a real collection and +real NFTs against the actual Metaplex program loaded from a local fixture, so +the freeze and thaw paths are exercised for real rather than mocked. + +## Notes + +- `max_stake` caps how many NFTs one user may stake at once. +- `freeze_period_days` is a minimum staking duration; `unstake` rejects until it + has elapsed. Rewards still accrue and can be claimed during it. +- Reward token decimals are set when the pool is created, and points are scaled + by them at payout. diff --git a/tokens/nft-staking/anchor/.gitignore b/tokens/nft-staking/anchor/.gitignore new file mode 100644 index 000000000..2e0446b07 --- /dev/null +++ b/tokens/nft-staking/anchor/.gitignore @@ -0,0 +1,7 @@ +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/tokens/nft-staking/anchor/.mocharc.json b/tokens/nft-staking/anchor/.mocharc.json new file mode 100644 index 000000000..7068542ef --- /dev/null +++ b/tokens/nft-staking/anchor/.mocharc.json @@ -0,0 +1,4 @@ +{ + "extension": ["ts"], + "spec": "tests/**/*.ts" +} diff --git a/tokens/nft-staking/anchor/.prettierignore b/tokens/nft-staking/anchor/.prettierignore new file mode 100644 index 000000000..414258343 --- /dev/null +++ b/tokens/nft-staking/anchor/.prettierignore @@ -0,0 +1,7 @@ +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/tokens/nft-staking/anchor/Anchor.toml b/tokens/nft-staking/anchor/Anchor.toml new file mode 100644 index 000000000..b09b4d05e --- /dev/null +++ b/tokens/nft-staking/anchor/Anchor.toml @@ -0,0 +1,24 @@ +[toolchain] +anchor_version = "1.0.2" +solana_version = "3.1.8" + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +nft_staking = "gphHxoVFMHZXMfapWDGtCfQhX7cjmXYcFNn5mMnJTjJ" + +[programs.devnet] +nft_staking = "gphHxoVFMHZXMfapWDGtCfQhX7cjmXYcFNn5mMnJTjJ" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +# Staking is time-based, and litesvm is the only harness here that can warp its +# clock — so the whole lifecycle (accrual, claiming, the freeze period) is +# covered there. Metaplex Token Metadata comes from a local fixture +# (tests/fixtures/token_metadata.so), dumped by prepare.mjs. +test = "pnpm mocha --import=tsx -t 1000000 tests/litesvm.test.ts" diff --git a/tokens/nft-staking/anchor/Cargo.toml b/tokens/nft-staking/anchor/Cargo.toml new file mode 100644 index 000000000..14a951cee --- /dev/null +++ b/tokens/nft-staking/anchor/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = [ + "programs/*" +] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 + +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tokens/nft-staking/anchor/package.json b/tokens/nft-staking/anchor/package.json new file mode 100644 index 000000000..7ae2e8118 --- /dev/null +++ b/tokens/nft-staking/anchor/package.json @@ -0,0 +1,32 @@ +{ + "type": "module", + "pnpm": { + "overrides": { + "litesvm": "^0.8.0" + } + }, + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", + "postinstall": "zx prepare.mjs" + }, + "dependencies": { + "@anchor-lang/core": "1.0.0-rc.5", + "@solana/spl-token": "^0.4.14", + "@solana/web3.js": "^1.98.4" + }, + "license": "MIT", + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^26.1.0", + "anchor-litesvm": "^0.2.1", + "chai": "^6.2.2", + "litesvm": "^0.8.0", + "mocha": "^11.7.5", + "prettier": "^3.7.4", + "tsx": "^4.19.2", + "typescript": "^5.9.3", + "zx": "^8.1.4" + } +} diff --git a/tokens/nft-staking/anchor/pnpm-lock.yaml b/tokens/nft-staking/anchor/pnpm-lock.yaml new file mode 100644 index 000000000..3f582108c --- /dev/null +++ b/tokens/nft-staking/anchor/pnpm-lock.yaml @@ -0,0 +1,2442 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + litesvm: ^0.8.0 + +importers: + + .: + dependencies: + '@anchor-lang/core': + specifier: 1.0.0-rc.5 + version: 1.0.0-rc.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/spl-token': + specifier: ^0.4.14 + version: 0.4.15(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/web3.js': + specifier: ^1.98.4 + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + devDependencies: + '@solana-program/token': + specifier: ^0.6.0 + version: 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))) + '@types/chai': + specifier: ^5.2.3 + version: 5.2.3 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^26.1.0 + version: 26.5.0 + anchor-litesvm: + specifier: ^0.2.1 + version: 0.2.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + chai: + specifier: ^6.2.2 + version: 6.2.2 + litesvm: + specifier: ^0.8.0 + version: 0.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + mocha: + specifier: ^11.7.5 + version: 11.8.0 + prettier: + specifier: ^3.7.4 + version: 3.9.6 + tsx: + specifier: ^4.19.2 + version: 4.23.13 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + zx: + specifier: ^8.1.4 + version: 8.8.5 + +packages: + + '@anchor-lang/borsh@1.2.0': + resolution: {integrity: sha512-qUC90JezAXyetwCqhLxkxh/r9ofRCd2D0fkEoEex7Vurw3pGDtf1r779v0cM4fsvOEIskK9nja/GkZOnlvJqYw==} + engines: {node: '>=10'} + peerDependencies: + '@solana/web3.js': ^1.69.1 + + '@anchor-lang/core@1.0.0-rc.5': + resolution: {integrity: sha512-4iPy4RiEFn6obzYY7zx8IaGAXz2fvJ0uCTF6agAcUBjGNZeypfEb4ZZh6TfLnJy78Lh06JeB7XGqKsaBCMEmQA==} + engines: {node: '>=17'} + + '@anchor-lang/errors@1.2.0': + resolution: {integrity: sha512-muEmHFs2UDrcmGUuj+IWiYW2/AazI3tMv6f6OT5KNwOuSLG4j9g4zv8Pqvo+Cj9F6deP4P2c6DWhSUBHdYL4+g==} + engines: {node: '>=10'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@coral-xyz/anchor-errors@0.31.1': + resolution: {integrity: sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==} + engines: {node: '>=10'} + + '@coral-xyz/anchor@0.31.1': + resolution: {integrity: sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA==} + engines: {node: '>=17'} + + '@coral-xyz/borsh@0.31.1': + resolution: {integrity: sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw==} + engines: {node: '>=10'} + peerDependencies: + '@solana/web3.js': ^1.69.0 + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@solana-program/token@0.6.0': + resolution: {integrity: sha512-omkZh4Tt9rre4wzWHNOhOEHyenXQku3xyc/UrKvShexA/Qlhza67q7uRwmwEDUs4QqoDBidSZPooOmepnA/jig==} + peerDependencies: + '@solana/kit': ^3.0 + + '@solana/accounts@3.0.3': + resolution: {integrity: sha512-KqlePrlZaHXfu8YQTCxN204ZuVm9o68CCcUr6l27MG2cuRUtEM1Ta0iR8JFkRUAEfZJC4Cu0ZDjK/v49loXjZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/addresses@3.0.3': + resolution: {integrity: sha512-AuMwKhJI89ANqiuJ/fawcwxNKkSeHH9CApZd2xelQQLS7X8uxAOovpcmEgiObQuiVP944s9ScGUT62Bdul9qYg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/assertions@3.0.3': + resolution: {integrity: sha512-2qspxdbWp2y62dfCIlqeWQr4g+hE8FYSSwcaP6itwMwGRb8393yDGCJfI/znuzJh6m/XVWhMHIgFgsBwnevCmg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/buffer-layout-utils@0.3.0': + resolution: {integrity: sha512-MuQOCC1j0np1xH9yAv0ZWWfwvr7Bt7Sz4LId11Wi4wDdAmJ+lobE+vHg/mZmGcihF0BIkqVBNxGmlv8QE5DrtA==} + engines: {node: '>= 10'} + + '@solana/buffer-layout@4.0.1': + resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} + engines: {node: '>=5.10'} + + '@solana/codecs-core@2.0.0-rc.1': + resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-core@3.0.3': + resolution: {integrity: sha512-emKykJ3h1DmnDOY29Uv9eJXP8E/FHzvlUBJ6te+5EbKdFjj7vdlKYPfDxOI6iGdXTY+YC/ELtbNBh6QwF2uEDQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-data-structures@2.0.0-rc.1': + resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-data-structures@3.0.3': + resolution: {integrity: sha512-R15cLp8riJvToXziW8lP6AMSwsztGhEnwgyGmll32Mo0Yjq+hduW2/fJrA/TJs6tA/OgTzMQjlxgk009EqZHCw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@2.0.0-rc.1': + resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@3.0.3': + resolution: {integrity: sha512-pfXkH9J0glrM8qj6389GAn30+cJOxzXLR2FsPOHCUMXrqLhGjMMZAWhsQkpOQ37SGc/7EiQsT/gmyGC7gxHqJQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-strings@2.0.0-rc.1': + resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5' + + '@solana/codecs-strings@3.0.3': + resolution: {integrity: sha512-VHBXnnTVtcQ1j+7Vrz+qSYo38no+jiHRdGnhFspRXEHNJbllzwKqgBE7YN3qoIXH+MKxgJUcwO5KHmdzf8Wn2A==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' + + '@solana/codecs@2.0.0-rc.1': + resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs@3.0.3': + resolution: {integrity: sha512-GOHwTlIQsCoJx9Ryr6cEf0FHKAQ7pY4aO4xgncAftrv0lveTQ1rPP2inQ1QT0gJllsIa8nwbfXAADs9nNJxQDA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@2.0.0-rc.1': + resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} + hasBin: true + peerDependencies: + typescript: '>=5' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@3.0.3': + resolution: {integrity: sha512-1l84xJlHNva6io62PcYfUamwWlc0eM95nHgCrKX0g0cLoC6D6QHYPCEbEVkR+C5UtP9JDgyQM8MFiv+Ei5tO9Q==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/fast-stable-stringify@3.0.3': + resolution: {integrity: sha512-ED0pxB6lSEYvg+vOd5hcuQrgzEDnOrURFgp1ZOY+lQhJkQU6xo+P829NcJZQVP1rdU2/YQPAKJKEseyfe9VMIw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/functional@3.0.3': + resolution: {integrity: sha512-2qX1kKANn8995vOOh5S9AmF4ItGZcfbny0w28Eqy8AFh+GMnSDN4gqpmV2LvxBI9HibXZptGH3RVOMk82h1Mpw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instruction-plans@3.0.3': + resolution: {integrity: sha512-eqoaPtWtmLTTpdvbt4BZF5H6FIlJtXi9H7qLOM1dLYonkOX2Ncezx5NDCZ9tMb2qxVMF4IocYsQnNSnMfjQF1w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instructions@3.0.3': + resolution: {integrity: sha512-4csIi8YUDb5j/J+gDzmYtOvq7ZWLbCxj4t0xKn+fPrBk/FD2pK29KVT3Fu7j4Lh1/ojunQUP9X4NHwUexY3PnA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/keys@3.0.3': + resolution: {integrity: sha512-tp8oK9tMadtSIc4vF4aXXWkPd4oU5XPW8nf28NgrGDWGt25fUHIydKjkf2hPtMt9i1WfRyQZ33B5P3dnsNqcPQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/kit@3.0.3': + resolution: {integrity: sha512-CEEhCDmkvztd1zbgADsEQhmj9GyWOOGeW1hZD+gtwbBSF5YN1uofS/pex5MIh/VIqKRj+A2UnYWI1V+9+q/lyQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/nominal-types@3.0.3': + resolution: {integrity: sha512-aZavCiexeUAoMHRQg4s1AHkH3wscbOb70diyfjhwZVgFz1uUsFez7csPp9tNFkNolnadVb2gky7yBk3IImQJ6A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/options@2.0.0-rc.1': + resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} + peerDependencies: + typescript: '>=5' + + '@solana/options@3.0.3': + resolution: {integrity: sha512-jarsmnQ63RN0JPC5j9sgUat07NrL9PC71XU7pUItd6LOHtu4+wJMio3l5mT0DHVfkfbFLL6iI6+QmXSVhTNF3g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/programs@3.0.3': + resolution: {integrity: sha512-JZlVE3/AeSNDuH3aEzCZoDu8GTXkMpGXxf93zXLzbxfxhiQ/kHrReN4XE/JWZ/uGWbaFZGR5B3UtdN2QsoZL7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/promises@3.0.3': + resolution: {integrity: sha512-K+UflGBVxj30XQMHTylHHZJdKH5QG3oj5k2s42GrZ/Wbu72oapVJySMBgpK45+p90t8/LEqV6rRPyTXlet9J+Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-api@3.0.3': + resolution: {integrity: sha512-Yym9/Ama62OY69rAZgbOCAy1QlqaWAyb0VlqFuwSaZV1pkFCCFSwWEJEsiN1n8pb2ZP+RtwNvmYixvWizx9yvA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-parsed-types@3.0.3': + resolution: {integrity: sha512-/koM05IM2fU91kYDQxXil3VBNlOfcP+gXE0js1sdGz8KonGuLsF61CiKB5xt6u1KEXhRyDdXYLjf63JarL4Ozg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec-types@3.0.3': + resolution: {integrity: sha512-A6Jt8SRRetnN3CeGAvGJxigA9zYRslGgWcSjueAZGvPX+MesFxEUjSWZCfl+FogVFvwkqfkgQZQbPAGZQFJQ6Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec@3.0.3': + resolution: {integrity: sha512-MZn5/8BebB6MQ4Gstw6zyfWsFAZYAyLzMK+AUf/rSfT8tPmWiJ/mcxnxqOXvFup/l6D67U8pyGpIoFqwCeZqqA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-api@3.0.3': + resolution: {integrity: sha512-MGgVK3PUS15qsjuhimpzGZrKD/CTTvS0mAlQ0Jw84zsr1RJVdQJK/F0igu07BVd172eTZL8d90NoAQ3dahW5pA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-channel-websocket@3.0.3': + resolution: {integrity: sha512-zUzUlb8Cwnw+SHlsLrSqyBRtOJKGc+FvSNJo/vWAkLShoV0wUDMPv7VvhTngJx3B/3ANfrOZ4i08i9QfYPAvpQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 + + '@solana/rpc-subscriptions-spec@3.0.3': + resolution: {integrity: sha512-9KpQ32OBJWS85mn6q3gkM0AjQe1LKYlMU7gpJRrla/lvXxNLhI95tz5K6StctpUreVmRWTVkNamHE69uUQyY8A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions@3.0.3': + resolution: {integrity: sha512-LRvz6NaqvtsYFd32KwZ+rwYQ9XCs+DWjV8BvBLsJpt9/NWSuHf/7Sy/vvP6qtKxut692H/TMvHnC4iulg0WmiQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transformers@3.0.3': + resolution: {integrity: sha512-lzdaZM/dG3s19Tsk4mkJA5JBoS1eX9DnD7z62gkDwrwJDkDBzkAJT9aLcsYFfTmwTfIp6uU2UPgGYc97i1wezw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transport-http@3.0.3': + resolution: {integrity: sha512-bIXFwr2LR5A97Z46dI661MJPbHnPfcShBjFzOS/8Rnr8P4ho3j/9EUtjDrsqoxGJT3SLWj5OlyXAlaDAvVTOUQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-types@3.0.3': + resolution: {integrity: sha512-petWQ5xSny9UfmC3Qp2owyhNU0w9SyBww4+v7tSVyXMcCC9v6j/XsqTeimH1S0qQUllnv0/FY83ohFaxofmZ6Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc@3.0.3': + resolution: {integrity: sha512-3oukAaLK78GegkKcm6iNmRnO4mFeNz+BMvA8T56oizoBNKiRVEq/6DFzVX/LkmZ+wvD601pAB3uCdrTPcC0YKQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/signers@3.0.3': + resolution: {integrity: sha512-UwCd/uPYTZiwd283JKVyOWLLN5sIgMBqGDyUmNU3vo9hcmXKv5ZGm/9TvwMY2z35sXWuIOcj7etxJ8OoWc/ObQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/spl-token-group@0.0.7': + resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token-metadata@0.1.6': + resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token@0.4.15': + resolution: {integrity: sha512-3Lof3mNov8NVQ3PalIWb1Jgr/TZ6lYM+/sexv2TLqdhNFVth2OfWmH3d7QucgMjSbokkjNiNlRr6I8Fd269uaw==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.5 + + '@solana/subscribable@3.0.3': + resolution: {integrity: sha512-FJ27LKGHLQ5GGttPvTOLQDLrrOZEgvaJhB7yYaHAhPk25+p+erBaQpjePhfkMyUbL1FQbxn1SUJmS6jUuaPjlQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/sysvars@3.0.3': + resolution: {integrity: sha512-GnHew+QeKCs2f9ow+20swEJMH4mDfJA/QhtPgOPTYQx/z69J4IieYJ7fZenSHnA//lJ45fVdNdmy1trypvPLBQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-confirmation@3.0.3': + resolution: {integrity: sha512-dXx0OLtR95LMuARgi2dDQlL1QYmk56DOou5q9wKymmeV3JTvfDExeWXnOgjRBBq/dEfj4ugN1aZuTaS18UirFw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-messages@3.0.3': + resolution: {integrity: sha512-s+6NWRnBhnnjFWV4x2tzBzoWa6e5LiIxIvJlWwVQBFkc8fMGY04w7jkFh0PM08t/QFKeXBEWkyBDa/TFYdkWug==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transactions@3.0.3': + resolution: {integrity: sha512-iMX+n9j4ON7H1nKlWEbMqMOpKYC6yVGxKKmWHT1KdLRG7v+03I4DnDeFoI+Zmw56FA+7Bbne8jwwX60Q1vk/MQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@26.5.0': + resolution: {integrity: sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + anchor-litesvm@0.2.1: + resolution: {integrity: sha512-WBFxuW982eXTZqxmoE0lewuPFpSipAd/MoyuRpPmYJ4bshU5eNM14XAsViQ2ARkIxeDP/W2BMoi9rRZlG/lzXw==} + engines: {node: '>= 20'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-x@3.0.11: + resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bigint-buffer@1.1.5: + resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} + engines: {node: '>= 10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bn.js@5.2.5: + resolution: {integrity: sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==} + + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + buffer-layout@1.2.2: + resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==} + engines: {node: '>=4.5'} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + + fast-stable-stringify@1.0.0: + resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + + fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jayson@4.3.0: + resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==} + engines: {node: '>=8'} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + litesvm-darwin-arm64@0.8.0: + resolution: {integrity: sha512-XYa0oOA7FVbMYKvIDbBznWUjkHQD8J/fn5kPMBSX4QWf4yfFda/+L4JHeSngx+dBCM0LyEyLeakVrnmdR1KYPQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + litesvm-darwin-x64@0.8.0: + resolution: {integrity: sha512-lrxU6VXEqY7MHHIskdjB88xM9OiGR2ZcXf+fMESnetCk6rVUM6Llfb386wsMiqvi1+DowJwShxluBsCutWo2Sw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + litesvm-linux-arm64-gnu@0.8.0: + resolution: {integrity: sha512-D0pdYTQkoibPCfza2x1urSjIpHdwHVagHs0fBMHpzxSEvXND2qqDR3jXYf6xcE0sAq0knXjQmGz7WzP/HhFeFw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + litesvm-linux-arm64-musl@0.8.0: + resolution: {integrity: sha512-g7vgYPJZC6cT1gaWA1M2SbZu+Sngs9FuxsybDSE1Mmelmaejlguf7X/ymUuhehaSjEY+QSi+ydWtwzgE95JL0w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + litesvm-linux-x64-gnu@0.8.0: + resolution: {integrity: sha512-nn599p+XuOJoQN2XTSaY4Yz1ZqYxLNUbvt30kImuRUs2ZlIv5FANzM+VUsZgSzWV0phW8m6stX3mpdVv0iIuFQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + litesvm-linux-x64-musl@0.8.0: + resolution: {integrity: sha512-UH4v1GM4hNOjEIzx/dBZQ5mxWGFOd85qs7IBou070dLjH0vdZMh4avQCQyc127+A7aRfdc8+wK7t4SEIn4EEgw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + litesvm@0.8.0: + resolution: {integrity: sha512-P0Ly11FSr1f77bKwaRf0VQQZYdsw6Oi3OLKBxgBN5iJcB7W7lTjFB1tnVcJqdn1NMz6upqU/04rZFCC/JfYBWg==} + engines: {node: '>= 20'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mocha@11.8.0: + resolution: {integrity: sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rpc-websockets@9.3.9: + resolution: {integrity: sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superstruct@0.15.5: + resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==} + + superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.29.1: + resolution: {integrity: sha512-2xO3p6gANwOmhSFwNy2MMYISbHTtM+K3bY0bqpD1gOGpUPIe+yFXCobxbz0s2jm7E0faMxIFcZUcCKa+N6Ozlg==} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + utf-8-validate@6.0.6: + resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==} + engines: {node: '>=6.14.2'} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + workerpool@9.3.4: + resolution: {integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@7.5.13: + resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zx@8.8.5: + resolution: {integrity: sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==} + engines: {node: '>= 12.17.0'} + hasBin: true + +snapshots: + + '@anchor-lang/borsh@1.2.0(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: 5.2.5 + buffer-layout: 1.2.2 + + '@anchor-lang/core@1.0.0-rc.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@anchor-lang/borsh': 1.2.0(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@anchor-lang/errors': 1.2.0 + '@noble/hashes': 1.8.0 + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: 5.2.5 + bs58: 4.0.1 + buffer-layout: 1.2.2 + camelcase: 6.3.0 + cross-fetch: 3.2.0 + eventemitter3: 4.0.7 + pako: 2.2.0 + superstruct: 0.15.5 + toml: 3.0.0 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@anchor-lang/errors@1.2.0': {} + + '@babel/runtime@7.29.7': {} + + '@coral-xyz/anchor-errors@0.31.1': {} + + '@coral-xyz/anchor@0.31.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@coral-xyz/anchor-errors': 0.31.1 + '@coral-xyz/borsh': 0.31.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@noble/hashes': 1.8.0 + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: 5.2.5 + bs58: 4.0.1 + buffer-layout: 1.2.2 + camelcase: 6.3.0 + cross-fetch: 3.2.0 + eventemitter3: 4.0.7 + pako: 2.2.0 + superstruct: 0.15.5 + toml: 3.0.0 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@coral-xyz/borsh@0.31.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: 5.2.5 + buffer-layout: 1.2.2 + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@iarna/toml@2.2.5': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': + dependencies: + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + + '@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 3.0.3(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/assertions@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/buffer-layout-utils@0.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bigint-buffer: 1.1.5 + bignumber.js: 9.3.1 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@solana/buffer-layout@4.0.1': + dependencies: + buffer: 6.0.3 + + '@solana/codecs-core@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-core@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-core@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-data-structures@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + + '@solana/codecs-strings@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + + '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/codecs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/errors@2.0.0-rc.1(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 12.1.0 + typescript: 5.9.3 + + '@solana/errors@2.3.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.9.3 + + '@solana/errors@3.0.3(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.0 + typescript: 5.9.3 + + '@solana/fast-stable-stringify@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/functional@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/instruction-plans@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/instructions@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/keys@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 3.0.3(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + dependencies: + '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/nominal-types@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/options@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/promises@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/rpc-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-parsed-types@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/rpc-spec-types@3.0.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/rpc-spec@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/rpc-subscriptions-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6) + + '@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-transformers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transport-http@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + undici-types: 7.29.1 + + '@solana/rpc-types@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/rpc-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-transport-http': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/signers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token@0.4.15(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + + '@solana/subscribable@3.0.3(typescript@5.9.3)': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/sysvars@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-messages@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 3.0.3(typescript@5.9.3) + '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) + '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@babel/runtime': 7.29.7 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + agentkeepalive: 4.6.0 + bn.js: 5.2.5 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + node-fetch: 2.7.0 + rpc-websockets: 9.3.9 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 26.5.0 + + '@types/deep-eql@4.0.2': {} + + '@types/mocha@10.0.10': {} + + '@types/node@12.20.55': {} + + '@types/node@26.5.0': + dependencies: + undici-types: 8.9.0 + + '@types/uuid@10.0.0': {} + + '@types/ws@7.4.7': + dependencies: + '@types/node': 26.5.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.5.0 + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + anchor-litesvm@0.2.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6): + dependencies: + '@coral-xyz/anchor': 0.31.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@iarna/toml': 2.2.5 + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + litesvm: 0.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + base-x@3.0.11: + dependencies: + safe-buffer: 5.2.1 + + base64-js@1.5.1: {} + + bigint-buffer@1.1.5: + dependencies: + bindings: 1.5.0 + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bn.js@5.2.5: {} + + borsh@0.7.0: + dependencies: + bn.js: 5.2.5 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + browser-stdout@1.3.1: {} + + bs58@4.0.1: + dependencies: + base-x: 3.0.11 + + buffer-layout@1.2.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + camelcase@6.3.0: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + commander@14.0.0: {} + + commander@14.0.3: {} + + commander@2.20.3: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + delay@5.0.0: {} + + diff@7.0.0: {} + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es6-promise@4.2.8: {} + + es6-promisify@5.0.0: + dependencies: + es6-promise: 4.2.8 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + eyes@0.1.8: {} + + fast-stable-stringify@1.0.0: {} + + fastestsmallesttextencoderdecoder@1.0.22: {} + + file-uri-to-path@1.0.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + has-flag@4.0.0: {} + + he@1.2.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + ieee754@1.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + isomorphic-ws@4.0.1(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + dependencies: + ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6) + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-stringify-safe@5.0.1: {} + + litesvm-darwin-arm64@0.8.0: + optional: true + + litesvm-darwin-x64@0.8.0: + optional: true + + litesvm-linux-arm64-gnu@0.8.0: + optional: true + + litesvm-linux-arm64-musl@0.8.0: + optional: true + + litesvm-linux-x64-gnu@0.8.0: + optional: true + + litesvm-linux-x64-musl@0.8.0: + optional: true + + litesvm@0.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6): + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + fastestsmallesttextencoderdecoder: 1.0.22 + optionalDependencies: + litesvm-darwin-arm64: 0.8.0 + litesvm-darwin-x64: 0.8.0 + litesvm-linux-arm64-gnu: 0.8.0 + litesvm-linux-arm64-musl: 0.8.0 + litesvm-linux-x64-gnu: 0.8.0 + litesvm-linux-x64-musl: 0.8.0 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minipass@7.1.3: {} + + mocha@11.8.0: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.5.0 + he: 1.2.0 + is-path-inside: 3.0.3 + js-yaml: 4.3.2 + log-symbols: 4.1.0 + minimatch: 9.0.9 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.4 + yargs: 17.7.3 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + ms@2.1.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: + optional: true + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + pako@2.2.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + picocolors@1.1.1: {} + + prettier@3.9.6: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + rpc-websockets@9.3.9: + dependencies: + '@swc/helpers': 0.5.23 + '@types/uuid': 10.0.0 + '@types/ws': 8.18.1 + buffer: 6.0.3 + eventemitter3: 5.0.4 + uuid: 14.0.2 + ws: 8.21.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + safe-buffer@5.2.1: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-json-comments@3.1.1: {} + + superstruct@0.15.5: {} + + superstruct@2.0.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + text-encoding-utf-8@1.0.2: {} + + toml@3.0.0: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.29.1: {} + + undici-types@8.9.0: {} + + utf-8-validate@6.0.6: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + uuid@14.0.2: {} + + uuid@8.3.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + workerpool@9.3.4: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zx@8.8.5: {} diff --git a/tokens/nft-staking/anchor/prepare.mjs b/tokens/nft-staking/anchor/prepare.mjs new file mode 100644 index 000000000..af71cee7e --- /dev/null +++ b/tokens/nft-staking/anchor/prepare.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env zx + +import { mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { $ } from 'zx'; + +const programs = [ + { + id: 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', + name: 'token_metadata.so', + }, +]; + +const outputDir = 'tests/fixtures'; +const overwrite = true; + +try { + for (const program of programs) { + const { id, name } = program; + const outputFile = join(outputDir, name); + await $`solana config set -um`; + + try { + await mkdir(outputDir, { recursive: true }); + if (overwrite) await rm(outputFile, { force: true }); + await $`solana program dump ${id} ${outputFile}`; + console.log(`Program ${id} dumped to ${outputFile}`); + } catch (error) { + console.error(`Error dumping ${id}: ${error.message}`); + } + } +} catch (error) { + console.error(`Unexpected error: ${error.message}`); +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/Cargo.toml b/tokens/nft-staking/anchor/programs/nft-staking/Cargo.toml new file mode 100644 index 000000000..9f68c1387 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nft-staking" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "nft_staking" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +anchor-debug = [] +custom-heap = [] +custom-panic = [] + +[dependencies] +anchor-lang = { version = "1.0.2", features = ["init-if-needed"] } +anchor-spl = { version = "1.0.2", features = ["metadata"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/tokens/nft-staking/anchor/programs/nft-staking/Xargo.toml b/tokens/nft-staking/anchor/programs/nft-staking/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs new file mode 100644 index 000000000..0036b9538 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs @@ -0,0 +1,4 @@ +pub const ANCHOR_DISCRIMINATOR: usize = 8; + +/// Rewards accrue per whole day staked. +pub const SECONDS_PER_DAY: i64 = 86_400; diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs new file mode 100644 index 000000000..c6c1bbf8c --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs @@ -0,0 +1,23 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum StakeError { + #[msg("This NFT does not belong to any collection")] + MissingCollection, + #[msg("This NFT's collection is not the one this pool accepts")] + InvalidCollection, + #[msg("This NFT's collection has not been verified by the collection authority")] + UnverifiedCollection, + #[msg("The token account does not hold the NFT being staked")] + NftNotHeld, + #[msg("This user has already staked the maximum number of NFTs")] + MaxStakeReached, + #[msg("This stake position belongs to another user")] + InvalidOwner, + #[msg("The freeze period has not elapsed yet")] + FreezePeriodNotPassed, + #[msg("No whole day has elapsed since the last claim")] + NothingToClaim, + #[msg("Arithmetic overflow")] + Overflow, +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/events.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/events.rs new file mode 100644 index 000000000..76a989e64 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/events.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +/// Emitted by every state-changing instruction so indexers can follow a pool +/// from transaction logs instead of polling every stake account. +#[event] +pub struct NftStaked { + pub user: Pubkey, + pub mint: Pubkey, + pub staked_at: i64, +} + +#[event] +pub struct RewardsClaimed { + pub user: Pubkey, + pub mint: Pubkey, + pub points: u64, + /// The checkpoint after this claim: rewards are paid up to (not past) here. + pub claimed_through: i64, +} + +#[event] +pub struct NftUnstaked { + pub user: Pubkey, + pub mint: Pubkey, + /// Rewards settled by the unstake itself, on top of any earlier claims. + pub final_points: u64, + pub unstaked_at: i64, +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs new file mode 100644 index 000000000..aa8116e98 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs @@ -0,0 +1,85 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; + +use crate::{ + instructions::shared::{mint_reward_tokens, settle_rewards}, + RewardsClaimed, StakeAccount, StakeConfig, StakeError, UserAccount, +}; + +#[derive(Accounts)] +pub struct Claim<'info> { + #[account(mut)] + pub user: Signer<'info>, + + pub nft_mint: Account<'info, Mint>, + + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, StakeConfig>, + + #[account( + mut, + seeds = [b"rewards", config.key().as_ref()], + bump = config.rewards_bump, + )] + pub rewards_mint: Account<'info, Mint>, + + #[account( + init_if_needed, + payer = user, + associated_token::mint = rewards_mint, + associated_token::authority = user, + )] + pub rewards_token_account: Account<'info, TokenAccount>, + + #[account( + mut, + seeds = [b"stake", nft_mint.key().as_ref(), config.key().as_ref()], + bump = stake_account.bump, + constraint = stake_account.owner == user.key() @ StakeError::InvalidOwner, + )] + pub stake_account: Account<'info, StakeAccount>, + + #[account( + mut, + seeds = [b"user", user.key().as_ref()], + bump = user_account.bump, + )] + pub user_account: Account<'info, UserAccount>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +impl<'info> Claim<'info> { + pub fn claim(&mut self) -> Result<()> { + let now = Clock::get()?.unix_timestamp; + let points_per_day = self.config.points_per_day; + + // Settling advances the checkpoint on `stake_account`. Calling this + // instruction twice in a row therefore pays out once and then finds + // nothing left to pay — the same seconds cannot be claimed again. + let points = settle_rewards(&mut self.stake_account, points_per_day, now)?; + require!(points > 0, StakeError::NothingToClaim); + + mint_reward_tokens(&self.config, &self.rewards_mint, &self.rewards_token_account, &self.token_program, points)?; + + self.user_account.points_earned = + self.user_account.points_earned.checked_add(points).ok_or(StakeError::Overflow)?; + + emit!(RewardsClaimed { + user: self.user.key(), + mint: self.nft_mint.key(), + points, + claimed_through: self.stake_account.last_claimed_at, + }); + + Ok(()) + } +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs new file mode 100644 index 000000000..34b8d28a4 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs @@ -0,0 +1,60 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token}; + +use crate::{StakeConfig, ANCHOR_DISCRIMINATOR}; + +#[derive(Accounts)] +#[instruction(points_per_day: u64, max_stake: u8, freeze_period_days: u32, reward_decimals: u8)] +pub struct InitializeConfig<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + /// The collection every staked NFT must belong to. + pub collection_mint: Account<'info, Mint>, + + #[account( + init, + payer = admin, + space = ANCHOR_DISCRIMINATOR + StakeConfig::INIT_SPACE, + seeds = [b"config"], + bump, + )] + pub config: Account<'info, StakeConfig>, + + /// The reward token. Its mint authority is the config PDA, so rewards can + /// only ever be minted by this program, from `claim` and `unstake`. + #[account( + init, + payer = admin, + seeds = [b"rewards", config.key().as_ref()], + bump, + mint::decimals = reward_decimals, + mint::authority = config, + )] + pub rewards_mint: Account<'info, Mint>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, +} + +impl<'info> InitializeConfig<'info> { + pub fn initialize_config( + &mut self, + points_per_day: u64, + max_stake: u8, + freeze_period_days: u32, + bumps: &InitializeConfigBumps, + ) -> Result<()> { + self.config.set_inner(StakeConfig { + admin: self.admin.key(), + collection: self.collection_mint.key(), + points_per_day, + max_stake, + freeze_period_days, + rewards_bump: bumps.rewards_mint, + bump: bumps.config, + }); + + Ok(()) + } +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs new file mode 100644 index 000000000..9a9002987 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +use crate::{UserAccount, ANCHOR_DISCRIMINATOR}; + +#[derive(Accounts)] +pub struct InitializeUser<'info> { + #[account(mut)] + pub user: Signer<'info>, + + #[account( + init, + payer = user, + space = ANCHOR_DISCRIMINATOR + UserAccount::INIT_SPACE, + seeds = [b"user", user.key().as_ref()], + bump, + )] + pub user_account: Account<'info, UserAccount>, + + pub system_program: Program<'info, System>, +} + +impl<'info> InitializeUser<'info> { + pub fn initialize_user(&mut self, bumps: &InitializeUserBumps) -> Result<()> { + self.user_account.set_inner(UserAccount { points_earned: 0, amount_staked: 0, bump: bumps.user_account }); + + Ok(()) + } +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/mod.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/mod.rs new file mode 100644 index 000000000..a3080e0ed --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/mod.rs @@ -0,0 +1,12 @@ +pub mod claim; +pub mod initialize_config; +pub mod initialize_user; +pub mod shared; +pub mod stake; +pub mod unstake; + +pub use claim::*; +pub use initialize_config::*; +pub use initialize_user::*; +pub use stake::*; +pub use unstake::*; diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs new file mode 100644 index 000000000..4ee27910a --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs @@ -0,0 +1,69 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount}; + +use crate::{StakeAccount, StakeConfig, StakeError, SECONDS_PER_DAY}; + +/// Settles the rewards accrued since the stake position's checkpoint, advancing +/// that checkpoint by exactly the span it pays for. +/// +/// Settling and checkpointing together is what stops the same seconds being +/// paid twice; advancing by whole days rather than to `now` is what keeps the +/// leftover part-day banked for the next claim. +/// +/// Zero means nothing has accrued yet: `claim` rejects it, `unstake` allows it. +pub fn settle_rewards(stake_account: &mut StakeAccount, points_per_day: u64, now: i64) -> Result { + let elapsed = now.checked_sub(stake_account.last_claimed_at).ok_or(StakeError::Overflow)?; + + // A validator clock can step backwards across a restart. + if elapsed <= 0 { + return Ok(0); + } + + let full_days = elapsed / SECONDS_PER_DAY; + if full_days == 0 { + return Ok(0); + } + + let points = (full_days as u64).checked_mul(points_per_day).ok_or(StakeError::Overflow)?; + let settled_span = full_days.checked_mul(SECONDS_PER_DAY).ok_or(StakeError::Overflow)?; + + stake_account.last_claimed_at = + stake_account.last_claimed_at.checked_add(settled_span).ok_or(StakeError::Overflow)?; + + Ok(points) +} + +/// Mints `points` reward tokens to `destination`, scaled by the reward mint's +/// decimals, with the config PDA signing as the mint authority. +pub fn mint_reward_tokens<'info>( + config: &Account<'info, StakeConfig>, + rewards_mint: &Account<'info, Mint>, + destination: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, + points: u64, +) -> Result<()> { + if points == 0 { + return Ok(()); + } + + let amount = points + .checked_mul(10u64.checked_pow(rewards_mint.decimals as u32).ok_or(StakeError::Overflow)?) + .ok_or(StakeError::Overflow)?; + + // The config PDA is the reward mint's authority, so the program signs here. + let seeds = &[b"config".as_ref(), &[config.bump]]; + let signer_seeds = &[&seeds[..]]; + + mint_to( + CpiContext::new_with_signer( + token_program.key(), + MintTo { + mint: rewards_mint.to_account_info(), + to: destination.to_account_info(), + authority: config.to_account_info(), + }, + signer_seeds, + ), + amount, + ) +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs new file mode 100644 index 000000000..df99ae140 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs @@ -0,0 +1,137 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + metadata::{freeze_delegated_account, FreezeDelegatedAccount, MasterEditionAccount, Metadata, MetadataAccount}, + token::{approve, Approve, Mint, Token, TokenAccount}, +}; + +use crate::{NftStaked, StakeAccount, StakeConfig, StakeError, UserAccount, ANCHOR_DISCRIMINATOR}; + +#[derive(Accounts)] +pub struct Stake<'info> { + #[account(mut)] + pub user: Signer<'info>, + + pub nft_mint: Account<'info, Mint>, + + /// The NFT stays here, in the owner's own token account, for the whole + /// stake. The program never takes custody of it — it takes *delegate + /// authority* over this account and freezes it in place, so the NFT is + /// immobilised while remaining visible in the owner's wallet. + #[account( + mut, + associated_token::mint = nft_mint, + associated_token::authority = user, + )] + pub nft_token_account: Account<'info, TokenAccount>, + + #[account( + seeds = [b"metadata", metadata_program.key().as_ref(), nft_mint.key().as_ref()], + seeds::program = metadata_program.key(), + bump, + )] + pub metadata: Account<'info, MetadataAccount>, + + /// Required by Metaplex to freeze: the master edition PDA is the NFT + /// mint's freeze authority, and Token Metadata signs as it on our behalf. + #[account( + seeds = [b"metadata", metadata_program.key().as_ref(), nft_mint.key().as_ref(), b"edition"], + seeds::program = metadata_program.key(), + bump, + )] + pub edition: Account<'info, MasterEditionAccount>, + + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, StakeConfig>, + + #[account( + init, + payer = user, + space = ANCHOR_DISCRIMINATOR + StakeAccount::INIT_SPACE, + seeds = [b"stake", nft_mint.key().as_ref(), config.key().as_ref()], + bump, + )] + pub stake_account: Account<'info, StakeAccount>, + + #[account( + mut, + seeds = [b"user", user.key().as_ref()], + bump = user_account.bump, + )] + pub user_account: Account<'info, UserAccount>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub metadata_program: Program<'info, Metadata>, +} + +impl<'info> Stake<'info> { + pub fn stake(&mut self, bumps: &StakeBumps) -> Result<()> { + // `approve` and the Metaplex freeze both succeed on a zero-balance + // account, so without this anyone could open an empty ATA for a + // collection NFT and farm rewards from an NFT they never owned. + require!(self.nft_token_account.amount == 1, StakeError::NftNotHeld); + + require!(self.user_account.amount_staked < self.config.max_stake, StakeError::MaxStakeReached); + + // `ok_or` rather than `unwrap`: an NFT with no collection is a caller + // mistake to report, not a panic. + let collection = self.metadata.collection.as_ref().ok_or(StakeError::MissingCollection)?; + require!(collection.verified, StakeError::UnverifiedCollection); + require_keys_eq!(collection.key, self.config.collection, StakeError::InvalidCollection); + + let now = Clock::get()?.unix_timestamp; + + self.stake_account.set_inner(StakeAccount { + owner: self.user.key(), + mint: self.nft_mint.key(), + staked_at: now, + // Accrual starts now, so the first claim can only ever pay for + // time after this instruction. + last_claimed_at: now, + bump: bumps.stake_account, + }); + + // Step 1: make this NFT's stake account the SPL delegate for the token + // account. The user signs this, because only the owner can delegate. + approve( + CpiContext::new( + self.token_program.key(), + Approve { + to: self.nft_token_account.to_account_info(), + delegate: self.stake_account.to_account_info(), + authority: self.user.to_account_info(), + }, + ), + 1, + )?; + + // Step 2: freeze the token account. Only the delegate may ask Metaplex + // to do this, and the delegate is a PDA — so the program signs as it. + let nft_mint_key = self.nft_mint.key(); + let config_key = self.config.key(); + let seeds = &[b"stake".as_ref(), nft_mint_key.as_ref(), config_key.as_ref(), &[bumps.stake_account]]; + let signer_seeds = &[&seeds[..]]; + + freeze_delegated_account(CpiContext::new_with_signer( + self.metadata_program.key(), + FreezeDelegatedAccount { + metadata: self.metadata.to_account_info(), + delegate: self.stake_account.to_account_info(), + token_account: self.nft_token_account.to_account_info(), + edition: self.edition.to_account_info(), + mint: self.nft_mint.to_account_info(), + token_program: self.token_program.to_account_info(), + }, + signer_seeds, + ))?; + + self.user_account.amount_staked = self.user_account.amount_staked.checked_add(1).ok_or(StakeError::Overflow)?; + + emit!(NftStaked { user: self.user.key(), mint: self.nft_mint.key(), staked_at: now }); + + Ok(()) + } +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs new file mode 100644 index 000000000..b7cb47f29 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs @@ -0,0 +1,140 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + metadata::{thaw_delegated_account, MasterEditionAccount, Metadata, MetadataAccount, ThawDelegatedAccount}, + token::{revoke, Mint, Revoke, Token, TokenAccount}, +}; + +use crate::{ + instructions::shared::{mint_reward_tokens, settle_rewards}, + NftUnstaked, StakeAccount, StakeConfig, StakeError, UserAccount, SECONDS_PER_DAY, +}; + +#[derive(Accounts)] +pub struct Unstake<'info> { + #[account(mut)] + pub user: Signer<'info>, + + pub nft_mint: Box>, + + #[account( + mut, + associated_token::mint = nft_mint, + associated_token::authority = user, + )] + pub nft_token_account: Box>, + + // Metaplex's `Metadata` is a large struct, and this instruction carries a + // lot of accounts. Boxing the big ones keeps them on the heap instead of + // the (much smaller) instruction stack frame. + #[account( + seeds = [b"metadata", metadata_program.key().as_ref(), nft_mint.key().as_ref()], + seeds::program = metadata_program.key(), + bump, + )] + pub metadata: Box>, + + #[account( + seeds = [b"metadata", metadata_program.key().as_ref(), nft_mint.key().as_ref(), b"edition"], + seeds::program = metadata_program.key(), + bump, + )] + pub edition: Box>, + + #[account( + seeds = [b"config"], + bump = config.bump, + )] + pub config: Account<'info, StakeConfig>, + + #[account( + mut, + seeds = [b"rewards", config.key().as_ref()], + bump = config.rewards_bump, + )] + pub rewards_mint: Box>, + + #[account( + init_if_needed, + payer = user, + associated_token::mint = rewards_mint, + associated_token::authority = user, + )] + pub rewards_token_account: Box>, + + /// Closing this returns its rent to the user and, just as importantly, + /// makes the position un-claimable afterwards — there is no checkpoint + /// left to settle against. + #[account( + mut, + close = user, + seeds = [b"stake", nft_mint.key().as_ref(), config.key().as_ref()], + bump = stake_account.bump, + constraint = stake_account.owner == user.key() @ StakeError::InvalidOwner, + )] + pub stake_account: Account<'info, StakeAccount>, + + #[account( + mut, + seeds = [b"user", user.key().as_ref()], + bump = user_account.bump, + )] + pub user_account: Account<'info, UserAccount>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub metadata_program: Program<'info, Metadata>, +} + +impl<'info> Unstake<'info> { + pub fn unstake(&mut self) -> Result<()> { + let now = Clock::get()?.unix_timestamp; + + let staked_for = now.checked_sub(self.stake_account.staked_at).ok_or(StakeError::Overflow)?; + let freeze_period = + (self.config.freeze_period_days as i64).checked_mul(SECONDS_PER_DAY).ok_or(StakeError::Overflow)?; + require!(staked_for >= freeze_period, StakeError::FreezePeriodNotPassed); + + // Pay out whatever is still owed before the position disappears, so + // unstaking never silently forfeits earned rewards. Unlike `claim`, + // zero is fine here — it just means nothing was outstanding. + let points_per_day = self.config.points_per_day; + let points = settle_rewards(&mut self.stake_account, points_per_day, now)?; + mint_reward_tokens(&self.config, &self.rewards_mint, &self.rewards_token_account, &self.token_program, points)?; + + self.user_account.points_earned = + self.user_account.points_earned.checked_add(points).ok_or(StakeError::Overflow)?; + + // Thaw before revoking: Metaplex requires the delegate to still be set + // and signing, and revoking first would strip exactly that authority. + let nft_mint_key = self.nft_mint.key(); + let config_key = self.config.key(); + let seeds = &[b"stake".as_ref(), nft_mint_key.as_ref(), config_key.as_ref(), &[self.stake_account.bump]]; + let signer_seeds = &[&seeds[..]]; + + thaw_delegated_account(CpiContext::new_with_signer( + self.metadata_program.key(), + ThawDelegatedAccount { + metadata: self.metadata.to_account_info(), + delegate: self.stake_account.to_account_info(), + token_account: self.nft_token_account.to_account_info(), + edition: self.edition.to_account_info(), + mint: self.nft_mint.to_account_info(), + token_program: self.token_program.to_account_info(), + }, + signer_seeds, + ))?; + + revoke(CpiContext::new( + self.token_program.key(), + Revoke { source: self.nft_token_account.to_account_info(), authority: self.user.to_account_info() }, + ))?; + + self.user_account.amount_staked = self.user_account.amount_staked.checked_sub(1).ok_or(StakeError::Overflow)?; + + emit!(NftUnstaked { user: self.user.key(), mint: self.nft_mint.key(), final_points: points, unstaked_at: now }); + + Ok(()) + } +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs new file mode 100644 index 000000000..8529054de --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs @@ -0,0 +1,54 @@ +pub mod constants; +pub mod error; +pub mod events; +pub mod instructions; +pub mod state; + +use anchor_lang::prelude::*; + +pub use constants::*; +pub use error::*; +pub use events::*; +pub use instructions::*; +pub use state::*; + +declare_id!("gphHxoVFMHZXMfapWDGtCfQhX7cjmXYcFNn5mMnJTjJ"); + +#[program] +pub mod nft_staking { + use super::*; + + /// Creates the pool and its reward mint. The reward mint's authority is the + /// config PDA, so only this program can ever mint rewards. + pub fn initialize_config( + context: Context, + points_per_day: u64, + max_stake: u8, + freeze_period_days: u32, + _reward_decimals: u8, + ) -> Result<()> { + context.accounts.initialize_config(points_per_day, max_stake, freeze_period_days, &context.bumps) + } + + /// Creates the caller's per-user totals account. Called once per user. + pub fn initialize_user(context: Context) -> Result<()> { + context.accounts.initialize_user(&context.bumps) + } + + /// Stakes an NFT by delegating its token account to a PDA and freezing it + /// in place. The NFT never leaves the owner's wallet. + pub fn stake(context: Context) -> Result<()> { + context.accounts.stake(&context.bumps) + } + + /// Pays out the rewards accrued since the last claim, without unstaking. + pub fn claim(context: Context) -> Result<()> { + context.accounts.claim() + } + + /// Settles any outstanding rewards, thaws and un-delegates the NFT, and + /// closes the stake position. + pub fn unstake(context: Context) -> Result<()> { + context.accounts.unstake() + } +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/state/mod.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/state/mod.rs new file mode 100644 index 000000000..44ce482c0 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/state/mod.rs @@ -0,0 +1,7 @@ +pub mod stake_account; +pub mod stake_config; +pub mod user_account; + +pub use stake_account::*; +pub use stake_config::*; +pub use user_account::*; diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_account.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_account.rs new file mode 100644 index 000000000..f593d8af1 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_account.rs @@ -0,0 +1,20 @@ +use anchor_lang::prelude::*; + +/// One staked NFT, at seeds `["stake", nft_mint, config]`. +/// +/// This account is also the SPL delegate for the staked NFT's token account, +/// which is what lets the program freeze and thaw it while it stays in the +/// owner's wallet. +#[account] +#[derive(InitSpace)] +pub struct StakeAccount { + pub owner: Pubkey, + pub mint: Pubkey, + pub staked_at: i64, + /// The accrual checkpoint: rewards have been paid out up to this instant. + /// + /// Every payout advances it by exactly the time it paid for, so the same + /// seconds can never be claimed twice. See `instructions::shared`. + pub last_claimed_at: i64, + pub bump: u8, +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_config.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_config.rs new file mode 100644 index 000000000..02693ddb9 --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/state/stake_config.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +/// Pool-wide settings. One per program, at seeds `["config"]`. +#[account] +#[derive(InitSpace)] +pub struct StakeConfig { + pub admin: Pubkey, + /// Only NFTs whose verified collection matches this mint may be staked. + pub collection: Pubkey, + /// Reward points earned per NFT, per whole day staked. + pub points_per_day: u64, + /// How many NFTs one user may stake at once. + pub max_stake: u8, + /// How long an NFT must stay staked before it can be unstaked. + pub freeze_period_days: u32, + pub rewards_bump: u8, + pub bump: u8, +} diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs new file mode 100644 index 000000000..7ef833c0b --- /dev/null +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs @@ -0,0 +1,16 @@ +use anchor_lang::prelude::*; + +/// Per-user totals, at seeds `["user", user]`. Created once and reused across +/// every NFT that user stakes. +#[account] +#[derive(InitSpace)] +pub struct UserAccount { + /// Lifetime points paid out to this user, across all their stake positions. + /// A running total for display; the authoritative per-NFT accrual state + /// lives on each `StakeAccount`. + pub points_earned: u64, + /// How many NFTs this user currently has staked, checked against + /// `StakeConfig::max_stake`. + pub amount_staked: u8, + pub bump: u8, +} diff --git a/tokens/nft-staking/anchor/tests/litesvm.test.ts b/tokens/nft-staking/anchor/tests/litesvm.test.ts new file mode 100644 index 000000000..4d4a85c7e --- /dev/null +++ b/tokens/nft-staking/anchor/tests/litesvm.test.ts @@ -0,0 +1,540 @@ +import * as anchor from '@anchor-lang/core'; +import { + AccountLayout, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; +import { LiteSVMProvider } from 'anchor-litesvm'; +import { assert } from 'chai'; +import { LiteSVM } from 'litesvm'; +import IDL from '../target/idl/nft_staking.json' with { type: 'json' }; +import type { NftStaking } from '../target/types/nft_staking.ts'; +import { + createMasterEditionV3, + createMetadataAccountV3, + masterEditionPda, + metadataPda, + TOKEN_METADATA_PROGRAM_ID, + verifyCollection, +} from './metaplex.ts'; +import { expectAnchorError } from './utils.ts'; + +const PROGRAM_ID = new PublicKey(IDL.address); + +const SECONDS_PER_DAY = 86_400n; + +// Pool settings used throughout. max_stake is 1 so the cap is easy to hit. +const POINTS_PER_DAY = 10; +const MAX_STAKE = 1; +const FREEZE_PERIOD_DAYS = 2; +const REWARD_DECIMALS = 6; +const ONE_DAY_OF_REWARDS = BigInt(POINTS_PER_DAY) * 10n ** BigInt(REWARD_DECIMALS); + +// SPL token account states. +const INITIALIZED = 1; +const FROZEN = 2; + +describe('nft-staking litesvm', () => { + const client = new LiteSVM(); + client.addProgramFromFile(PROGRAM_ID, 'target/deploy/nft_staking.so'); + client.addProgramFromFile(TOKEN_METADATA_PROGRAM_ID, 'tests/fixtures/token_metadata.so'); + const provider = new LiteSVMProvider(client); + anchor.setProvider(provider); + const wallet = provider.wallet as anchor.Wallet; + const program = new anchor.Program(IDL, provider); + + // The wallet is the pool admin and mints every NFT. `staker` is the person + // actually staking, so the tests exercise a real second signer. + const staker = Keypair.generate(); + const otherUser = Keypair.generate(); + + const config = PublicKey.findProgramAddressSync([Buffer.from('config')], PROGRAM_ID)[0]; + const rewardsMint = PublicKey.findProgramAddressSync([Buffer.from('rewards'), config.toBuffer()], PROGRAM_ID)[0]; + const userAccount = PublicKey.findProgramAddressSync( + [Buffer.from('user'), staker.publicKey.toBuffer()], + PROGRAM_ID, + )[0]; + + const stakePda = (nftMint: PublicKey) => + PublicKey.findProgramAddressSync([Buffer.from('stake'), nftMint.toBuffer(), config.toBuffer()], PROGRAM_ID)[0]; + + const tokenAccount = (address: PublicKey) => AccountLayout.decode(client.getAccount(address)!.data); + + const rewardsBalance = () => { + const ata = getAssociatedTokenAddressSync(rewardsMint, staker.publicKey); + const account = client.getAccount(ata); + return account === null ? 0n : AccountLayout.decode(account.data).amount; + }; + + /** Moves the validator clock forward and lets identical transactions resend. */ + const warpDays = (days: number) => { + const clock = client.getClock(); + clock.unixTimestamp += BigInt(Math.round(days * Number(SECONDS_PER_DAY))); + client.setClock(clock); + client.expireBlockhash(); + }; + + /** + * Mints a fresh NFT: mint account, one token to `owner`, metadata, and a + * master edition (which moves the mint's freeze authority to the edition + * PDA — the thing that makes freeze-in-place staking possible). + */ + const createNft = async ({ + owner, + collection, + verify = false, + }: { + owner: PublicKey; + collection?: PublicKey; + verify?: boolean; + }) => { + const mintKeypair = Keypair.generate(); + const mint = mintKeypair.publicKey; + const ata = getAssociatedTokenAddressSync(mint, owner); + const lamports = await provider.connection.getMinimumBalanceForRentExemption(MINT_SIZE); + + const tx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint, + space: MINT_SIZE, + lamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction(mint, 0, wallet.publicKey, wallet.publicKey), + createAssociatedTokenAccountInstruction(wallet.publicKey, ata, owner, mint), + // Supply must be exactly 1 before the master edition can be created. + createMintToInstruction(mint, ata, wallet.publicKey, 1), + createMetadataAccountV3({ + mint, + mintAuthority: wallet.publicKey, + payer: wallet.publicKey, + updateAuthority: wallet.publicKey, + name: 'Staking Test NFT', + symbol: 'STK', + uri: '', + collection, + }), + createMasterEditionV3({ + mint, + updateAuthority: wallet.publicKey, + mintAuthority: wallet.publicKey, + payer: wallet.publicKey, + }), + ); + + if (collection && verify) { + tx.add( + verifyCollection({ + mint, + collectionMint: collection, + collectionAuthority: wallet.publicKey, + payer: wallet.publicKey, + }), + ); + } + + await provider.sendAndConfirm(tx, [mintKeypair]); + return { mint, ata }; + }; + + const stakeAccounts = (nftMint: PublicKey, nftTokenAccount: PublicKey) => ({ + user: staker.publicKey, + nftMint, + nftTokenAccount, + metadata: metadataPda(nftMint), + edition: masterEditionPda(nftMint), + config, + stakeAccount: stakePda(nftMint), + userAccount, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + metadataProgram: TOKEN_METADATA_PROGRAM_ID, + }); + + const claimAccounts = (nftMint: PublicKey, user: Keypair, users: PublicKey) => ({ + user: user.publicKey, + nftMint, + config, + rewardsMint, + rewardsTokenAccount: getAssociatedTokenAddressSync(rewardsMint, user.publicKey), + stakeAccount: stakePda(nftMint), + userAccount: users, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }); + + // NFTs used across the suite, filled in by the setup test. + let collectionMint: PublicKey; + let otherCollectionMint: PublicKey; + let nftA: { mint: PublicKey; ata: PublicKey }; + let nftB: { mint: PublicKey; ata: PublicKey }; + let nftNotHeld: { mint: PublicKey; ata: PublicKey }; + let nftNoCollection: { mint: PublicKey; ata: PublicKey }; + let nftUnverified: { mint: PublicKey; ata: PublicKey }; + let nftWrongCollection: { mint: PublicKey; ata: PublicKey }; + let stakedAt: bigint; + + it('Test preparation - mints a collection and the NFTs each case needs', async () => { + client.airdrop(staker.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); + client.airdrop(otherUser.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); + + collectionMint = (await createNft({ owner: wallet.publicKey })).mint; + otherCollectionMint = (await createNft({ owner: wallet.publicKey })).mint; + + nftA = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: true }); + nftB = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: true }); + nftNoCollection = await createNft({ owner: staker.publicKey }); + nftUnverified = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: false }); + nftWrongCollection = await createNft({ + owner: staker.publicKey, + collection: otherCollectionMint, + verify: true, + }); + + // Held by the wallet, not the staker. The staker still gets an (empty) + // associated token account for it, which is the whole point of the + // "does not hold the NFT" case below. + const held = await createNft({ owner: wallet.publicKey, collection: collectionMint, verify: true }); + const emptyAta = getAssociatedTokenAddressSync(held.mint, staker.publicKey); + await provider.sendAndConfirm( + new Transaction().add( + createAssociatedTokenAccountInstruction(wallet.publicKey, emptyAta, staker.publicKey, held.mint), + ), + ); + nftNotHeld = { mint: held.mint, ata: emptyAta }; + + assert.strictEqual(tokenAccount(nftA.ata).amount, 1n, 'staker should hold nftA'); + assert.strictEqual(tokenAccount(nftNotHeld.ata).amount, 0n, 'staker should not hold nftNotHeld'); + }); + + it('Initializes the pool and its reward mint', async () => { + await program.methods + .initializeConfig(new anchor.BN(POINTS_PER_DAY), MAX_STAKE, FREEZE_PERIOD_DAYS, REWARD_DECIMALS) + .accountsPartial({ + admin: wallet.publicKey, + collectionMint, + config, + rewardsMint, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .rpc(); + + const configAccount = await program.account.stakeConfig.fetch(config); + assert.strictEqual(configAccount.collection.toBase58(), collectionMint.toBase58()); + assert.strictEqual(configAccount.pointsPerDay.toNumber(), POINTS_PER_DAY); + assert.strictEqual(configAccount.maxStake, MAX_STAKE); + assert.strictEqual(configAccount.freezePeriodDays, FREEZE_PERIOD_DAYS); + + // The reward mint must be controlled by the program, not the admin - + // otherwise the admin could mint rewards out of thin air. + const mintAccount = client.getAccount(rewardsMint)!; + assert.strictEqual(new PublicKey(mintAccount.data.subarray(4, 36)).toBase58(), config.toBase58()); + }); + + it('Initializes the user accounts', async () => { + await program.methods + .initializeUser() + .accountsPartial({ user: staker.publicKey, userAccount, systemProgram: SystemProgram.programId }) + .signers([staker]) + .rpc(); + + await program.methods + .initializeUser() + .accountsPartial({ + user: otherUser.publicKey, + userAccount: PublicKey.findProgramAddressSync( + [Buffer.from('user'), otherUser.publicKey.toBuffer()], + PROGRAM_ID, + )[0], + systemProgram: SystemProgram.programId, + }) + .signers([otherUser]) + .rpc(); + + const account = await program.account.userAccount.fetch(userAccount); + assert.strictEqual(account.amountStaked, 0); + assert.strictEqual(account.pointsEarned.toNumber(), 0); + }); + + // Both `approve` and the Metaplex freeze succeed on a zero-balance token + // account, so without an explicit balance check anyone could open an empty + // ATA for a collection NFT and farm rewards from an NFT they never owned. + it('Rejects staking an NFT the signer does not actually hold', async () => { + await expectAnchorError( + program.methods + .stake() + .accountsPartial(stakeAccounts(nftNotHeld.mint, nftNotHeld.ata)) + .signers([staker]) + .rpc(), + 'NftNotHeld', + ); + }); + + it('Rejects an NFT with no collection at all', async () => { + await expectAnchorError( + program.methods + .stake() + .accountsPartial(stakeAccounts(nftNoCollection.mint, nftNoCollection.ata)) + .signers([staker]) + .rpc(), + 'MissingCollection', + ); + }); + + it('Rejects an NFT whose collection is unverified', async () => { + await expectAnchorError( + program.methods + .stake() + .accountsPartial(stakeAccounts(nftUnverified.mint, nftUnverified.ata)) + .signers([staker]) + .rpc(), + 'UnverifiedCollection', + ); + }); + + it('Rejects an NFT from a different collection', async () => { + await expectAnchorError( + program.methods + .stake() + .accountsPartial(stakeAccounts(nftWrongCollection.mint, nftWrongCollection.ata)) + .signers([staker]) + .rpc(), + 'InvalidCollection', + ); + }); + + it('Stakes an NFT by freezing it in the owner wallet', async () => { + await program.methods.stake().accountsPartial(stakeAccounts(nftA.mint, nftA.ata)).signers([staker]).rpc(); + + const stake = await program.account.stakeAccount.fetch(stakePda(nftA.mint)); + stakedAt = BigInt(stake.stakedAt.toString()); + assert.strictEqual(stake.owner.toBase58(), staker.publicKey.toBase58()); + assert.strictEqual(stake.mint.toBase58(), nftA.mint.toBase58()); + assert.strictEqual( + stake.lastClaimedAt.toString(), + stake.stakedAt.toString(), + 'accrual must start at the moment of staking', + ); + + // The NFT is frozen but still owned by, and sitting in, the staker's + // own token account - the program never took custody of it. + const nft = tokenAccount(nftA.ata); + assert.strictEqual(nft.state, FROZEN, 'the NFT token account should be frozen'); + assert.strictEqual(nft.amount, 1n, 'the NFT should still be in the staker wallet'); + assert.strictEqual(nft.owner.toBase58(), staker.publicKey.toBase58()); + assert.strictEqual(nft.delegateOption, 1, 'the stake account should be the delegate'); + assert.strictEqual(nft.delegate.toBase58(), stakePda(nftA.mint).toBase58()); + + const account = await program.account.userAccount.fetch(userAccount); + assert.strictEqual(account.amountStaked, 1); + }); + + it('Enforces the per-user stake cap', async () => { + await expectAnchorError( + program.methods.stake().accountsPartial(stakeAccounts(nftB.mint, nftB.ata)).signers([staker]).rpc(), + 'MaxStakeReached', + ); + }); + + it('Pays nothing before a whole day has passed', async () => { + await expectAnchorError( + program.methods + .claim() + .accountsPartial(claimAccounts(nftA.mint, staker, userAccount)) + .signers([staker]) + .rpc(), + 'NothingToClaim', + ); + assert.strictEqual(rewardsBalance(), 0n, 'a rejected claim must not mint anything'); + }); + + it('Pays one day of rewards after one day', async () => { + warpDays(1); + + await program.methods + .claim() + .accountsPartial(claimAccounts(nftA.mint, staker, userAccount)) + .signers([staker]) + .rpc(); + + assert.strictEqual(rewardsBalance(), ONE_DAY_OF_REWARDS, 'one day staked should pay one day of rewards'); + + const stake = await program.account.stakeAccount.fetch(stakePda(nftA.mint)); + assert.strictEqual( + BigInt(stake.lastClaimedAt.toString()), + stakedAt + SECONDS_PER_DAY, + 'the checkpoint should advance by exactly the day that was paid for', + ); + + const account = await program.account.userAccount.fetch(userAccount); + assert.strictEqual(account.pointsEarned.toNumber(), POINTS_PER_DAY); + }); + + // The core hazard of any accrual-over-time program: if a payout read the + // elapsed time but failed to record that it had paid for it, this second + // call would pay for the same day again. + it('Refuses to pay the same day twice', async () => { + const balanceBefore = rewardsBalance(); + client.expireBlockhash(); + + await expectAnchorError( + program.methods + .claim() + .accountsPartial(claimAccounts(nftA.mint, staker, userAccount)) + .signers([staker]) + .rpc(), + 'NothingToClaim', + ); + + assert.strictEqual(rewardsBalance(), balanceBefore, 'a repeated claim must not mint a second payout'); + }); + + it("Rejects a claim against another user's stake position", async () => { + const otherUserAccount = PublicKey.findProgramAddressSync( + [Buffer.from('user'), otherUser.publicKey.toBuffer()], + PROGRAM_ID, + )[0]; + + await expectAnchorError( + program.methods + .claim() + .accountsPartial(claimAccounts(nftA.mint, otherUser, otherUserAccount)) + .signers([otherUser]) + .rpc(), + 'InvalidOwner', + ); + }); + + it('Refuses to unstake before the freeze period has elapsed', async () => { + // One day in, against a two-day freeze period. + await expectAnchorError( + program.methods + .unstake() + .accountsPartial({ + user: staker.publicKey, + nftMint: nftA.mint, + nftTokenAccount: nftA.ata, + metadata: metadataPda(nftA.mint), + edition: masterEditionPda(nftA.mint), + config, + rewardsMint, + rewardsTokenAccount: getAssociatedTokenAddressSync(rewardsMint, staker.publicKey), + stakeAccount: stakePda(nftA.mint), + userAccount, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + metadataProgram: TOKEN_METADATA_PROGRAM_ID, + }) + .signers([staker]) + .rpc(), + 'FreezePeriodNotPassed', + ); + + assert.strictEqual(tokenAccount(nftA.ata).state, FROZEN, 'a rejected unstake must leave the NFT frozen'); + }); + + // Only whole days pay out. If a claim snapped its checkpoint to `now` + // instead of advancing it by the days it actually paid for, the leftover + // half day here would be swallowed and the next claim would come up short. + it('Banks the part-day remainder instead of forfeiting it', async () => { + warpDays(1.5); + + await program.methods + .claim() + .accountsPartial(claimAccounts(nftA.mint, staker, userAccount)) + .signers([staker]) + .rpc(); + + assert.strictEqual(rewardsBalance(), 2n * ONE_DAY_OF_REWARDS, 'one and a half days should pay one day'); + + const stake = await program.account.stakeAccount.fetch(stakePda(nftA.mint)); + assert.strictEqual( + BigInt(stake.lastClaimedAt.toString()), + stakedAt + 2n * SECONDS_PER_DAY, + 'the checkpoint should sit on the day boundary, not on `now`', + ); + + // Only another half day passes, but combined with the banked remainder + // that is a full day - so it must pay out. + warpDays(0.5); + + await program.methods + .claim() + .accountsPartial(claimAccounts(nftA.mint, staker, userAccount)) + .signers([staker]) + .rpc(); + + assert.strictEqual( + rewardsBalance(), + 3n * ONE_DAY_OF_REWARDS, + 'the half day banked earlier should combine with this one and pay out', + ); + }); + + it('Unstakes, settling the final rewards and returning the NFT', async () => { + warpDays(1); + + const rentBefore = client.getBalance(staker.publicKey)!; + const stakeAccountLamports = client.getAccount(stakePda(nftA.mint))!.lamports; + + await program.methods + .unstake() + .accountsPartial({ + user: staker.publicKey, + nftMint: nftA.mint, + nftTokenAccount: nftA.ata, + metadata: metadataPda(nftA.mint), + edition: masterEditionPda(nftA.mint), + config, + rewardsMint, + rewardsTokenAccount: getAssociatedTokenAddressSync(rewardsMint, staker.publicKey), + stakeAccount: stakePda(nftA.mint), + userAccount, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + metadataProgram: TOKEN_METADATA_PROGRAM_ID, + }) + .signers([staker]) + .rpc(); + + // The last day is settled by the unstake itself, so nothing is lost by + // unstaking without claiming first. + assert.strictEqual(rewardsBalance(), 4n * ONE_DAY_OF_REWARDS, 'unstake should settle the outstanding day'); + + const nft = tokenAccount(nftA.ata); + assert.strictEqual(nft.state, INITIALIZED, 'the NFT should be thawed'); + assert.strictEqual(nft.amount, 1n, 'the NFT should still be in the staker wallet'); + assert.strictEqual(nft.delegateOption, 0, 'the delegate should be revoked'); + + assert.isNull(client.getAccount(stakePda(nftA.mint)), 'the stake account should be closed'); + assert.isAbove( + Number(client.getBalance(staker.publicKey)!), + Number(rentBefore), + 'closing the stake position should return its rent', + ); + assert.isAbove(stakeAccountLamports, 0, 'the stake account should have held rent while open'); + + const account = await program.account.userAccount.fetch(userAccount); + assert.strictEqual(account.amountStaked, 0); + assert.strictEqual(account.pointsEarned.toNumber(), 4 * POINTS_PER_DAY); + }); + + it('Can stake again after unstaking', async () => { + client.expireBlockhash(); + + await program.methods.stake().accountsPartial(stakeAccounts(nftB.mint, nftB.ata)).signers([staker]).rpc(); + + assert.strictEqual(tokenAccount(nftB.ata).state, FROZEN, 'the freed slot should allow a new stake'); + }); +}); diff --git a/tokens/nft-staking/anchor/tests/metaplex.ts b/tokens/nft-staking/anchor/tests/metaplex.ts new file mode 100644 index 000000000..8be35c76b --- /dev/null +++ b/tokens/nft-staking/anchor/tests/metaplex.ts @@ -0,0 +1,180 @@ +// Hand-rolled instructions for the Metaplex Token Metadata program. +// +// Built directly from the wire format (discriminator + borsh-encoded args + +// documented account order) so the tests need no `mpl-token-metadata` +// dependency, matching how the native examples in this repo do it. The program +// itself is loaded into litesvm from tests/fixtures/token_metadata.so. + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js'; + +export const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); + +// Variants of the Token Metadata program's instruction enum. +const CREATE_METADATA_ACCOUNT_V3 = 33; +const CREATE_MASTER_EDITION_V3 = 17; +const VERIFY_COLLECTION = 18; + +// --- minimal borsh writers ------------------------------------------------- + +const u8 = (n: number) => Buffer.from([n]); + +const u16 = (n: number) => { + const b = Buffer.alloc(2); + b.writeUInt16LE(n); + return b; +}; + +const u64 = (n: bigint) => { + const b = Buffer.alloc(8); + b.writeBigUInt64LE(n); + return b; +}; + +const borshString = (s: string) => { + const bytes = Buffer.from(s, 'utf8'); + const len = Buffer.alloc(4); + len.writeUInt32LE(bytes.length); + return Buffer.concat([len, bytes]); +}; + +/** `None` for any inner type is a single zero byte. */ +const NONE = u8(0); + +// --- PDAs ------------------------------------------------------------------ + +export const metadataPda = (mint: PublicKey) => + PublicKey.findProgramAddressSync( + [Buffer.from('metadata'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()], + TOKEN_METADATA_PROGRAM_ID, + )[0]; + +export const masterEditionPda = (mint: PublicKey) => + PublicKey.findProgramAddressSync( + [Buffer.from('metadata'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('edition')], + TOKEN_METADATA_PROGRAM_ID, + )[0]; + +// --- instructions ---------------------------------------------------------- + +/** + * `CreateMetadataAccountV3`: creates the metadata account for `mint`. + * + * When `collection` is given it is written with `verified: false` — Metaplex + * will not accept a self-asserted `true`. Verification is a separate, + * authority-signed step; see `verifyCollection`. + */ +export const createMetadataAccountV3 = ({ + mint, + mintAuthority, + payer, + updateAuthority, + name, + symbol, + uri, + collection, +}: { + mint: PublicKey; + mintAuthority: PublicKey; + payer: PublicKey; + updateAuthority: PublicKey; + name: string; + symbol: string; + uri: string; + collection?: PublicKey; +}): TransactionInstruction => { + const dataV2 = Buffer.concat([ + borshString(name), + borshString(symbol), + borshString(uri), + u16(0), // seller_fee_basis_points + NONE, // creators + collection ? Buffer.concat([u8(1), u8(0), collection.toBuffer()]) : NONE, + NONE, // uses + ]); + + const data = Buffer.concat([ + u8(CREATE_METADATA_ACCOUNT_V3), + dataV2, + u8(1), // is_mutable + NONE, // collection_details + ]); + + return new TransactionInstruction({ + programId: TOKEN_METADATA_PROGRAM_ID, + keys: [ + { pubkey: metadataPda(mint), isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: mintAuthority, isSigner: true, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: updateAuthority, isSigner: true, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data, + }); +}; + +/** + * `CreateMasterEditionV3`: marks the mint as an NFT and moves its mint and + * freeze authorities to the master edition PDA. + * + * This is what makes staking possible at all: the freeze authority has to be + * the master edition for Token Metadata to freeze the token account on a + * delegate's behalf. + */ +export const createMasterEditionV3 = ({ + mint, + updateAuthority, + mintAuthority, + payer, +}: { + mint: PublicKey; + updateAuthority: PublicKey; + mintAuthority: PublicKey; + payer: PublicKey; +}): TransactionInstruction => + new TransactionInstruction({ + programId: TOKEN_METADATA_PROGRAM_ID, + keys: [ + { pubkey: masterEditionPda(mint), isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: updateAuthority, isSigner: true, isWritable: false }, + { pubkey: mintAuthority, isSigner: true, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: metadataPda(mint), isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + // max_supply: Some(0) — a one-of-one, no prints. + data: Buffer.concat([u8(CREATE_MASTER_EDITION_V3), u8(1), u64(0n)]), + }); + +/** + * `VerifyCollection`: the collection's update authority signs to confirm that + * `mint` really belongs to `collectionMint`, flipping `collection.verified` + * to true. Until this runs, the collection field on an NFT is just a claim — + * which is exactly why the staking program checks the flag. + */ +export const verifyCollection = ({ + mint, + collectionMint, + collectionAuthority, + payer, +}: { + mint: PublicKey; + collectionMint: PublicKey; + collectionAuthority: PublicKey; + payer: PublicKey; +}): TransactionInstruction => + new TransactionInstruction({ + programId: TOKEN_METADATA_PROGRAM_ID, + keys: [ + { pubkey: metadataPda(mint), isSigner: false, isWritable: true }, + { pubkey: collectionAuthority, isSigner: true, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: collectionMint, isSigner: false, isWritable: false }, + { pubkey: metadataPda(collectionMint), isSigner: false, isWritable: true }, + { pubkey: masterEditionPda(collectionMint), isSigner: false, isWritable: false }, + ], + data: u8(VERIFY_COLLECTION), + }); diff --git a/tokens/nft-staking/anchor/tests/utils.ts b/tokens/nft-staking/anchor/tests/utils.ts new file mode 100644 index 000000000..3c2d49213 --- /dev/null +++ b/tokens/nft-staking/anchor/tests/utils.ts @@ -0,0 +1,14 @@ +import { assert } from 'chai'; + +// Asserts `promise` rejects with the given Anchor custom error code, not just +// "something failed". +export const expectAnchorError = async (promise: Promise, code: string) => { + let caught: any; + try { + await promise; + } catch (error) { + caught = error; + } + assert.isDefined(caught, `expected the transaction to fail with ${code}`); + assert.strictEqual(caught?.error?.errorCode?.code, code, `expected ${code}, got: ${caught}`); +}; diff --git a/tokens/nft-staking/anchor/tsconfig.json b/tokens/nft-staking/anchor/tsconfig.json new file mode 100644 index 000000000..c02443141 --- /dev/null +++ b/tokens/nft-staking/anchor/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai", "node"], + "typeRoots": ["./node_modules/@types"], + "lib": ["esnext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true + } +} From 937fdd707f75e26d7d1165a609fe4cd110c0bbb6 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 10 Sep 2026 03:36:21 -0700 Subject: [PATCH 2/3] fix(nft-staking): address review feedback and CI lockfile drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failure: pnpm-lock.yaml still listed @solana-program/token after the test switched to @solana/spl-token's AccountLayout, so --frozen-lockfile rejected the install. Lockfile regenerated. Review feedback: - Seed StakeConfig by its admin. A single ["config"] address was a land grab: whoever initialized first owned the only pool the program could ever have, with no update or close path. Pools are now per-operator, which also matches how token-fundraiser scopes its state. - Validate pool settings at creation. unstake settles rewards before it thaws the NFT, so a rate whose scaled payout can overflow u64 would make both claim and unstake revert and strand the NFT frozen with no recovery. initialize_config now rejects max_stake == 0, reward decimals above the SPL convention of 9, and any rate that cannot survive MAX_ACCRUAL_DAYS of accrual. - prepare.mjs no longer runs `solana config set -um`, which persistently repointed the machine's default cluster at mainnet as a side effect of installing. The cluster is passed per command instead. - prepare.mjs no longer swallows dump failures. It deletes the fixture before fetching, so a failed dump left no fixture and reported success, surfacing later as litesvm failing to open a missing file. Hit this during development; it now fails the install. Tests: 19 passing (two new — a second admin running their own pool, and rejection of settings that would strand a staked NFT). Verified the new validation test fails when the checks are removed. Co-Authored-By: Claude Opus 5 --- tokens/nft-staking/README.md | 8 +- tokens/nft-staking/anchor/pnpm-lock.yaml | 612 ------------------ tokens/nft-staking/anchor/prepare.mjs | 31 +- .../programs/nft-staking/src/constants.rs | 8 + .../anchor/programs/nft-staking/src/error.rs | 2 + .../nft-staking/src/instructions/claim.rs | 2 +- .../src/instructions/initialize_config.rs | 22 +- .../nft-staking/src/instructions/shared.rs | 3 +- .../nft-staking/src/instructions/stake.rs | 2 +- .../nft-staking/src/instructions/unstake.rs | 2 +- .../anchor/programs/nft-staking/src/lib.rs | 10 +- .../nft-staking/anchor/tests/litesvm.test.ts | 79 ++- 12 files changed, 142 insertions(+), 639 deletions(-) diff --git a/tokens/nft-staking/README.md b/tokens/nft-staking/README.md index de67de8db..fa2e4d665 100644 --- a/tokens/nft-staking/README.md +++ b/tokens/nft-staking/README.md @@ -44,13 +44,19 @@ this program mints under rules it sets itself, and none of it touches consensus. | Account | Seeds | Holds | | -------------- | ----------------------------- | -------------------------------------------------------------- | -| `StakeConfig` | `["config"]` | Collection, reward rate, stake cap, freeze period | +| `StakeConfig` | `["config", admin]` | Collection, reward rate, stake cap, freeze period | | `UserAccount` | `["user", user]` | Lifetime points earned, how many NFTs are currently staked | | `StakeAccount` | `["stake", nft_mint, config]` | Owner, mint, `staked_at`, and the `last_claimed_at` checkpoint | `StakeAccount` doubles as the SPL delegate for the staked NFT's token account, which is what lets the program freeze and thaw it. +Pools are seeded by their admin rather than living at a single `["config"]` +address, so anyone can run one and no one can take the only slot. +`initialize_config` also validates its own settings: `unstake` pays out before it thaws, +so a reward rate large enough to overflow `u64` would leave the NFT frozen with +no way to recover it. Those settings are rejected up front instead. + ## The part worth reading closely: paying for time, once Rewards here are a function of elapsed time rather than of a balance somebody diff --git a/tokens/nft-staking/anchor/pnpm-lock.yaml b/tokens/nft-staking/anchor/pnpm-lock.yaml index 3f582108c..fdbd6bee0 100644 --- a/tokens/nft-staking/anchor/pnpm-lock.yaml +++ b/tokens/nft-staking/anchor/pnpm-lock.yaml @@ -21,9 +21,6 @@ importers: specifier: ^1.98.4 version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) devDependencies: - '@solana-program/token': - specifier: ^0.6.0 - version: 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))) '@types/chai': specifier: ^5.2.3 version: 5.2.3 @@ -267,29 +264,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@solana-program/token@0.6.0': - resolution: {integrity: sha512-omkZh4Tt9rre4wzWHNOhOEHyenXQku3xyc/UrKvShexA/Qlhza67q7uRwmwEDUs4QqoDBidSZPooOmepnA/jig==} - peerDependencies: - '@solana/kit': ^3.0 - - '@solana/accounts@3.0.3': - resolution: {integrity: sha512-KqlePrlZaHXfu8YQTCxN204ZuVm9o68CCcUr6l27MG2cuRUtEM1Ta0iR8JFkRUAEfZJC4Cu0ZDjK/v49loXjZQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/addresses@3.0.3': - resolution: {integrity: sha512-AuMwKhJI89ANqiuJ/fawcwxNKkSeHH9CApZd2xelQQLS7X8uxAOovpcmEgiObQuiVP944s9ScGUT62Bdul9qYg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/assertions@3.0.3': - resolution: {integrity: sha512-2qspxdbWp2y62dfCIlqeWQr4g+hE8FYSSwcaP6itwMwGRb8393yDGCJfI/znuzJh6m/XVWhMHIgFgsBwnevCmg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/buffer-layout-utils@0.3.0': resolution: {integrity: sha512-MuQOCC1j0np1xH9yAv0ZWWfwvr7Bt7Sz4LId11Wi4wDdAmJ+lobE+vHg/mZmGcihF0BIkqVBNxGmlv8QE5DrtA==} engines: {node: '>= 10'} @@ -309,23 +283,11 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-core@3.0.3': - resolution: {integrity: sha512-emKykJ3h1DmnDOY29Uv9eJXP8E/FHzvlUBJ6te+5EbKdFjj7vdlKYPfDxOI6iGdXTY+YC/ELtbNBh6QwF2uEDQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-data-structures@2.0.0-rc.1': resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} peerDependencies: typescript: '>=5' - '@solana/codecs-data-structures@3.0.3': - resolution: {integrity: sha512-R15cLp8riJvToXziW8lP6AMSwsztGhEnwgyGmll32Mo0Yjq+hduW2/fJrA/TJs6tA/OgTzMQjlxgk009EqZHCw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-numbers@2.0.0-rc.1': resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} peerDependencies: @@ -337,36 +299,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-numbers@3.0.3': - resolution: {integrity: sha512-pfXkH9J0glrM8qj6389GAn30+cJOxzXLR2FsPOHCUMXrqLhGjMMZAWhsQkpOQ37SGc/7EiQsT/gmyGC7gxHqJQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-strings@2.0.0-rc.1': resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} peerDependencies: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5' - '@solana/codecs-strings@3.0.3': - resolution: {integrity: sha512-VHBXnnTVtcQ1j+7Vrz+qSYo38no+jiHRdGnhFspRXEHNJbllzwKqgBE7YN3qoIXH+MKxgJUcwO5KHmdzf8Wn2A==} - engines: {node: '>=20.18.0'} - peerDependencies: - fastestsmallesttextencoderdecoder: ^1.0.22 - typescript: '>=5.3.3' - '@solana/codecs@2.0.0-rc.1': resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} peerDependencies: typescript: '>=5' - '@solana/codecs@3.0.3': - resolution: {integrity: sha512-GOHwTlIQsCoJx9Ryr6cEf0FHKAQ7pY4aO4xgncAftrv0lveTQ1rPP2inQ1QT0gJllsIa8nwbfXAADs9nNJxQDA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/errors@2.0.0-rc.1': resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} hasBin: true @@ -380,157 +323,11 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/errors@3.0.3': - resolution: {integrity: sha512-1l84xJlHNva6io62PcYfUamwWlc0eM95nHgCrKX0g0cLoC6D6QHYPCEbEVkR+C5UtP9JDgyQM8MFiv+Ei5tO9Q==} - engines: {node: '>=20.18.0'} - hasBin: true - peerDependencies: - typescript: '>=5.3.3' - - '@solana/fast-stable-stringify@3.0.3': - resolution: {integrity: sha512-ED0pxB6lSEYvg+vOd5hcuQrgzEDnOrURFgp1ZOY+lQhJkQU6xo+P829NcJZQVP1rdU2/YQPAKJKEseyfe9VMIw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/functional@3.0.3': - resolution: {integrity: sha512-2qX1kKANn8995vOOh5S9AmF4ItGZcfbny0w28Eqy8AFh+GMnSDN4gqpmV2LvxBI9HibXZptGH3RVOMk82h1Mpw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/instruction-plans@3.0.3': - resolution: {integrity: sha512-eqoaPtWtmLTTpdvbt4BZF5H6FIlJtXi9H7qLOM1dLYonkOX2Ncezx5NDCZ9tMb2qxVMF4IocYsQnNSnMfjQF1w==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/instructions@3.0.3': - resolution: {integrity: sha512-4csIi8YUDb5j/J+gDzmYtOvq7ZWLbCxj4t0xKn+fPrBk/FD2pK29KVT3Fu7j4Lh1/ojunQUP9X4NHwUexY3PnA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/keys@3.0.3': - resolution: {integrity: sha512-tp8oK9tMadtSIc4vF4aXXWkPd4oU5XPW8nf28NgrGDWGt25fUHIydKjkf2hPtMt9i1WfRyQZ33B5P3dnsNqcPQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/kit@3.0.3': - resolution: {integrity: sha512-CEEhCDmkvztd1zbgADsEQhmj9GyWOOGeW1hZD+gtwbBSF5YN1uofS/pex5MIh/VIqKRj+A2UnYWI1V+9+q/lyQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/nominal-types@3.0.3': - resolution: {integrity: sha512-aZavCiexeUAoMHRQg4s1AHkH3wscbOb70diyfjhwZVgFz1uUsFez7csPp9tNFkNolnadVb2gky7yBk3IImQJ6A==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/options@2.0.0-rc.1': resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} peerDependencies: typescript: '>=5' - '@solana/options@3.0.3': - resolution: {integrity: sha512-jarsmnQ63RN0JPC5j9sgUat07NrL9PC71XU7pUItd6LOHtu4+wJMio3l5mT0DHVfkfbFLL6iI6+QmXSVhTNF3g==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/programs@3.0.3': - resolution: {integrity: sha512-JZlVE3/AeSNDuH3aEzCZoDu8GTXkMpGXxf93zXLzbxfxhiQ/kHrReN4XE/JWZ/uGWbaFZGR5B3UtdN2QsoZL7w==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/promises@3.0.3': - resolution: {integrity: sha512-K+UflGBVxj30XQMHTylHHZJdKH5QG3oj5k2s42GrZ/Wbu72oapVJySMBgpK45+p90t8/LEqV6rRPyTXlet9J+Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-api@3.0.3': - resolution: {integrity: sha512-Yym9/Ama62OY69rAZgbOCAy1QlqaWAyb0VlqFuwSaZV1pkFCCFSwWEJEsiN1n8pb2ZP+RtwNvmYixvWizx9yvA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-parsed-types@3.0.3': - resolution: {integrity: sha512-/koM05IM2fU91kYDQxXil3VBNlOfcP+gXE0js1sdGz8KonGuLsF61CiKB5xt6u1KEXhRyDdXYLjf63JarL4Ozg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-spec-types@3.0.3': - resolution: {integrity: sha512-A6Jt8SRRetnN3CeGAvGJxigA9zYRslGgWcSjueAZGvPX+MesFxEUjSWZCfl+FogVFvwkqfkgQZQbPAGZQFJQ6Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-spec@3.0.3': - resolution: {integrity: sha512-MZn5/8BebB6MQ4Gstw6zyfWsFAZYAyLzMK+AUf/rSfT8tPmWiJ/mcxnxqOXvFup/l6D67U8pyGpIoFqwCeZqqA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-subscriptions-api@3.0.3': - resolution: {integrity: sha512-MGgVK3PUS15qsjuhimpzGZrKD/CTTvS0mAlQ0Jw84zsr1RJVdQJK/F0igu07BVd172eTZL8d90NoAQ3dahW5pA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-subscriptions-channel-websocket@3.0.3': - resolution: {integrity: sha512-zUzUlb8Cwnw+SHlsLrSqyBRtOJKGc+FvSNJo/vWAkLShoV0wUDMPv7VvhTngJx3B/3ANfrOZ4i08i9QfYPAvpQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - ws: ^8.18.0 - - '@solana/rpc-subscriptions-spec@3.0.3': - resolution: {integrity: sha512-9KpQ32OBJWS85mn6q3gkM0AjQe1LKYlMU7gpJRrla/lvXxNLhI95tz5K6StctpUreVmRWTVkNamHE69uUQyY8A==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-subscriptions@3.0.3': - resolution: {integrity: sha512-LRvz6NaqvtsYFd32KwZ+rwYQ9XCs+DWjV8BvBLsJpt9/NWSuHf/7Sy/vvP6qtKxut692H/TMvHnC4iulg0WmiQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-transformers@3.0.3': - resolution: {integrity: sha512-lzdaZM/dG3s19Tsk4mkJA5JBoS1eX9DnD7z62gkDwrwJDkDBzkAJT9aLcsYFfTmwTfIp6uU2UPgGYc97i1wezw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-transport-http@3.0.3': - resolution: {integrity: sha512-bIXFwr2LR5A97Z46dI661MJPbHnPfcShBjFzOS/8Rnr8P4ho3j/9EUtjDrsqoxGJT3SLWj5OlyXAlaDAvVTOUQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc-types@3.0.3': - resolution: {integrity: sha512-petWQ5xSny9UfmC3Qp2owyhNU0w9SyBww4+v7tSVyXMcCC9v6j/XsqTeimH1S0qQUllnv0/FY83ohFaxofmZ6Q==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/rpc@3.0.3': - resolution: {integrity: sha512-3oukAaLK78GegkKcm6iNmRnO4mFeNz+BMvA8T56oizoBNKiRVEq/6DFzVX/LkmZ+wvD601pAB3uCdrTPcC0YKQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/signers@3.0.3': - resolution: {integrity: sha512-UwCd/uPYTZiwd283JKVyOWLLN5sIgMBqGDyUmNU3vo9hcmXKv5ZGm/9TvwMY2z35sXWuIOcj7etxJ8OoWc/ObQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/spl-token-group@0.0.7': resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} engines: {node: '>=16'} @@ -549,36 +346,6 @@ packages: peerDependencies: '@solana/web3.js': ^1.95.5 - '@solana/subscribable@3.0.3': - resolution: {integrity: sha512-FJ27LKGHLQ5GGttPvTOLQDLrrOZEgvaJhB7yYaHAhPk25+p+erBaQpjePhfkMyUbL1FQbxn1SUJmS6jUuaPjlQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/sysvars@3.0.3': - resolution: {integrity: sha512-GnHew+QeKCs2f9ow+20swEJMH4mDfJA/QhtPgOPTYQx/z69J4IieYJ7fZenSHnA//lJ45fVdNdmy1trypvPLBQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transaction-confirmation@3.0.3': - resolution: {integrity: sha512-dXx0OLtR95LMuARgi2dDQlL1QYmk56DOou5q9wKymmeV3JTvfDExeWXnOgjRBBq/dEfj4ugN1aZuTaS18UirFw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transaction-messages@3.0.3': - resolution: {integrity: sha512-s+6NWRnBhnnjFWV4x2tzBzoWa6e5LiIxIvJlWwVQBFkc8fMGY04w7jkFh0PM08t/QFKeXBEWkyBDa/TFYdkWug==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - - '@solana/transactions@3.0.3': - resolution: {integrity: sha512-iMX+n9j4ON7H1nKlWEbMqMOpKYC6yVGxKKmWHT1KdLRG7v+03I4DnDeFoI+Zmw56FA+7Bbne8jwwX60Q1vk/MQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/web3.js@1.98.4': resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} @@ -723,10 +490,6 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} - commander@14.0.0: - resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} - engines: {node: '>=20'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1101,9 +864,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.29.1: - resolution: {integrity: sha512-2xO3p6gANwOmhSFwNy2MMYISbHTtM+K3bY0bqpD1gOGpUPIe+yFXCobxbz0s2jm7E0faMxIFcZUcCKa+N6Ozlg==} - undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} @@ -1351,38 +1111,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)))': - dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - - '@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/addresses@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 3.0.3(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/assertions@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - '@solana/buffer-layout-utils@0.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -1409,11 +1137,6 @@ snapshots: '@solana/errors': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - '@solana/codecs-core@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) @@ -1421,13 +1144,6 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.9.3) typescript: 5.9.3 - '@solana/codecs-data-structures@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) @@ -1440,12 +1156,6 @@ snapshots: '@solana/errors': 2.3.0(typescript@5.9.3) typescript: 5.9.3 - '@solana/codecs-numbers@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) @@ -1454,14 +1164,6 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.3 - '@solana/codecs-strings@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 - '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) @@ -1473,17 +1175,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/codecs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/options': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/errors@2.0.0-rc.1(typescript@5.9.3)': dependencies: chalk: 5.6.2 @@ -1496,78 +1187,6 @@ snapshots: commander: 14.0.3 typescript: 5.9.3 - '@solana/errors@3.0.3(typescript@5.9.3)': - dependencies: - chalk: 5.6.2 - commander: 14.0.0 - typescript: 5.9.3 - - '@solana/fast-stable-stringify@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/functional@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/instruction-plans@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/instructions@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/keys@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/assertions': 3.0.3(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': - dependencies: - '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/nominal-types@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3) @@ -1579,168 +1198,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/options@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/programs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/promises@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/rpc-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/rpc-parsed-types@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/rpc-spec-types@3.0.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@solana/rpc-spec@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/rpc-subscriptions-api@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6) - - '@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/rpc-transformers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/rpc-transport-http@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - undici-types: 7.29.1 - - '@solana/rpc-types@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/rpc@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/rpc-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 3.0.3(typescript@5.9.3) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-transport-http': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/signers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -1772,71 +1229,6 @@ snapshots: - typescript - utf-8-validate - '@solana/subscribable@3.0.3(typescript@5.9.3)': - dependencies: - '@solana/errors': 3.0.3(typescript@5.9.3) - typescript: 5.9.3 - - '@solana/sysvars@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 3.0.3(typescript@5.9.3) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - - '@solana/transaction-messages@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - '@solana/transactions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 3.0.3(typescript@5.9.3) - '@solana/codecs-data-structures': 3.0.3(typescript@5.9.3) - '@solana/codecs-numbers': 3.0.3(typescript@5.9.3) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 3.0.3(typescript@5.9.3) - '@solana/functional': 3.0.3(typescript@5.9.3) - '@solana/instructions': 3.0.3(typescript@5.9.3) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@babel/runtime': 7.29.7 @@ -2000,8 +1392,6 @@ snapshots: commander@12.1.0: {} - commander@14.0.0: {} - commander@14.0.3: {} commander@2.20.3: {} @@ -2368,8 +1758,6 @@ snapshots: typescript@5.9.3: {} - undici-types@7.29.1: {} - undici-types@8.9.0: {} utf-8-validate@6.0.6: diff --git a/tokens/nft-staking/anchor/prepare.mjs b/tokens/nft-staking/anchor/prepare.mjs index af71cee7e..cfa85603c 100644 --- a/tokens/nft-staking/anchor/prepare.mjs +++ b/tokens/nft-staking/anchor/prepare.mjs @@ -12,23 +12,20 @@ const programs = [ ]; const outputDir = 'tests/fixtures'; -const overwrite = true; -try { - for (const program of programs) { - const { id, name } = program; - const outputFile = join(outputDir, name); - await $`solana config set -um`; +// The cluster is passed per command rather than via `solana config set`, so +// installing this example never changes the machine's default cluster. +const cluster = 'https://api.mainnet-beta.solana.com'; - try { - await mkdir(outputDir, { recursive: true }); - if (overwrite) await rm(outputFile, { force: true }); - await $`solana program dump ${id} ${outputFile}`; - console.log(`Program ${id} dumped to ${outputFile}`); - } catch (error) { - console.error(`Error dumping ${id}: ${error.message}`); - } - } -} catch (error) { - console.error(`Unexpected error: ${error.message}`); +for (const { id, name } of programs) { + const outputFile = join(outputDir, name); + + await mkdir(outputDir, { recursive: true }); + await rm(outputFile, { force: true }); + + // No try/catch: the tests load these fixtures unconditionally, so a failed + // dump has to fail the install rather than surface later as litesvm + // refusing to open a missing file. + await $`solana program dump ${id} ${outputFile} --url ${cluster}`; + console.log(`Program ${id} dumped to ${outputFile}`); } diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs index 0036b9538..94d3dfc32 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/constants.rs @@ -2,3 +2,11 @@ pub const ANCHOR_DISCRIMINATOR: usize = 8; /// Rewards accrue per whole day staked. pub const SECONDS_PER_DAY: i64 = 86_400; + +/// The most decimals a pool may give its reward mint, matching the SPL convention. +pub const MAX_REWARD_DECIMALS: u8 = 9; + +/// A pool must stay solvent in `u64` for at least this long. `unstake` pays out +/// before it thaws, so a reward rate that can overflow would strand the NFT +/// frozen with no way to recover it — the bound is enforced at config time. +pub const MAX_ACCRUAL_DAYS: u64 = 365 * 100; diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs index c6c1bbf8c..3780983c4 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/error.rs @@ -20,4 +20,6 @@ pub enum StakeError { NothingToClaim, #[msg("Arithmetic overflow")] Overflow, + #[msg("Pool settings would let rewards overflow, or allow no stakes at all")] + InvalidConfig, } diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs index aa8116e98..eec7a5d1f 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs @@ -17,7 +17,7 @@ pub struct Claim<'info> { pub nft_mint: Account<'info, Mint>, #[account( - seeds = [b"config"], + seeds = [b"config", config.admin.as_ref()], bump = config.bump, )] pub config: Account<'info, StakeConfig>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs index 34b8d28a4..3527822d4 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_config.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Mint, Token}; -use crate::{StakeConfig, ANCHOR_DISCRIMINATOR}; +use crate::{StakeConfig, StakeError, ANCHOR_DISCRIMINATOR, MAX_ACCRUAL_DAYS, MAX_REWARD_DECIMALS}; #[derive(Accounts)] #[instruction(points_per_day: u64, max_stake: u8, freeze_period_days: u32, reward_decimals: u8)] @@ -12,11 +12,14 @@ pub struct InitializeConfig<'info> { /// The collection every staked NFT must belong to. pub collection_mint: Account<'info, Mint>, + /// Seeded by `admin`, so pools are per-operator. A single `["config"]` + /// address would be a land grab: whoever called first would own the only + /// pool the program can ever have. #[account( init, payer = admin, space = ANCHOR_DISCRIMINATOR + StakeConfig::INIT_SPACE, - seeds = [b"config"], + seeds = [b"config", admin.key().as_ref()], bump, )] pub config: Account<'info, StakeConfig>, @@ -43,8 +46,23 @@ impl<'info> InitializeConfig<'info> { points_per_day: u64, max_stake: u8, freeze_period_days: u32, + reward_decimals: u8, bumps: &InitializeConfigBumps, ) -> Result<()> { + require!(max_stake > 0, StakeError::InvalidConfig); + require!(reward_decimals <= MAX_REWARD_DECIMALS, StakeError::InvalidConfig); + + // `unstake` settles rewards before it thaws the NFT, so a rate that can + // overflow would leave the NFT frozen with no way out. Reject any pool + // whose payout cannot survive `MAX_ACCRUAL_DAYS` of accrual. + require!( + points_per_day + .checked_mul(10u64.pow(reward_decimals as u32)) + .and_then(|scaled| scaled.checked_mul(MAX_ACCRUAL_DAYS)) + .is_some(), + StakeError::InvalidConfig + ); + self.config.set_inner(StakeConfig { admin: self.admin.key(), collection: self.collection_mint.key(), diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs index 4ee27910a..0b51c6c2c 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/shared.rs @@ -51,7 +51,8 @@ pub fn mint_reward_tokens<'info>( .ok_or(StakeError::Overflow)?; // The config PDA is the reward mint's authority, so the program signs here. - let seeds = &[b"config".as_ref(), &[config.bump]]; + let admin = config.admin; + let seeds = &[b"config".as_ref(), admin.as_ref(), &[config.bump]]; let signer_seeds = &[&seeds[..]]; mint_to( diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs index df99ae140..a32e149d0 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs @@ -41,7 +41,7 @@ pub struct Stake<'info> { pub edition: Account<'info, MasterEditionAccount>, #[account( - seeds = [b"config"], + seeds = [b"config", config.admin.as_ref()], bump = config.bump, )] pub config: Account<'info, StakeConfig>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs index b7cb47f29..4ebde472d 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs @@ -42,7 +42,7 @@ pub struct Unstake<'info> { pub edition: Box>, #[account( - seeds = [b"config"], + seeds = [b"config", config.admin.as_ref()], bump = config.bump, )] pub config: Account<'info, StakeConfig>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs index 8529054de..99d54668c 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/lib.rs @@ -25,9 +25,15 @@ pub mod nft_staking { points_per_day: u64, max_stake: u8, freeze_period_days: u32, - _reward_decimals: u8, + reward_decimals: u8, ) -> Result<()> { - context.accounts.initialize_config(points_per_day, max_stake, freeze_period_days, &context.bumps) + context.accounts.initialize_config( + points_per_day, + max_stake, + freeze_period_days, + reward_decimals, + &context.bumps, + ) } /// Creates the caller's per-user totals account. Called once per user. diff --git a/tokens/nft-staking/anchor/tests/litesvm.test.ts b/tokens/nft-staking/anchor/tests/litesvm.test.ts index 4d4a85c7e..e3b15ef68 100644 --- a/tokens/nft-staking/anchor/tests/litesvm.test.ts +++ b/tokens/nft-staking/anchor/tests/litesvm.test.ts @@ -53,8 +53,13 @@ describe('nft-staking litesvm', () => { // actually staking, so the tests exercise a real second signer. const staker = Keypair.generate(); const otherUser = Keypair.generate(); + const otherAdmin = Keypair.generate(); - const config = PublicKey.findProgramAddressSync([Buffer.from('config')], PROGRAM_ID)[0]; + // Pools are seeded by their admin, so the derivation includes the wallet. + const config = PublicKey.findProgramAddressSync( + [Buffer.from('config'), wallet.publicKey.toBuffer()], + PROGRAM_ID, + )[0]; const rewardsMint = PublicKey.findProgramAddressSync([Buffer.from('rewards'), config.toBuffer()], PROGRAM_ID)[0]; const userAccount = PublicKey.findProgramAddressSync( [Buffer.from('user'), staker.publicKey.toBuffer()], @@ -185,6 +190,7 @@ describe('nft-staking litesvm', () => { it('Test preparation - mints a collection and the NFTs each case needs', async () => { client.airdrop(staker.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); client.airdrop(otherUser.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); + client.airdrop(otherAdmin.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); collectionMint = (await createNft({ owner: wallet.publicKey })).mint; otherCollectionMint = (await createNft({ owner: wallet.publicKey })).mint; @@ -240,6 +246,77 @@ describe('nft-staking litesvm', () => { assert.strictEqual(new PublicKey(mintAccount.data.subarray(4, 36)).toBase58(), config.toBase58()); }); + // A pool is seeded by its admin, so initializing one never blocks anyone + // else from running their own. + it('Lets a second admin run their own pool', async () => { + const otherConfig = PublicKey.findProgramAddressSync( + [Buffer.from('config'), otherAdmin.publicKey.toBuffer()], + PROGRAM_ID, + )[0]; + + await program.methods + .initializeConfig(new anchor.BN(POINTS_PER_DAY), MAX_STAKE, FREEZE_PERIOD_DAYS, REWARD_DECIMALS) + .accountsPartial({ + admin: otherAdmin.publicKey, + collectionMint, + config: otherConfig, + rewardsMint: PublicKey.findProgramAddressSync( + [Buffer.from('rewards'), otherConfig.toBuffer()], + PROGRAM_ID, + )[0], + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([otherAdmin]) + .rpc(); + + const account = await program.account.stakeConfig.fetch(otherConfig); + assert.strictEqual(account.admin.toBase58(), otherAdmin.publicKey.toBase58()); + }); + + // `unstake` settles rewards before it thaws, so a rate that can overflow + // would strand the NFT frozen. These have to be rejected up front. + it('Rejects pool settings that would strand a staked NFT', async () => { + const badAdmin = Keypair.generate(); + client.airdrop(badAdmin.publicKey, BigInt(10 * LAMPORTS_PER_SOL)); + + const badConfig = PublicKey.findProgramAddressSync( + [Buffer.from('config'), badAdmin.publicKey.toBuffer()], + PROGRAM_ID, + )[0]; + const badRewardsMint = PublicKey.findProgramAddressSync( + [Buffer.from('rewards'), badConfig.toBuffer()], + PROGRAM_ID, + )[0]; + + const initWith = (pointsPerDay: number | bigint, maxStake: number, decimals: number) => + program.methods + .initializeConfig(new anchor.BN(pointsPerDay.toString()), maxStake, FREEZE_PERIOD_DAYS, decimals) + .accountsPartial({ + admin: badAdmin.publicKey, + collectionMint, + config: badConfig, + rewardsMint: badRewardsMint, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([badAdmin]) + .rpc(); + + // A pool nobody can stake in. + await expectAnchorError(initWith(POINTS_PER_DAY, 0, REWARD_DECIMALS), 'InvalidConfig'); + + client.expireBlockhash(); + // More decimals than an SPL mint conventionally carries. + await expectAnchorError(initWith(POINTS_PER_DAY, MAX_STAKE, 20), 'InvalidConfig'); + + client.expireBlockhash(); + // Scaled payout that overflows u64 well inside the pool's lifetime. + await expectAnchorError(initWith(2n ** 60n, MAX_STAKE, REWARD_DECIMALS), 'InvalidConfig'); + + assert.isNull(client.getAccount(badConfig), 'no config should have been created'); + }); + it('Initializes the user accounts', async () => { await program.methods .initializeUser() From 9199d2b1ea4f68606a52f17f5074a119501b9cd1 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 10 Sep 2026 04:25:07 -0700 Subject: [PATCH 3/3] fix(nft-staking): scope UserAccount to its pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the per-admin config change: UserAccount was still seeded on ["user", user] alone, so the stake cap and points total were shared across every pool. A user at max_stake in one pool was wrongly blocked from staking in another, and pools could not enforce their limits independently — a regression introduced by making configs per-admin. UserAccount is now seeded ["user", config, user], and initialize_user takes the pool it belongs to. New test "Enforces stake caps per pool, not globally" stakes into a second admin's pool while already at the cap in the first, and asserts each pool tracks its own count. Verified it fails with the old global seeds. 20 passing. Co-Authored-By: Claude Opus 5 --- tokens/nft-staking/README.md | 10 +- .../nft-staking/src/instructions/claim.rs | 2 +- .../src/instructions/initialize_user.rs | 13 ++- .../nft-staking/src/instructions/stake.rs | 2 +- .../nft-staking/src/instructions/unstake.rs | 2 +- .../nft-staking/src/state/user_account.rs | 11 +- .../nft-staking/anchor/tests/litesvm.test.ts | 101 ++++++++++++++---- 7 files changed, 105 insertions(+), 36 deletions(-) diff --git a/tokens/nft-staking/README.md b/tokens/nft-staking/README.md index fa2e4d665..15dbc518e 100644 --- a/tokens/nft-staking/README.md +++ b/tokens/nft-staking/README.md @@ -35,7 +35,7 @@ this program mints under rules it sets itself, and none of it touches consensus. | Instruction | What it does | | ------------------- | ----------------------------------------------------------------------------------------- | | `initialize_config` | Creates the pool and its reward mint, whose mint authority is the config PDA | -| `initialize_user` | Creates the caller's per-user totals account | +| `initialize_user` | Creates the caller's totals account for one pool | | `stake` | Verifies collection membership, delegates the token account to a PDA, and freezes it | | `claim` | Pays out rewards accrued since the last claim — callable while the NFT is still staked | | `unstake` | Settles any outstanding rewards, thaws and un-delegates the NFT, closes the stake account | @@ -45,14 +45,16 @@ this program mints under rules it sets itself, and none of it touches consensus. | Account | Seeds | Holds | | -------------- | ----------------------------- | -------------------------------------------------------------- | | `StakeConfig` | `["config", admin]` | Collection, reward rate, stake cap, freeze period | -| `UserAccount` | `["user", user]` | Lifetime points earned, how many NFTs are currently staked | +| `UserAccount` | `["user", config, user]` | Lifetime points earned, how many NFTs are currently staked | | `StakeAccount` | `["stake", nft_mint, config]` | Owner, mint, `staked_at`, and the `last_claimed_at` checkpoint | `StakeAccount` doubles as the SPL delegate for the staked NFT's token account, which is what lets the program freeze and thaw it. Pools are seeded by their admin rather than living at a single `["config"]` -address, so anyone can run one and no one can take the only slot. +address, so anyone can run one and no one can take the only slot. `UserAccount` +is scoped to its pool for the same reason — the stake cap and points total +belong to one pool, so hitting the cap in one does not lock a user out of another. `initialize_config` also validates its own settings: `unstake` pays out before it thaws, so a reward rate large enough to overflow `u64` would leave the NFT frozen with no way to recover it. Those settings are rejected up front instead. @@ -114,7 +116,7 @@ the freeze and thaw paths are exercised for real rather than mocked. ## Notes -- `max_stake` caps how many NFTs one user may stake at once. +- `max_stake` caps how many NFTs one user may stake at once, per pool. - `freeze_period_days` is a minimum staking duration; `unstake` rejects until it has elapsed. Rewards still accrue and can be claimed during it. - Reward token decimals are set when the pool is created, and points are scaled diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs index eec7a5d1f..5ee202c09 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/claim.rs @@ -47,7 +47,7 @@ pub struct Claim<'info> { #[account( mut, - seeds = [b"user", user.key().as_ref()], + seeds = [b"user", config.key().as_ref(), user.key().as_ref()], bump = user_account.bump, )] pub user_account: Account<'info, UserAccount>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs index 9a9002987..bc6c93fc7 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/initialize_user.rs @@ -1,17 +1,26 @@ use anchor_lang::prelude::*; -use crate::{UserAccount, ANCHOR_DISCRIMINATOR}; +use crate::{StakeConfig, UserAccount, ANCHOR_DISCRIMINATOR}; #[derive(Accounts)] pub struct InitializeUser<'info> { #[account(mut)] pub user: Signer<'info>, + #[account( + seeds = [b"config", config.admin.as_ref()], + bump = config.bump, + )] + pub config: Account<'info, StakeConfig>, + + /// Scoped to the pool as well as the user: the stake cap and the points + /// total belong to one pool, so a user hitting the cap in one must not + /// affect their standing in another. #[account( init, payer = user, space = ANCHOR_DISCRIMINATOR + UserAccount::INIT_SPACE, - seeds = [b"user", user.key().as_ref()], + seeds = [b"user", config.key().as_ref(), user.key().as_ref()], bump, )] pub user_account: Account<'info, UserAccount>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs index a32e149d0..20ba962d0 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/stake.rs @@ -57,7 +57,7 @@ pub struct Stake<'info> { #[account( mut, - seeds = [b"user", user.key().as_ref()], + seeds = [b"user", config.key().as_ref(), user.key().as_ref()], bump = user_account.bump, )] pub user_account: Account<'info, UserAccount>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs index 4ebde472d..8c82a7025 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/instructions/unstake.rs @@ -76,7 +76,7 @@ pub struct Unstake<'info> { #[account( mut, - seeds = [b"user", user.key().as_ref()], + seeds = [b"user", config.key().as_ref(), user.key().as_ref()], bump = user_account.bump, )] pub user_account: Account<'info, UserAccount>, diff --git a/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs b/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs index 7ef833c0b..38aae37d7 100644 --- a/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs +++ b/tokens/nft-staking/anchor/programs/nft-staking/src/state/user_account.rs @@ -1,16 +1,17 @@ use anchor_lang::prelude::*; -/// Per-user totals, at seeds `["user", user]`. Created once and reused across -/// every NFT that user stakes. +/// Per-user totals within one pool, at seeds `["user", config, user]`. +/// Created once per pool and reused across every NFT that user stakes there. #[account] #[derive(InitSpace)] pub struct UserAccount { - /// Lifetime points paid out to this user, across all their stake positions. + /// Lifetime points paid out to this user in this pool, across all their + /// stake positions in it. /// A running total for display; the authoritative per-NFT accrual state /// lives on each `StakeAccount`. pub points_earned: u64, - /// How many NFTs this user currently has staked, checked against - /// `StakeConfig::max_stake`. + /// How many NFTs this user currently has staked in this pool, checked + /// against `StakeConfig::max_stake`. pub amount_staked: u8, pub bump: u8, } diff --git a/tokens/nft-staking/anchor/tests/litesvm.test.ts b/tokens/nft-staking/anchor/tests/litesvm.test.ts index e3b15ef68..b4ace90bb 100644 --- a/tokens/nft-staking/anchor/tests/litesvm.test.ts +++ b/tokens/nft-staking/anchor/tests/litesvm.test.ts @@ -61,13 +61,28 @@ describe('nft-staking litesvm', () => { PROGRAM_ID, )[0]; const rewardsMint = PublicKey.findProgramAddressSync([Buffer.from('rewards'), config.toBuffer()], PROGRAM_ID)[0]; - const userAccount = PublicKey.findProgramAddressSync( - [Buffer.from('user'), staker.publicKey.toBuffer()], + const userPda = (poolConfig: PublicKey, owner: PublicKey) => + PublicKey.findProgramAddressSync([Buffer.from('user'), poolConfig.toBuffer(), owner.toBuffer()], PROGRAM_ID)[0]; + + const userAccount = userPda(config, staker.publicKey); + + // A second pool under a different admin, used to prove pools are isolated. + const otherConfig = PublicKey.findProgramAddressSync( + [Buffer.from('config'), otherAdmin.publicKey.toBuffer()], + PROGRAM_ID, + )[0]; + const otherRewardsMint = PublicKey.findProgramAddressSync( + [Buffer.from('rewards'), otherConfig.toBuffer()], PROGRAM_ID, )[0]; - const stakePda = (nftMint: PublicKey) => - PublicKey.findProgramAddressSync([Buffer.from('stake'), nftMint.toBuffer(), config.toBuffer()], PROGRAM_ID)[0]; + const stakePdaIn = (nftMint: PublicKey, poolConfig: PublicKey) => + PublicKey.findProgramAddressSync( + [Buffer.from('stake'), nftMint.toBuffer(), poolConfig.toBuffer()], + PROGRAM_ID, + )[0]; + + const stakePda = (nftMint: PublicKey) => stakePdaIn(nftMint, config); const tokenAccount = (address: PublicKey) => AccountLayout.decode(client.getAccount(address)!.data); @@ -181,6 +196,7 @@ describe('nft-staking litesvm', () => { let otherCollectionMint: PublicKey; let nftA: { mint: PublicKey; ata: PublicKey }; let nftB: { mint: PublicKey; ata: PublicKey }; + let nftC: { mint: PublicKey; ata: PublicKey }; let nftNotHeld: { mint: PublicKey; ata: PublicKey }; let nftNoCollection: { mint: PublicKey; ata: PublicKey }; let nftUnverified: { mint: PublicKey; ata: PublicKey }; @@ -197,6 +213,7 @@ describe('nft-staking litesvm', () => { nftA = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: true }); nftB = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: true }); + nftC = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: true }); nftNoCollection = await createNft({ owner: staker.publicKey }); nftUnverified = await createNft({ owner: staker.publicKey, collection: collectionMint, verify: false }); nftWrongCollection = await createNft({ @@ -249,21 +266,13 @@ describe('nft-staking litesvm', () => { // A pool is seeded by its admin, so initializing one never blocks anyone // else from running their own. it('Lets a second admin run their own pool', async () => { - const otherConfig = PublicKey.findProgramAddressSync( - [Buffer.from('config'), otherAdmin.publicKey.toBuffer()], - PROGRAM_ID, - )[0]; - await program.methods .initializeConfig(new anchor.BN(POINTS_PER_DAY), MAX_STAKE, FREEZE_PERIOD_DAYS, REWARD_DECIMALS) .accountsPartial({ admin: otherAdmin.publicKey, collectionMint, config: otherConfig, - rewardsMint: PublicKey.findProgramAddressSync( - [Buffer.from('rewards'), otherConfig.toBuffer()], - PROGRAM_ID, - )[0], + rewardsMint: otherRewardsMint, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }) @@ -320,7 +329,12 @@ describe('nft-staking litesvm', () => { it('Initializes the user accounts', async () => { await program.methods .initializeUser() - .accountsPartial({ user: staker.publicKey, userAccount, systemProgram: SystemProgram.programId }) + .accountsPartial({ + user: staker.publicKey, + config, + userAccount, + systemProgram: SystemProgram.programId, + }) .signers([staker]) .rpc(); @@ -328,10 +342,8 @@ describe('nft-staking litesvm', () => { .initializeUser() .accountsPartial({ user: otherUser.publicKey, - userAccount: PublicKey.findProgramAddressSync( - [Buffer.from('user'), otherUser.publicKey.toBuffer()], - PROGRAM_ID, - )[0], + config, + userAccount: userPda(config, otherUser.publicKey), systemProgram: SystemProgram.programId, }) .signers([otherUser]) @@ -422,6 +434,54 @@ describe('nft-staking litesvm', () => { ); }); + // The cap and the points total live on a per-pool account, so being maxed + // out in one pool must not affect another pool run by a different admin. + it('Enforces stake caps per pool, not globally', async () => { + const otherUserAccount = userPda(otherConfig, staker.publicKey); + + await program.methods + .initializeUser() + .accountsPartial({ + user: staker.publicKey, + config: otherConfig, + userAccount: otherUserAccount, + systemProgram: SystemProgram.programId, + }) + .signers([staker]) + .rpc(); + + // Already at max_stake in the first pool; this must still go through. + await program.methods + .stake() + .accountsPartial({ + user: staker.publicKey, + nftMint: nftC.mint, + nftTokenAccount: nftC.ata, + metadata: metadataPda(nftC.mint), + edition: masterEditionPda(nftC.mint), + config: otherConfig, + stakeAccount: stakePdaIn(nftC.mint, otherConfig), + userAccount: otherUserAccount, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + metadataProgram: TOKEN_METADATA_PROGRAM_ID, + }) + .signers([staker]) + .rpc(); + + assert.strictEqual(tokenAccount(nftC.ata).state, FROZEN, 'the second pool should have staked the NFT'); + assert.strictEqual( + (await program.account.userAccount.fetch(otherUserAccount)).amountStaked, + 1, + 'the second pool tracks its own count', + ); + assert.strictEqual( + (await program.account.userAccount.fetch(userAccount)).amountStaked, + 1, + 'the first pool is unaffected', + ); + }); + it('Pays nothing before a whole day has passed', async () => { await expectAnchorError( program.methods @@ -476,10 +536,7 @@ describe('nft-staking litesvm', () => { }); it("Rejects a claim against another user's stake position", async () => { - const otherUserAccount = PublicKey.findProgramAddressSync( - [Buffer.from('user'), otherUser.publicKey.toBuffer()], - PROGRAM_ID, - )[0]; + const otherUserAccount = userPda(config, otherUser.publicKey); await expectAnchorError( program.methods