diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index 3f6ff74..0000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", - "categories": { - "correctness": "warn" - }, - "rules": { - "curly": "error", - "no-unused-vars": "allow" - } -} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index dd44972..0000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -*.md diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index e982a03..0000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "arrowParens": "always", - "bracketSpacing": true, - "endOfLine": "lf", - "printWidth": 80, - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false -} diff --git a/CLAUDE.md b/CLAUDE.md index 13b4177..16426c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ bun run dev bun build src/index.ts --compile --outfile jup # Format code -bunx prettier --write . +bun run fmt # Run all tests: lint, typecheck, tests bun run ci @@ -30,11 +30,11 @@ bun run ci - `ConfigCommand` — `config list`, `config set` - `KeysCommand` — `keys list/add/delete/edit/use/solana-import` -- `LendCommand` — `lend earn tokens/positions/deposit/withdraw` +- `LendCommand` — `lend earn tokens/positions/deposit/withdraw`, `lend borrow vaults/positions/operate` - `PerpsCommand` — `perps positions/markets/open/set/close` - `PredictionsCommand` — `predictions events/positions/open/close/history` - `SignCommand` — `sign --tx ` -- `SpotCommand` — `spot tokens/quote/swap/portfolio/transfer/reclaim` +- `SpotCommand` — `spot tokens/quote/swap/history`, `portfolio`, `transfer`, `reclaim` - `UpdateCommand` — `update` (self-update CLI to latest version) - `VrfdCommand` — `vrfd check/submit` @@ -63,6 +63,8 @@ bun run ci **Lend deposit/withdraw flow:** LendClient.getTokens → resolve jlToken → Swap.execute → LendClient.getPositions (updated state) +**Lend borrow flow:** listBorrowVaults / listBorrowPositions → buildBorrowOperateTransaction → Signer.signTransaction → RPC send + **Predictions flow:** PredictionsClient.postOrder → Signer.signTransaction → PredictionsClient.postExecute **Vrfd flow:** VrfdClient.checkEligibility → VrfdClient.craftTxn → Signer.signTransaction → VrfdClient.execute @@ -87,7 +89,7 @@ When adding new commands, both input (options/arguments) and output (JSON/table) ## Code Style -- Prettier with default config (double quotes, semicolons, 80 char width, trailing commas in ES5 positions) +- Oxfmt with double quotes, semicolons, 80 character line width, and trailing commas in ES5 positions - TypeScript strict mode, ESNext target, bundler module resolution - All core modules use static methods (no instantiation for Config, Output, NumberConverter, clients) - Signer and KeyPair use instance methods with static factory constructors diff --git a/README.md b/README.md index 2d695da..faa6c20 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,23 @@ jup keys add key1 --seed-phrase "word1 word2 ..." --derivation-path "m/44'/501'/ # Or import from a private key (accepts hex, base58, base64, or JSON byte array) jup keys add key1 --private-key -# Sign a base64 transaction returned from --dry-run -jup sign --tx - -# View your spot portfolio -jup spot portfolio +# View your portfolio +jup portfolio +# View your spot trade history (does not require a Jupiter Core API key) +jup spot history # Swap 1 SOL to USDC jup spot swap --from SOL --to USDC --amount 1 # Reclaim rent from empty token accounts -jup spot reclaim +jup reclaim + +# Create a daily DCA order for ten 10-USDC purchases of SOL +jup dca create --from USDC --to SOL --amount 100 --interval 86400 --orders 10 + +# Send a 10-USDC invite +jup invite send --token USDC --amount 10 + +# Show JLP pool statistics +jup jlp info # Open a 3x long SOL position with 10 USDC jup perps open --asset SOL --side long --amount 10 --input USDC --leverage 3 @@ -52,6 +60,10 @@ jup lend earn tokens jup lend earn deposit --token USDC --amount 100 # View your lending positions jup lend earn positions +# View borrow vaults +jup lend borrow vaults +# Deposit collateral and borrow in one transaction +jup lend borrow operate --vault 1 --collateral 0.03 --debt 1.1 # Browse prediction markets jup predictions events --category crypto @@ -71,7 +83,7 @@ jup vrfd submit --token --project-twitter @projecthandle --descri > [!NOTE] > This CLI is designed to be LLM friendly and **all commands are non-interactive**. Set JSON output mode globally for structured responses: `jup config set --output json`, or use `-f json` flag on individual commands. > -> Use `--dry-run` on any transacting command to preview the result without signing or submitting on-chain. In JSON mode, the response includes the unsigned base64 `transaction`, which can be signed with the `sign` command. +> Use `--dry-run` on any transacting command to sign and simulate the transaction without submitting it. In JSON mode, the response includes the signed serialized transaction and concise simulation result. Configure the RPC used for simulation and direct on-chain submissions with `jup config set --rpc-url `. [Read the docs](./docs/) for specific guides, examples, and workflows: @@ -79,10 +91,13 @@ jup vrfd submit --token --project-twitter @projecthandle --descri - [Update](docs/update.md): Self-update the CLI - [Config](docs/config.md): CLI settings and configurations - [Keys](docs/keys.md): Private key management -- [Sign](docs/sign.md): Sign transactions from `--dry-run` +- [Sign](docs/sign.md): Sign an externally supplied transaction - [Spot](docs/spot.md): Spot trading, transfers, token search and portfolio data +- [DCA](docs/dca.md): Time-based recurring orders +- [Invite](docs/invite.md): Jupiter Send token invites +- [JLP](docs/jlp.md): JLP pool information, minting, and burning - [Perps](docs/perps.md): Perps trading (leveraged longs/shorts) -- [Lend](docs/lend.md): Lending and yield farming +- [Lend](docs/lend.md): Lending, borrowing, and yield farming - [Predictions](docs/predictions.md): Prediction markets - [Vrfd](docs/vrfd.md): Token verification diff --git a/bun.lock b/bun.lock index bbe79eb..6a7e380 100644 --- a/bun.lock +++ b/bun.lock @@ -1,331 +1,294 @@ { - "lockfileVersion": 1, + "lockfileVersion": 2, "configVersion": 1, "workspaces": { "": { - "name": "cli", + "name": "@jup-ag/cli", "dependencies": { "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", - "@solana-program/token": "^0.12.0", + "@solana-program/system": "^0.14.0", + "@solana-program/token": "^0.14.0", + "@solana-program/token-2022": "^0.16.0", "@solana/keychain": "^1.1.0", "@solana/kit": "^6.7.0", "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^14.0.3", + "jupiter-api-ts": "^0.4.1", "ky": "^1.14.3", "micro-key-producer": "^0.8.5", }, "devDependencies": { "@types/bun": "latest", + "oxfmt": "^0.65.0", "oxlint": "^1.53.0", - "prettier": "^3.8.1", "typescript": "^5.9.3", }, }, }, "packages": { - "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + "@aws-sdk/client-kms": ["@aws-sdk/client-kms@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-zE7Z+jeKxCULk+m7XCZguZtBeWbreEZYtJcDrSkPJoAW9esux3Fx66+wp5WQVY8T4X5dYdsYymGvPIhEfTQpew=="], - "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + "@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw=="], - "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w=="], - "@aws-sdk/client-kms": ["@aws-sdk/client-kms@3.1018.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", "@aws-sdk/credential-provider-node": "^3.972.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CAWH8nTzbeWDolybD2u1tDbJWVOkOosOcbxDhG1e/mP6h3ZVwryNR8GGYcVNpU5kPGPh8OjccTt0qdF9bJXr/Q=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.15", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-login": "^3.972.77", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg=="], - "@aws-sdk/core": ["@aws-sdk/core@3.973.25", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.77", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/credential-provider-env": "^3.972.23", "@aws-sdk/credential-provider-http": "^3.972.25", "@aws-sdk/credential-provider-login": "^3.972.25", "@aws-sdk/credential-provider-process": "^3.972.23", "@aws-sdk/credential-provider-sso": "^3.972.25", "@aws-sdk/credential-provider-web-identity": "^3.972.25", "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-G/v/PicYn4qs7xCv4vT6I4QKdvMyRvsgIFNBkUueCGlbLo7/PuKcNKgUozmLSsaYnE7jIl6UrfkP07EUubr48w=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/token-providers": "3.1116.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-bUdmyJeVua7SmD+g2a65x2/0YqsGn4K2k4GawI43js0odaNaIzpIhLtHehUnPnfLuyhPWbJR1NyuIO4iMVfM0w=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.26", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.23", "@aws-sdk/credential-provider-http": "^3.972.25", "@aws-sdk/credential-provider-ini": "^3.972.25", "@aws-sdk/credential-provider-process": "^3.972.23", "@aws-sdk/credential-provider-sso": "^3.972.25", "@aws-sdk/credential-provider-web-identity": "^3.972.25", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5XSK74rCXxCNj+UWv5bjq1EccYkiyW4XOHFU9NXnsCcQF8dJuHdua1qFg0m/LIwVOWklbKsrcnMtfxIXwgvwzQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.46", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/token-providers": "3.1018.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-r4OGAfHmlEa1QBInHWz+/dOD4tRljcjVNQe9wJ/AJNXEj1d2WdsRLppvRFImRV6FIs+bTpjtL0a23V5ELQpRPw=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-uM1OtoJgj+yK3MlAmda8uR9WJJCdm5HB25JyCeFL5a5q1Fbafalf4uKidFO3/L0Pgd+Fsflkb4cM6jHIswi3QQ=="], + "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="], - - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ=="], - - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig=="], - - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.15", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-k6WAVNkub5DrU46iPQvH1m0xc1n+0dX79+i287tYJzf5g1yU2rX3uf4xNeL5JvK1NtYgfwMnsxHqhOXFBn367A=="], - - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ=="], - - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1018.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-97OPNJHy37wmGOX44xAcu6E9oSTiqK9uPcy/fWpmN5uB3JuEp1f6x60Xot/jp+FxwhQWIFUsVJFnm3QKqt7T6Q=="], - - "@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], - - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="], - - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], - - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="], - - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.12", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg=="], - - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="], - - "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], - - "@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], + "@noble/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="], - "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@noble/curves": ["@noble/curves@2.4.0", "", { "dependencies": { "@noble/hashes": "2.4.0" } }, "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.53.0", "", { "os": "android", "cpu": "arm" }, "sha512-JC89/jAx4d2zhDIbK8MC4L659FN1WiMXMBkNg7b33KXSkYpUgcbf+0nz7+EPRg+VwWiZVfaoFkNHJ7RXYb5Neg=="], + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.53.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CY+pZfi+uyeU7AwFrEnjsNT+VfxYmKLMuk7bVxArd8f+09hQbJb8f7C7EpvTfNqrCK1J8zZlaYI4LltmEctgbQ=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.65.0", "", { "os": "android", "cpu": "arm" }, "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.53.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0aqsC4HDQ94oI6kMz64iaOJ1f3bCVArxvaHJGOScBvFz6CcQedXi5b70Xg09CYjKNaHA56dW0QJfoZ/111kz1A=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.65.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.53.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e+KvuaWtnisyWojO/t5qKDbp2dvVpg+1dl4MGnTb21QpY4+4+9Y1XmZPaztcA2XNvy4BIaXFW+9JH9tMpSBqUg=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.65.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.53.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hpU0ZHVeblFjmZDfgi9BxhhCpURh0KjoFy5V+Tvp9sg/fRcnMUEfaJrgz+jQfOX4jctlVWrAs1ANs91+5iV+zA=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.65.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.53.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ccKxOpw+X4xa2pO+qbTOpxQ2x1+Ag3ViRQMnWt3gHp1LcpNgS1xd6GYc3OvehmHtrXqEV3YGczZ0I1qpBB4/2A=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.65.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.53.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UBkBvmzSmlyH2ZObQMDKW/TuyTmUtP/XClPUyU2YLwj0qLopZTZxnDz4VG5d3wz1HQuZXO0o1QqsnQUW1v4a6Q=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.65.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.53.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PQJJ1izoH9p61las6rZ0BWOznAhTDMmdUPL2IEBLuXFwhy2mSloYHvRkk39PSYJ1DyG+trqU5Z9ZbtHSGH6plg=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.65.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.53.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GXI1o4Thn/rtnRIL38BwrDMwVcUbIHKCsOixIWf/CkU3fCG3MXFzFTtDMt+34ik0Qk452d8kcpksL0w/hUkMZA=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.53.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Uahk7IVs2yBamCgeJ3XKpKT9Vh+de0pDKISFKnjEcI3c/w2CFHk1+W6Q6G3KI56HGwE9PWCp6ayhA9whXWkNIQ=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.53.0", "", { "os": "linux", "cpu": "none" }, "sha512-sWtcU9UkrKMWsGKdFy8R6jkm9Q0VVG1VCpxVuh0HzRQQi3ENI1Nh5CkpsdfUs2MKRcOoHKbXqTscunuXjhxoxQ=="], + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.65.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.53.0", "", { "os": "linux", "cpu": "none" }, "sha512-aXew1+HDvCdExijX/8NBVC854zJwxhKP3l9AHFSHQNo4EanlHtzDMIlIvP3raUkL0vXtFCkTFYezzU5HjstB8A=="], + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.53.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-rVpyBSqPGou9sITcsoXqUoGBUH74bxYLYOAGUqN599Zu6BQBlBU9hh3bJQ/20D1xrhhrsbiCpVPvXpLPM5nL1w=="], + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.53.0", "", { "os": "linux", "cpu": "x64" }, "sha512-eOyeQ8qFQ2geXmlWJuXAOaek0hFhbMLlYsU457NMLKDRoC43Xf+eDPZ9Yk0n9jDaGJ5zBl/3Dy8wo41cnIXuLA=="], + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.65.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.53.0", "", { "os": "linux", "cpu": "x64" }, "sha512-S6rBArW/zD1tob8M9PwKYrRmz+j1ss1+wjbRAJCWKd7TC3JB6noDiA95pIj9zOZVVp04MIzy5qymnYusrEyXzg=="], + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.53.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sd/A0Ny5sN0D/MJtlk7w2jGY4bJQou7gToa9WZF7Sj6HTyVzvlzKJWiOHfr4SulVk4ndiFQ8rKmF9rXP0EcF3A=="], + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.53.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-QC3q7b51Er/ZurEFcFzc7RpQ/YEoEBLJuCp3WoOzhSHHH/nkUKFy+igOxlj1z3LayhEZPDQQ7sXvv2PM2cdG3Q=="], + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.65.0", "", { "os": "none", "cpu": "arm64" }, "sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.53.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3OvLgOqwd705hWHV2i8ni80pilvg6BUgpC2+xtVu++e/q28LKVohGh5J5QYJOrRMfWmxK0M/AUu43vUw62LAKQ=="], + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.65.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.53.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xTiOkntexCdJytZ7ArIIgl3vGW5ujMM3sJNM7/+iqGAVJagCqjFFWn68HRWRLeyT66c95uR+CeFmQFI6mLQqDw=="], + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.65.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ=="], - "@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.65.0", "", { "os": "win32", "cpu": "x64" }, "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg=="], - "@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.80.0", "", { "os": "android", "cpu": "arm" }, "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA=="], - "@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.80.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.80.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.80.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA=="], - "@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.80.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.80.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.80.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.80.0", "", { "os": "none", "cpu": "arm64" }, "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.80.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.80.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.80.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="], + "@scure/base": ["@scure/base@2.4.0", "", {}, "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], + "@scure/bip32": ["@scure/bip32@2.4.0", "", { "dependencies": { "@noble/curves": "2.4.0", "@noble/hashes": "2.4.0", "@scure/base": "2.4.0" } }, "sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="], + "@scure/bip39": ["@scure/bip39@2.4.0", "", { "dependencies": { "@noble/hashes": "2.4.0" } }, "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="], + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], - "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + "@solana-program/record": ["@solana-program/record@0.4.0", "", { "dependencies": { "@solana-program/system": "^0.14.0" }, "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-d4HSGdfiY8TMJHprbw4Zo5ZI+FiMRrh7zJ9nrddAM59Qbp7Rl88wQdATedbs7cXqW8zE/VRHGYdajairqNrbYg=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + "@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + "@solana-program/token": ["@solana-program/token@0.14.0", "", { "dependencies": { "@solana-program/system": "^0.12.0" }, "peerDependencies": { "@solana/kit": "^6.5.0" } }, "sha512-zpLMr6JZndlsQCQvrm0gezfwdr1lmzOGqN6v2WIYXjtDmbF+xg6zh/MAp2UFHRpv3uDmylEdjtQo05pa2OeaYg=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + "@solana-program/token-2022": ["@solana-program/token-2022@0.16.0", "", { "dependencies": { "@noble/curves": "^1.9.7", "@solana-program/record": "^0.4.0", "@solana-program/system": "^0.14.0", "@solana-program/zk-elgamal-proof": "^0.4.0" }, "peerDependencies": { "@solana/kit": "^8.0.0", "@solana/sysvars": "^8.0.0", "@solana/zk-sdk": "^0.5.1" }, "optionalPeers": ["@solana/zk-sdk"] }, "sha512-s3EgLnKEbhWI3kUR9/GlcOFDfL/OfXQnQ3m/YoEHLR4uQ/nSRtM2nFeUJ6Vbt+lmpXSJpyVurN7Kr/NTkY9Eqg=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="], + "@solana-program/zk-elgamal-proof": ["@solana-program/zk-elgamal-proof@0.4.0", "", { "dependencies": { "@solana-program/system": "^0.14.0" }, "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-bf2Q3f25xcCNaLm/MR4lwoAguJzEW2eeLdrJb6rUjMcA63qcgyJOWl6To7Bn4e+0OyWBa+J7YP2BUhi//VPz3g=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="], + "@solana/accounts": ["@solana/accounts@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+FxfDOrnifoPlBkF+fr8eeQdgM6xtIgAg9xKMu3WnIz60oZd4Xnry6+ff6t+ePPoZZp397FSg9ZJet68VCWm5Q=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="], + "@solana/addresses": ["@solana/addresses@6.10.0", "", { "dependencies": { "@solana/assertions": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/nominal-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-vEoCGBTxG0HCERAn84KXkrJjl+pDaNzOpZ0qbgcPS98fYxP5yzbKB8SNOY2bzrbkRUmmw5Q3hqTRERemUN2Gcw=="], - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], + "@solana/assertions": ["@solana/assertions@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-lKSAdVo+P/6Lp4vs6shstXmFOpvxrABwn4o1462tb7sKkNapk6o9pPFVPGw4DUgPS3WqWRs1j2tmpuVjhQRntg=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], + "@solana/codecs": ["@solana/codecs@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/fixed-points": "6.10.0", "@solana/options": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-lLVuxod4ChWp9i7OvpgIykYG8Q9OGPVXKnHM9VlzDDLylsx7Y1FoQL00sHa7PqFkJVmkBufaA6dcGbQ7FU+lAQ=="], - "@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="], + "@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="], + "@solana/codecs-data-structures": ["@solana/codecs-data-structures@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CNasJW3bq5u+632Zt5aJ8rOjAjv2HyenpV8o9kAIqdmV4CBpjCCoBnKn8LkuR/sbeREZxJYfhKTXO/9ruAkw7A=="], - "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + "@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@solana/codecs-strings": ["@solana/codecs-strings@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": ">=5.4.0" }, "optionalPeers": ["fastestsmallesttextencoderdecoder", "typescript"] }, "sha512-zlaqkg7K6F6IN4V/Ec8TWkTn054gxv7ZLagvGkuEyAdPQ6BzzsehOm2TqCuyXgJJTCGPLY1bEk6yH9NxANe0kA=="], - "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + "@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - "@solana-program/system": ["@solana-program/system@0.12.0", "", { "peerDependencies": { "@solana/kit": "^6.1.0" } }, "sha512-ZnAAWeGVMWNtJhw3GdifI2HnhZ0A0H0qs8tBkcFvxp/8wIavvO+GOM4Jd0N22u2+Lni2zcwvcrxrsxj6Mjphng=="], + "@solana/fast-stable-stringify": ["@solana/fast-stable-stringify@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-iCNed27wk6PKSS3QUtHovRfMWF/jbVWogs2vB4tukKUCsqG4rDfDInIwZ6ur/nY6XTrgi2gMMdZq9GAUlWsbfw=="], - "@solana-program/token": ["@solana-program/token@0.12.0", "", { "dependencies": { "@solana-program/system": "^0.12.0" }, "peerDependencies": { "@solana/kit": "^6.1.0" } }, "sha512-hnidRNuFhmqUdW5aWkKTJ+cdzuotVMNwLsTyAk0Nd8VjLDld+vQC0fugHWqm5GPrvYe0hCNAhtpJcZVnNp7rOA=="], + "@solana/fixed-points": ["@solana/fixed-points@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-ZkKL0alXH3L7/wMiVG8YUuG8qBKunlM810+YBD7nUPRhifiGsX1zwADViHLYNqLr/jUk0mTYFUcKznTpB/K+Gg=="], - "@solana/accounts": ["@solana/accounts@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/rpc-spec": "6.7.0", "@solana/rpc-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-RNUJtnFKGjr4rdlnj5zUyhZolDl+EC2X5Sw0XAjXCsyp5A0TbymWA/XbyUGw9FYdv2TP1x10lD8TiH/IRo4dlA=="], + "@solana/functional": ["@solana/functional@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-P8cevu4mAqHTXC37h1TVoOh8zhWB2tlOI/R9vWjYPpcLwcyWf8p2qq4LEGHl5kY+1C+4PNX39HsmCocXOPCDkQ=="], - "@solana/addresses": ["@solana/addresses@6.7.0", "", { "dependencies": { "@solana/assertions": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/nominal-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-jmzTxmEXXPDyh1vIJnxjGWxpX1p8DiS201M0yzPynlOf3XhQrk5nfVYw5O/U3+g64pEyOi/W6qbTVAVOov+oVw=="], + "@solana/instruction-plans": ["@solana/instruction-plans@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/promises": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-YG7mo4zykzdc6ZTV0BuN6pveK9qeBySzlYYerq578A4eQu3xcypMAYRGAvhMZtWTanjjmD6CKtM0M7kVp0TNxg=="], - "@solana/assertions": ["@solana/assertions@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/nv3tgZNYmIz2mezzfjQcAUl1QN70lGYqKtR+LWFlKt2Srv0V9ugyerniRInMpuPjb/loVA9UxdPne0hzFt4Uw=="], + "@solana/instructions": ["@solana/instructions@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-0TToYF+8LXQ3ofPMx+yF6yaM9l4YJvcAPMy0qV5JsrBUFlWXBSANRuudKBQLHMvb+a3OiUTq5X7omuorKMBB3A=="], - "@solana/codecs": ["@solana/codecs@6.7.0", "", { "dependencies": { "@solana/codecs-core": "6.7.0", "@solana/codecs-data-structures": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/options": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-1LLDxHMDZj2OqPlSbgQY8uRw+AkvsdPQDJWkn783hzpBtU3oj7whehoqOETtTqbWtF1hK3T/cESz31LTL919pA=="], + "@solana/keychain": ["@solana/keychain@1.4.0", "", { "dependencies": { "@solana/addresses": "^6.9.0", "@solana/keychain-aws-kms": "1.4.0", "@solana/keychain-cdp": "1.4.0", "@solana/keychain-core": "1.4.0", "@solana/keychain-crossmint": "1.4.0", "@solana/keychain-dfns": "1.4.0", "@solana/keychain-fireblocks": "1.4.0", "@solana/keychain-gcp-kms": "1.4.0", "@solana/keychain-memory": "1.4.0", "@solana/keychain-openfort": "1.4.0", "@solana/keychain-para": "1.4.0", "@solana/keychain-privy": "1.4.0", "@solana/keychain-turnkey": "1.4.0", "@solana/keychain-utila": "1.4.0", "@solana/keychain-vault": "1.4.0" }, "peerDependencies": { "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-dTPasXGQrMUrWsmtNuaoVWqbDVRU/DcLek2BjttaXc+890oOeAbCrkyWvlcziWSbg9ywyLshuJG/vd9qPt73XQ=="], - "@solana/codecs-core": ["@solana/codecs-core@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-IVBuoFyN8upzJJ/Qg3Wc/Go8XKusZXAjqsglgCLCTgOaS68kb9E9CC2N8cGkqFDb2zJuOqGm80ZBdQaL/T7qiA=="], + "@solana/keychain-aws-kms": ["@solana/keychain-aws-kms@1.4.0", "", { "dependencies": { "@aws-sdk/client-kms": "^3.1066.0", "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-YR/pSwara3SVfokYj43rJUen73NmlqPMP5e98i/twYdFn69/ElVBQcYOtNwSWITDENh2zuinxhEnHfpEbcqbDQ=="], - "@solana/codecs-data-structures": ["@solana/codecs-data-structures@6.7.0", "", { "dependencies": { "@solana/codecs-core": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n9SUQd5hGLAmYV4fJL6XEEdL5oZ+gfUxPe7rENPAXxw2gwjc4/axV3s251a3duKNJPW0CVwdmMM4tcUxJmmpGw=="], + "@solana/keychain-cdp": ["@solana/keychain-cdp@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-4jEUoB+IwN5lUb0keTdHHM0g5COF/YQj27CMR8t6Pl2uFcX8YC9MmOqgAYn3WSKUV7PvyA14jAT2A5r5ng4ang=="], - "@solana/codecs-numbers": ["@solana/codecs-numbers@6.7.0", "", { "dependencies": { "@solana/codecs-core": "6.7.0", "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-bk5b+JXVGoU6AdITBSd48NrCA2RsS8jpbXlyCnDsHJrr84TMJfk1lvFYXnv6DYm8ObdgaqIoQCo8lpvITEFavQ=="], + "@solana/keychain-core": ["@solana/keychain-core@1.4.0", "", { "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-WhNYopIPdHAEwJ3RDQ7VtUbKrOa2sEV9Z/Q9RxQNlfVDykzC7j/hQrlMcl5Uc3HTTA1J1/3Hg3wbnZPshSy6xg=="], - "@solana/codecs-strings": ["@solana/codecs-strings@6.7.0", "", { "dependencies": { "@solana/codecs-core": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/errors": "6.7.0" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": "^5.0.0" }, "optionalPeers": ["fastestsmallesttextencoderdecoder", "typescript"] }, "sha512-X+vmiHll/8ATjW1GCny5jP5odZXq2f6luUmyMsEuUvBugA8KqFUqb9FezdHDizuWU234im+0iDFMjvvFNt0ROw=="], + "@solana/keychain-crossmint": ["@solana/keychain-crossmint@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-it+ZDt+5tmnCgh7hsNMtALxEZve83kotsUWJd9bnGXJpbu3v6zRnr4rdWAP1PecPnYxiu8VeiU/FiELju/m5oQ=="], - "@solana/errors": ["@solana/errors@6.7.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "14.0.3" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-wxxo56b2NHcXXXTr/tixpDCa8xzW8v4/gNnDPcctwof21/Jv8qNNyBNgu/xbREuN1SQNthDM5d5c7k/v67uQkw=="], + "@solana/keychain-dfns": ["@solana/keychain-dfns@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-751Z3UyvDgR0tRqtNak61e7BdeRG9Syb2GZ3QMHAow4YcnWPRSn6Kv3j10ej6ZQ3PLugEbYBX9kLiSM/dKP4yg=="], - "@solana/fast-stable-stringify": ["@solana/fast-stable-stringify@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-1RAk+8PDrF1jDiZLbuPNsH6+g2XPR4jiN823ZqXaDcqgSjeCB59mkyH5KNuwhLFIBjxL+IApWdG+JL/H4WJu9g=="], + "@solana/keychain-fireblocks": ["@solana/keychain-fireblocks@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0", "jose": "^6.2.3" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-VqmU3SHjwSQdKL1k0oAvV1ZxxmdPb0qKw5rXV6p2fO6aSJax8hDb39Q7ottXj7Y2I2KtF1Ko7SEeVHJwb7ooCg=="], - "@solana/functional": ["@solana/functional@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-RcR+RRJKH43Ko98ru+LKDkjtcs3sHGz26HWL2IyPujni8RH3VsK2hg7HiLqO8Y4R53yzwsn2iZ3D+q8SkYbTpg=="], + "@solana/keychain-gcp-kms": ["@solana/keychain-gcp-kms@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0", "google-auth-library": "^10.7.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-ml4Oj96LsAcU9FeJO91vSywD6iqQF38eIMc16CD1GuDU0mXtNMHngdnSHfdAZUr0jtZQDu7fyDuqyo15P77CtQ=="], - "@solana/instruction-plans": ["@solana/instruction-plans@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/instructions": "6.7.0", "@solana/keys": "6.7.0", "@solana/promises": "6.7.0", "@solana/transaction-messages": "6.7.0", "@solana/transactions": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-JTCPahwY76ojHPW+P+xsT08pr7pZNPxQm6uuRBLRjQXmRaNu5ZENgJHGjm1C/gwOCq9qlzPTR3R2T9I5/88GOA=="], + "@solana/keychain-memory": ["@solana/keychain-memory@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-QzxOTZY5gtvIOSJblhprcReWITJbCcL+SAHCqzxd1PZD0Q3CJi32y9faWhojZYvIga8T2uE1uIK0hPStc5ec1Q=="], - "@solana/instructions": ["@solana/instructions@6.7.0", "", { "dependencies": { "@solana/codecs-core": "6.7.0", "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-VlDcmxWXsLPO3BXMxQrv/wHDWz2HZ47XAYIgEJlIvzGYm77GEpawyNQSoPUb6F6zTBuJmn02HW8gvmEBYPqs7w=="], + "@solana/keychain-openfort": ["@solana/keychain-openfort@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-srdpBO8njPh9mq1gk1+D2d+qc/P6w4aRAwp0p9Vo8k+AJNNY0ilBBwv67i4X5dkQPrCtxjOldSXVTW7DdwRmbQ=="], - "@solana/keychain": ["@solana/keychain@1.1.0", "", { "dependencies": { "@solana/addresses": "^6.0.1", "@solana/keychain-aws-kms": "1.1.0", "@solana/keychain-cdp": "1.1.0", "@solana/keychain-core": "1.1.0", "@solana/keychain-crossmint": "1.1.0", "@solana/keychain-dfns": "1.1.0", "@solana/keychain-fireblocks": "1.1.0", "@solana/keychain-gcp-kms": "1.1.0", "@solana/keychain-para": "1.1.0", "@solana/keychain-privy": "1.1.0", "@solana/keychain-turnkey": "1.1.0", "@solana/keychain-vault": "1.1.0" }, "peerDependencies": { "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-DZtWCu7JIsm0lPWsF4SpoKGW2hDWMS6v4HtxxKBt5ByzGlbc5gkky4Y/zOb/nKJQLCnwYIlrH4y+SDeBDtVihw=="], + "@solana/keychain-para": ["@solana/keychain-para@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-zOvV4s681+ySVrHaWxuuSyFbtoK3mFalkNf1+PgaMmx8QltTaUPQv1NBLOwgdoQ+ZnHDTGfHQTlDKnhUhwAvxQ=="], - "@solana/keychain-aws-kms": ["@solana/keychain-aws-kms@1.1.0", "", { "dependencies": { "@aws-sdk/client-kms": "^3.991.0", "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-pQguK53KzyPzYmKem4OhG3cQACkOicKKd36sDmJ9fcG0VR7dB1WlrQ7L6CS8jOkff5qHxWMytHJHFfa58dWmtw=="], + "@solana/keychain-privy": ["@solana/keychain-privy@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-0djvQfNdvfiWHCnrq2vHQtna4BJ/7VU+3f1xNqXRxc8jA6gSIBrOL0iqn2Jn0cJXda6408Un9owIPW3ssnM7yg=="], - "@solana/keychain-cdp": ["@solana/keychain-cdp@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-jjCpXgyPSVfX/Qwr+0v60kykNeLpHASBx4EYKpkD6MQpoeK4UQ6wqabphEaCBeakRh1mhjw3c+0YSkr19bEg5A=="], + "@solana/keychain-turnkey": ["@solana/keychain-turnkey@1.4.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-v+lMpUcg4WSQ7qwtR38uWQFwNg/71sopaYU9wYRFkG5FaVFHX7Yv38IsV0vAXoj5pu7vRVW3XK4OeFA0azLOhQ=="], - "@solana/keychain-core": ["@solana/keychain-core@1.1.0", "", { "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-EKF2cskojsspQmeST0wn9j/1g326iEmr/TEospmGHUu5rEePTHA3yS9zhuEB631tzjbJpJH3ZtKyan30zxnhVQ=="], + "@solana/keychain-utila": ["@solana/keychain-utila@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0", "jose": "^6.2.3" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-jpophCYwV7C0ZONH3Bw1MduCYpDjCbDhFw8c+pokwVOYFbkCdiagDowBxCRMwNXgQpisSpb74W/8Wd/Pyp9dqg=="], - "@solana/keychain-crossmint": ["@solana/keychain-crossmint@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-1dFsguXyNYZHe7Qeb81Pv4flc1vXSti6j/Kyc82m4hICMEwBf0KinCBVovwuy4XtXveo/q5zSDCiPwfvRsCmGg=="], + "@solana/keychain-vault": ["@solana/keychain-vault@1.4.0", "", { "dependencies": { "@solana/keychain-core": "1.4.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-tM4cASSr+ZSkEZPqKkgccp+67Z6JMRMoVi9hMxhrJO25XFXL1nfzh+DXTXf18D1/EZw/N6O/lljVfSluDgd5Vw=="], - "@solana/keychain-dfns": ["@solana/keychain-dfns@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-sGvr61pEv+aZh6Pvfw2lNyIjfUNxaUOGNSX6BTC3Xhw6CG047G3E3nY1HbSWa6iYtORkiwTDaVodTJQfZdPfTQ=="], + "@solana/keys": ["@solana/keys@6.10.0", "", { "dependencies": { "@solana/assertions": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/promises": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-26IRfdm/hTUCmM7MeEeX0ULSbCM6OzkZTkfkrPircqmRM7xyNqP4hq7u0P7wjb9dl7NfgyG6K7cdvUxrj2e3mA=="], - "@solana/keychain-fireblocks": ["@solana/keychain-fireblocks@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0", "jose": "^6.1.3" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-/VjJ6hbuB88Xhr0dHWZXXCR0bLMTHFnz3dCF0vWWxRnqmGNm/uz48uFVRypSf8RdOC0I7FFaAy4GTr3MxEc4QQ=="], + "@solana/kit": ["@solana/kit@6.10.0", "", { "dependencies": { "@solana/accounts": "6.10.0", "@solana/addresses": "6.10.0", "@solana/codecs": "6.10.0", "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/instruction-plans": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/offchain-messages": "6.10.0", "@solana/plugin-core": "6.10.0", "@solana/plugin-interfaces": "6.10.0", "@solana/program-client-core": "6.10.0", "@solana/programs": "6.10.0", "@solana/rpc": "6.10.0", "@solana/rpc-api": "6.10.0", "@solana/rpc-parsed-types": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-subscriptions": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/signers": "6.10.0", "@solana/subscribable": "6.10.0", "@solana/sysvars": "6.10.0", "@solana/transaction-confirmation": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-/WnnQp3uARh2JCFSfAakejTAqwmXVuMVTcRn5r2yDwY2yzZ4R6mt/Cl59VPimVLNSoTyN/KsEwhv9omr3ERazQ=="], - "@solana/keychain-gcp-kms": ["@solana/keychain-gcp-kms@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0", "google-auth-library": "^10.6.2" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-3h4FZYmNj6m6bAb23aSDVFh5tc+2n5Cdbx8xQGU9G+CYoxeh60nKw8zqbPgctLdzPYtXQWgmiPC3Q83u/EGO/g=="], + "@solana/nominal-types": ["@solana/nominal-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-9ykyBBvnkInH7fCacjJi7zu2PJyd+OCt+VTjIISv070fHzKIMFqZqJJ/dJ0SRH2aHwfB3n86iVsmtBtuxi4KKA=="], - "@solana/keychain-para": ["@solana/keychain-para@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-+iYtergtginXOFI5EhUOGxgsdNgcxHaepfMQz9A8s/LDKdzH9M6SVS/7UlHZYwGwgoK948b7X6T/YfnD2o8adQ=="], + "@solana/offchain-messages": ["@solana/offchain-messages@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/keys": "6.10.0", "@solana/nominal-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-RiEgAueeMkFMC1suOXBIcmCZgtXRxy24yk0DldPB37bB4zwOF1SAaRjNRPjIkGK8RhCYrEpPosnzLyavw9ueRg=="], - "@solana/keychain-privy": ["@solana/keychain-privy@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-NiPOeK2mEpKBlnJIDD7MLLpq35i6TTO3B4n5JuBltoT8fbwl3N9Hy9ttMrcrxYwftfSlVfRpF+vWNsqO4FSMUA=="], + "@solana/options": ["@solana/options@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-RO9UT3UYD8/Cu2uM6ZXbKvLeMnVD42+g9JRds7Pfs4AhiOyg4R4TJrQUAppTgavPTO3PBRlWtWOC05ZH/yAIbg=="], - "@solana/keychain-turnkey": ["@solana/keychain-turnkey@1.1.0", "", { "dependencies": { "@noble/curves": "^2.0.1", "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-5Njzq3mg1HuG5wjzbZ7WHoYLus3Cf0j94vpgsXUeDG0fJhIdRSU/rL0wD+HTzu7MRw9GlEENjkb4dRGgdnMI9g=="], + "@solana/plugin-core": ["@solana/plugin-core@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-JE70YTQOfFACVFGvoJon4Scc/eHUWjMu8Ovo35CcV2kHTAHYMCd4UkBd2gmlhK0vRMMomsQi1ZLPlAlTq0OoUQ=="], - "@solana/keychain-vault": ["@solana/keychain-vault@1.1.0", "", { "dependencies": { "@solana/keychain-core": "1.1.0" }, "peerDependencies": { "@solana/addresses": ">=6.0.1", "@solana/codecs-core": ">=6.0.1", "@solana/codecs-strings": ">=6.0.1", "@solana/keys": ">=6.0.1", "@solana/signers": ">=6.0.1", "@solana/transactions": ">=6.0.1" } }, "sha512-vUFIFtWLxo3JLbyqHER0YnsTryxZwQQ+1ZqfloerXRTa8VgFBtRrYw8RSQ/rdBPfrtmrMyf58P+/4ayzAn8MPQ=="], + "@solana/plugin-interfaces": ["@solana/plugin-interfaces@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/instruction-plans": "6.10.0", "@solana/keys": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/signers": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-vr0/l9wcM4orwGr8cjkFWaJ9A4HvzuAv00jMFNMg0Spd0GZqnwnpW+D/fXa1lIJnTRaF3EeEjLh4VjKU037T0Q=="], - "@solana/keys": ["@solana/keys@6.7.0", "", { "dependencies": { "@solana/assertions": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/nominal-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qyrBOLuluxkI+JamwpfsuwCIgFtapts+TcTeG4w0bxbE9TXJAJKb/CP9JhVt4XgNkW+awX4YTk0itpHOZL5y3Q=="], + "@solana/program-client-core": ["@solana/program-client-core@6.10.0", "", { "dependencies": { "@solana/accounts": "6.10.0", "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0", "@solana/instruction-plans": "6.10.0", "@solana/instructions": "6.10.0", "@solana/plugin-interfaces": "6.10.0", "@solana/rpc-api": "6.10.0", "@solana/signers": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-4PPbTLdC1ylHIuvhOFDP8RnSkXPCFjNFWGslzc+UFKnoR4ajzBcByX94jmaruDMk5ncxgj7tr9pzJTvfGHIaMA=="], - "@solana/kit": ["@solana/kit@6.7.0", "", { "dependencies": { "@solana/accounts": "6.7.0", "@solana/addresses": "6.7.0", "@solana/codecs": "6.7.0", "@solana/errors": "6.7.0", "@solana/functional": "6.7.0", "@solana/instruction-plans": "6.7.0", "@solana/instructions": "6.7.0", "@solana/keys": "6.7.0", "@solana/offchain-messages": "6.7.0", "@solana/plugin-core": "6.7.0", "@solana/plugin-interfaces": "6.7.0", "@solana/program-client-core": "6.7.0", "@solana/programs": "6.7.0", "@solana/rpc": "6.7.0", "@solana/rpc-api": "6.7.0", "@solana/rpc-parsed-types": "6.7.0", "@solana/rpc-spec-types": "6.7.0", "@solana/rpc-subscriptions": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/signers": "6.7.0", "@solana/sysvars": "6.7.0", "@solana/transaction-confirmation": "6.7.0", "@solana/transaction-messages": "6.7.0", "@solana/transactions": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/N/lkkYSQ4frD2K61jBZenvU92GoETiraCoWlsWe3OzqeJABMToLG3fwTpG/tU5T+ErHNqjmRdAHAQXvSe5muw=="], + "@solana/programs": ["@solana/programs@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-qn/HeLP5KGUJXVub3fyGe69/rWaLX4jzwm6V/1pNxJDbdF+MBdgn18hP6F+VmhfdNmwK0lue3J/1HQ1UTMuQeQ=="], - "@solana/nominal-types": ["@solana/nominal-types@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-XY+6gF9TzonHiI6VBjtcrEfxT/0KlmF3KRFGSKM5JLEfhs3w+E26op3TOqHuOX7H6llZBgxybaZmXKrvo2rt/w=="], + "@solana/promises": ["@solana/promises@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-oJSIn+VBBMWDo8oqw7RV3tI6Jih+Ieup6FcQLYLDUriaeo7+8l1Zdezl8zh7SIfeU4lOfAbRg6mR0huaS/Lltg=="], - "@solana/offchain-messages": ["@solana/offchain-messages@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-data-structures": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/keys": "6.7.0", "@solana/nominal-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-c9IDz6hrzkklq0y7fhPXiY973cDILLL3ZIGIZPofEg12s/VxvUIFYseN/syVAQSUkzeYrDywZQLlF6sptAxWdQ=="], + "@solana/rpc": ["@solana/rpc@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/fast-stable-stringify": "6.10.0", "@solana/functional": "6.10.0", "@solana/rpc-api": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-transport-http": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-EwxsqoD+NXV+m+iobnWNtATD93gTgaNsOiQOzYB1/2e+8S6fl6obdNPB55yfXgtl4jt6GV6/ae4xuPhLv76vvg=="], - "@solana/options": ["@solana/options@6.7.0", "", { "dependencies": { "@solana/codecs-core": "6.7.0", "@solana/codecs-data-structures": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-B6nmUvfNpW+E6Oq9MtCzI6ExK5b3AJIRqjz+x1RBdA9rUCF6alM/4bjCSoADCZZVCTMts7FJF59r9XZrwONp2w=="], + "@solana/rpc-api": ["@solana/rpc-api@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/keys": "6.10.0", "@solana/rpc-parsed-types": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-RjPIVsAb/85P1ptoO3WpC0x7QG6gG/e4q/3lo6gbSznUZOcoM+8sSBnCX7BwP1ZkCDS6NK/ClXLnhhhYZx+OGg=="], - "@solana/plugin-core": ["@solana/plugin-core@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-tO9PAa9nFxZdrlYYHuG8pP9WbO/Szo1rr2YT2Mpj8PKDDqLsrQ2cCC8GerHbdLOr1s+cE1O7fotbydGDqeC9yQ=="], + "@solana/rpc-parsed-types": ["@solana/rpc-parsed-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-5275mvSV1mxhwvrMVa+K7BU/nAetpHfcb+8Ql9rtA8RRf6DyiimFQFZUukE4Ez6XJihEpCHNy98yhkgai9wytQ=="], - "@solana/plugin-interfaces": ["@solana/plugin-interfaces@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/instruction-plans": "6.7.0", "@solana/keys": "6.7.0", "@solana/rpc-spec": "6.7.0", "@solana/rpc-subscriptions-spec": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/signers": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-RVlZEx3gqCsPZ2aQtxRc+8TX0m8LqPFakZn9ceq40WPuZwqE1khx4G82aYSxwJiiZ3HIepMPChRYSG+7tcoRRA=="], + "@solana/rpc-spec": ["@solana/rpc-spec@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/subscribable": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-yQdbWw5mZEWrwsunHR9NHkuhMXIB9sPOObwm18D53v5tAJnxTB0IcHvO647XqFDLTK/yQ4AdDtlYD1vsY07AMQ=="], - "@solana/program-client-core": ["@solana/program-client-core@6.7.0", "", { "dependencies": { "@solana/accounts": "6.7.0", "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/errors": "6.7.0", "@solana/instruction-plans": "6.7.0", "@solana/instructions": "6.7.0", "@solana/plugin-interfaces": "6.7.0", "@solana/rpc-api": "6.7.0", "@solana/signers": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/ReQ26eOUjdL26smpv1732H+amGOCAB6dMnCCgaqCxS1pzY6bAfJoS6SU+z4ev8KzF7kAdlZG/f+LrtI74hFyA=="], + "@solana/rpc-spec-types": ["@solana/rpc-spec-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-NDZrKyZrJk4HaMFhTE/lAiMB824cWAodKqDHyKi0UteHU9pyRmil3BN1jt7e+j08mwMWwfklSgyrTaq52g6DIQ=="], - "@solana/programs": ["@solana/programs@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-RbJ9jWRaSGSQgyDyaN2FBXjqb5ussSjmIRgG5SsXehCQeJqb91qVGRpA55x7H6pbIPX5WDIiBgxZTK0nS4ja7Q=="], + "@solana/rpc-subscriptions": ["@solana/rpc-subscriptions@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/fast-stable-stringify": "6.10.0", "@solana/functional": "6.10.0", "@solana/promises": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-subscriptions-api": "6.10.0", "@solana/rpc-subscriptions-channel-websocket": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/subscribable": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-6mfuHp/K7unFKCOTCCBC9ziEGnxe2tyJ74EbR51QUnBeCUdYD7Hhdpxic1WRSJ3UeNW/mG4OzFM6z8Wi64Eh9Q=="], - "@solana/promises": ["@solana/promises@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-LKdYoYr6KXqSsJ39VjlZ15l+QuCx/bzoug62CShJqud41BUFUTPm7mD5sbV+rc8k0W1+MY8OKFA8G1i7zljw7w=="], + "@solana/rpc-subscriptions-api": ["@solana/rpc-subscriptions-api@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/keys": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CRPQoTtT1cOwOQUsqS7jgo7wYdAj7jB5ab/UmMPWVpecf2FNMhWhgvxP2s82M7VkDGTGl13qaQ0WySmi7Egrlg=="], - "@solana/rpc": ["@solana/rpc@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/fast-stable-stringify": "6.7.0", "@solana/functional": "6.7.0", "@solana/rpc-api": "6.7.0", "@solana/rpc-spec": "6.7.0", "@solana/rpc-spec-types": "6.7.0", "@solana/rpc-transformers": "6.7.0", "@solana/rpc-transport-http": "6.7.0", "@solana/rpc-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-Z2gE4U4ou8st8CwV1NjoVvVjeEfJi1Xnz5Rs5bd6vIneHyB3UjVk8hHmjNpaYCI/11zdFL1gp4D2tga38pNrHg=="], + "@solana/rpc-subscriptions-channel-websocket": ["@solana/rpc-subscriptions-channel-websocket@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/subscribable": "6.10.0", "ws": "^8.21.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-KkqP1186HELPlJftA88SNAT2znR8knCVzsUipXVzY4zfW8sN3LOa0ePMzh9VZ/V+J+raTt55laR87ovAO0n+zw=="], - "@solana/rpc-api": ["@solana/rpc-api@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/keys": "6.7.0", "@solana/rpc-parsed-types": "6.7.0", "@solana/rpc-spec": "6.7.0", "@solana/rpc-transformers": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/transaction-messages": "6.7.0", "@solana/transactions": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-Tzz1Mr2ejmLV2vKqPPru51u4EiwOUdMGHgby6j9XKMDBNcLIYPnAJKfIgbTZUJljM/MPFzZSzBGUzmoTo4pqTQ=="], + "@solana/rpc-subscriptions-spec": ["@solana/rpc-subscriptions-spec@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/promises": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/subscribable": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nWMwGaG4ulzeX2sskY5TywXF3cwEd8FDmUpLe2JBWxE8XDAOGOKcsYPYFcBgb8ee9KqfPT2PTNdcz9jOhJf34w=="], - "@solana/rpc-parsed-types": ["@solana/rpc-parsed-types@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-DwX4O918uAodrJyMh5TmsovOGeXRfF46Qwgbh57ZknFk3whrnahAHnPV9H5uR41hBExfZ/RE4u4IvLySca3Y8g=="], + "@solana/rpc-transformers": ["@solana/rpc-transformers@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-2nFUrVTiE720pJOY4XKx3HuYmishw0of/4oScu76YGm6O8wsmvFvPNAkrEinmieWXQkfpBfRvLZmpl8PaAy+ug=="], - "@solana/rpc-spec": ["@solana/rpc-spec@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/rpc-spec-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-dCCwZQdWW176fs7Db9qOrCGii7WnIhe+x5DM1p8TFov+4X/4APD4VD5/sgUcTEFKobawYaqrH2f7kk/kMSbE3w=="], + "@solana/rpc-transport-http": ["@solana/rpc-transport-http@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "undici-types": "^8.4.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-JrdNuYi0nBbD3X8JUtgX1dQJwIwz/WJvmigDdELysXfGB2bTJpfjqGDLhCLOz2sRl66FASIEqgG/LVa2C9VXcA=="], - "@solana/rpc-spec-types": ["@solana/rpc-spec-types@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-ohameGNJmtjZm7bIIGSaRIRI2G6Ikx77MfCyi9egxZo+CSS6X8WYQyqibVFjq/5RuFz9KnQp6+o2wOn3UL78sg=="], + "@solana/rpc-types": ["@solana/rpc-types@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/fixed-points": "6.10.0", "@solana/nominal-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-zaSecTfCPvz/vcoAmKD6XoRstGHTr1EKJBD8T9UcpEFFB6CtF6DxerDB+wrzkamuT6msmnR2DWXMrYOGDAsgIg=="], - "@solana/rpc-subscriptions": ["@solana/rpc-subscriptions@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/fast-stable-stringify": "6.7.0", "@solana/functional": "6.7.0", "@solana/promises": "6.7.0", "@solana/rpc-spec-types": "6.7.0", "@solana/rpc-subscriptions-api": "6.7.0", "@solana/rpc-subscriptions-channel-websocket": "6.7.0", "@solana/rpc-subscriptions-spec": "6.7.0", "@solana/rpc-transformers": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/subscribable": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-QocXnamF9fkgRMneApAepFZEX6sqjmFcmNX1h+fx05KKouQRgJ5fMEmV10Ga9DFtdc8CJtOPqYjyYRk2b/ygLQ=="], + "@solana/signers": ["@solana/signers@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/offchain-messages": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+vtCc+mT1FpGxrA5oL2aaMxSHiMJ2hH5PcDIfjo2XJkHz2klZiCZyT5F9+zpltc9vdi1QTElQq59Sfplmtd33A=="], - "@solana/rpc-subscriptions-api": ["@solana/rpc-subscriptions-api@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/keys": "6.7.0", "@solana/rpc-subscriptions-spec": "6.7.0", "@solana/rpc-transformers": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/transaction-messages": "6.7.0", "@solana/transactions": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-924U8fmFd+5ZkkK5023LWsR//qIkx7TinNxuFEnpbM4x/Yfi2O50xLelnx6hWV+dOJRQSJDzpMoPa3edQXUyQQ=="], + "@solana/subscribable": ["@solana/subscribable@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/promises": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-VsR6XMwkiDBkZJUcoGkEOhf397pOV75gKCL9Bx8bpi2T3Bbs0CxUpMn4yaUgAnRba3eXmjbXMNCXjttfa6sKbw=="], - "@solana/rpc-subscriptions-channel-websocket": ["@solana/rpc-subscriptions-channel-websocket@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/functional": "6.7.0", "@solana/rpc-subscriptions-spec": "6.7.0", "@solana/subscribable": "6.7.0", "ws": "^8.19.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-QlyqG31WCZtHrRinxeysNpmWOUJ8dYVl6/IKDACSH8QwlTmKkeIi/U197RfZ24ul+nDQ/arrVBp3wBFWVK8MWg=="], + "@solana/sysvars": ["@solana/sysvars@6.10.0", "", { "dependencies": { "@solana/accounts": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-cG13p1+onxz+20iWjwWQr1Z1jQwPm0fnjoW75fqZq7p4rVCie3L2sXvaJsYPjWKrUvpOzOIEHnqZGkG05rCpjg=="], - "@solana/rpc-subscriptions-spec": ["@solana/rpc-subscriptions-spec@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/promises": "6.7.0", "@solana/rpc-spec-types": "6.7.0", "@solana/subscribable": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-F+zK+l3INGGzrKN5+4VV8wemS18IvNYtr43h05VMwPF4qcibR+RnAxZ0Eq6FiCjQ+nF8dA5lE1eQxkkLoNxThQ=="], + "@solana/transaction-confirmation": ["@solana/transaction-confirmation@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/keys": "6.10.0", "@solana/promises": "6.10.0", "@solana/rpc": "6.10.0", "@solana/rpc-subscriptions": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-ULvtg65qfenh4T/GYcIlKSUv5EqDcng9UN0dxbHU4kuZdR2e0B8HN2xDC4WhcFQVeFJSbTZmaYFkeTY/Y4gfGQ=="], - "@solana/rpc-transformers": ["@solana/rpc-transformers@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/functional": "6.7.0", "@solana/nominal-types": "6.7.0", "@solana/rpc-spec-types": "6.7.0", "@solana/rpc-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-0kKwDbvYRfc5jvppqdkgxI620qF/rxnfayR0XLvdu4W+mOKMNEbBccBzrMqwm0tjVWGRgRHtpQtZ7t9fKWDX8A=="], + "@solana/transaction-messages": ["@solana/transaction-messages@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/instructions": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-s7v8G3BTxGlKYIj3eWCG0g1296v+1LBt16mVnlRH5FuyaJ5AdhlhtRho5HUDpdwE8EXun+y1c48V6uhcZ8wdbQ=="], - "@solana/rpc-transport-http": ["@solana/rpc-transport-http@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0", "@solana/rpc-spec": "6.7.0", "@solana/rpc-spec-types": "6.7.0", "undici-types": "^7.22.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-FDj6y+LT+TY5cn4bgYjZss73vqqThc60P7CLSDUqYzTtqNE+IfEe7h6pmP4oJvOVWr6sWwYICHkgD3x2BFtPZA=="], + "@solana/transactions": ["@solana/transactions@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-VADSqP9OTYmhrox4pcgDd4+RjVmednXSE0+8Y7SPK4PN1pK5Az2RJ0nSsy0xcTnaOr8mF/crwFktqPrRQwSbQA=="], - "@solana/rpc-types": ["@solana/rpc-types@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/nominal-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-M5F1ommNsp/aNWZIDUdboOHAftrPaUH6wcho71bIh3WaTVAtk2vYyyTaow+lX5sI3psVE/XB8f2WVcKH+p1TIA=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], - "@solana/signers": ["@solana/signers@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/errors": "6.7.0", "@solana/instructions": "6.7.0", "@solana/keys": "6.7.0", "@solana/nominal-types": "6.7.0", "@solana/offchain-messages": "6.7.0", "@solana/transaction-messages": "6.7.0", "@solana/transactions": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-r2cFkxJFf7AjxXkZL0aXbiipscis7xzs1acmZzD2pgy7w7Hz6bw1w3lWihYL/pgjprYafnpH7XG36jJzKF0+WA=="], - - "@solana/subscribable": ["@solana/subscribable@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-cAEM3u8Z9qTjWwMEZ75tMRoFWArPnseP3INz0FM8FxsjD5uxZ5rH6H0uPfUBBiI/n+pqdkmQMX39hmdXE39hXQ=="], - - "@solana/sysvars": ["@solana/sysvars@6.7.0", "", { "dependencies": { "@solana/accounts": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-data-structures": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/errors": "6.7.0", "@solana/rpc-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-TDMmDh1Nq/F0B0IN6P/eujx96oLo9n794jn7ZAT+a7cSrMM9QtxPXvEaW8cqp0cB6lWWkKolPfPSOilZK74WcQ=="], - - "@solana/transaction-confirmation": ["@solana/transaction-confirmation@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/keys": "6.7.0", "@solana/promises": "6.7.0", "@solana/rpc": "6.7.0", "@solana/rpc-subscriptions": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/transaction-messages": "6.7.0", "@solana/transactions": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-yalZYuOQDvLpRftcxdKYRbPkEjPRNZ1pFavdpj4TWPQryYbq86xgxJmvP2GW8UnCIBEp42PucbdHXuXM33Cx+A=="], - - "@solana/transaction-messages": ["@solana/transaction-messages@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-data-structures": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/errors": "6.7.0", "@solana/functional": "6.7.0", "@solana/instructions": "6.7.0", "@solana/nominal-types": "6.7.0", "@solana/rpc-types": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-sbr26okH0DNrufhMMegMyqL1z1+ISXn/NcIWeEh7xXiMUGZlWL1z045PrRdqD2qTkMh8c474DBEzfGWj8AiQOA=="], - - "@solana/transactions": ["@solana/transactions@6.7.0", "", { "dependencies": { "@solana/addresses": "6.7.0", "@solana/codecs-core": "6.7.0", "@solana/codecs-data-structures": "6.7.0", "@solana/codecs-numbers": "6.7.0", "@solana/codecs-strings": "6.7.0", "@solana/errors": "6.7.0", "@solana/functional": "6.7.0", "@solana/instructions": "6.7.0", "@solana/keys": "6.7.0", "@solana/nominal-types": "6.7.0", "@solana/rpc-types": "6.7.0", "@solana/transaction-messages": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-4qiVyr2VfJdjmKg9aLA+nulHaoNLOEBr2f6+aWDQIuRMNiaYHLwXQ2BzfVROtou1TuUrdO9R6JGwI/C477bW4g=="], - - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - - "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -339,7 +302,7 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -349,7 +312,7 @@ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], @@ -357,19 +320,15 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], - - "fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + "gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="], "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], + "google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -377,17 +336,19 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "jupiter-api-ts": ["jupiter-api-ts@0.4.1", "", {}, "sha512-U2p7GB7+neikqbCN1ZPZFis3HDLphPS9q0H5JlICjoNfk/QfoBUe+Fqbd+MIQcb/AzUtnGpIx5eNX92pxj0KxA=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="], - "micro-key-producer": ["micro-key-producer@0.8.5", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/curves": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/base": "^2.0.0", "micro-packed": "^0.8.0" }, "bin": { "gpgkp": "bin/gpgkp.js" } }, "sha512-aPvW8Dyp0My4Un3DL4WIwG8blHM7AwYL5VwKWXSHztxJxfVRlRaa/z77qGMP5OO3Zmq2Xje+dV9Be4keSTfVYA=="], + "micro-key-producer": ["micro-key-producer@0.8.6", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "micro-packed": "^0.8.0" }, "bin": { "gpgkp": "bin/gpgkp.js" } }, "sha512-jlAjz5sZevIwLXHbcE9TmrPRjeNa+3cnXYeVlu97lLuq2yXmrrM46Ei8ZSMELHVMuziaQDne7wkVW8SYyPG94g=="], "micro-packed": ["micro-packed@0.8.0", "", { "dependencies": { "@scure/base": "2.0.0" } }, "sha512-AKb8znIvg9sooythbXzyFeChEY0SkW0C6iXECpy/ls0e5BtwXO45J9wD9SLzBztnS4XmF/5kwZknsq+jyynd/A=="], @@ -397,11 +358,9 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "oxlint": ["oxlint@1.53.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.53.0", "@oxlint/binding-android-arm64": "1.53.0", "@oxlint/binding-darwin-arm64": "1.53.0", "@oxlint/binding-darwin-x64": "1.53.0", "@oxlint/binding-freebsd-x64": "1.53.0", "@oxlint/binding-linux-arm-gnueabihf": "1.53.0", "@oxlint/binding-linux-arm-musleabihf": "1.53.0", "@oxlint/binding-linux-arm64-gnu": "1.53.0", "@oxlint/binding-linux-arm64-musl": "1.53.0", "@oxlint/binding-linux-ppc64-gnu": "1.53.0", "@oxlint/binding-linux-riscv64-gnu": "1.53.0", "@oxlint/binding-linux-riscv64-musl": "1.53.0", "@oxlint/binding-linux-s390x-gnu": "1.53.0", "@oxlint/binding-linux-x64-gnu": "1.53.0", "@oxlint/binding-linux-x64-musl": "1.53.0", "@oxlint/binding-openharmony-arm64": "1.53.0", "@oxlint/binding-win32-arm64-msvc": "1.53.0", "@oxlint/binding-win32-ia32-msvc": "1.53.0", "@oxlint/binding-win32-x64-msvc": "1.53.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-TLW0PzGbpO1JxUnuy1pIqVPjQUGh4fNfxu5XJbdFIRFVaJ0UFzTjjk/hSFTMRxN6lZub53xL/IwJNEkrh7VtDg=="], - - "path-expression-matcher": ["path-expression-matcher@1.2.0", "", {}, "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ=="], + "oxfmt": ["oxfmt@0.65.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.65.0", "@oxfmt/binding-android-arm64": "0.65.0", "@oxfmt/binding-darwin-arm64": "0.65.0", "@oxfmt/binding-darwin-x64": "0.65.0", "@oxfmt/binding-freebsd-x64": "0.65.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.65.0", "@oxfmt/binding-linux-arm-musleabihf": "0.65.0", "@oxfmt/binding-linux-arm64-gnu": "0.65.0", "@oxfmt/binding-linux-arm64-musl": "0.65.0", "@oxfmt/binding-linux-ppc64-gnu": "0.65.0", "@oxfmt/binding-linux-riscv64-gnu": "0.65.0", "@oxfmt/binding-linux-riscv64-musl": "0.65.0", "@oxfmt/binding-linux-s390x-gnu": "0.65.0", "@oxfmt/binding-linux-x64-gnu": "0.65.0", "@oxfmt/binding-linux-x64-musl": "0.65.0", "@oxfmt/binding-openharmony-arm64": "0.65.0", "@oxfmt/binding-win32-arm64-msvc": "0.65.0", "@oxfmt/binding-win32-ia32-msvc": "0.65.0", "@oxfmt/binding-win32-x64-msvc": "0.65.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "oxlint": ["oxlint@1.80.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.80.0", "@oxlint/binding-android-arm64": "1.80.0", "@oxlint/binding-darwin-arm64": "1.80.0", "@oxlint/binding-darwin-x64": "1.80.0", "@oxlint/binding-freebsd-x64": "1.80.0", "@oxlint/binding-linux-arm-gnueabihf": "1.80.0", "@oxlint/binding-linux-arm-musleabihf": "1.80.0", "@oxlint/binding-linux-arm64-gnu": "1.80.0", "@oxlint/binding-linux-arm64-musl": "1.80.0", "@oxlint/binding-linux-ppc64-gnu": "1.80.0", "@oxlint/binding-linux-riscv64-gnu": "1.80.0", "@oxlint/binding-linux-riscv64-musl": "1.80.0", "@oxlint/binding-linux-s390x-gnu": "1.80.0", "@oxlint/binding-linux-x64-gnu": "1.80.0", "@oxlint/binding-linux-x64-musl": "1.80.0", "@oxlint/binding-openharmony-arm64": "1.80.0", "@oxlint/binding-win32-arm64-msvc": "1.80.0", "@oxlint/binding-win32-ia32-msvc": "1.80.0", "@oxlint/binding-win32-x64-msvc": "1.80.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -409,30 +368,28 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strnum": ["strnum@2.2.2", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - - "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@solana-program/token/@solana-program/system": ["@solana-program/system@0.12.2", "", { "peerDependencies": { "@solana/kit": "^6.4.0" } }, "sha512-MaBeOxlvTruQhA7UYkOb3hVTEHPPagOtd+PvTm6a8rGgvEAP0kD4BbC37NceOaR4ABNqdaCmD5OMVRKgrE6KAg=="], - "@solana/rpc-transport-http/undici-types": ["undici-types@7.22.0", "", {}, "sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw=="], + "@solana-program/token-2022/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@solana/rpc-transport-http/undici-types": ["undici-types@8.10.0", "", {}, "sha512-ibvdovq3nCFs8Msrd95BW+zUOq+aOVbT+wpHUoPWhztbHEoPc6oof51iFDB6Es8lTKvNvVW9jNSAB8dwrKTMGg=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "micro-packed/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], - "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@solana-program/token-2022/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], } } diff --git a/docs/config.md b/docs/config.md index 923b66b..4ddf7f0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -33,3 +33,15 @@ jup config set --api-key # Delete or unset the API key jup config set --api-key ``` + +### Set an RPC URL + +Signable commands use this RPC URL to simulate or submit transactions. It +defaults to `https://api.mainnet.solana.com` when no custom endpoint is set. + +```bash +jup config set --rpc-url https://your-rpc.example.com + +# Return to the mainnet default +jup config set --rpc-url +``` diff --git a/docs/dca.md b/docs/dca.md new file mode 100644 index 0000000..43eb1e4 --- /dev/null +++ b/docs/dca.md @@ -0,0 +1,51 @@ +# DCA Orders + +Requires a Jupiter API key. Creating and cancelling orders also requires an +active key. See [setup](setup.md). + +The `dca` command manages time-based recurring orders. It does not expose +deprecated price-based recurring orders. + +## Create an order + +```bash +jup dca create --from USDC --to SOL --amount 100 --interval 86400 --orders 10 +jup dca create --from USDC --to SOL --raw-amount 100000000 --interval 86400 --orders 10 --key mykey +jup dca create --from USDC --to SOL --amount 100 --interval 86400 --orders 10 --dry-run +``` + +- `--from` and `--to` accept a token symbol or mint address. +- Provide exactly one of `--amount` (human-readable units) or `--raw-amount` + (on-chain units). The amount is the total amount deposited for the order. +- `--interval` is the number of seconds between purchases, such as `86400` for + one day. `--orders` is the total number of purchases. +- `--min-price` and `--max-price` optionally set USD price bounds. +- `--start-at` optionally sets a UNIX timestamp; otherwise the order begins + immediately. +- `--key` overrides the active key. +- `--dry-run` signs and simulates the transaction with the configured RPC + without submitting it. + +## Cancel an order + +```bash +jup dca cancel --order +jup dca cancel --order --key mykey +jup dca cancel --order --dry-run +``` + +Use the recurring order account returned by `jup dca list` as `--order`. + +## List orders + +```bash +jup dca list +jup dca list --key mykey +jup dca list --address +jup dca list --status history --token USDC +jup dca list --page 2 +``` + +With no wallet option, the command uses the active key's wallet. `--key` and +`--address` are mutually exclusive. `--status` is `active` by default and can +be set to `history`; `--token` filters by input token. diff --git a/docs/invite.md b/docs/invite.md new file mode 100644 index 0000000..1b53ed2 --- /dev/null +++ b/docs/invite.md @@ -0,0 +1,60 @@ +# Jupiter Send Invites + +Requires a Jupiter API key. Sending and clawing back an invite also require an +active key. See [setup](setup.md). + +The `invite` command creates Jupiter Send invites that recipients can claim +without a wallet at creation time. An invite code is sensitive: treat it like a +password because it is needed to claim an unclaimed invite. + +## Send an invite + +```bash +jup invite send --token USDC --amount 10 +jup invite send --token SOL --amount 0.1 --key mykey +jup invite send --token USDC --raw-amount 10000000 --dry-run +``` + +- `--token` accepts a symbol or mint address and defaults to SOL. +- Provide exactly one of `--amount` (human-readable units) or `--raw-amount` + (on-chain units). +- `--key` overrides the active key. +- `--dry-run` signs and simulates without submitting. + +The response contains a shareable `inviteCode`, its `invitePda`, `expiry`, and +transaction `signature`. Save the invite code securely; it is not stored by the +CLI and is never printed again by list/history commands. + +## Claw back an invite + +```bash +jup invite clawback --invite +jup invite clawback --invite --key mykey +jup invite clawback --invite --dry-run +``` + +Use an unclaimed invite PDA returned by `jup invite list`. The sender signs the +clawback transaction; dry-run simulates it without submitting. + +## List pending invites + +```bash +jup invite list +jup invite list --key mykey +jup invite list --address +jup invite list --page 2 +``` + +With no wallet option, this uses the active key's wallet. `--key` and +`--address` are mutually exclusive. + +## View invite history + +```bash +jup invite history +jup invite history --key mykey +jup invite history --address +jup invite history --page 2 +``` + +History includes invites where the wallet was the sender or recipient. diff --git a/docs/jlp.md b/docs/jlp.md new file mode 100644 index 0000000..87369d2 --- /dev/null +++ b/docs/jlp.md @@ -0,0 +1,42 @@ +# JLP + +The `jlp` command shows Jupiter Liquidity Provider pool data and creates the +transactions needed to mint or burn JLP. Mint and burn transactions are signed +with the active key (or `--key`) and submitted through the configured RPC. + +## Pool information + +```bash +jup jlp info +``` + +Displays the pool AUM, JLP price and supply, APR/APY, and the current and +target weights for each custody token. + +## Mint JLP + +```bash +jup jlp mint --token SOL --amount 1 +jup jlp mint --token USDC --amount 100 --key mykey +jup jlp mint --token USDC --raw-amount 100000000 --dry-run +``` + +`--token` accepts `SOL`, `WBTC`, `ETH`, or `USDC`. Provide exactly one of +`--amount` (human-readable units) or `--raw-amount` (on-chain units). + +## Burn JLP + +```bash +jup jlp burn --amount 10 --receive USDC +jup jlp burn --amount 10 --receive SOL --key mykey +jup jlp burn --raw-amount 10000000 --receive USDC --dry-run +``` + +`--receive` accepts `SOL`, `WBTC`, `ETH`, or `USDC`. JLP has six decimals, and +the amount must be supplied through exactly one of `--amount` or +`--raw-amount`. + +## Dry runs + +Use `--dry-run` to sign and simulate the transaction without broadcasting it. +The output includes the serialized signed transaction and simulation result. diff --git a/docs/lend.md b/docs/lend.md index 6ee724f..6eb7f6b 100644 --- a/docs/lend.md +++ b/docs/lend.md @@ -1,8 +1,8 @@ # Lending -Requires: an active key for `deposit` and `withdraw` commands. See [setup](setup.md). +Requires: an active key for `earn deposit`, `earn withdraw`, and `borrow operate`. See [setup](setup.md). -## Commands +## Earn commands ### View available tokens @@ -46,6 +46,7 @@ jup lend earn positions --token USDC ```js // Example JSON response: { + "address": "5vft...8Y2N", // active key wallet unless --address or --key is set "positions": [ { "token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, // underlying token @@ -79,6 +80,7 @@ jup lend earn deposit --token USDC --amount 100 --dry-run ```js // Example JSON response: { + "address": "5vft...8Y2N", "token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, "depositedAmount": "100", // human-readable amount just deposited "depositedUsd": 100.00, // USD value of deposit @@ -109,6 +111,7 @@ jup lend earn withdraw --token USDC --amount 50 --dry-run ```js // Example JSON response: { + "address": "5vft...8Y2N", "token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, "withdrawnAmount": "50", // human-readable amount just withdrawn "withdrawnUsd": 50.00, // USD value of withdrawal @@ -119,7 +122,135 @@ jup lend earn withdraw --token USDC --amount 50 --dry-run } ``` -## Workflows +## Borrow commands + +Borrow uses a single `operate` transaction. Positive `--collateral` deposits, negative withdraws. Positive `--debt` borrows, negative repays. Use `all` to withdraw all collateral or repay all debt (including interest dust). `--position 0` (the default) opens a new position. + +Vault IDs and position NFT IDs are scoped to a market. Pass the same `--market` to `vaults`, `positions`, and `operate`. Omitting it uses `main`. + +### View vaults + +```bash +jup lend borrow vaults +jup lend borrow vaults --market ethena +``` + +- Shows borrow vaults with LTV, borrow APY, and available borrow liquidity +- `--market` selects `main` (default) or `ethena` + +```js +// Example JSON response: +{ + "market": "main", + "vaults": [ + { + "vaultId": 1, // pass to operate as --vault + "address": "nMzV...nb1", + "supplyToken": { "id": "So11...1112", "symbol": "SOL", "decimals": 9 }, + "borrowToken": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, + "ltvPct": 80, // max LTV; 80 means 80% + "liquidationThresholdPct": 85, + "supplyApyPct": 1.25, // 1.25 means 1.25% + "borrowApyPct": 4.71, + "totalSupplyUsd": 133600000, + "totalBorrowUsd": 61780, + "borrowableUsd": 50000000, + "withdrawableUsd": 80000000, + "minimumBorrowingAmount": 1, + "totalPositions": 9071 + } + ] +} +``` + +### View borrow positions + +```bash +jup lend borrow positions +jup lend borrow positions --key mykey +jup lend borrow positions --address +jup lend borrow positions --vault 1 +jup lend borrow positions --market ethena +``` + +- With no options, uses the active key's wallet +- `--address` looks up any wallet without needing a key +- `--vault` filters by vault ID from `lend borrow vaults` +- `--market` selects `main` (default) or `ethena` + +```js +// Example JSON response: +{ + "address": "5vft...8Y2N", // active key wallet unless --address or --key is set + "market": "main", + "positions": [ + { + "positionId": 9062, // pass to operate as --position + "positionPubkey": "7xKp...2mNq", + "vaultId": 1, + "supplyToken": { "id": "So11...1112", "symbol": "SOL", "decimals": 9 }, + "borrowToken": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, + "collateralAmount": 0.03, + "collateralUsd": 2.05, + "debtAmount": 1.1, + "debtUsd": 1.1, + "ltvPct": 53.66, + "isLiquidated": false + } + ] +} +``` + +### Operate (deposit, borrow, repay, withdraw) + +```bash +jup lend borrow operate --vault 1 --collateral 0.03 +jup lend borrow operate --vault 1 --position 9062 --debt 1.1 +jup lend borrow operate --vault 1 --position 9062 --collateral 0.03 --debt 1.1 +jup lend borrow operate --vault 1 --position 9062 --debt=-1.1 +jup lend borrow operate --vault 1 --position 9062 --debt all +jup lend borrow operate --vault 1 --position 9062 --collateral all +jup lend borrow operate --vault 1 --position 9062 --raw-debt 1100000 +jup lend borrow operate --vault 1 --collateral 0.03 --dry-run +jup lend borrow operate --vault 1 --market ethena --collateral 0.03 +``` + +- `--vault` (required) — vault ID from `lend borrow vaults` +- `--position` — position NFT ID. Defaults to `0`, which creates a new position +- `--collateral` — human-readable collateral delta of the supply token. Positive deposits, negative withdraws (`--collateral=-0.01`), `all` withdraws everything +- `--debt` — human-readable debt delta of the borrow token. Positive borrows, negative repays (`--debt=-1.1`), `all` repays everything including interest dust +- `--raw-collateral` / `--raw-debt` — on-chain units (signed integer strings) +- `--market` selects `main` (default) or `ethena`. Vault and position IDs are market-scoped +- Exactly one of `--collateral` or `--raw-collateral` may be set; same for `--debt` / `--raw-debt` +- At least one amount option is required +- `--key` overrides the active key for this transaction +- `--dry-run` signs and simulates without submitting. JSON includes `serializedTransaction` + +Depositing WSOL collateral requires a funded WSOL token account. The API does not wrap native SOL. Borrowing or withdrawing WSOL credits a WSOL token account, not native SOL. + +Repaying the exact displayed debt can leave interest dust. Use `--debt all` before withdrawing all collateral. + +```js +// Example JSON response: +{ + "address": "5vft...8Y2N", + "market": "main", + "vaultId": 1, + "positionId": 9062, // newly assigned when --position was 0 + "supplyToken": { "id": "So11...1112", "symbol": "SOL", "decimals": 9 }, + "borrowToken": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, + "collateralDelta": "0.03", // human-readable delta just applied, or "all" + "debtDelta": "1.1", + "collateralAmount": 0.03, // total collateral after the operation + "collateralUsd": 2.05, + "debtAmount": 1.1, // total debt after the operation + "debtUsd": 1.1, + "ltvPct": 53.66, + "signature": "3dV9...8zG1" +} +``` + +## Earn workflows ### Check available tokens then deposit @@ -137,8 +268,28 @@ jup lend earn positions jup lend earn withdraw --token USDC --amount 50 ``` -### Withdraw entire position +### Withdraw entire earn position ```bash jup lend earn withdraw --token USDC ``` + +## Borrow workflows + +### Deposit collateral then borrow + +```bash +jup lend borrow vaults +# Pick a vault ID +jup lend borrow operate --vault 1 --collateral 0.03 +jup lend borrow positions +jup lend borrow operate --vault 1 --position 9062 --debt 1.1 +``` + +### Repay and withdraw + +```bash +jup lend borrow positions +jup lend borrow operate --vault 1 --position 9062 --debt all +jup lend borrow operate --vault 1 --position 9062 --collateral all +``` diff --git a/docs/spot.md b/docs/spot.md index 76322a0..505424b 100644 --- a/docs/spot.md +++ b/docs/spot.md @@ -1,6 +1,8 @@ # Spot Trading -Requires: an active key for swap and transfer commands. See [setup](setup.md). +Requires: a Jupiter API key for token search, quotes, swaps, transfers, and +token-filtered history. An active key is also required for signing swaps, +transfers, and reclaim transactions. See [setup](setup.md). ## Token resolution @@ -9,10 +11,9 @@ Anywhere a token is specified (`--from`, `--to`, `--token`, `--search`), you can - **Symbol** (e.g. `SOL`, `USDC`, `JUP`) — the CLI auto-resolves to the best-matching token - **Mint address** (e.g. `So11111111111111111111111111111111111111112`) — exact match -Token resolution depends on the context: - -- **Wallet-bound options** (`swap --from`, `transfer --token`, `reclaim --token`) resolve against the tokens in your wallet. This ensures the CLI matches the token you actually hold, not a different token with the same symbol. If the token is not found in your wallet, the command errors. If multiple tokens share the same symbol, the CLI asks you to use the mint address instead. -- **All other options** (`swap --to`, `quote --from`, `quote --to`, `tokens --search`, `history --token`) resolve via Jupiter's global token search, picking the top result. +Token arguments resolve through Jupiter Tokens V2 search, which picks the top +result. A comma-separated list of up to 100 mint addresses can be looked up in +one request. Use a mint address whenever a symbol might be ambiguous. ## Commands @@ -35,6 +36,7 @@ jup spot quote --from --to --raw-amount 1000000000 - `--amount` uses human-readable units (e.g. `1` SOL = 1 SOL) - `--raw-amount` uses on-chain units (e.g. `1000000000` lamports = 1 SOL) - Exactly one of `--amount` or `--raw-amount` is required +- Requires a Jupiter API key ```js // Example JSON response: @@ -59,8 +61,9 @@ jup spot swap --from SOL --to USDC --amount 1 --dry-run ``` - `--key` overrides the active key for this transaction -- `--slippage` sets max slippage in basis points (e.g. `50` = 0.5%). Recommended to be emtpy: Jupiter's Real-Time Slippage Estimation (RTSE) automatically picks an optimal value. -- `--dry-run` previews the swap without signing or submitting. The API still simulates the transaction, so errors like insufficient balance are caught. In JSON mode, the response includes the unsigned base64 `transaction` for external signing. +- `--slippage` sets max slippage in basis points (e.g. `50` = 0.5%). Omit it to use Jupiter's default. +- `--dry-run` signs and simulates the transaction through the configured RPC without submitting it. In JSON mode, the response includes the signed serialized transaction and simulation result. +- Requires a Jupiter API key ```js // Example JSON response: @@ -81,52 +84,45 @@ jup spot swap --from SOL --to USDC --amount 1 --dry-run ### View portfolio ```bash -jup spot portfolio -jup spot portfolio --key mykey -jup spot portfolio --address +jup portfolio +jup portfolio --key mykey +jup portfolio --address ``` - With no options, uses the active key's wallet -- `--address` looks up any wallet without needing a key +- `--address` looks up any wallet without needing a signing key; the Portfolio API still requires a Jupiter API key ```js // Example JSON response: { "totalValue": 1250.50, // total USD net worth - "tokens": [ + "positions": [ { - "id": "So11...1112", // mint address - "symbol": "SOL", - "decimals": 9, - "amount": 10.5, // human-readable decimal balance - "rawAmount": "10500000000", // on-chain integer balance - "value": 892.50, // USD value of holdings - "price": 85.00, // current token USD price - "priceChange": 0.032, // 24h price change; 0.032 means 3.2% - "isVerified": true + "platformId": "wallet", + "label": "Wallet", + "value": 892.50 } ] } ``` -### Reclaim rent from ATA +### Reclaim rent from token accounts ```bash -jup spot reclaim -jup spot reclaim --key mykey -jup spot reclaim --token USDC -jup spot reclaim --dry-run +jup reclaim +jup reclaim --key mykey +jup reclaim --token USDC +jup reclaim --dry-run ``` -- With no options, reclaims rent from all empty Associated Token Accounts (ATA) owned by the active key's wallet -- `--dry-run` previews reclaimable amount without executing. JSON response includes the unsigned base64 `transactions` array. +- With no options, reclaims rent from every empty, non-frozen token account owned by the active key's wallet +- The command revalidates accounts via the configured RPC, constructs close-account transactions locally, and submits them directly to that RPC +- `--dry-run` signs and simulates every locally built transaction without submitting it. JSON response includes the signed serialized transactions and each simulation result. ```js // Example JSON response: { - "totalLamportsReclaimed": 5000, // divide by 10^9 for total SOL reclaimed - "totalValueReclaimed": 0.005, // USD value of reclaimed SOL - "networkFeeLamports": 5000, // divide by 10^9 for SOL fee + "totalLamportsReclaimed": "5000", // rent returned before transaction fees "signatures": [ // array of tx signatures for each batch of reclaim tx "3dV98zG...", ] @@ -171,16 +167,17 @@ jup spot history --address --limit 10 --offset 123 ### Transfer tokens ```bash -jup spot transfer --token SOL --to --amount 1 -jup spot transfer --token USDC --to --amount 50 -jup spot transfer --token --to --raw-amount 1000000000 -jup spot transfer --token SOL --to --amount 1 --key mykey -jup spot transfer --token SOL --to --amount 1 --dry-run +jup transfer --token SOL --to --amount 1 +jup transfer --token USDC --to --amount 50 +jup transfer --token --to --raw-amount 1000000000 +jup transfer --token SOL --to --amount 1 --key mykey +jup transfer --token SOL --to --amount 1 --dry-run ``` -- Works with both SOL and any SPL token +- Works with both SOL and any SPL token. The CLI builds the System Program or SPL Token transaction locally and sends it through the configured RPC URL. - `--token` accepts a symbol or mint address -- `--dry-run` previews the transfer without signing. JSON response includes the unsigned base64 `transaction`. +- `--dry-run` signs and simulates the transfer without submitting it. JSON response includes the signed `serializedTransaction` and simulation result. +- Requires a Jupiter API key to resolve `--token` ```js // Example JSON response: @@ -190,7 +187,6 @@ jup spot transfer --token SOL --to --amount 1 --dry-run "token": { "id": "EPjF...USDC", "symbol": "USDC", "decimals": 6 }, "amount": "50", // human-readable decimal amount "value": 50.00, // USD value of transfer - "networkFeeLamports": 5000, // divide by 10^9 for SOL fee "signature": "4xK29zH..." // tx signature viewable on an explorer } ``` @@ -214,6 +210,6 @@ jup spot tokens --search ### Check holdings then transfer ```bash -jup spot portfolio -jup spot transfer --token USDC --to --amount 50 +jup portfolio +jup transfer --token USDC --to --amount 50 ``` diff --git a/llms.txt b/llms.txt index b3ec76c..29cfe8d 100644 --- a/llms.txt +++ b/llms.txt @@ -15,6 +15,6 @@ On failure, commands exit with non-zero code with an error message. In JSON mode - [Sign](docs/sign.md): Sign transactions from `--dry-run` - [Spot](docs/spot.md): Spot trading, transfers, token search and portfolio data - [Perps](docs/perps.md): Perps trading (leveraged longs/shorts) -- [Lend](docs/lend.md): Lending and yield farming +- [Lend](docs/lend.md): Lending, borrowing, and yield farming - [Predictions](docs/predictions.md): Prediction markets - [Vrfd](docs/vrfd.md): Token verification diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..f4b2c5f --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,13 @@ +export default { + arrowParens: "always", + bracketSpacing: true, + endOfLine: "lf", + ignorePatterns: ["*.md"], + printWidth: 80, + semi: true, + singleQuote: false, + sortPackageJson: false, + tabWidth: 2, + trailingComma: "es5", + useTabs: false, +}; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..a4df876 --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "oxlint"; + +export default defineConfig({ + categories: { + correctness: "warn", + }, + rules: { + curly: "error", + "no-unused-vars": "allow", + }, +}); diff --git a/package.json b/package.json index 8e6a0f8..0b434a4 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "build": "bun build src/index.ts --outdir dist --target node --format esm --minify", "build:binary": "bash scripts/build.sh", "typecheck": "bunx tsc --noEmit", - "fmt": "bunx prettier --write .", - "fmt:check": "bunx prettier --check .", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", "lint": "bunx oxlint src", "test": "bun test", "ci": "bun run fmt:check && bun run lint && bun run typecheck && bun run test", @@ -35,19 +35,22 @@ "dependencies": { "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", - "@solana-program/token": "^0.12.0", + "@solana-program/system": "^0.14.0", + "@solana-program/token": "^0.14.0", + "@solana-program/token-2022": "^0.16.0", "@solana/keychain": "^1.1.0", "@solana/kit": "^6.7.0", "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^14.0.3", + "jupiter-api-ts": "^0.4.1", "ky": "^1.14.3", "micro-key-producer": "^0.8.5" }, "devDependencies": { "@types/bun": "latest", + "oxfmt": "^0.65.0", "oxlint": "^1.53.0", - "prettier": "^3.8.1", "typescript": "^5.9.3" } } diff --git a/src/clients/ClientConfig.ts b/src/clients/ClientConfig.ts deleted file mode 100644 index eb4212e..0000000 --- a/src/clients/ClientConfig.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Config } from "../lib/Config.ts"; - -export class ClientConfig { - static readonly #API_KEY = Config.load().apiKey; - - public static readonly host = this.#API_KEY - ? "https://api.jup.ag" - : "https://lite-api.jup.ag"; - - public static readonly headers: Record = { - "x-client-platform": "jupiter.cli", - ...(this.#API_KEY && { - "x-api-key": this.#API_KEY, - }), - }; -} diff --git a/src/clients/DatapiClient.ts b/src/clients/DatapiClient.ts deleted file mode 100644 index 512ca24..0000000 --- a/src/clients/DatapiClient.ts +++ /dev/null @@ -1,188 +0,0 @@ -import ky from "ky"; - -import { ClientConfig } from "./ClientConfig.ts"; - -export type SwapStats = { - priceChange?: number | undefined; - holderChange?: number | undefined; - liquidityChange?: number | undefined; - volumeChange?: number | undefined; - buyVolume?: number | undefined; - sellVolume?: number | undefined; - buyOrganicVolume?: number | undefined; - sellOrganicVolume?: number | undefined; - numBuys?: number | undefined; - numSells?: number | undefined; - numTraders?: number | undefined; - numOrganicBuyers?: number | undefined; -}; - -export type Token = { - id: string; - name: string; - symbol: string; - icon?: string | undefined; - decimals: number; - twitter?: string | undefined; - telegram?: string | undefined; - website?: string | undefined; - dev?: string | undefined; - circSupply?: number | undefined; - totalSupply?: number | undefined; - tokenProgram: string; - launchpad?: string | undefined; - graduatedPool?: string | undefined; - holderCount?: number | undefined; - fdv?: number | undefined; - mcap?: number | undefined; - usdPrice?: number | undefined; - liquidity?: number | undefined; - stats5m?: SwapStats | undefined; - stats1h?: SwapStats | undefined; - stats6h?: SwapStats | undefined; - stats24h?: SwapStats | undefined; - firstPool?: - | { - id: string; - createdAt: string; - } - | undefined; - audit?: - | { - mintAuthorityDisabled: boolean | undefined; - freezeAuthorityDisabled: boolean | undefined; - topHoldersPercentage: number | undefined; - lpBurnedPercentage: number | undefined; - knownRugger: boolean | undefined; - knownRuggerTopHolder: boolean | undefined; - soulBound: boolean | undefined; - permanentControlEnabled: boolean | undefined; - highSingleOwnership: boolean | undefined; - mutableFees: boolean | undefined; - } - | undefined; - scaledUiConfig?: - | { - multiplier: number; - newMultiplier: number; - newMultiplierEffectiveAt: string; - circSupplyPrescaled: number; - totalSupplyPrescaled: number; - usdPricePrescaled: number; - } - | undefined; - organicScore: number; - organicScoreLabel: "high" | "medium" | "low"; - ctLikes?: number | undefined; - smartCtLikes?: number | undefined; - isVerified?: boolean | undefined; - cexes?: string[] | undefined; - tags?: string[] | undefined; - stockData?: { id: string } | undefined; - apy?: - | { - jupEarn?: number | undefined; - } - | undefined; -}; - -type GetSearchTokensRequest = { - query: string; - filters?: string | undefined; - limit?: string | undefined; - sortBy?: string | undefined; -}; - -type GetSearchTokensResponse = Token[]; - -export type SpotTrade = { - type: "buy" | "sell"; - usdVolume: number; - profit: number; - cost: number; - txHash: string; - assetId: string; - blockTime: string; - amount: number; - price: number; -}; - -type GetSpotHistoryResponse = { - userTrades: SpotTrade[]; - next: string | null; -}; - -export class DatapiClient { - static readonly #ky = ky.create({ - prefixUrl: ClientConfig.host, - headers: ClientConfig.headers, - throwHttpErrors: false, - }); - - public static async getTokensSearch( - req: GetSearchTokensRequest - ): Promise { - return this.#ky.get("tokens/v2/search", { searchParams: req }).json(); - } - - public static async getSwapsByAddress(params: { - address: string; - assetId?: string; - after?: string; - before?: string; - limit?: number; - offset?: string; - }): Promise { - const searchParams: Record = { - addresses: params.address, - includeCapitalSide: "true", - }; - if (params.assetId) { - searchParams.assetId = params.assetId; - } - if (params.after) { - searchParams.fromTs = params.after; - } - if (params.before) { - searchParams.toTs = params.before; - } - if (params.limit) { - searchParams.limit = Math.min(params.limit, 30); - } - if (params.offset) { - searchParams.offset = params.offset; - } - return this.#ky.get("_datapi/v1/txs/users", { searchParams }).json(); - } - - public static async getTokensByMints(mints: string[]): Promise { - if (mints.length === 0) { - return []; - } - const BATCH_SIZE = 100; - const batches: string[][] = []; - for (let i = 0; i < mints.length; i += BATCH_SIZE) { - batches.push(mints.slice(i, i + BATCH_SIZE)); - } - const resolved = await Promise.all( - batches.map((batch) => - this.getTokensSearch({ - query: batch.join(","), - limit: BATCH_SIZE.toString(), - }) - ) - ); - return resolved.flat(); - } - - public static async resolveToken(input: string): Promise { - const [token] = await this.getTokensSearch({ - query: input, - limit: "1", - }); - if (!token) { - throw new Error(`Token not found: ${input}`); - } - return token; - } -} diff --git a/src/clients/LendClient.ts b/src/clients/LendClient.ts deleted file mode 100644 index 46ba57f..0000000 --- a/src/clients/LendClient.ts +++ /dev/null @@ -1,83 +0,0 @@ -import ky from "ky"; - -import { ClientConfig } from "./ClientConfig.ts"; - -export type LendAsset = { - address: string; - chain_id: string; - name: string; - symbol: string; - decimals: number; - logo_url: string; - price: string; - coingecko_id: string; -}; - -export type LiquiditySupplyData = { - modeWithInterest: boolean; - supply: string; - withdrawalLimit: string; - lastUpdateTimestamp: string; - expandPercent: string; - expandDuration: string; - baseWithdrawalLimit: string; - withdrawableUntilLimit: string; - withdrawable: string; -}; - -export type LendToken = { - id: number; - address: string; - name: string; - symbol: string; - decimals: number; - assetAddress: string; - asset: LendAsset; - totalAssets: string; - totalSupply: string; - convertToShares: string; - convertToAssets: string; - rewardsRate: string; - supplyRate: string; - totalRate: string; - rebalanceDifference: string; - liquiditySupplyData: LiquiditySupplyData; -}; - -export type LendPosition = { - token: LendToken; - ownerAddress: string; - shares: string; - underlyingAssets: string; - underlyingBalance: string; - allowance: string; -}; - -export type LendEarning = { - address: string; - ownerAddress: string; - earnings: number; - slot: number; -}; - -export class LendClient { - static readonly #ky = ky.create({ - prefixUrl: `${ClientConfig.host}/lend/v1`, - headers: ClientConfig.headers, - }); - - public static async getTokens(): Promise { - return this.#ky.get("earn/tokens").json(); - } - - public static async getPositions(users: string): Promise { - return this.#ky.get("earn/positions", { searchParams: { users } }).json(); - } - - public static async getEarnings(req: { - user: string; - positions: string; - }): Promise { - return this.#ky.get("earn/earnings", { searchParams: req }).json(); - } -} diff --git a/src/clients/PerpsClient.ts b/src/clients/PerpsClient.ts deleted file mode 100644 index e7fbf6c..0000000 --- a/src/clients/PerpsClient.ts +++ /dev/null @@ -1,404 +0,0 @@ -import ky from "ky"; - -import { resolveAsset } from "../lib/Asset.ts"; -import { ClientConfig } from "./ClientConfig.ts"; - -export type MarketStatsResponse = { - price: string; - priceChange24H: string; - priceHigh24H: string; - priceLow24H: string; - volume: string; -}; - -export type TpslRequest = { - positionRequestPubkey: string; - requestType: "tp" | "sl"; - desiredMint: string; - desiredToken: string; - collateralUsdDelta: string; - entirePosition: boolean; - sizeUsd: string; - sizePercentage: string; - triggerPriceUsd: string | null; - openTime: string; -}; - -export type Position = { - asset: string; - assetMint: string; - collateralToken: string; - collateralMint: string; - positionPubkey: string; - side: string; - leverage: string; - sizeUsd: string; - sizeTokenAmount: string; - collateralUsd: string; - valueUsd: string; - entryPriceUsd: string; - markPriceUsd: string; - liquidationPriceUsd: string; - openFeesUsd: string; - closeFeesUsd: string; - borrowFeesUsd: string; - totalFeesUsd: string; - pnlBeforeFeesUsd: string; - pnlBeforeFeesPct: string; - pnlAfterFeesUsd: string; - pnlAfterFeesPct: string; - createdTime: number; - updatedTime: number; - tpslRequests: TpslRequest[]; -}; - -export type GetPositionsResponse = { - count: number; - dataList: Position[]; -}; - -export type LimitOrder = { - collateralMint: string; - collateralCustody: string; - collateralUsd: string; - collateralUsdAtTriggerPrice: string; - collateralTokenAmount: string; - custody: string; - executed: boolean; - inputMint: string; - liquidationPriceUsd: string; - marketMint: string; - maxSizeUsdDelta: string; - minSizeUsdDelta: string; - openTime: string; - positionPubkey: string; - positionRequestPubkey: string; - side: string; - sizeUsdDelta: string; - triggerPrice: string | null; - triggerToLiquidationPercent: string | null; -}; - -export type GetLimitOrdersResponse = { - count: number; - dataList: LimitOrder[]; -}; - -export type Trade = { - action: "Increase" | "Decrease"; - collateralUsdDelta: string; - createdTime: number; - baseFee: string; - borrowFee: string; - liquidationFee: string; - priceImpactFee: string; - fee: string; - integratorFeeTokenAmount: string; - integratorFeeTokenMint: string | null; - mint: string; - orderType: string; - owner: string; - pnl: string | null; - pnlPercentage: string | null; - positionName: string; - positionPubkey: string; - price: string; - side: "long" | "short"; - size: string; - transactionFeeLamports: string; - priorityFeeLamports: string; - swapFeeBps: string | null; - swapFeeUsd: string | null; - swapFeeTokenAmount: string | null; - swapFeeTokenMint: string | null; - txHash: string; - updatedTime: number; -}; - -export type GetTradesResponse = { - count: number; - dataList: Trade[]; -}; - -export type TxMetadata = { - blockhash: string; - lastValidBlockHeight: string; - transactionFeeLamports?: string; - accountRentLamports?: string; -}; - -export type IncreasePositionQuote = { - averagePriceUsd: string; - collateralUsdDelta: string; - leverage: string; - liquidationPriceUsd: string; - openFeeUsd: string; - outstandingBorrowFeeUsd: string; - positionCollateralUsd: string; - positionSizeUsd: string; - priceImpactFeeBps: string; - priceImpactFeeUsd: string; - side: string; - sizeUsdDelta: string; - sizeTokenDelta: string; -}; - -export type IncreasePositionResponse = { - positionPubkey: string; - quote: IncreasePositionQuote; - serializedTxBase64: string; - tpsl?: { - hasProfit: boolean; - pnlUsd: string; - pnlPercent: string; - pubkey: string; - requestType: string; - }[]; - txMetadata: TxMetadata; -}; - -export type DecreasePositionQuote = { - closeFeeUsd: string; - collateralUsdDelta: string; - leverage: string; - liquidationPriceUsd: string; - outstandingBorrowFeeUsd: string; - pnlAfterFeesUsd: string; - pnlAfterFeesPercent: string; - pnlBeforeFeesUsd: string; - pnlBeforeFeesPercent: string; - positionCollateralUsd: string; - positionSizeUsd: string; - priceImpactFeeBps: string; - priceImpactFeeUsd: string; - side: string; - sizeUsdDelta: string; - totalFeeUsd: string; - transferAmountToken: string; - transferAmountUsd: string; - transferTokenMint: string; -}; - -export type DecreasePositionResponse = { - positionPubkey: string; - quote: DecreasePositionQuote; - serializedTxBase64: string; - txMetadata: TxMetadata; -}; - -export type CloseAllResponse = { - serializedTxs: { - serializedTxBase64: string; - positionRequestPubkey: string; - }[]; - txMetadata: TxMetadata; -}; - -export type LimitOrderResponse = { - positionPubkey: string | null; - positionRequestPubkey?: string | null; - quote: IncreasePositionQuote; - serializedTxBase64: string | null; - txMetadata: TxMetadata | null; -}; - -export type TpslResponse = { - tpslPubkeys?: string[]; - requireKeeperSignature: boolean; - serializedTxBase64: string; - tpslRequests: { - estimatedPnlUsd: string; - estimatedPnlPercent: string; - hasProfit: boolean; - requestType: string; - positionRequestPubkey: string; - }[]; - transactionType: string; - txMetadata: TxMetadata; -}; - -export type CancelResponse = { - serializedTxBase64: string; - txMetadata: TxMetadata; - positionPubkey?: string | null; - positionRequestPubkey?: string | null; - requireKeeperSignature?: boolean | null; - transactionType?: string; -}; - -export type ExecuteResponse = { - action: string; - txid: string; -}; - -export class PerpsClient { - static readonly #ky = ky.create({ - prefixUrl: "https://perps-api.jup.ag/v2", - headers: ClientConfig.headers, - }); - - public static async getMarkets(): Promise< - ({ asset: string } & MarketStatsResponse)[] - > { - return Promise.all( - ["SOL", "BTC", "ETH"].map(async (asset) => { - const mint = resolveAsset(asset).id; - const stats: MarketStatsResponse = await this.#ky - .get("market-stats", { searchParams: { mint } }) - .json(); - return { asset, ...stats }; - }) - ); - } - - public static async getPositions( - walletAddress: string - ): Promise { - return this.#ky - .get("positions", { searchParams: { walletAddress } }) - .json(); - } - - public static async getLimitOrders( - walletAddress: string - ): Promise { - return this.#ky - .get("orders/limit", { searchParams: { walletAddress } }) - .json(); - } - - public static async getTrades(params: { - walletAddress: string; - action?: string; - mint?: string; - side?: string; - start?: number; - end?: number; - createdAtAfter?: string; - createdAtBefore?: string; - }): Promise { - const searchParams: Record = { - walletAddress: params.walletAddress, - }; - if (params.action) { - searchParams.action = params.action; - } - if (params.mint) { - searchParams.mint = params.mint; - } - if (params.side) { - searchParams.side = params.side; - } - if (params.start !== undefined) { - searchParams.start = params.start; - } - if (params.end !== undefined) { - searchParams.end = params.end; - } - if (params.createdAtAfter) { - searchParams.createdAtAfter = params.createdAtAfter; - } - if (params.createdAtBefore) { - searchParams.createdAtBefore = params.createdAtBefore; - } - return this.#ky.get("trades", { searchParams }).json(); - } - - public static async postIncreasePosition(req: { - asset: string; - inputToken: string; - inputTokenAmount?: string; - side: string; - maxSlippageBps: string; - leverage?: string; - sizeUsdDelta?: string; - walletAddress: string; - tpsl?: { - receiveToken: string; - triggerPrice: string; - requestType: string; - }[]; - }): Promise { - return this.#ky.post("positions/increase", { json: req }).json(); - } - - public static async postDecreasePosition(req: { - positionPubkey: string; - receiveToken: string; - sizeUsdDelta?: string; - entirePosition?: boolean; - maxSlippageBps: string; - }): Promise { - return this.#ky.post("positions/decrease", { json: req }).json(); - } - - public static async postCloseAll( - walletAddress: string - ): Promise { - return this.#ky - .post("positions/close-all", { json: { walletAddress } }) - .json(); - } - - public static async postLimitOrder(req: { - asset: string; - inputToken: string; - inputTokenAmount?: string; - side: string; - triggerPrice: string; - leverage?: string; - sizeUsdDelta?: string; - walletAddress: string; - }): Promise { - return this.#ky.post("orders/limit", { json: req }).json(); - } - - public static async patchLimitOrder(req: { - positionRequestPubkey: string; - triggerPrice: string; - }): Promise { - return this.#ky.patch("orders/limit", { json: req }).json(); - } - - public static async deleteLimitOrder( - positionRequestPubkey: string - ): Promise { - return this.#ky - .delete("orders/limit", { json: { positionRequestPubkey } }) - .json(); - } - - public static async postTpsl(req: { - walletAddress: string; - positionPubkey: string; - tpsl: { - receiveToken: string; - triggerPrice: string; - requestType: string; - entirePosition: boolean; - sizeUsdDelta?: string; - }[]; - }): Promise { - return this.#ky.post("tpsl", { json: req }).json(); - } - - public static async patchTpsl(req: { - positionRequestPubkey: string; - triggerPrice: string; - }): Promise { - return this.#ky.patch("tpsl", { json: req }).json(); - } - - public static async deleteTpsl( - positionRequestPubkey: string - ): Promise { - return this.#ky.delete("tpsl", { json: { positionRequestPubkey } }).json(); - } - - public static async postExecute(req: { - action: string; - serializedTxBase64: string; - }): Promise { - return this.#ky.post("transaction/execute", { json: req }).json(); - } -} diff --git a/src/clients/PredictionsClient.ts b/src/clients/PredictionsClient.ts deleted file mode 100644 index 21f4dbc..0000000 --- a/src/clients/PredictionsClient.ts +++ /dev/null @@ -1,326 +0,0 @@ -import ky, { type SearchParamsOption } from "ky"; - -import { ClientConfig } from "./ClientConfig.ts"; - -export type EventMetadata = { - eventId: string; - title: string; - subtitle: string; - slug: string; - series: string; - closeTime: string; - imageUrl: string; - isLive: boolean; -}; - -export type MarketPricing = { - buyYesPriceUsd: number | null; - buyNoPriceUsd: number | null; - sellYesPriceUsd: number | null; - sellNoPriceUsd: number | null; - volume: number; -}; - -export type Market = { - marketId: string; - title: string; - status: "open" | "closed" | "cancelled"; - result: "yes" | "no" | null; - openTime: number; - closeTime: number; - isTeamMarket: boolean; - rulesPrimary: string; - rulesSecondary: string; - resolveAt: number | null; - marketResultPubkey: string | null; - imageUrl: string | null; - pricing: MarketPricing; -}; - -export type PredictionEvent = { - eventId: string; - isActive: boolean; - isLive: boolean; - category: string; - subcategory: string; - tags: string[]; - metadata: EventMetadata; - markets: Market[]; - volumeUsd: string; - volume24hr: "string"; - closeCondition: string; - beginAt: string | null; - rulesPdf: string; -}; - -export type Pagination = { - start: number; - end: number; - total: number; - hasNext: boolean; -}; - -export type GetEventsResponse = { - data: PredictionEvent[]; - pagination: Pagination; -}; - -export type PredictionPosition = { - pubkey: string; - ownerPubkey: string; - marketId: string; - isYes: boolean; - contracts: string; - totalCostUsd: number; - valueUsd: number; - pnlUsd: number; - pnlUsdPercent: number; - claimed: boolean; - claimedUsd: number; - openedAt: string; - updatedAt: string; - eventMetadata: { title: string }; - marketMetadata: { - title: string; - status: string; - result: "yes" | "no" | null; - openTime: number; - closeTime: number; - }; -}; - -export type GetPositionsResponse = { - data: PredictionPosition[]; - pagination: Pagination; -}; - -export type OrderDetails = { - orderPubkey: string; - orderAtaPubkey: string; - userPubkey: string; - marketId: string; - marketIdHash: string; - positionPubkey: string; - isBuy: boolean; - isYes: boolean; - contracts: string; - newContracts: string; - maxBuyPriceUsd: string | null; - minSellPriceUsd: string | null; - externalOrderId: string; - orderCostUsd: string; - newAvgPriceUsd: string; - newSizeUsd: string; - newPayoutUsd: string; - estimatedProtocolFeeUsd: string; - estimatedVenueFeeUsd: string; - estimatedIntegratorFeeUsd: string; - estimatedTotalFeeUsd: string; -}; - -export type CreateOrderResponse = { - transaction: string; - txMeta: { blockhash: string; lastValidBlockHeight: string }; - externalOrderId: string; - requiredSigners: string[]; - order: OrderDetails; -}; - -export type ExecuteOrderResponse = { - signature: string; -}; - -export type ClaimPositionResponse = { - transaction: string; - txMeta: { blockhash: string; lastValidBlockHeight: string }; - position: { - positionPubkey: string; - marketPubkey: string; - userPubkey: string; - ownerPubkey: string; - isYes: boolean; - contracts: string; - payoutAmountUsd: string; - }; -}; - -export type CloseAllPositionsResponse = { - data: (CreateOrderResponse | ClaimPositionResponse)[]; -}; - -export type HistoryEvent = { - id: number; - eventType: string; - signature: string; - slot: string; - timestamp: number; - orderPubkey: string; - positionPubkey: string; - marketId: string; - ownerPubkey: string; - keeperPubkey: string; - isBuy: boolean; - isYes: boolean; - contracts: string; - filledContracts: string; - maxFillPriceUsd: string; - avgFillPriceUsd: string; - realizedPnl: string | null; - payoutAmountUsd: string; - marketMetadata: { - title: string; - status: string; - result: string | null; - }; - eventMetadata: { - title: string; - subtitle: string; - slug: string; - imageUrl: string; - isLive: boolean; - }; -}; - -export type GetHistoryResponse = { - data: HistoryEvent[]; - pagination: Pagination; -}; - -export class PredictionsClient { - static readonly #ky = ky.create({ - prefixUrl: `${ClientConfig.host}/prediction/v1`, - headers: ClientConfig.headers, - }); - - public static async searchEvents(params: { - query: string; - start?: number; - end?: number; - }): Promise<{ data: PredictionEvent[] }> { - const searchParams: SearchParamsOption = { - query: params.query, - includeMarkets: true, - }; - if (params.start !== undefined) { - searchParams.start = params.start; - } - if (params.end !== undefined) { - searchParams.end = params.end; - } - return this.#ky.get("events/search", { searchParams }).json(); - } - - public static async getEvent(eventId: string): Promise { - return this.#ky - .get(`events/${eventId}`, { searchParams: { includeMarkets: true } }) - .json(); - } - - public static async getEvents(params: { - filter?: string; - sortBy?: string; - sortDirection?: string; - category?: string; - start?: number; - end?: number; - }): Promise { - const searchParams: Record = { - includeMarkets: true, - }; - if (params.filter) { - searchParams.filter = params.filter; - } - if (params.sortBy) { - searchParams.sortBy = params.sortBy; - } - if (params.sortDirection) { - searchParams.sortDirection = params.sortDirection; - } - if (params.category) { - searchParams.category = params.category; - } - if (params.start !== undefined) { - searchParams.start = params.start; - } - if (params.end !== undefined) { - searchParams.end = params.end; - } - return this.#ky.get("events", { searchParams }).json(); - } - - public static async getPositions( - ownerPubkey: string - ): Promise { - return this.#ky.get("positions", { searchParams: { ownerPubkey } }).json(); - } - - public static async getPosition( - positionPubkey: string - ): Promise { - return this.#ky.get(`positions/${positionPubkey}`).json(); - } - - public static async postOrder(req: { - isBuy: boolean; - ownerPubkey: string; - marketId: string; - isYes: boolean; - depositAmount: string; - depositMint?: string; - }): Promise { - return this.#ky - .post("orders", { json: { ...req, skipSigning: true } }) - .json(); - } - - public static async postExecute(req: { - signedTransaction: string; - }): Promise { - return this.#ky.post("orders/execute", { json: req }).json(); - } - - public static async closePosition( - positionPubkey: string, - ownerPubkey: string - ): Promise { - return this.#ky - .delete(`positions/${positionPubkey}`, { json: { ownerPubkey } }) - .json(); - } - - public static async closeAllPositions( - ownerPubkey: string - ): Promise { - return this.#ky - .delete("positions", { - json: { ownerPubkey, minSellPriceSlippageBps: 200 }, - }) - .json(); - } - - public static async getHistory(params: { - ownerPubkey: string; - start?: number; - end?: number; - }): Promise { - const searchParams: Record = { - ownerPubkey: params.ownerPubkey, - }; - if (params.start !== undefined) { - searchParams.start = params.start; - } - if (params.end !== undefined) { - searchParams.end = params.end; - } - return this.#ky.get("history", { searchParams }).json(); - } - - public static async claimPosition( - positionPubkey: string, - ownerPubkey: string - ): Promise { - return this.#ky - .post(`positions/${positionPubkey}/claim`, { json: { ownerPubkey } }) - .json(); - } -} diff --git a/src/clients/UltraClient.ts b/src/clients/UltraClient.ts deleted file mode 100644 index bcbfb43..0000000 --- a/src/clients/UltraClient.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { Address, Base64EncodedBytes } from "@solana/kit"; -import ky from "ky"; - -import { ClientConfig } from "./ClientConfig.ts"; - -type GetOrderRequest = { - inputMint: string; - outputMint: string; - amount: string; - taker?: string | undefined; - slippageBps?: string | undefined; -}; - -export type GetOrderResponse = { - error?: string | undefined; - errorCode?: number | undefined; - errorMessage?: string | undefined; - requestId: string; - inAmount: string; - inUsdValue: number; - outAmount: string; - outUsdValue: number; - otherAmountThreshold: string; - priceImpact: number; - taker: string | null; - transaction: "" | Base64EncodedBytes; // empty string denotes invalid transaction - router: string; - platformFee: { - feeBps: number; - }; - gasless?: boolean | undefined; - prioritizationFeePayer?: string | undefined; - prioritizationFeeLamports?: number | undefined; - rentFeePayer?: string | undefined; - rentFeeLamports?: number | undefined; - signatureFeePayer?: string | undefined; - signatureFeeLamports?: number | undefined; -}; - -export type PostExecuteRequest = { - requestId: string; - signedTransaction: string; -}; - -export type PostExecuteResponse = { - code: number; - inputAmountResult: string; - outputAmountResult: string; - signature: string; -}; - -export type HoldingsTokenAccount = { - account: string; - amount: string; - uiAmount: number; - uiAmountString: string; - isFrozen: boolean; - isAssociatedTokenAccount: boolean; - decimals: number; - programId: string; - lamports: string; - reclaimableLamports?: string; - excludeFromNetWorth?: boolean; -}; - -export type GetHoldingsResponse = { - amount: string; - uiAmount: number; - uiAmountString: string; - tokens: Record; -}; - -type GetTransferTokenTxRequest = { - senderAddress: Address; - senderTokenAccountAddress: Address; - receiverAddress: Address; - receiverTokenAccountAddress?: Address; // If present, `receiverAddress` will be ignored and sending to non-initialised ATAs won't work - amount: string; - mint: string; - tokenDecimals: string; - tokenProgramId: string; -}; - -type GetTransferSolTxRequest = { - senderAddress: Address; - receiverAddress: Address; - amount: string; -}; - -type GetTransferTxResponse = - | { - requestId: string; - transaction: string; // base64 encoded wire tx - expireAt: string; - feeAmount?: number | undefined; - feeUsdAmount?: number | undefined; - } - | { - error: string; - }; - -type PostExecuteTransferRequest = { - requestId: string; - signedTransaction: string; -}; - -type PostExecuteTransferResponse = { - code: number; - signature: string; -}; - -export type PostReclaimCraftRequest = { - owner: string; - mints: string[]; -}; - -export type PostReclaimCraftResponse = { - transactions: ReclaimTransaction[]; - code: number; - error?: string; - totalLamportsReclaimed: string; - netLamportsReclaimed: string; - netReclaimedUsdAmount?: number; - serviceFeeLamports: string; - serviceFeeUsdAmount?: number; - gasCostLamports: string; - gasCostUsdAmount?: number; - skippedMints?: SkippedMint[]; - totalTime: number; - expireAt: string; -}; - -type ReclaimTransaction = { - requestId: string; - transaction: string; -}; - -type SkippedMint = { - mint: string; - reason: "verified" | "frozen"; -}; - -export type PostReclaimExecuteRequest = { - requestId: string; - signedTransaction: string; -}; - -export type PostReclaimExecuteResponse = { - status: "Success" | "Failed"; - signature: string; - code: number; - error?: string; - totalTime: number; -}; - -export class UltraClient { - static readonly #ky = ky.create({ - prefixUrl: `${ClientConfig.host}/ultra/v1`, - headers: ClientConfig.headers, - }); - - public static async getOrder( - req: GetOrderRequest - ): Promise { - return this.#ky - .get("order", { searchParams: req, throwHttpErrors: false }) - .json(); - } - - public static async getHoldings( - address: string - ): Promise { - return this.#ky.get(`holdings/${address}`).json(); - } - - public static async postExecute(req: PostExecuteRequest) { - return this.#ky.post("execute", { json: req }).json(); - } - - public static async getTransferTokenTx( - req: GetTransferTokenTxRequest - ): Promise { - return this.#ky - .get("transfer/craft-token", { - searchParams: req, - throwHttpErrors: false, - }) - .json(); - } - - public static async getTransferSolTx( - req: GetTransferSolTxRequest - ): Promise { - return this.#ky - .get("transfer/craft-native", { - searchParams: req, - throwHttpErrors: false, - }) - .json(); - } - - public static async postExecuteTransfer( - req: PostExecuteTransferRequest - ): Promise { - return this.#ky.post("transfer/execute", { json: req }).json(); - } - - public static async postReclaimCraft(req: PostReclaimCraftRequest) { - return this.#ky - .post("reclaim/craft", { - json: req, - }) - .json(); - } - - public static async postReclaimExecute(req: PostReclaimExecuteRequest) { - return this.#ky - .post("reclaim/execute", { - json: req, - }) - .json(); - } -} diff --git a/src/clients/VrfdClient.ts b/src/clients/VrfdClient.ts deleted file mode 100644 index e974bf3..0000000 --- a/src/clients/VrfdClient.ts +++ /dev/null @@ -1,108 +0,0 @@ -import ky from "ky"; - -import { ClientConfig } from "./ClientConfig.ts"; - -export type CheckEligibilityResponse = { - tokenExists: boolean; - isVerified: boolean; - canVerify: boolean; - canMetadata: boolean; - verificationError?: string; - metadataError?: string; -}; - -export type CraftTxnResponse = { - receiverAddress: string; - mint: string; - amount: string; - tokenDecimals: number; - tokenUsdRate?: number; - feeLamports: number; - feeUsdAmount?: number; - feeMint: string; - feeTokenDecimals: number; - feeAmount: number; - transaction?: string; - requestId: string; - totalTime: number; - expireAt?: string; - error?: string; - code: number; - gasless: boolean; -}; - -export type TokenMetadata = { - tokenId: string; - icon?: string | null; - name?: string | null; - symbol?: string | null; - website?: string | null; - telegram?: string | null; - twitter?: string | null; - twitterCommunity?: string | null; - discord?: string | null; - instagram?: string | null; - tiktok?: string | null; - circulatingSupply?: string | null; - useCirculatingSupply?: boolean | null; - tokenDescription?: string | null; - coingeckoCoinId?: string | null; - useCoingeckoCoinId?: boolean | null; - circulatingSupplyUrl?: string | null; - useCirculatingSupplyUrl?: boolean | null; - otherUrl?: string | null; -}; - -export type ExecuteRequest = { - transaction: string; - requestId: string; - senderAddress: string; - tokenId: string; - twitterHandle: string; - senderTwitterHandle?: string; - description: string; - tokenMetadata?: TokenMetadata; -}; - -export type ExecuteResponse = { - status: "Success" | "Failed"; - signature?: string; - error?: string; - code?: number; - totalTime: number; - verificationCreated: boolean; - metadataCreated: boolean; -}; - -export class VrfdClient { - static readonly #ky = ky.create({ - prefixUrl: ClientConfig.host, - headers: ClientConfig.headers, - }); - - public static async checkEligibility( - tokenId: string - ): Promise { - return this.#ky - .get("tokens/v2/verify/express/check-eligibility", { - searchParams: { tokenId }, - }) - .json(); - } - - public static async craftTxn( - senderAddress: string - ): Promise { - return this.#ky - .get("tokens/v2/verify/express/craft-txn", { - searchParams: { senderAddress }, - }) - .json(); - } - - public static async execute(req: ExecuteRequest): Promise { - return this.#ky - .post("tokens/v2/verify/express/execute", { json: req }) - .json(); - } -} diff --git a/src/commands/ConfigCommand.test.ts b/src/commands/ConfigCommand.test.ts new file mode 100644 index 0000000..091e6c5 --- /dev/null +++ b/src/commands/ConfigCommand.test.ts @@ -0,0 +1,93 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { Command } from "commander"; +import { mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { Config } from "../lib/Config.ts"; +import { Output } from "../lib/Output.ts"; +import { ConfigCommand } from "./ConfigCommand.ts"; + +const TEST_DIR = join(tmpdir(), `jup-config-command-test-${Date.now()}`); +const origSettingsFile = Config.SETTINGS_FILE; +const originalLog = console.log; + +beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }); + // @ts-expect-error test override + Config.SETTINGS_FILE = join(TEST_DIR, "settings.json"); + Output.outputOverride = "json"; +}); + +afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); + // @ts-expect-error restore + Config.SETTINGS_FILE = origSettingsFile; + Output.outputOverride = undefined; + console.log = originalLog; +}); + +test("config set stores an HTTPS RPC URL and lists it", async () => { + const output: string[] = []; + console.log = (message?: unknown) => output.push(String(message)); + const program = new Command(); + program.exitOverride(); + ConfigCommand.register(program); + + await program.parseAsync([ + "node", + "jup", + "config", + "set", + "--rpc-url", + "https://rpc.example.com", + ]); + + expect(Config.load().rpcUrl).toBe("https://rpc.example.com"); + expect(JSON.parse(output.at(-1)!)).toMatchObject({ + rpcUrl: "https://rpc.example.com", + }); +}); + +test("config set clears an RPC URL when --rpc-url has no value", async () => { + Config.set({ rpcUrl: "https://rpc.example.com" }); + console.log = () => {}; + const program = new Command(); + program.exitOverride(); + ConfigCommand.register(program); + + await program.parseAsync(["node", "jup", "config", "set", "--rpc-url"]); + + expect(Config.load().rpcUrl).toBeUndefined(); + expect(Config.getRpcUrl()).toBe("https://api.mainnet.solana.com"); +}); + +test("config list displays the effective default RPC URL", async () => { + const output: string[] = []; + console.log = (message?: unknown) => output.push(String(message)); + const program = new Command(); + program.exitOverride(); + ConfigCommand.register(program); + + await program.parseAsync(["node", "jup", "config", "list"]); + + expect(JSON.parse(output.at(-1)!)).toMatchObject({ + rpcUrl: "https://api.mainnet.solana.com", + }); +}); + +test("config list redacts the API key while displaying the RPC URL", async () => { + Config.set({ apiKey: "secret", rpcUrl: "https://rpc.example.com" }); + const output: string[] = []; + console.log = (message?: unknown) => output.push(String(message)); + const program = new Command(); + program.exitOverride(); + ConfigCommand.register(program); + + await program.parseAsync(["node", "jup", "config", "list"]); + + expect(JSON.parse(output.at(-1)!)).toMatchObject({ + apiKey: "", + rpcUrl: "https://rpc.example.com", + }); +}); diff --git a/src/commands/ConfigCommand.ts b/src/commands/ConfigCommand.ts index bde73c8..c83fc05 100644 --- a/src/commands/ConfigCommand.ts +++ b/src/commands/ConfigCommand.ts @@ -21,17 +21,24 @@ export class ConfigCommand { "--api-key [key]", "Use an API key from https://portal.jup.ag/ for higher rate limits" ) + .option( + "--rpc-url [url]", + "Set the Solana RPC URL (omit to use public mainnet default)" + ) .action((opts) => this.set(opts)); } private static list(): void { const settings = Config.load(); + const display = { + ...this.redactForDisplay(settings), + rpcUrl: Config.getRpcUrl(), + }; if (Output.isJson()) { - Output.json(this.redactForDisplay(settings)); + Output.json(display); return; } - const display = this.redactForDisplay(settings); const data = Object.entries(display).map(([key, value]) => ({ setting: key, value: value != null && value !== "" ? String(value) : "", @@ -56,16 +63,21 @@ export class ConfigCommand { activeKey?: string; output?: "table" | "json"; apiKey?: string | true; + rpcUrl?: string | true; }): void { if (opts.output && opts.output !== "table" && opts.output !== "json") { throw new Error("Invalid --output format. Must be 'table' or 'json'."); } - // --api-key without a value (Commander passes `true`) clears the key - const { apiKey, ...rest } = opts; - Config.set({ - ...rest, - apiKey: typeof apiKey === "string" ? apiKey : undefined, - }); + // An option without a value is `true` in Commander and clears that setting. + const { apiKey, rpcUrl, ...rest } = opts; + const settings: Parameters[0] = { ...rest }; + if (apiKey !== undefined) { + settings.apiKey = typeof apiKey === "string" ? apiKey : undefined; + } + if (rpcUrl !== undefined) { + settings.rpcUrl = typeof rpcUrl === "string" ? rpcUrl : undefined; + } + Config.set(settings); this.list(); } } diff --git a/src/commands/InviteCommand.test.ts b/src/commands/InviteCommand.test.ts new file mode 100644 index 0000000..0281394 --- /dev/null +++ b/src/commands/InviteCommand.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test"; +import { Command } from "commander"; +import type { JupiterApi } from "jupiter-api-ts"; + +import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { InviteCommand } from "./InviteCommand.ts"; + +type InviteCodeMethods = { + createInviteCode(): string; + deriveInviteSigner(inviteCode: string): ReturnType; +}; + +const inviteCode = InviteCommand as unknown as InviteCodeMethods; + +test("invite registers send, clawback, list, and history", () => { + const program = new Command(); + + InviteCommand.register( + program, + undefined as unknown as SolanaRpc, + undefined as unknown as JupiterApi + ); + + const invite = program.commands.find( + (command) => command.name() === "invite" + )!; + expect(invite.commands.map((command) => command.name())).toEqual([ + "send", + "clawback", + "list", + "history", + ]); +}); + +test("creates 12-character base58 invite codes", () => { + expect(inviteCode.createInviteCode()).toMatch(/^[1-9A-HJ-NP-Za-km-z]{12}$/); +}); + +test("derives the same signer from the same invite code", async () => { + const code = "123456789ABC"; + const first = await inviteCode.deriveInviteSigner(code); + const second = await inviteCode.deriveInviteSigner(code); + + expect(first.address).toBe(second.address); +}); + +test("rejects invalid invite codes", async () => { + await expect(inviteCode.deriveInviteSigner("invalid-code")).rejects.toThrow( + "Invite code must be a 12-character base58 string." + ); +}); diff --git a/src/commands/InviteCommand.ts b/src/commands/InviteCommand.ts new file mode 100644 index 0000000..c3eccac --- /dev/null +++ b/src/commands/InviteCommand.ts @@ -0,0 +1,314 @@ +import { + address, + getBase58Decoder, + getBase58Encoder, + getProgramDerivedAddress, + type Base64EncodedBytes, +} from "@solana/kit"; +import type { Command } from "commander"; +import type { JupiterApi } from "jupiter-api-ts"; +import { createHash, randomBytes } from "node:crypto"; + +import { Asset, resolveToken } from "../lib/Asset.ts"; +import { Config } from "../lib/Config.ts"; +import { requireJupiterApiKey } from "../lib/JupiterApi.ts"; +import { NumberConverter } from "../lib/NumberConverter.ts"; +import { Output } from "../lib/Output.ts"; +import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { Swap } from "../lib/Swap.ts"; + +const INVITE_PROGRAM_ADDRESS = address( + "inv1tEtSwRMtM44tbvJGNiTxMvDfPVnX9StyqXfDfks" +); +const INVITE_CODE_PATTERN = /^[1-9A-HJ-NP-Za-km-z]{12}$/; + +type Submission = { + signature: string | null; + serializedTransaction?: string; + simulation?: Awaited>; +}; + +export class InviteCommand { + static #api: JupiterApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + rpc: SolanaRpc, + api: JupiterApi + ): void { + this.#rpc = rpc; + this.#api = api; + const invite = program + .command("invite") + .description("Jupiter Send invites"); + invite + .command("send") + .description("Create and send a token invite") + .option( + "--token ", + "Token to send (symbol or mint address)", + "SOL" + ) + .option("--amount ", "Amount in human-readable units") + .option("--raw-amount ", "Amount in on-chain units") + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.send(opts); + }); + invite + .command("clawback") + .description("Claw back an unclaimed invite") + .requiredOption("--invite ", "Invite PDA from invite list") + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.clawback(opts); + }); + this.registerListCommand(invite, "list", "List pending invites", (opts) => + this.list(opts, false) + ); + this.registerListCommand(invite, "history", "List invite history", (opts) => + this.list(opts, true) + ); + } + + private static registerListCommand( + invite: Command, + name: string, + description: string, + action: (opts: { + key?: string; + address?: string; + page?: string; + }) => Promise + ): void { + invite + .command(name) + .description(description) + .option("--key ", "Key to use (overrides active key)") + .option("--address
", "Wallet address to look up") + .option("--page ", "Pagination page number") + .action(action); + } + + private static async send(opts: { + token: string; + amount?: string; + rawAmount?: string; + key?: string; + }): Promise { + requireJupiterApiKey(); + Swap.validateAmountOpts(opts); + const sender = await Signer.load(opts.key ?? Config.load().activeKey); + const token = (await resolveToken(this.#api, [opts.token]))[0]!; + const amount = + opts.rawAmount ?? + NumberConverter.toChainAmount(opts.amount!, token.decimals); + if (!/^[1-9]\d*$/.test(amount)) { + throw new Error("--amount must resolve to a positive integer amount."); + } + const inviteCode = this.createInviteCode(); + const inviteSigner = await this.deriveInviteSigner(inviteCode); + const response = await this.#api.send.postSendV1CraftSend({ + body: { + inviteSigner: inviteSigner.address, + sender: sender.address, + amount, + ...(token.id === Asset.SOL.id ? {} : { mint: token.id }), + }, + }); + if (response.error || !response.data?.tx) { + throw new Error("Unable to craft the invite transaction."); + } + const submission = await this.submit( + [sender, inviteSigner], + response.data.tx + ); + const invitePda = await this.invitePda(inviteSigner.address); + this.outputSubmission({ + action: "send", + inviteCode, + invitePda, + expiry: response.data.expiry, + signature: submission.signature, + serializedTransaction: submission.serializedTransaction, + simulation: submission.simulation, + }); + } + + private static async clawback(opts: { + invite: string; + key?: string; + }): Promise { + requireJupiterApiKey(); + const sender = await Signer.load(opts.key ?? Config.load().activeKey); + const response = await this.#api.send.postSendV1CraftClawback({ + body: { invitePDA: opts.invite, sender: sender.address }, + }); + if (response.error || !response.data?.tx) { + throw new Error("Unable to craft the invite clawback transaction."); + } + const submission = await this.submit([sender], response.data.tx); + this.outputSubmission({ + action: "clawback", + invitePda: opts.invite, + signature: submission.signature, + serializedTransaction: submission.serializedTransaction, + simulation: submission.simulation, + }); + } + + private static async list( + opts: { key?: string; address?: string; page?: string }, + history: boolean + ): Promise { + requireJupiterApiKey(); + if (opts.key && opts.address) { + throw new Error("Only one of --address or --key can be provided."); + } + const page = opts.page === undefined ? undefined : this.page(opts.page); + const wallet = + opts.address ?? + (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); + const response = history + ? await this.#api.send.getSendV1InviteHistory({ + query: { address: wallet, ...(page === undefined ? {} : { page }) }, + }) + : await this.#api.send.getSendV1PendingInvites({ + query: { address: wallet, ...(page === undefined ? {} : { page }) }, + }); + if (response.error || !response.data) { + throw new Error(`Unable to get invite ${history ? "history" : "list"}.`); + } + const invites = response.data.invites.map((invite) => ({ + invitePda: invite.invite_pda, + sender: invite.sender, + receiver: invite.receiver, + mint: invite.mint, + amount: invite.amount, + expiry: invite.expiry, + createdAt: invite.creation_time, + confirmed: invite.confirmed === 1, + action: invite.action, + })); + if (Output.isJson()) { + Output.json({ invites, hasMoreData: response.data.hasMoreData }); + return; + } + Output.table({ + type: "horizontal", + headers: { + invitePda: "Invite PDA", + mint: "Mint", + amount: "Amount", + expiry: "Expiry", + receiver: "Receiver", + confirmed: "Confirmed", + }, + rows: invites, + }); + if (response.data.hasMoreData && page !== undefined) { + console.log(`\nNext page: ${page + 1}`); + } + } + + private static async submit( + signers: Signer[], + tx: string + ): Promise { + const signedTransaction = await Signer.signTransactionWith( + signers, + tx as Base64EncodedBytes + ); + if (Config.dryRun) { + return { + signature: null, + serializedTransaction: signedTransaction, + simulation: await this.#rpc.simulate(signedTransaction), + }; + } + return { signature: await this.#rpc.send(signedTransaction) }; + } + + private static async invitePda(inviteSigner: string): Promise { + const [invitePda] = await getProgramDerivedAddress({ + programAddress: INVITE_PROGRAM_ADDRESS, + seeds: [ + new TextEncoder().encode("invite"), + getBase58Encoder().encode(inviteSigner), + ], + }); + return invitePda; + } + + private static createInviteCode(): string { + return getBase58Decoder().decode(randomBytes(13)).slice(0, 12); + } + + private static async deriveInviteSigner(inviteCode: string): Promise { + if (!INVITE_CODE_PATTERN.test(inviteCode)) { + throw new Error("Invite code must be a 12-character base58 string."); + } + const seed = createHash("sha256") + .update(`invite:${inviteCode}`) + .digest("hex"); + return Signer.fromPrivateKey(seed); + } + + private static outputSubmission(data: { + action: string; + signature: string | null; + inviteCode?: string; + invitePda: string; + expiry?: string; + serializedTransaction?: string; + simulation?: Awaited>; + }): void { + if (Output.isJson()) { + Output.json({ ...(Config.dryRun ? { dryRun: true } : {}), ...data }); + return; + } + if (Config.dryRun) { + console.log(Output.DRY_RUN_LABEL); + } + Output.table({ + type: "vertical", + rows: [ + { label: "Action", value: data.action }, + ...(data.inviteCode + ? [{ label: "Invite Code", value: data.inviteCode }] + : []), + { label: "Invite PDA", value: data.invitePda }, + ...(data.expiry ? [{ label: "Expiry", value: data.expiry }] : []), + ...(!Config.dryRun + ? [{ label: "Tx Signature", value: data.signature ?? "" }] + : [ + { + label: "Serialized Transaction", + value: data.serializedTransaction!, + }, + { + label: "Simulation", + value: data.simulation!.success ? "Success" : "Failed", + }, + ...(data.simulation!.error + ? [{ label: "Simulation Error", value: data.simulation!.error }] + : []), + ]), + ], + }); + } + + private static page(value: string): number { + const page = Number(value); + if (!Number.isSafeInteger(page) || page < 1) { + throw new Error("--page must be a positive integer."); + } + return page; + } +} diff --git a/src/commands/JlpCommand.test.ts b/src/commands/JlpCommand.test.ts new file mode 100644 index 0000000..5b6ad9c --- /dev/null +++ b/src/commands/JlpCommand.test.ts @@ -0,0 +1,118 @@ +import { expect, test } from "bun:test"; +import { Command } from "commander"; +import type { JupiterPerpsApi } from "jupiter-api-ts"; + +import { Output } from "../lib/Output.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { JlpCommand } from "./JlpCommand.ts"; + +type JlpPrivateMethods = { + info(): Promise; + poolToken(value: string, option: string): string; +}; + +const jlpMethods = JlpCommand as unknown as JlpPrivateMethods; + +test("jlp registers info, mint, and burn commands", () => { + const program = new Command(); + + JlpCommand.register( + program, + undefined as unknown as JupiterPerpsApi, + undefined as unknown as SolanaRpc + ); + + const jlp = program.commands.find((command) => command.name() === "jlp")!; + expect(jlp.commands.map((command) => command.name())).toEqual([ + "info", + "mint", + "burn", + ]); + expect( + jlp.commands + .find((command) => command.name() === "mint")! + .options.map((option) => option.long) + ).toEqual(["--token", "--amount", "--raw-amount", "--key", "--dry-run"]); + expect( + jlp.commands + .find((command) => command.name() === "burn")! + .options.map((option) => option.long) + ).toEqual(["--amount", "--raw-amount", "--receive", "--key", "--dry-run"]); +}); + +test("jlp info returns pool data as JSON", async () => { + const program = new Command(); + const api = { + getJlpInfo: async () => ({ + data: { + aumUsd: "100000000", + aumUsdFormatted: "$100.00", + aumLimitUsd: "1000000000", + aumLimitUsdFormatted: "$1,000.00", + jlpPriceUsd: "1000000", + jlpPriceUsdFormatted: "$1.00", + jlpTotalSupply: "100000000", + jlpTotalSupplyFormatted: "100 JLP", + jlpAprBps: "1000", + jlpAprPct: "10", + jlpApyBps: "1050", + jlpApyPct: "10.5", + jlpAprLastUpdatedTimestamp: "0", + jlpRealizedFeeUsd: "0", + custodies: [ + { + pubkey: "custody", + symbol: "SOL", + name: "Solana", + mint: "mint", + aumUsd: "100000000", + aumUsdFormatted: "$100.00", + aumTokenAmount: "1000000000", + aumTokenAmountFormatted: "1 SOL", + currentWeightagePct: "25", + targetWeightagePct: "30", + utilizationPct: "5", + owned: "0", + locked: "0", + guaranteedUsd: "0", + globalShortSizes: "0", + globalShortAveragePrice: "0", + shortPnlDelta: "0", + shortTradersHasProfit: false, + totalStakedAmountLamports: "0", + totalStakedAmountUsd: "0", + totalStakedAmountUsdFormatted: "$0.00", + }, + ], + }, + }), + }; + JlpCommand.register( + program, + api as unknown as JupiterPerpsApi, + undefined as unknown as SolanaRpc + ); + const output: string[] = []; + const originalLog = console.log; + Output.outputOverride = "json"; + console.log = (value: string) => output.push(value); + + try { + await jlpMethods.info(); + } finally { + console.log = originalLog; + Output.outputOverride = undefined; + } + + expect(JSON.parse(output[0]!)).toMatchObject({ + aumUsdFormatted: "$100.00", + custodies: [{ symbol: "SOL", currentWeightagePct: "25" }], + }); +}); + +test("jlp accepts WBTC as a documented pool token", () => { + expect(jlpMethods.poolToken("wbtc", "--token")).toBe("WBTC"); + expect(() => jlpMethods.poolToken("JUP", "--token")).toThrow( + "Invalid --token. Must be SOL, WBTC, ETH, or USDC." + ); +}); diff --git a/src/commands/JlpCommand.ts b/src/commands/JlpCommand.ts new file mode 100644 index 0000000..2541131 --- /dev/null +++ b/src/commands/JlpCommand.ts @@ -0,0 +1,302 @@ +import type { Base64EncodedBytes } from "@solana/kit"; +import type { Command } from "commander"; +import type { JupiterPerpsApi } from "jupiter-api-ts"; + +import { Asset } from "../lib/Asset.ts"; +import { Config } from "../lib/Config.ts"; +import { NumberConverter } from "../lib/NumberConverter.ts"; +import { Output } from "../lib/Output.ts"; +import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { Swap } from "../lib/Swap.ts"; + +const JLP_DECIMALS = 6; + +type PoolToken = "SOL" | "WBTC" | "ETH" | "USDC"; +type Submission = { + signature?: string; + serializedTransaction?: string; + simulation?: Awaited>; +}; + +export class JlpCommand { + static #perps: JupiterPerpsApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + perpsApi: JupiterPerpsApi, + rpc: SolanaRpc + ): void { + this.#perps = perpsApi; + this.#rpc = rpc; + const jlp = program + .command("jlp") + .description("Jupiter Liquidity Provider token operations"); + jlp + .command("info") + .description("Show JLP pool statistics and custody weights") + .action(() => this.info()); + jlp + .command("mint") + .description("Deposit a pool token to mint JLP") + .requiredOption("--token ", "Deposit token (SOL, WBTC, ETH, USDC)") + .option("--amount ", "Amount in human-readable units") + .option("--raw-amount ", "Amount in on-chain units") + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.mint(opts); + }); + jlp + .command("burn") + .description("Burn JLP to receive a pool token") + .option("--amount ", "Amount in human-readable units") + .option("--raw-amount ", "Amount in on-chain units") + .requiredOption( + "--receive ", + "Token to receive (SOL, WBTC, ETH, USDC)" + ) + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.burn(opts); + }); + } + + private static async info(): Promise { + const response = await this.#perps.getJlpInfo(); + if (response.error || !response.data) { + throw new Error("Unable to get JLP pool information."); + } + const info = response.data; + + if (Output.isJson()) { + Output.json(info); + return; + } + + Output.table({ + type: "vertical", + rows: [ + { label: "AUM", value: info.aumUsdFormatted }, + { label: "AUM Limit", value: info.aumLimitUsdFormatted }, + { label: "JLP Price", value: info.jlpPriceUsdFormatted }, + { label: "JLP Supply", value: info.jlpTotalSupplyFormatted }, + { label: "APR", value: `${info.jlpAprPct}%` }, + { label: "APY", value: `${info.jlpApyPct}%` }, + ], + }); + Output.table({ + type: "horizontal", + headers: { + symbol: "Token", + aum: "AUM", + amount: "Pool Amount", + currentWeight: "Current Weight", + targetWeight: "Target Weight", + utilization: "Utilization", + }, + rows: info.custodies.map((custody) => ({ + symbol: custody.symbol, + aum: custody.aumUsdFormatted, + amount: custody.aumTokenAmountFormatted, + currentWeight: `${custody.currentWeightagePct}%`, + targetWeight: `${custody.targetWeightagePct}%`, + utilization: `${custody.utilizationPct}%`, + })), + }); + } + + private static async mint(opts: { + token: string; + amount?: string; + rawAmount?: string; + key?: string; + }): Promise { + Swap.validateAmountOpts(opts); + const token = this.poolToken(opts.token, "--token"); + const signer = await Signer.load(opts.key ?? Config.load().activeKey); + const depositTokenAmount = this.amount( + opts.amount, + opts.rawAmount, + this.asset(token).decimals + ); + const response = await this.#perps.postJlpMint({ + body: { + depositToken: token, + depositTokenAmount, + walletAddress: signer.address, + }, + }); + if (response.error || !response.data?.serializedTxBase64) { + throw new Error("Unable to build the JLP mint transaction."); + } + const submission = await this.submit( + signer, + response.data.serializedTxBase64 + ); + const depositedAmount = NumberConverter.fromChainAmount( + depositTokenAmount, + this.asset(token).decimals + ); + const mintedAmount = NumberConverter.fromChainAmount( + response.data.mintTokenAmount, + JLP_DECIMALS + ); + this.outputSubmission({ + action: "mint", + deposited: `${depositedAmount} ${token}`, + received: `${mintedAmount} JLP`, + fee: `${response.data.mintFeePercent}%`, + submission, + }); + } + + private static async burn(opts: { + receive: string; + amount?: string; + rawAmount?: string; + key?: string; + }): Promise { + Swap.validateAmountOpts(opts); + const receive = this.poolToken(opts.receive, "--receive"); + const signer = await Signer.load(opts.key ?? Config.load().activeKey); + const burnTokenAmount = this.amount( + opts.amount, + opts.rawAmount, + JLP_DECIMALS + ); + const response = await this.#perps.postJlpBurn({ + body: { + burnTokenAmount, + receiveToken: receive, + walletAddress: signer.address, + }, + }); + if (response.error || !response.data?.serializedTxBase64) { + throw new Error("Unable to build the JLP burn transaction."); + } + const submission = await this.submit( + signer, + response.data.serializedTxBase64 + ); + const burnedAmount = NumberConverter.fromChainAmount( + burnTokenAmount, + JLP_DECIMALS + ); + const receivedAmount = NumberConverter.fromChainAmount( + response.data.receiveTokenAmount, + this.asset(receive).decimals + ); + this.outputSubmission({ + action: "burn", + deposited: `${burnedAmount} JLP`, + received: `${receivedAmount} ${receive}`, + fee: `${response.data.burnFeePercent}%`, + submission, + }); + } + + private static async submit( + signer: Signer, + transaction: string + ): Promise { + const serializedTransaction = await signer.signTransaction( + transaction as Base64EncodedBytes + ); + if (Config.dryRun) { + return { + serializedTransaction, + simulation: await this.#rpc.simulate(serializedTransaction), + }; + } + return { signature: await this.#rpc.send(serializedTransaction) }; + } + + private static outputSubmission(opts: { + action: "mint" | "burn"; + deposited: string; + received: string; + fee: string; + submission: Submission; + }): void { + if (Output.isJson()) { + Output.json({ + ...(Config.dryRun ? { dryRun: true } : {}), + action: opts.action, + deposited: opts.deposited, + received: opts.received, + fee: opts.fee, + ...opts.submission, + }); + return; + } + if (Config.dryRun) { + console.log(Output.DRY_RUN_LABEL); + } + Output.table({ + type: "vertical", + rows: [ + { label: "Action", value: opts.action }, + { label: "Deposited", value: opts.deposited }, + { label: "Received", value: opts.received }, + { label: "Fee", value: opts.fee }, + ...(!Config.dryRun + ? [{ label: "Tx Signature", value: opts.submission.signature! }] + : [ + { + label: "Serialized Transaction", + value: opts.submission.serializedTransaction!, + }, + { + label: "Simulation", + value: opts.submission.simulation!.success + ? "Success" + : "Failed", + }, + ...(opts.submission.simulation!.error + ? [ + { + label: "Simulation Error", + value: opts.submission.simulation!.error, + }, + ] + : []), + ]), + ], + }); + } + + private static amount( + amount: string | undefined, + rawAmount: string | undefined, + decimals: number + ): string { + const resolved = + rawAmount ?? NumberConverter.toChainAmount(amount!, decimals); + if (!/^[1-9]\d*$/.test(resolved)) { + throw new Error("--amount must resolve to a positive integer amount."); + } + return resolved; + } + + private static poolToken(value: string, option: string): PoolToken { + const token = value.toUpperCase(); + if ( + token === "SOL" || + token === "WBTC" || + token === "ETH" || + token === "USDC" + ) { + return token; + } + throw new Error(`Invalid ${option}. Must be SOL, WBTC, ETH, or USDC.`); + } + + private static asset(token: PoolToken) { + return token === "WBTC" ? Asset.BTC : Asset[token]; + } +} diff --git a/src/commands/LendCommand.test.ts b/src/commands/LendCommand.test.ts new file mode 100644 index 0000000..3104f1a --- /dev/null +++ b/src/commands/LendCommand.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test"; +import { Command } from "commander"; +import type { JupiterApi } from "jupiter-api-ts"; + +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { LendCommand } from "./LendCommand.ts"; + +test("lend registers earn and borrow as sibling command groups", () => { + const program = new Command(); + + LendCommand.register( + program, + undefined as unknown as SolanaRpc, + undefined as unknown as JupiterApi + ); + + const lend = program.commands.find((command) => command.name() === "lend")!; + expect(lend.commands.map((command) => command.name())).toEqual([ + "earn", + "borrow", + ]); + expect( + lend.commands + .find((command) => command.name() === "earn")! + .commands.map((command) => command.name()) + ).toEqual(["tokens", "positions", "deposit", "withdraw"]); + expect( + lend.commands + .find((command) => command.name() === "borrow")! + .commands.map((command) => command.name()) + ).toEqual(["vaults", "positions", "operate"]); +}); diff --git a/src/commands/LendCommand.ts b/src/commands/LendCommand.ts index d5fd99b..4d25105 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -1,18 +1,45 @@ +import type { Base64EncodedBytes } from "@solana/kit"; import type { Command } from "commander"; +import type { JupiterApi, JupiterApiTypes } from "jupiter-api-ts"; -import { DatapiClient } from "../clients/DatapiClient.ts"; -import { LendClient, type LendToken } from "../clients/LendClient.ts"; import { Config } from "../lib/Config.ts"; +import { requireJupiterApiKey } from "../lib/JupiterApi.ts"; import { NumberConverter } from "../lib/NumberConverter.ts"; import { Output } from "../lib/Output.ts"; import { Signer } from "../lib/Signer.ts"; -import { Swap } from "../lib/Swap.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; + +type LendToken = JupiterApiTypes.LendTokenInfo; +type BorrowVault = JupiterApiTypes.LendBorrowBorrowVault; +type BorrowPosition = JupiterApiTypes.LendBorrowBorrowPosition; +type BorrowToken = JupiterApiTypes.LendBorrowToken; +type BorrowMarket = JupiterApiTypes.LendBorrowMarket; + +const MIN_I128 = "-170141183460469231731687303715884105728"; + +type LendTransactionSubmission = { + serializedTransaction: string; + signature?: string; + simulation?: { + success: boolean; + error?: string; + }; +}; export class LendCommand { - public static register(program: Command): void { + static #api: JupiterApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + rpc: SolanaRpc, + api: JupiterApi + ): void { + this.#rpc = rpc; + this.#api = api; const lend = program .command("lend") - .description("Lending and earning yield"); + .description("Lending, borrowing, and earning yield"); const earn = lend .command("earn") .description("Earn yield by lending tokens"); @@ -40,7 +67,11 @@ export class LendCommand { "Amount in on-chain units (no decimal conversion)" ) .option("--key ", "Key to use for signing") - .action((opts) => this.deposit(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.deposit(opts); + }); earn .command("withdraw") .description("Withdraw tokens from lending") @@ -54,11 +85,58 @@ export class LendCommand { "Amount in on-chain units (no decimal conversion)" ) .option("--key ", "Key to use for signing") - .action((opts) => this.withdraw(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.withdraw(opts); + }); + const borrow = lend + .command("borrow") + .description("Borrow against collateral"); + borrow + .command("vaults") + .description("View borrow vaults with LTV and rates") + .option("--market ", "Borrow market (main or ethena)", "main") + .action((opts) => this.borrowVaults(opts)); + borrow + .command("positions") + .description("View borrow positions") + .option("--key ", "Key to use (overrides active key)") + .option("--address
", "Wallet address to look up") + .option("--vault ", "Filter by vault ID") + .option("--market ", "Borrow market (main or ethena)", "main") + .action((opts) => this.borrowPositions(opts)); + borrow + .command("operate") + .description("Deposit, borrow, repay, or withdraw in one transaction") + .requiredOption("--vault ", "Vault ID from `lend borrow vaults`") + .option( + "--position ", + "Position NFT ID (0 creates a new position)", + "0" + ) + .option( + "--collateral ", + "Collateral delta (positive deposits, negative withdraws, or 'all')" + ) + .option( + "--debt ", + "Debt delta (positive borrows, negative repays, or 'all')" + ) + .option("--raw-collateral ", "Collateral delta in on-chain units") + .option("--raw-debt ", "Debt delta in on-chain units") + .option("--market ", "Borrow market (main or ethena)", "main") + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.borrowOperate(opts); + }); } private static async tokens(): Promise { - const lendTokens = await LendClient.getTokens(); + requireJupiterApiKey(); + const lendTokens = await this.#getTokens(); const tokens = lendTokens.map((lt) => { const price = Number(lt.asset.price); @@ -119,15 +197,10 @@ export class LendCommand { address?: string; token?: string; }): Promise { - if (opts.address && opts.key) { - throw new Error("Only one of --address or --key can be provided."); - } - - const address = - opts.address ?? - (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); + requireJupiterApiKey(); + const address = await this.#resolveAddress(opts); - const allPositions = await LendClient.getPositions(address); + const allPositions = await this.#getPositions(address); let activePositions = allPositions.filter((p) => p.shares !== "0"); if (opts.token) { @@ -141,14 +214,14 @@ export class LendCommand { if (activePositions.length === 0) { if (Output.isJson()) { - Output.json({ positions: [] }); + Output.json({ address, positions: [] }); return; } throw new Error("No active lending positions found."); } const jlAddresses = activePositions.map((p) => p.token.address).join(","); - const earnings = await LendClient.getEarnings({ + const earnings = await this.#getEarnings({ user: address, positions: jlAddresses, }); @@ -189,10 +262,14 @@ export class LendCommand { }); if (Output.isJson()) { - Output.json({ positions }); + Output.json({ address, positions }); return; } + Output.table({ + type: "vertical", + rows: [{ label: "Address", value: address }], + }); Output.table({ type: "horizontal", headers: { @@ -216,42 +293,52 @@ export class LendCommand { rawAmount?: string; key?: string; }): Promise { - Swap.validateAmountOpts(opts); + requireJupiterApiKey(); + this.#validateAmountOpts(opts); - const lendTokens = await LendClient.getTokens(); + const lendTokens = await this.#getTokens(); const lendToken = this.resolveLendToken(lendTokens, opts.token); const settings = Config.load(); - const [signer, inputToken, outputToken] = await Promise.all([ - Signer.load(opts.key ?? settings.activeKey), - DatapiClient.resolveToken(lendToken.assetAddress), - DatapiClient.resolveToken(lendToken.address), - ]); - - const swap = await Swap.execute({ - signer, - inputToken, - outputToken, - amount: opts.amount, - rawAmount: opts.rawAmount, + const signer = await Signer.load(opts.key ?? settings.activeKey); + const rawAmount = + opts.rawAmount ?? + NumberConverter.toChainAmount(opts.amount!, lendToken.asset.decimals); + const response = await this.#api.lend.lend.postLendV1EarnDeposit({ + body: { + asset: lendToken.assetAddress, + signer: signer.address, + amount: rawAmount, + }, }); + if (response.error || !response.data) { + throw new Error("Unable to build the Lend deposit transaction."); + } const apyPct = this.rateToPct(lendToken.totalRate); + const depositedAmount = NumberConverter.fromChainAmount( + rawAmount, + lendToken.asset.decimals + ); + const submission = await this.#submitTransaction( + signer, + response.data.transaction + ); if (Config.dryRun) { if (Output.isJson()) { Output.json({ dryRun: true, + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, decimals: lendToken.asset.decimals, }, - depositedAmount: swap.inAmount, - depositedUsd: swap.order.inUsdValue, + depositedAmount, apyPct, - signature: null, - transaction: swap.order.transaction, + serializedTransaction: submission.serializedTransaction, + simulation: submission.simulation!, }); return; } @@ -260,14 +347,26 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Deposited", - value: `${swap.inAmount} ${lendToken.asset.symbol} (${Output.formatDollar(swap.order.inUsdValue)})`, + value: `${depositedAmount} ${lendToken.asset.symbol}`, }, { label: "APY", value: Output.formatPercentageChange(apyPct), }, + { + label: "Serialized Transaction", + value: submission.serializedTransaction, + }, + { + label: "Simulation", + value: submission.simulation!.success ? "Passed" : "Failed", + }, + ...(submission.simulation!.error + ? [{ label: "Error", value: submission.simulation!.error }] + : []), ], }); return; @@ -280,17 +379,17 @@ export class LendCommand { if (Output.isJson()) { Output.json({ + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, decimals: lendToken.asset.decimals, }, - depositedAmount: swap.inAmount, - depositedUsd: swap.order.inUsdValue, + depositedAmount, positionAmount, positionUsd: positionAmount * price, apyPct, - signature: swap.result!.signature, + signature: submission.signature!, }); return; } @@ -298,9 +397,10 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Deposited", - value: `${swap.inAmount} ${lendToken.asset.symbol} (${Output.formatDollar(swap.order.inUsdValue)})`, + value: `${depositedAmount} ${lendToken.asset.symbol}`, }, { label: "Position", @@ -312,7 +412,7 @@ export class LendCommand { }, { label: "Tx Signature", - value: swap.result!.signature, + value: submission.signature!, }, ], }); @@ -324,21 +424,20 @@ export class LendCommand { rawAmount?: string; key?: string; }): Promise { - if (opts.amount && opts.rawAmount) { - throw new Error("Only one of --amount or --raw-amount can be provided."); - } + requireJupiterApiKey(); + this.#validateWithdrawAmountOpts(opts); - const lendTokens = await LendClient.getTokens(); + const lendTokens = await this.#getTokens(); const lendToken = this.resolveLendToken(lendTokens, opts.token); const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - let rawAmount = opts.rawAmount; + let withdrawnRawAmount: string; + let transaction: string; if (!opts.amount && !opts.rawAmount) { - // Withdraw entire position — use full shares balance - const positions = await LendClient.getPositions(signer.address); + const positions = await this.#getPositions(signer.address); const position = positions.find( (p) => p.token.address === lendToken.address ); @@ -347,47 +446,59 @@ export class LendCommand { `No lending position found for ${lendToken.asset.symbol}.` ); } - rawAmount = position.shares; - } else if (opts.amount) { - // Convert underlying amount to jlToken amount using convertToShares - const chainAmount = NumberConverter.toChainAmount( - opts.amount, - lendToken.asset.decimals - ); - const shares = - (BigInt(chainAmount) * BigInt(lendToken.convertToShares)) / - BigInt(10 ** lendToken.decimals); - rawAmount = shares.toString(); + const response = await this.#api.lend.lend.postLendV1EarnRedeem({ + body: { + asset: lendToken.assetAddress, + signer: signer.address, + shares: position.shares, + }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build the Lend redeem transaction."); + } + transaction = response.data.transaction; + withdrawnRawAmount = ( + (BigInt(position.shares) * BigInt(lendToken.convertToAssets)) / + BigInt(10 ** lendToken.decimals) + ).toString(); + } else { + withdrawnRawAmount = + opts.rawAmount ?? + NumberConverter.toChainAmount(opts.amount!, lendToken.asset.decimals); + const response = await this.#api.lend.lend.postLendV1EarnWithdraw({ + body: { + asset: lendToken.assetAddress, + signer: signer.address, + amount: withdrawnRawAmount, + }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build the Lend withdraw transaction."); + } + transaction = response.data.transaction; } - const [inputToken, outputToken] = await Promise.all([ - DatapiClient.resolveToken(lendToken.address), - DatapiClient.resolveToken(lendToken.assetAddress), - ]); - - const swap = await Swap.execute({ - signer, - inputToken, - outputToken, - rawAmount, - }); - const apyPct = this.rateToPct(lendToken.totalRate); + const withdrawnAmount = NumberConverter.fromChainAmount( + withdrawnRawAmount, + lendToken.asset.decimals + ); + const submission = await this.#submitTransaction(signer, transaction); if (Config.dryRun) { if (Output.isJson()) { Output.json({ dryRun: true, + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, decimals: lendToken.asset.decimals, }, - withdrawnAmount: swap.outAmount, - withdrawnUsd: swap.order.outUsdValue, + withdrawnAmount, apyPct, - signature: null, - transaction: swap.order.transaction, + serializedTransaction: submission.serializedTransaction, + simulation: submission.simulation!, }); return; } @@ -396,14 +507,26 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Withdrawn", - value: `${swap.outAmount} ${lendToken.asset.symbol} (${Output.formatDollar(swap.order.outUsdValue)})`, + value: `${withdrawnAmount} ${lendToken.asset.symbol}`, }, { label: "APY", value: Output.formatPercentageChange(apyPct), }, + { + label: "Serialized Transaction", + value: submission.serializedTransaction, + }, + { + label: "Simulation", + value: submission.simulation!.success ? "Passed" : "Failed", + }, + ...(submission.simulation!.error + ? [{ label: "Error", value: submission.simulation!.error }] + : []), ], }); return; @@ -416,17 +539,17 @@ export class LendCommand { if (Output.isJson()) { Output.json({ + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, decimals: lendToken.asset.decimals, }, - withdrawnAmount: swap.outAmount, - withdrawnUsd: swap.order.outUsdValue, + withdrawnAmount, positionAmount, positionUsd: positionAmount * price, apyPct, - signature: swap.result!.signature, + signature: submission.signature!, }); return; } @@ -434,9 +557,10 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Withdrawn", - value: `${swap.outAmount} ${lendToken.asset.symbol} (${Output.formatDollar(swap.order.outUsdValue)})`, + value: `${withdrawnAmount} ${lendToken.asset.symbol}`, }, { label: "Position", @@ -448,7 +572,7 @@ export class LendCommand { }, { label: "Tx Signature", - value: swap.result!.signature, + value: submission.signature!, }, ], }); @@ -458,7 +582,7 @@ export class LendCommand { address: string, lendToken: LendToken ): Promise<{ positionAmount: number; price: number }> { - const positions = await LendClient.getPositions(address); + const positions = await this.#getPositions(address); const position = positions.find( (p) => p.token.address === lendToken.address ); @@ -495,6 +619,584 @@ export class LendCommand { return match; } + static async #getTokens(): Promise { + const response = await this.#api.lend.lend.getLendV1EarnTokens(); + if (response.error || !response.data) { + throw new Error("Unable to get Lend tokens."); + } + return response.data; + } + + static async #getPositions( + users: string + ): Promise { + const response = await this.#api.lend.lend.getLendV1EarnPositions({ + query: { users }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get Lend positions."); + } + return response.data; + } + + static async #getEarnings({ + user, + positions, + }: { + user: string; + positions: string; + }): Promise { + const response = await this.#api.lend.lend.getLendV1EarnEarnings({ + query: { user, positions }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get Lend earnings."); + } + return [response.data]; + } + + static #validateAmountOpts(opts: { + amount?: string; + rawAmount?: string; + }): void { + if (!opts.amount && !opts.rawAmount) { + throw new Error("Either --amount or --raw-amount must be provided."); + } + this.#validateWithdrawAmountOpts(opts); + } + + static #validateWithdrawAmountOpts(opts: { + amount?: string; + rawAmount?: string; + }): void { + if (opts.amount && opts.rawAmount) { + throw new Error("Only one of --amount or --raw-amount can be provided."); + } + } + + static async #submitTransaction( + signer: Signer, + transaction: string + ): Promise { + const serializedTransaction = await signer.signTransaction( + transaction as Base64EncodedBytes + ); + if (Config.dryRun) { + return { + serializedTransaction, + simulation: await this.#rpc.simulate(serializedTransaction), + }; + } + return { + serializedTransaction, + signature: await this.#rpc.send(serializedTransaction), + }; + } + + private static async borrowVaults(opts: { market?: string }): Promise { + requireJupiterApiKey(); + const market = this.#parseMarket(opts.market); + const vaults = await this.#getBorrowVaults(market); + const rows = vaults.map((vault) => this.#mapBorrowVault(vault)); + + if (Output.isJson()) { + Output.json({ market, vaults: rows }); + return; + } + + Output.table({ + type: "horizontal", + headers: { + vault: "Vault", + pair: "Pair", + ltv: "LTV", + borrowApy: "Borrow APY", + borrowable: "Borrowable", + }, + rows: rows.map((vault) => ({ + vault: String(vault.vaultId), + pair: `${vault.supplyToken.symbol}/${vault.borrowToken.symbol}`, + ltv: this.#formatPct(vault.ltvPct), + borrowApy: Output.formatPercentageChange( + vault.borrowApyPct ?? undefined + ), + borrowable: Output.formatDollar(vault.borrowableUsd), + })), + }); + } + + private static async borrowPositions(opts: { + key?: string; + address?: string; + vault?: string; + market?: string; + }): Promise { + requireJupiterApiKey(); + const market = this.#parseMarket(opts.market); + const address = await this.#resolveAddress(opts); + + let positions = (await this.#getBorrowPositions(address, market)).filter( + (position) => + position.supply !== "0" || + position.borrow !== "0" || + position.dustBorrow !== "0" + ); + + if (opts.vault) { + const vaultId = this.#parseVaultId(opts.vault); + positions = positions.filter((position) => position.vaultId === vaultId); + } + + if (positions.length === 0) { + if (Output.isJson()) { + Output.json({ address, market, positions: [] }); + return; + } + throw new Error("No active borrow positions found."); + } + + const rows = positions.map((position) => this.#mapBorrowPosition(position)); + + if (Output.isJson()) { + Output.json({ address, market, positions: rows }); + return; + } + + Output.table({ + type: "vertical", + rows: [{ label: "Address", value: address }], + }); + Output.table({ + type: "horizontal", + headers: { + position: "Position", + vault: "Vault", + collateral: "Collateral", + debt: "Debt", + ltv: "LTV", + }, + rows: rows.map((position) => ({ + position: String(position.positionId), + vault: String(position.vaultId), + collateral: this.#formatTokenValue( + position.collateralAmount, + position.supplyToken.symbol, + position.collateralUsd + ), + debt: this.#formatTokenValue( + position.debtAmount, + position.borrowToken.symbol, + position.debtUsd + ), + ltv: this.#formatPct(position.ltvPct), + })), + }); + } + + private static async borrowOperate(opts: { + vault: string; + position?: string; + collateral?: string; + debt?: string; + rawCollateral?: string; + rawDebt?: string; + market?: string; + key?: string; + }): Promise { + requireJupiterApiKey(); + this.#validateOperateAmountOpts(opts); + + const market = this.#parseMarket(opts.market); + const vaultId = this.#parseVaultId(opts.vault); + const positionId = this.#parsePositionId(opts.position ?? "0"); + const vault = (await this.#getBorrowVaults(market)).find( + (candidate) => candidate.id === vaultId + ); + if (!vault) { + throw new Error( + `Vault ${vaultId} was not found in the ${market} market.` + ); + } + + const settings = Config.load(); + const signer = await Signer.load(opts.key ?? settings.activeKey); + const colAmount = this.#resolveOperateAmount( + opts.collateral, + opts.rawCollateral, + vault.supplyToken.decimals, + "collateral" + ); + const debtAmount = this.#resolveOperateAmount( + opts.debt, + opts.rawDebt, + vault.borrowToken.decimals, + "debt" + ); + + const response = await this.#api.lend.borrow.buildBorrowOperateTransaction({ + body: { + vaultId, + positionId, + signer: signer.address, + colAmount, + debtAmount, + }, + query: { market }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build the Lend borrow transaction."); + } + + const nftId = response.data.nftId; + const supplyToken = this.#borrowToken(vault.supplyToken); + const borrowToken = this.#borrowToken(vault.borrowToken); + const collateralDelta = this.#formatOperateDelta( + colAmount, + vault.supplyToken.decimals + ); + const debtDelta = this.#formatOperateDelta( + debtAmount, + vault.borrowToken.decimals + ); + const submission = await this.#submitTransaction( + signer, + response.data.transaction + ); + + if (Config.dryRun) { + if (Output.isJson()) { + Output.json({ + dryRun: true, + address: signer.address, + market, + vaultId, + positionId: nftId, + supplyToken, + borrowToken, + collateralDelta, + debtDelta, + serializedTransaction: submission.serializedTransaction, + simulation: submission.simulation!, + }); + return; + } + + console.log(Output.DRY_RUN_LABEL); + Output.table({ + type: "vertical", + rows: [ + { label: "Address", value: signer.address }, + { label: "Market", value: market }, + { label: "Vault", value: String(vaultId) }, + { label: "Position", value: String(nftId) }, + { + label: "Collateral", + value: `${collateralDelta} ${supplyToken.symbol}`, + }, + { + label: "Debt", + value: `${debtDelta} ${borrowToken.symbol}`, + }, + { + label: "Serialized Transaction", + value: submission.serializedTransaction, + }, + { + label: "Simulation", + value: submission.simulation!.success ? "Passed" : "Failed", + }, + ...(submission.simulation!.error + ? [{ label: "Error", value: submission.simulation!.error }] + : []), + ], + }); + return; + } + + const current = await this.#fetchBorrowPosition( + signer.address, + nftId, + market + ); + + if (Output.isJson()) { + Output.json({ + address: signer.address, + market, + vaultId, + positionId: nftId, + supplyToken, + borrowToken, + collateralDelta, + debtDelta, + collateralAmount: current?.collateralAmount ?? 0, + collateralUsd: current?.collateralUsd ?? 0, + debtAmount: current?.debtAmount ?? 0, + debtUsd: current?.debtUsd ?? 0, + ltvPct: current?.ltvPct ?? null, + signature: submission.signature!, + }); + return; + } + + Output.table({ + type: "vertical", + rows: [ + { label: "Address", value: signer.address }, + { label: "Market", value: market }, + { label: "Vault", value: String(vaultId) }, + { label: "Position", value: String(nftId) }, + { + label: "Collateral", + value: `${collateralDelta} ${supplyToken.symbol}`, + }, + { + label: "Debt", + value: `${debtDelta} ${borrowToken.symbol}`, + }, + { + label: "Position Collateral", + value: current + ? this.#formatTokenValue( + current.collateralAmount, + supplyToken.symbol, + current.collateralUsd + ) + : "0", + }, + { + label: "Position Debt", + value: current + ? this.#formatTokenValue( + current.debtAmount, + borrowToken.symbol, + current.debtUsd + ) + : "0", + }, + { label: "Tx Signature", value: submission.signature! }, + ], + }); + } + + static async #getBorrowVaults(market: BorrowMarket): Promise { + const response = await this.#api.lend.borrow.listBorrowVaults({ + query: { market }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get borrow vaults."); + } + return response.data; + } + + static async #getBorrowPositions( + users: string, + market: BorrowMarket + ): Promise { + const response = await this.#api.lend.borrow.listBorrowPositions({ + query: { users, market }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get borrow positions."); + } + return response.data; + } + + static async #fetchBorrowPosition( + address: string, + positionId: number, + market: BorrowMarket + ) { + const positions = await this.#getBorrowPositions(address, market); + const position = positions.find((candidate) => candidate.id === positionId); + return position ? this.#mapBorrowPosition(position) : null; + } + + static #mapBorrowVault(vault: BorrowVault) { + const supply = this.#tokenUsd(vault.totalSupply, vault.supplyToken); + const borrow = this.#tokenUsd(vault.totalBorrow, vault.borrowToken); + const borrowable = this.#tokenUsd(vault.borrowable, vault.borrowToken); + const withdrawable = this.#tokenUsd(vault.withdrawable, vault.supplyToken); + return { + vaultId: vault.id, + address: vault.address, + supplyToken: this.#borrowToken(vault.supplyToken), + borrowToken: this.#borrowToken(vault.borrowToken), + ltvPct: this.#factorToPct(vault.collateralFactor), + liquidationThresholdPct: this.#factorToPct(vault.liquidationThreshold), + supplyApyPct: vault.supplyRate ? this.rateToPct(vault.supplyRate) : null, + borrowApyPct: vault.borrowRate ? this.rateToPct(vault.borrowRate) : null, + totalSupplyUsd: supply.usd, + totalBorrowUsd: borrow.usd, + borrowableUsd: borrowable.usd, + withdrawableUsd: withdrawable.usd, + minimumBorrowingAmount: Number( + NumberConverter.fromChainAmount( + vault.minimumBorrowing ?? "0", + vault.borrowToken.decimals + ) + ), + totalPositions: vault.totalPositions ?? null, + }; + } + + static #mapBorrowPosition(position: BorrowPosition) { + const vault = position.vault; + const debtRaw = + position.dustBorrow && position.dustBorrow !== "0" + ? position.dustBorrow + : position.borrow; + const collateral = this.#tokenUsd(position.supply, vault.supplyToken); + const debt = this.#tokenUsd(debtRaw, vault.borrowToken); + return { + positionId: position.id, + positionPubkey: position.address, + vaultId: position.vaultId, + supplyToken: this.#borrowToken(vault.supplyToken), + borrowToken: this.#borrowToken(vault.borrowToken), + collateralAmount: collateral.amount, + collateralUsd: collateral.usd, + debtAmount: debt.amount, + debtUsd: debt.usd, + ltvPct: collateral.usd > 0 ? (debt.usd / collateral.usd) * 100 : null, + isLiquidated: position.isLiquidated, + }; + } + + static #borrowToken(token: BorrowToken): { + id: string; + symbol: string; + decimals: number; + } { + return { + id: token.address, + symbol: token.uiSymbol || token.symbol, + decimals: token.decimals, + }; + } + + static #tokenUsd( + amount: string | undefined, + token: BorrowToken + ): { amount: number; usd: number } { + const human = Number( + NumberConverter.fromChainAmount(amount ?? "0", token.decimals) + ); + return { amount: human, usd: human * Number(token.price) }; + } + + static async #resolveAddress(opts: { + key?: string; + address?: string; + }): Promise { + if (opts.address && opts.key) { + throw new Error("Only one of --address or --key can be provided."); + } + return ( + opts.address ?? + (await Signer.loadAddress(opts.key ?? Config.load().activeKey)) + ); + } + + static #parseMarket(value?: string): BorrowMarket { + const market = (value ?? "main").toLowerCase(); + if (market !== "main" && market !== "ethena") { + throw new Error("Invalid --market. Must be main or ethena."); + } + return market; + } + + static #parseVaultId(value: string): number { + const id = Number(value); + if (!Number.isInteger(id) || id <= 0) { + throw new Error("Invalid --vault. Must be a positive integer."); + } + return id; + } + + static #parsePositionId(value: string): number { + const id = Number(value); + if (!Number.isInteger(id) || id < 0) { + throw new Error("Invalid --position. Must be a non-negative integer."); + } + return id; + } + + static #validateOperateAmountOpts(opts: { + collateral?: string; + debt?: string; + rawCollateral?: string; + rawDebt?: string; + }): void { + if ( + !opts.collateral && + !opts.debt && + !opts.rawCollateral && + !opts.rawDebt + ) { + throw new Error( + "Provide --collateral, --debt, --raw-collateral, or --raw-debt." + ); + } + } + + static #resolveOperateAmount( + human: string | undefined, + raw: string | undefined, + decimals: number, + kind: "collateral" | "debt" + ): string { + if (human && raw) { + throw new Error( + `Only one of --${kind} or --raw-${kind} can be provided.` + ); + } + if (!human && !raw) { + return "0"; + } + if (raw) { + return raw; + } + if (human!.toLowerCase() === "all") { + return MIN_I128; + } + return NumberConverter.toChainAmount(human!, decimals); + } + + static #formatOperateDelta(raw: string, decimals: number): string { + if (raw === MIN_I128) { + return "all"; + } + return NumberConverter.fromChainAmount(raw, decimals); + } + + static #formatTokenValue( + amount: number, + symbol: string, + usd: number + ): string { + return `${amount.toLocaleString("en-US", { + maximumFractionDigits: 6, + })} ${symbol} (${Output.formatDollar(usd)})`; + } + + static #formatPct(value: number | null): string { + if (value === null) { + return "\u2014"; + } + return `${value.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}%`; + } + + static #factorToPct(factor: string | undefined): number | null { + if (factor === undefined) { + return null; + } + return Number(factor) / 10; + } + private static rateToPct(rate: string): number { return Number(rate) / 100; } diff --git a/src/commands/PerpsCommand.ts b/src/commands/PerpsCommand.ts index 313872c..3692b03 100644 --- a/src/commands/PerpsCommand.ts +++ b/src/commands/PerpsCommand.ts @@ -1,16 +1,43 @@ import type { Base64EncodedBytes } from "@solana/kit"; import chalk from "chalk"; import type { Command } from "commander"; +import type { JupiterPerpsApi } from "jupiter-api-ts"; -import { PerpsClient } from "../clients/PerpsClient.ts"; import { Asset, resolveAsset } from "../lib/Asset.ts"; import { Config } from "../lib/Config.ts"; import { NumberConverter } from "../lib/NumberConverter.ts"; import { Output } from "../lib/Output.ts"; import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; + +type PerpsExecuteAction = + | "cancel-tpsl" + | "create-tpsl" + | "update-tpsl" + | "cancel-limit-order" + | "create-limit-order" + | "update-limit-order" + | "increase-position" + | "decrease-position"; + +type PerpsSubmission = { + action: string; + txid: string | null; + serializedTransaction?: string; + simulation?: Awaited>; +}; export class PerpsCommand { - public static register(program: Command): void { + static #perps: JupiterPerpsApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + perpsApi: JupiterPerpsApi, + rpc: SolanaRpc + ): void { + this.#perps = perpsApi; + this.#rpc = rpc; const perps = program .command("perps") .description("Perpetual futures trading"); @@ -41,7 +68,11 @@ export class PerpsCommand { .option("--sl ", "Stop-loss trigger price") .option("--slippage ", "Max slippage in basis points", "200") .option("--key ", "Key to use for signing") - .action((opts) => this.open(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.open(opts); + }); perps .command("set") .description("Update TP/SL or limit order trigger price") @@ -51,7 +82,11 @@ export class PerpsCommand { .option("--sl ", "Stop-loss trigger price") .option("--limit ", "New trigger price for limit order") .option("--key ", "Key to use for signing") - .action((opts) => this.set(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.set(opts); + }); perps .command("close") .description("Close a position, cancel a limit order, or cancel TP/SL") @@ -65,7 +100,11 @@ export class PerpsCommand { ) .option("--slippage ", "Max slippage in basis points", "200") .option("--key ", "Key to use for signing") - .action((opts) => this.close(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.close(opts); + }); perps .command("history") .description("View past trading activity") @@ -85,19 +124,59 @@ export class PerpsCommand { private static async signAndExecute( signer: Signer, - action: string, + action: PerpsExecuteAction, serializedTxBase64: string - ): Promise<{ action: string; txid: string | null }> { - if (Config.dryRun) { - return { action, txid: null }; - } + ): Promise { const signedTx = await signer.signTransaction( serializedTxBase64 as Base64EncodedBytes ); - return PerpsClient.postExecute({ action, serializedTxBase64: signedTx }); + if (Config.dryRun) { + return { + action, + txid: null, + serializedTransaction: signedTx, + simulation: await this.#rpc.simulate(signedTx), + }; + } + const response = await this.#perps.postTransactionExecute({ + body: { action, serializedTxBase64: signedTx }, + }); + if (response.error || !response.data) { + throw new Error("Unable to execute the perpetuals transaction."); + } + return response.data; + } + + private static async responseData( + response: Promise<{ data?: T; error?: unknown }>, + message: string + ): Promise { + const result = await response; + if (result.error || !result.data) { + throw new Error(message); + } + return result.data; } - private static normalizeSide(side: string): string { + private static dryRunRows( + result: PerpsSubmission + ): { label: string; value: string }[] { + if (!Config.dryRun) { + return []; + } + return [ + { label: "Serialized Transaction", value: result.serializedTransaction! }, + { + label: "Simulation", + value: result.simulation!.success ? "Success" : "Failed", + }, + ...(result.simulation!.error + ? [{ label: "Simulation Error", value: result.simulation!.error }] + : []), + ]; + } + + private static normalizeSide(side: string): "long" | "short" { const s = side.toLowerCase(); if (s === "buy" || s === "long") { return "long"; @@ -108,6 +187,24 @@ export class PerpsCommand { throw new Error("Invalid --side. Must be long, short, buy, or sell."); } + private static normalizeMarketAsset(asset: string): "SOL" | "BTC" | "ETH" { + const normalized = asset.toUpperCase(); + if (normalized === "SOL" || normalized === "BTC" || normalized === "ETH") { + return normalized; + } + throw new Error("Invalid --asset. Must be SOL, BTC, or ETH."); + } + + private static normalizeInputToken( + token: string + ): "SOL" | "BTC" | "ETH" | "USDC" { + const normalized = token.toUpperCase(); + if (["SOL", "BTC", "ETH", "USDC"].includes(normalized)) { + return normalized as "SOL" | "BTC" | "ETH" | "USDC"; + } + throw new Error("Invalid --input. Must be SOL, BTC, ETH, or USDC."); + } + private static async positions(opts: { key?: string; address?: string; @@ -121,8 +218,14 @@ export class PerpsCommand { (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); const [positionsRes, ordersRes] = await Promise.all([ - PerpsClient.getPositions(address), - PerpsClient.getLimitOrders(address), + this.responseData( + this.#perps.getPositions({ query: { walletAddress: address } }), + "Unable to get perpetual positions." + ), + this.responseData( + this.#perps.getOrdersLimit({ body: { walletAddress: address } }), + "Unable to get perpetual limit orders." + ), ]); const mintToName = new Map( @@ -254,7 +357,15 @@ export class PerpsCommand { } private static async markets(): Promise { - const markets = await PerpsClient.getMarkets(); + const markets = await Promise.all( + (["SOL", "BTC", "ETH"] as const).map(async (asset) => ({ + asset, + ...(await this.responseData( + this.#perps.getMarketStats({ query: { mint: Asset[asset].id } }), + "Unable to get perpetual market statistics." + )), + })) + ); const formatted = markets.map((r) => ({ asset: r.asset, @@ -318,8 +429,8 @@ export class PerpsCommand { } const side = this.normalizeSide(opts.side); - const asset = opts.asset.toUpperCase(); - const inputToken = opts.input.toUpperCase(); + const asset = this.normalizeMarketAsset(opts.asset); + const inputToken = this.normalizeInputToken(opts.input); const signer = await Signer.load(opts.key ?? Config.load().activeKey); // Build size params @@ -335,16 +446,22 @@ export class PerpsCommand { if (opts.limit) { // Limit order const triggerPrice = NumberConverter.toMicroUsd(opts.limit); - const res = await PerpsClient.postLimitOrder({ - asset, - inputToken, - inputTokenAmount, - side, - triggerPrice, - leverage: opts.leverage, - sizeUsdDelta, - walletAddress: signer.address, - }); + const res = await this.responseData( + this.#perps.postOrdersLimit({ + body: { + asset, + inputToken, + inputTokenAmount, + side, + triggerPrice, + leverage: opts.leverage, + sizeUsdDelta, + walletAddress: signer.address, + includeSerializedTx: true, + }, + }), + "Unable to build the perpetual limit-order transaction." + ); if (!res.serializedTxBase64) { throw new Error("API returned no transaction for limit order."); } @@ -367,7 +484,8 @@ export class PerpsCommand { leverage: Number(res.quote.leverage), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); return; @@ -395,15 +513,15 @@ export class PerpsCommand { { label: "Leverage", value: `${res.quote.leverage}x` }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); } else { // Market order const tpsl: { - receiveToken: string; + receiveToken: "SOL" | "BTC" | "ETH" | "USDC"; triggerPrice: string; - requestType: string; + requestType: "tp" | "sl"; }[] = []; if (opts.tp) { tpsl.push({ @@ -420,17 +538,27 @@ export class PerpsCommand { }); } - const res = await PerpsClient.postIncreasePosition({ - asset, - inputToken, - inputTokenAmount, - side, - maxSlippageBps: opts.slippage, - leverage: opts.leverage, - sizeUsdDelta, - walletAddress: signer.address, - tpsl: tpsl.length > 0 ? tpsl : undefined, - }); + const res = await this.responseData( + this.#perps.postPositionsIncrease({ + body: { + asset, + inputToken, + inputTokenAmount, + side, + maxSlippageBps: opts.slippage, + leverage: opts.leverage, + sizeUsdDelta, + walletAddress: signer.address, + tpsl: tpsl.length > 0 ? tpsl : undefined, + }, + }), + "Unable to build the perpetual position transaction." + ); + if (!res.serializedTxBase64) { + throw new Error( + "API returned no transaction for the perpetual position." + ); + } const result = await this.signAndExecute( signer, @@ -456,7 +584,8 @@ export class PerpsCommand { openFeeUsd: NumberConverter.fromMicroUsd(res.quote.openFeeUsd), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); return; @@ -498,7 +627,7 @@ export class PerpsCommand { }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); } @@ -530,10 +659,15 @@ export class PerpsCommand { if (opts.order) { // Update limit order trigger price - const res = await PerpsClient.patchLimitOrder({ - positionRequestPubkey: opts.order, - triggerPrice: NumberConverter.toMicroUsd(opts.limit!), - }); + const res = await this.responseData( + this.#perps.patchOrdersLimit({ + body: { + positionRequestPubkey: opts.order, + triggerPrice: NumberConverter.toMicroUsd(opts.limit!), + }, + }), + "Unable to build the perpetual limit-order update transaction." + ); if (!res.serializedTxBase64) { throw new Error("API returned no transaction for limit order update."); } @@ -551,7 +685,8 @@ export class PerpsCommand { triggerPriceUsd: Number(opts.limit), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); return; @@ -570,7 +705,7 @@ export class PerpsCommand { }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); return; @@ -578,7 +713,10 @@ export class PerpsCommand { // Set/update TP/SL on position // Fetch existing position to check for existing tpsl - const positionsRes = await PerpsClient.getPositions(signer.address); + const positionsRes = await this.responseData( + this.#perps.getPositions({ query: { walletAddress: signer.address } }), + "Unable to get perpetual positions." + ); const position = positionsRes.dataList.find( (p) => p.positionPubkey === opts.position ); @@ -591,7 +729,8 @@ export class PerpsCommand { action: string; triggerPriceUsd: number; signature: string | null; - transaction?: string; + serializedTransaction?: string; + simulation?: Awaited>; }[] = []; for (const [type, price] of [ @@ -608,10 +747,15 @@ export class PerpsCommand { if (existing) { // Update existing - const res = await PerpsClient.patchTpsl({ - positionRequestPubkey: existing.positionRequestPubkey, - triggerPrice: NumberConverter.toMicroUsd(price), - }); + const res = await this.responseData( + this.#perps.patchTpsl({ + body: { + positionRequestPubkey: existing.positionRequestPubkey, + triggerPrice: NumberConverter.toMicroUsd(price), + }, + }), + "Unable to build the perpetual TP/SL update transaction." + ); const result = await this.signAndExecute( signer, "update-tpsl", @@ -623,23 +767,31 @@ export class PerpsCommand { triggerPriceUsd: Number(price), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); } else { // Create new - const res = await PerpsClient.postTpsl({ - walletAddress: signer.address, - positionPubkey: opts.position!, - tpsl: [ - { - receiveToken: position.collateralToken, - triggerPrice: NumberConverter.toMicroUsd(price), - requestType: type, - entirePosition: true, + const res = await this.responseData( + this.#perps.postTpsl({ + body: { + walletAddress: signer.address, + positionPubkey: opts.position!, + tpsl: [ + { + receiveToken: this.normalizeInputToken( + position.collateralToken + ), + triggerPrice: NumberConverter.toMicroUsd(price), + requestType: type, + entirePosition: true, + }, + ], }, - ], - }); + }), + "Unable to build the perpetual TP/SL transaction." + ); const result = await this.signAndExecute( signer, "create-tpsl", @@ -651,7 +803,8 @@ export class PerpsCommand { triggerPriceUsd: Number(price), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); } @@ -671,9 +824,21 @@ export class PerpsCommand { } for (const r of results) { if (Config.dryRun) { - console.log( - `${r.type.toUpperCase()} ${r.action} at $${r.triggerPriceUsd}` - ); + Output.table({ + type: "vertical", + rows: [ + { + label: "Action", + value: `${r.type.toUpperCase()} ${r.action} at $${r.triggerPriceUsd}`, + }, + ...this.dryRunRows({ + action: r.action, + txid: null, + serializedTransaction: r.serializedTransaction, + simulation: r.simulation, + }), + ], + }); } else { console.log( `${r.type.toUpperCase()} ${r.action} at $${r.triggerPriceUsd} (tx: ${r.signature})` @@ -706,7 +871,17 @@ export class PerpsCommand { if (opts.order) { // Cancel limit order - const res = await PerpsClient.deleteLimitOrder(opts.order); + const res = await this.responseData( + this.#perps.deleteOrdersLimit({ + body: { positionRequestPubkey: opts.order }, + }), + "Unable to build the perpetual limit-order cancellation transaction." + ); + if (!res.serializedTxBase64) { + throw new Error( + "API returned no transaction for limit order cancellation." + ); + } const result = await this.signAndExecute( signer, "cancel-limit-order", @@ -719,14 +894,21 @@ export class PerpsCommand { action: "cancel-limit-order", signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); return; } if (Config.dryRun) { - console.log(`${Output.DRY_RUN_LABEL} Limit order would be cancelled`); + Output.table({ + type: "vertical", + rows: [ + { label: "Action", value: "Cancel Limit Order" }, + ...this.dryRunRows(result), + ], + }); } else { console.log(`Limit order cancelled (tx: ${result.txid})`); } @@ -735,7 +917,12 @@ export class PerpsCommand { if (opts.tpsl) { // Cancel TP/SL - const res = await PerpsClient.deleteTpsl(opts.tpsl); + const res = await this.responseData( + this.#perps.deleteTpsl({ + body: { positionRequestPubkey: opts.tpsl }, + }), + "Unable to build the perpetual TP/SL cancellation transaction." + ); const result = await this.signAndExecute( signer, "cancel-tpsl", @@ -748,14 +935,21 @@ export class PerpsCommand { action: "cancel-tpsl", signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); return; } if (Config.dryRun) { - console.log(`${Output.DRY_RUN_LABEL} TP/SL would be cancelled`); + Output.table({ + type: "vertical", + rows: [ + { label: "Action", value: "Cancel TP/SL" }, + ...this.dryRunRows(result), + ], + }); } else { console.log(`TP/SL cancelled (tx: ${result.txid})`); } @@ -765,48 +959,56 @@ export class PerpsCommand { // Close position(s) if (opts.position === "all") { // Close all positions - const res = await PerpsClient.postCloseAll(signer.address); + const res = await this.responseData( + this.#perps.postPositionsCloseAll({ + body: { walletAddress: signer.address }, + }), + "Unable to build perpetual close transactions." + ); if (res.serializedTxs.length === 0) { throw new Error("No open positions to close."); } - if (Config.dryRun) { - if (Output.isJson()) { - Output.json({ - dryRun: true, - action: "close-all", - signatures: null, - transactions: res.serializedTxs.map((tx) => tx.serializedTxBase64), - }); - return; - } - - console.log( - `${Output.DRY_RUN_LABEL} Would close ${res.serializedTxs.length} position${res.serializedTxs.length !== 1 ? "s" : ""}` - ); - return; - } - - const sigs: string[] = []; + const submissions: PerpsSubmission[] = []; for (const tx of res.serializedTxs) { const result = await this.signAndExecute( signer, "decrease-position", tx.serializedTxBase64 ); - sigs.push(result.txid!); + submissions.push(result); } if (Output.isJson()) { - Output.json({ action: "close-all", signatures: sigs }); + Output.json({ + ...(Config.dryRun && { dryRun: true }), + action: "close-all", + signatures: Config.dryRun ? null : submissions.map((r) => r.txid), + ...(Config.dryRun && { + transactions: submissions.map((r) => ({ + serializedTransaction: r.serializedTransaction, + simulation: r.simulation, + })), + }), + }); return; } Output.table({ type: "horizontal", - headers: { signature: "Tx Signature" }, - rows: sigs.map((s) => ({ signature: s })), + headers: Config.dryRun + ? { + serializedTransaction: "Serialized Transaction", + simulation: "Simulation", + } + : { signature: "Tx Signature" }, + rows: Config.dryRun + ? submissions.map((r) => ({ + serializedTransaction: r.serializedTransaction!, + simulation: r.simulation!.success ? "Success" : "Failed", + })) + : submissions.map((r) => ({ signature: r.txid! })), }); return; } @@ -814,7 +1016,10 @@ export class PerpsCommand { // Close single position (full or partial) let receiveToken = opts.receive?.toUpperCase(); if (!receiveToken) { - const positionsRes = await PerpsClient.getPositions(signer.address); + const positionsRes = await this.responseData( + this.#perps.getPositions({ query: { walletAddress: signer.address } }), + "Unable to get perpetual positions." + ); const position = positionsRes.dataList.find( (p) => p.positionPubkey === opts.position ); @@ -825,15 +1030,20 @@ export class PerpsCommand { } const entirePosition = !opts.size; - const res = await PerpsClient.postDecreasePosition({ - positionPubkey: opts.position!, - receiveToken, - sizeUsdDelta: opts.size - ? NumberConverter.toMicroUsd(opts.size) - : undefined, - entirePosition: entirePosition || undefined, - maxSlippageBps: opts.slippage, - }); + const res = await this.responseData( + this.#perps.postPositionsDecrease({ + body: { + positionPubkey: opts.position!, + receiveToken: this.normalizeInputToken(receiveToken), + sizeUsdDelta: opts.size + ? NumberConverter.toMicroUsd(opts.size) + : undefined, + entirePosition: entirePosition || undefined, + maxSlippageBps: opts.slippage, + }, + }), + "Unable to build the perpetual decrease transaction." + ); const result = await this.signAndExecute( signer, @@ -860,7 +1070,8 @@ export class PerpsCommand { feesUsd: NumberConverter.fromMicroUsd(res.quote.totalFeeUsd), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); return; @@ -898,7 +1109,7 @@ export class PerpsCommand { }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); } @@ -948,18 +1159,29 @@ export class PerpsCommand { (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); const mint = opts.asset ? resolveAsset(opts.asset).id : undefined; - const res = await PerpsClient.getTrades({ - walletAddress: address, - action: opts.action, - mint, - side: opts.side, - start: 0, - end: limit, - createdAtAfter: opts.after ? this.parseTimestamp(opts.after) : undefined, - createdAtBefore: opts.before - ? this.parseTimestamp(opts.before) - : undefined, - }); + const res = await this.responseData( + this.#perps.getTrades({ + query: { + walletAddress: address, + action: opts.action as "Increase" | "Decrease" | undefined, + mint: mint as + | "So11111111111111111111111111111111111111112" + | "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs" + | "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh" + | undefined, + side: opts.side as "long" | "short" | undefined, + start: 0, + end: limit, + createdAtAfter: opts.after + ? this.parseTimestamp(opts.after) + : undefined, + createdAtBefore: opts.before + ? this.parseTimestamp(opts.before) + : undefined, + }, + }), + "Unable to get perpetual trade history." + ); const mintToName = new Map( Object.entries(Asset).map(([name, a]) => [a.id, name]) diff --git a/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index 201b599..d225fed 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -1,21 +1,35 @@ import type { Base64EncodedBytes } from "@solana/kit"; import chalk from "chalk"; import type { Command } from "commander"; - -import { - PredictionsClient, - type ClaimPositionResponse, - type CreateOrderResponse, - type PredictionEvent, -} from "../clients/PredictionsClient.ts"; -import { resolveWalletAsset } from "../lib/Asset.ts"; +import type { + JupiterApi, + JupiterApiTypes, +} from "jupiter-api-ts"; +import { resolveToken } from "../lib/Asset.ts"; import { Config } from "../lib/Config.ts"; +import { requireJupiterApiKey } from "../lib/JupiterApi.ts"; import { NumberConverter } from "../lib/NumberConverter.ts"; import { Output } from "../lib/Output.ts"; import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; + +type PredictionSubmission = { + signature: string | null; + serializedTransaction?: string; + simulation?: Awaited>; +}; export class PredictionsCommand { - public static register(program: Command): void { + static #api: JupiterApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + rpc: SolanaRpc, + api: JupiterApi + ): void { + this.#rpc = rpc; + this.#api = api; const predictions = program .command("predictions") .description("Prediction markets"); @@ -59,7 +73,11 @@ export class PredictionsCommand { ) .option("--input ", "Input token (symbol or mint)", "USDC") .option("--key ", "Key to use for signing") - .action((opts) => this.open(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.open(opts); + }); predictions .command("close") .description("Close or claim a prediction position") @@ -67,8 +85,17 @@ export class PredictionsCommand { "--position ", "Position pubkey to close, or 'all'" ) + .option( + "--min-sell-price-slippage-bps ", + "Maximum close-all sell slippage in basis points", + "200" + ) .option("--key ", "Key to use for signing") - .action((opts) => this.close(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.close(opts); + }); predictions .command("history") .description("View prediction trading history") @@ -88,6 +115,7 @@ export class PredictionsCommand { offset: string; limit: string; }): Promise { + requireJupiterApiKey(); const hasListingOpts = opts.filter || opts.sort !== "volume24h" || @@ -104,22 +132,27 @@ export class PredictionsCommand { ); } - let data: PredictionEvent[]; + let data: JupiterApiTypes.PredictionEvent[]; let hasNext = false; let nextOffset: number | undefined; if (opts.id) { - const event = await PredictionsClient.getEvent(opts.id); - data = [event]; + const response = await this.#api.prediction.getPredictionV1EventsEventId({ + path: { eventId: opts.id }, + query: { includeMarkets: true }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get the prediction event."); + } + data = [response.data]; } else if (opts.search) { - const start = Number(opts.offset); - const end = start + Number(opts.limit); - const res = await PredictionsClient.searchEvents({ - query: opts.search, - start, - end, + const response = await this.#api.prediction.getPredictionV1EventsSearch({ + query: { query: opts.search, limit: Number(opts.limit) }, }); - data = res.data; + if (response.error || !response.data) { + throw new Error("Unable to search prediction events."); + } + data = response.data.data; } else { const start = Number(opts.offset); const limit = Number(opts.limit); @@ -127,42 +160,48 @@ export class PredictionsCommand { const { sortBy, sortDirection } = this.normalizeEventSort(opts.sort); - const res = await PredictionsClient.getEvents({ - filter: opts.filter, - sortBy, - sortDirection, - category: opts.category, - start, - end, + const response = await this.#api.prediction.getPredictionV1Events({ + query: { + includeMarkets: true, + filter: this.normalizeEventFilter(opts.filter), + sortBy, + sortDirection, + category: this.normalizeEventCategory(opts.category), + start, + end, + }, }); - data = res.data; - hasNext = res.pagination.hasNext; - nextOffset = res.pagination.end; + if (response.error || !response.data) { + throw new Error("Unable to get prediction events."); + } + data = response.data.data; + hasNext = response.data.pagination.hasNext; + nextOffset = response.data.pagination.end; } const events = data.map((e) => ({ eventId: e.eventId, - title: e.metadata.title, - category: e.category, + title: e.metadata?.title ?? "Unknown event", + category: e.category ?? "", isLive: e.isLive, - volumeUsd: NumberConverter.fromMicroUsd(e.volumeUsd), - volume24hUsd: NumberConverter.fromMicroUsd(e.volume24hr), + volumeUsd: NumberConverter.fromMicroUsd(e.volumeUsd ?? "0"), + volume24hUsd: NumberConverter.fromMicroUsd(e.volume24hr ?? "0"), startsAt: e.beginAt ? new Date(Number(e.beginAt) * 1000).toISOString() : null, - endsAt: e.metadata.closeTime + endsAt: e.metadata?.closeTime ? new Date(e.metadata.closeTime).toISOString() : null, markets: (e.markets ?? []).map((m) => ({ marketId: m.marketId, - title: m.title, - status: m.status, - rulesPrimary: m.rulesPrimary, - rulesSecondary: m.rulesSecondary, - yesPriceUsd: m.pricing.buyYesPriceUsd + title: m.title ?? "Unknown market", + status: m.status ?? "", + rulesPrimary: m.rulesPrimary ?? "", + rulesSecondary: m.rulesSecondary ?? "", + yesPriceUsd: m.pricing?.buyYesPriceUsd ? NumberConverter.fromMicroUsd(m.pricing.buyYesPriceUsd) : null, - noPriceUsd: m.pricing.buyNoPriceUsd + noPriceUsd: m.pricing?.buyNoPriceUsd ? NumberConverter.fromMicroUsd(m.pricing.buyNoPriceUsd) : null, result: m.result, @@ -225,14 +264,14 @@ export class PredictionsCommand { } private static normalizeEventSort(sort: string): { - sortBy: string; - sortDirection?: string; + sortBy: "volume" | "beginAt"; + sortDirection?: "asc" | "desc"; } { switch (sort) { case "volume": return { sortBy: "volume" }; case "volume24h": - return { sortBy: "volume24hr" }; + return { sortBy: "volume" }; case "recent": return { sortBy: "beginAt", sortDirection: "desc" }; default: @@ -242,6 +281,56 @@ export class PredictionsCommand { } } + private static normalizeEventFilter( + filter?: string + ): "new" | "live" | "trending" | "upcoming" | undefined { + if (!filter) { + return undefined; + } + if (["new", "live", "trending", "upcoming"].includes(filter)) { + return filter as "new" | "live" | "trending" | "upcoming"; + } + throw new Error( + "Invalid --filter. Must be new, live, trending, or upcoming." + ); + } + + private static normalizeEventCategory( + category: string + ): + | "all" + | "crypto" + | "sports" + | "politics" + | "esports" + | "culture" + | "economics" + | "tech" { + if ( + [ + "all", + "crypto", + "sports", + "politics", + "esports", + "culture", + "economics", + "tech", + ].includes(category) + ) { + return category as + | "all" + | "crypto" + | "sports" + | "politics" + | "esports" + | "culture" + | "economics" + | "tech"; + } + throw new Error("Invalid --category."); + } + private static normalizeSide(side: string): "yes" | "no" { const s = side.toLowerCase(); if (s === "yes" || s === "y") { @@ -258,6 +347,7 @@ export class PredictionsCommand { key?: string; address?: string; }): Promise { + requireJupiterApiKey(); if (opts.position && (opts.key || opts.address)) { throw new Error("--position cannot be combined with --key or --address."); } @@ -267,28 +357,43 @@ export class PredictionsCommand { let data; if (opts.position) { - const p = await PredictionsClient.getPosition(opts.position); - data = [p]; + const response = + await this.#api.prediction.getPredictionV1PositionsPositionPubkey({ + path: { positionPubkey: opts.position }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get the prediction position."); + } + data = [response.data]; } else { const address = opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const res = await PredictionsClient.getPositions(address); - data = res.data; + const response = await this.#api.prediction.getPredictionV1Positions({ + query: { ownerPubkey: address }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get prediction positions."); + } + data = response.data.data; } const positions = data.map((p) => ({ positionPubkey: p.pubkey, - event: p.eventMetadata.title, - market: p.marketMetadata.title, + event: p.eventMetadata.title ?? "Unknown event", + market: p.marketMetadata.title ?? "Unknown market", side: p.isYes ? "yes" : "no", contracts: Number(p.contracts), costUsd: NumberConverter.fromMicroUsd(p.totalCostUsd), - valueUsd: NumberConverter.fromMicroUsd(p.valueUsd), - pnlUsd: NumberConverter.fromMicroUsd(p.pnlUsd), + valueUsd: NumberConverter.fromMicroUsd(p.valueUsd ?? "0"), + pnlUsd: NumberConverter.fromMicroUsd(p.pnlUsd ?? "0"), pnlPct: p.pnlUsdPercent, - startsAt: new Date(p.marketMetadata.openTime * 1000).toISOString(), - endsAt: new Date(p.marketMetadata.closeTime * 1000).toISOString(), + startsAt: p.marketMetadata.openTime + ? new Date(p.marketMetadata.openTime * 1000).toISOString() + : "", + endsAt: p.marketMetadata.closeTime + ? new Date(p.marketMetadata.closeTime * 1000).toISOString() + : "", claimable: this.isClaimable(p), })); @@ -327,7 +432,7 @@ export class PredictionsCommand { side: sideColor(p.side), costUsd: Output.formatDollar(p.costUsd), valueUsd: Output.formatDollar(p.valueUsd), - pnl: `${Output.formatDollarChange(p.pnlUsd)} (${Output.formatPercentageChange(p.pnlPct)})`, + pnl: `${Output.formatDollarChange(p.pnlUsd)} (${Output.formatPercentageChange(p.pnlPct ?? undefined)})`, claimable: Output.formatBoolean(p.claimable), positionPubkey: p.positionPubkey, }; @@ -342,25 +447,32 @@ export class PredictionsCommand { input: string; key?: string; }): Promise { + requireJupiterApiKey(); const side = this.normalizeSide(opts.side); const isYes = side === "yes"; const signer = await Signer.load(opts.key ?? Config.load().activeKey); - const token = await resolveWalletAsset(signer.address, opts.input); + const token = (await resolveToken(this.#api, [opts.input]))[0]!; const depositAmount = NumberConverter.toChainAmount( opts.amount, token.decimals ); - const res = await PredictionsClient.postOrder({ - isBuy: true, - ownerPubkey: signer.address, - marketId: opts.market, - isYes, - depositAmount, - depositMint: token.id, + const response = await this.#api.prediction.postPredictionV1Orders({ + body: { + isBuy: true, + ownerPubkey: signer.address, + marketId: opts.market, + isYes, + depositAmount, + depositMint: token.id, + }, }); + if (response.error || !response.data) { + throw new Error("Unable to build the prediction order."); + } + const res = response.data; - const result = await this.signAndExecute(signer, res.transaction); + const result = await this.signAndExecute(signer, res); const { order } = res; const contracts = Number(order.contracts); @@ -384,7 +496,10 @@ export class PredictionsCommand { positionPayoutUsd, positionPubkey: order.positionPubkey, signature: result.signature, - ...(Config.dryRun && { transaction: res.transaction }), + ...(Config.dryRun && { + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, + }), }); return; } @@ -411,7 +526,24 @@ export class PredictionsCommand { { label: "Position", value: order.positionPubkey }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.signature! }] - : []), + : [ + { + label: "Serialized Transaction", + value: result.serializedTransaction!, + }, + { + label: "Simulation", + value: result.simulation!.success ? "Success" : "Failed", + }, + ...(result.simulation!.error + ? [ + { + label: "Simulation Error", + value: result.simulation!.error, + }, + ] + : []), + ]), ], }); } @@ -422,6 +554,7 @@ export class PredictionsCommand { limit: string; offset: string; }): Promise { + requireJupiterApiKey(); if (opts.address && opts.key) { throw new Error("Only one of --address or --key can be provided."); } @@ -434,11 +567,13 @@ export class PredictionsCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const res = await PredictionsClient.getHistory({ - ownerPubkey: address, - start, - end, + const response = await this.#api.prediction.getPredictionV1History({ + query: { ownerPubkey: address, start, end }, }); + if (response.error || !response.data) { + throw new Error("Unable to get prediction history."); + } + const res = response.data; const history = res.data.map((h) => ({ time: new Date(h.timestamp * 1000).toISOString(), @@ -506,7 +641,7 @@ export class PredictionsCommand { private static isClaimable(p: { claimed: boolean; isYes: boolean; - marketMetadata: { result: "yes" | "no" | null }; + marketMetadata: { result?: string | null }; }): boolean { return ( !p.claimed && @@ -517,25 +652,70 @@ export class PredictionsCommand { private static async signAndExecute( signer: Signer, - transaction: string - ): Promise<{ signature: string | null }> { - if (Config.dryRun) { - return { signature: null }; + build: + | JupiterApiTypes.PredictionCreateOrderResponse + | JupiterApiTypes.PredictionClaimPositionResponse + ): Promise { + if (!build.transaction) { + throw new Error("The prediction API did not return a transaction."); } const signedTx = await signer.signTransaction( - transaction as Base64EncodedBytes + build.transaction as Base64EncodedBytes ); - return PredictionsClient.postExecute({ signedTransaction: signedTx }); + if (Config.dryRun) { + return { + signature: null, + serializedTransaction: signedTx, + simulation: await this.#rpc.simulate(signedTx), + }; + } + const response = await this.#api.prediction.postPredictionV1Execute({ + body: { + signedTransaction: signedTx, + ...("execution" in build && build.execution + ? { context: build.execution.context } + : {}), + }, + }); + if (response.error || !response.data) { + throw new Error("Unable to execute the prediction transaction."); + } + if (response.data.status === "Failed") { + throw new Error( + response.data.error ?? "Prediction transaction execution failed." + ); + } + return { signature: response.data.signature ?? null }; } private static async close(opts: { position: string; + minSellPriceSlippageBps: string; key?: string; }): Promise { + requireJupiterApiKey(); + const minSellPriceSlippageBps = Number(opts.minSellPriceSlippageBps); + if ( + !Number.isInteger(minSellPriceSlippageBps) || + minSellPriceSlippageBps < 0 + ) { + throw new Error( + "--min-sell-price-slippage-bps must be a non-negative integer." + ); + } const signer = await Signer.load(opts.key ?? Config.load().activeKey); if (opts.position === "all") { - const res = await PredictionsClient.closeAllPositions(signer.address); + const response = await this.#api.prediction.deletePredictionV1Positions({ + body: { + ownerPubkey: signer.address, + minSellPriceSlippageBps, + }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build prediction close transactions."); + } + const res = response.data; if (res.data.length === 0) { throw new Error("No open positions to close."); @@ -545,20 +725,26 @@ export class PredictionsCommand { action: string; positionPubkey: string; signature: string | null; - transaction?: string; + serializedTransaction?: string; + simulation?: Awaited>; }[] = []; for (const item of res.data) { - const execResult = await this.signAndExecute(signer, item.transaction); + const execResult = await this.signAndExecute(signer, item); const isClaim = "position" in item; const pubkey = isClaim - ? (item as ClaimPositionResponse).position.positionPubkey - : (item as CreateOrderResponse).order.positionPubkey; + ? item.position.positionPubkey + : item.order.positionPubkey; results.push({ action: isClaim ? "claim" : "close", positionPubkey: pubkey, signature: execResult.signature, - ...(Config.dryRun && { transaction: item.transaction }), + ...(Config.dryRun + ? { + serializedTransaction: execResult.serializedTransaction, + simulation: execResult.simulation, + } + : {}), }); } @@ -578,10 +764,14 @@ export class PredictionsCommand { headers: { action: "Action", positionPubkey: "Position", + serializedTransaction: "Serialized Transaction", + simulation: "Simulation", }, rows: results.map((r) => ({ action: r.action, positionPubkey: r.positionPubkey, + serializedTransaction: r.serializedTransaction!, + simulation: r.simulation!.success ? "Success" : "Failed", })), }); return; @@ -600,15 +790,29 @@ export class PredictionsCommand { } // Single position - const position = await PredictionsClient.getPosition(opts.position); + const positionResponse = + await this.#api.prediction.getPredictionV1PositionsPositionPubkey({ + path: { positionPubkey: opts.position }, + }); + if (positionResponse.error || !positionResponse.data) { + throw new Error("Unable to get the prediction position."); + } + const position = positionResponse.data; const claimable = this.isClaimable(position); if (claimable) { - const res = await PredictionsClient.claimPosition( - opts.position, - signer.address - ); - const result = await this.signAndExecute(signer, res.transaction); + const response = + await this.#api.prediction.postPredictionV1PositionsPositionPubkeyClaim( + { + path: { positionPubkey: opts.position }, + body: { ownerPubkey: signer.address }, + } + ); + if (response.error || !response.data) { + throw new Error("Unable to build the prediction claim transaction."); + } + const res = response.data; + const result = await this.signAndExecute(signer, res); const contracts = Number(res.position.contracts); const payoutUsd = NumberConverter.fromMicroUsd( @@ -619,14 +823,17 @@ export class PredictionsCommand { Output.json({ ...(Config.dryRun && { dryRun: true }), action: "claim", - event: position.eventMetadata.title, - market: position.marketMetadata.title, + event: position.eventMetadata.title ?? "Unknown event", + market: position.marketMetadata.title ?? "Unknown market", side: position.isYes ? "yes" : "no", positionPubkey: opts.position, contracts, payoutUsd, signature: result.signature, - ...(Config.dryRun && { transaction: res.transaction }), + ...(Config.dryRun && { + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, + }), }); return; } @@ -638,8 +845,14 @@ export class PredictionsCommand { type: "vertical", rows: [ { label: "Action", value: "Claim Position" }, - { label: "Event", value: position.eventMetadata.title }, - { label: "Market", value: position.marketMetadata.title }, + { + label: "Event", + value: position.eventMetadata.title ?? "Unknown event", + }, + { + label: "Market", + value: position.marketMetadata.title ?? "Unknown market", + }, { label: "Side", value: position.isYes ? chalk.green("yes") : chalk.red("no"), @@ -648,15 +861,37 @@ export class PredictionsCommand { { label: "Position", value: opts.position }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.signature! }] - : []), + : [ + { + label: "Serialized Transaction", + value: result.serializedTransaction!, + }, + { + label: "Simulation", + value: result.simulation!.success ? "Success" : "Failed", + }, + ...(result.simulation!.error + ? [ + { + label: "Simulation Error", + value: result.simulation!.error, + }, + ] + : []), + ]), ], }); } else { - const res = await PredictionsClient.closePosition( - opts.position, - signer.address - ); - const result = await this.signAndExecute(signer, res.transaction); + const response = + await this.#api.prediction.deletePredictionV1PositionsPositionPubkey({ + path: { positionPubkey: opts.position }, + body: { ownerPubkey: signer.address }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build the prediction close transaction."); + } + const res = response.data; + const result = await this.signAndExecute(signer, res); const { order } = res; const contracts = Number(order.contracts); @@ -667,15 +902,18 @@ export class PredictionsCommand { Output.json({ ...(Config.dryRun && { dryRun: true }), action: "close", - event: position.eventMetadata.title, - market: position.marketMetadata.title, + event: position.eventMetadata.title ?? "Unknown event", + market: position.marketMetadata.title ?? "Unknown market", side: position.isYes ? "yes" : "no", positionPubkey: opts.position, contracts, costUsd, feeUsd, signature: result.signature, - ...(Config.dryRun && { transaction: res.transaction }), + ...(Config.dryRun && { + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, + }), }); return; } @@ -687,8 +925,14 @@ export class PredictionsCommand { type: "vertical", rows: [ { label: "Action", value: "Close Position" }, - { label: "Event", value: position.eventMetadata.title }, - { label: "Market", value: position.marketMetadata.title }, + { + label: "Event", + value: position.eventMetadata.title ?? "Unknown event", + }, + { + label: "Market", + value: position.marketMetadata.title ?? "Unknown market", + }, { label: "Side", value: position.isYes ? chalk.green("yes") : chalk.red("no"), @@ -698,7 +942,24 @@ export class PredictionsCommand { { label: "Position", value: opts.position }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.signature! }] - : []), + : [ + { + label: "Serialized Transaction", + value: result.serializedTransaction!, + }, + { + label: "Simulation", + value: result.simulation!.success ? "Success" : "Failed", + }, + ...(result.simulation!.error + ? [ + { + label: "Simulation Error", + value: result.simulation!.error, + }, + ] + : []), + ]), ], }); } @@ -712,6 +973,7 @@ export class PredictionsCommand { } private static formatDate(iso: string): string { - return new Date(iso).toLocaleDateString(); + const date = new Date(iso); + return isNaN(date.getTime()) ? "Unknown" : date.toLocaleDateString(); } } diff --git a/src/commands/RecurringCommand.test.ts b/src/commands/RecurringCommand.test.ts new file mode 100644 index 0000000..d780661 --- /dev/null +++ b/src/commands/RecurringCommand.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test"; +import { Command } from "commander"; +import type { JupiterApi } from "jupiter-api-ts"; + +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { RecurringCommand } from "./RecurringCommand.ts"; + +test("dca registers only time-based create, cancel, and list commands", () => { + const program = new Command(); + + RecurringCommand.register( + program, + undefined as unknown as SolanaRpc, + undefined as unknown as JupiterApi + ); + + const dca = program.commands.find((command) => command.name() === "dca")!; + expect(dca.commands.map((command) => command.name())).toEqual([ + "create", + "cancel", + "list", + ]); + expect( + dca.commands + .find((command) => command.name() === "create")! + .options.map((option) => option.long) + ).toEqual([ + "--from", + "--to", + "--amount", + "--raw-amount", + "--interval", + "--orders", + "--min-price", + "--max-price", + "--start-at", + "--key", + "--dry-run", + ]); +}); diff --git a/src/commands/RecurringCommand.ts b/src/commands/RecurringCommand.ts new file mode 100644 index 0000000..eecfd38 --- /dev/null +++ b/src/commands/RecurringCommand.ts @@ -0,0 +1,350 @@ +import type { Base64EncodedBytes } from "@solana/kit"; +import type { Command } from "commander"; +import type { JupiterApi } from "jupiter-api-ts"; + +import { resolveToken } from "../lib/Asset.ts"; +import { Config } from "../lib/Config.ts"; +import { requireJupiterApiKey } from "../lib/JupiterApi.ts"; +import { NumberConverter } from "../lib/NumberConverter.ts"; +import { Output } from "../lib/Output.ts"; +import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { Swap } from "../lib/Swap.ts"; + +type Submission = { + order: string | null; + signature: string | null; + serializedTransaction?: string; + simulation?: Awaited>; +}; + +export class RecurringCommand { + public static register( + program: Command, + rpc: SolanaRpc, + api: JupiterApi + ): void { + this.#rpc = rpc; + this.#api = api; + const dca = program.command("dca").description("Time-based DCA orders"); + dca + .command("create") + .description("Create a time-based DCA order") + .requiredOption("--from ", "Input token (symbol or mint address)") + .requiredOption("--to ", "Output token (symbol or mint address)") + .option("--amount ", "Total input amount in human-readable units") + .option("--raw-amount ", "Total input amount in on-chain units") + .requiredOption("--interval ", "Seconds between orders") + .requiredOption("--orders ", "Total number of orders") + .option("--min-price ", "Minimum USD price") + .option("--max-price ", "Maximum USD price") + .option("--start-at ", "UNIX timestamp to start at") + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.create(opts); + }); + dca + .command("cancel") + .description("Cancel an open DCA order") + .requiredOption("--order ", "Recurring order account") + .option("--key ", "Key to use for signing") + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.cancel(opts); + }); + dca + .command("list") + .description("List time-based DCA orders") + .option("--key ", "Key to use (overrides active key)") + .option("--address
", "Wallet address to look up") + .option("--status ", "Order status: active or history", "active") + .option( + "--token ", + "Filter by input token (symbol or mint address)" + ) + .option("--page ", "Pagination page number") + .action((opts) => this.list(opts)); + } + + static #api: JupiterApi; + static #rpc: SolanaRpc; + + private static async create(opts: { + from: string; + to: string; + amount?: string; + rawAmount?: string; + interval: string; + orders: string; + minPrice?: string; + maxPrice?: string; + startAt?: string; + key?: string; + }): Promise { + requireJupiterApiKey(); + Swap.validateAmountOpts(opts); + const interval = this.positiveInteger(opts.interval, "--interval"); + const numberOfOrders = this.positiveInteger(opts.orders, "--orders"); + const minPrice = this.optionalNumber(opts.minPrice, "--min-price"); + const maxPrice = this.optionalNumber(opts.maxPrice, "--max-price"); + const startAt = this.optionalInteger(opts.startAt, "--start-at"); + const signer = await Signer.load(opts.key ?? Config.load().activeKey); + const [inputToken, outputToken] = await resolveToken(this.#api, [ + opts.from, + opts.to, + ]); + const inAmount = Number( + opts.rawAmount ?? + NumberConverter.toChainAmount(opts.amount!, inputToken!.decimals) + ); + if (!Number.isSafeInteger(inAmount) || inAmount <= 0) { + throw new Error( + "--amount must resolve to a positive safe integer amount." + ); + } + + const response = await this.#api.recurring.createOrder({ + body: { + inputMint: inputToken!.id, + outputMint: outputToken!.id, + user: signer.address, + params: { + time: { + inAmount, + interval, + numberOfOrders, + ...(minPrice === undefined ? {} : { minPrice }), + ...(maxPrice === undefined ? {} : { maxPrice }), + ...(startAt === undefined ? {} : { startAt }), + }, + }, + }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build the DCA order."); + } + const submission = await this.submit(signer, response.data); + this.outputSubmission("create", submission); + } + + private static async cancel(opts: { + order: string; + key?: string; + }): Promise { + requireJupiterApiKey(); + const signer = await Signer.load(opts.key ?? Config.load().activeKey); + const response = await this.#api.recurring.cancelOrder({ + body: { order: opts.order, recurringType: "time", user: signer.address }, + }); + if (response.error || !response.data) { + throw new Error("Unable to build the DCA cancellation."); + } + const submission = await this.submit(signer, response.data); + this.outputSubmission("cancel", submission, opts.order); + } + + private static async list(opts: { + key?: string; + address?: string; + status: string; + token?: string; + page?: string; + }): Promise { + requireJupiterApiKey(); + if (opts.address && opts.key) { + throw new Error("Only one of --address or --key can be provided."); + } + if (opts.status !== "active" && opts.status !== "history") { + throw new Error("--status must be active or history."); + } + const page = + opts.page === undefined + ? null + : this.positiveInteger(opts.page, "--page"); + const user = + opts.address ?? + (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); + const mint = opts.token + ? (await resolveToken(this.#api, [opts.token]))[0]!.id + : null; + const response = await this.#api.recurring.getRecurringV1GetRecurringOrders( + { + query: { + user, + recurringType: "time", + orderStatus: opts.status, + page, + mint, + includeFailedTx: false, + }, + } + ); + if (response.error || !response.data || !("time" in response.data)) { + throw new Error("Unable to get DCA orders."); + } + const data = response.data; + const orders = data.time.map((order) => ({ + orderKey: order.orderKey, + inputMint: order.inputMint, + outputMint: order.outputMint, + inAmountPerCycle: order.inAmountPerCycle, + inDeposited: order.inDeposited, + inUsed: order.inUsed, + inWithdrawn: order.inWithdrawn, + cycleFrequency: order.cycleFrequency, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + userClosed: order.userClosed, + })); + if (Output.isJson()) { + Output.json({ + user: data.user, + status: data.orderStatus, + page: data.page, + totalPages: data.totalPages, + orders, + }); + return; + } + Output.table({ + type: "horizontal", + headers: { + orderKey: "Order", + inputMint: "Input Mint", + outputMint: "Output Mint", + inAmountPerCycle: "Per Cycle", + inDeposited: "Deposited", + inUsed: "Used", + cycleFrequency: "Interval", + userClosed: "Closed", + }, + rows: orders, + }); + if (data.page < data.totalPages) { + console.log(`\nNext page: ${data.page + 1}`); + } + } + + private static async submit( + signer: Signer, + build: { requestId: string; transaction: string } + ): Promise { + if (!build.requestId || !build.transaction) { + throw new Error("The DCA API did not return a transaction."); + } + const signedTransaction = await signer.signTransaction( + build.transaction as Base64EncodedBytes + ); + if (Config.dryRun) { + return { + order: null, + signature: null, + serializedTransaction: signedTransaction, + simulation: await this.#rpc.simulate(signedTransaction), + }; + } + const response = await this.#api.recurring.execute({ + body: { requestId: build.requestId, signedTransaction }, + }); + if (response.error || !response.data) { + throw new Error("Unable to execute the DCA transaction."); + } + if (response.data.status === "Failed") { + throw new Error( + response.data.error ?? "DCA transaction execution failed." + ); + } + return { + order: response.data.order ?? null, + signature: response.data.signature ?? null, + }; + } + + private static outputSubmission( + action: string, + submission: Submission, + order?: string + ): void { + if (Output.isJson()) { + Output.json({ + ...(Config.dryRun ? { dryRun: true } : {}), + action, + order: submission.order ?? order ?? null, + signature: submission.signature, + ...(Config.dryRun + ? { + serializedTransaction: submission.serializedTransaction, + simulation: submission.simulation, + } + : {}), + }); + return; + } + if (Config.dryRun) { + console.log(Output.DRY_RUN_LABEL); + } + Output.table({ + type: "vertical", + rows: [ + { label: "Action", value: action }, + ...((submission.order ?? order) + ? [{ label: "Order", value: submission.order ?? order! }] + : []), + ...(!Config.dryRun + ? [{ label: "Tx Signature", value: submission.signature ?? "" }] + : [ + { + label: "Serialized Transaction", + value: submission.serializedTransaction!, + }, + { + label: "Simulation", + value: submission.simulation!.success ? "Success" : "Failed", + }, + ...(submission.simulation!.error + ? [ + { + label: "Simulation Error", + value: submission.simulation!.error, + }, + ] + : []), + ]), + ], + }); + } + + private static positiveInteger(value: string, option: string): number { + const result = Number(value); + if (!Number.isSafeInteger(result) || result <= 0) { + throw new Error(`${option} must be a positive integer.`); + } + return result; + } + + private static optionalInteger( + value: string | undefined, + option: string + ): number | undefined { + return value === undefined + ? undefined + : this.positiveInteger(value, option); + } + + private static optionalNumber( + value: string | undefined, + option: string + ): number | undefined { + if (value === undefined) { + return undefined; + } + const result = Number(value); + if (!Number.isFinite(result) || result < 0) { + throw new Error(`${option} must be a non-negative number.`); + } + return result; + } +} diff --git a/src/commands/SpotCommand.test.ts b/src/commands/SpotCommand.test.ts new file mode 100644 index 0000000..2ce3b5e --- /dev/null +++ b/src/commands/SpotCommand.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test"; +import { Command } from "commander"; +import type { JupiterApi, JupiterDatapi } from "jupiter-api-ts"; + +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { SpotCommand } from "./SpotCommand.ts"; + +test("spot registers transfers, rent reclaim, and portfolio at the top level", () => { + const program = new Command(); + + SpotCommand.register( + program, + undefined as unknown as JupiterDatapi, + undefined as unknown as SolanaRpc, + undefined as unknown as JupiterApi + ); + + expect(program.commands.map((command) => command.name())).toEqual([ + "spot", + "portfolio", + "transfer", + "reclaim", + ]); + expect( + program.commands + .find((command) => command.name() === "spot")! + .commands.map((command) => command.name()) + ).toEqual(["tokens", "quote", "swap", "history"]); +}); diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index ab28856..3cd9380 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -1,30 +1,52 @@ -import { findAssociatedTokenPda } from "@solana-program/token"; -import { isAddress, type Address, type Base64EncodedBytes } from "@solana/kit"; +import { + address, + isAddress, + type Address, + type Base64EncodedWireTransaction, +} from "@solana/kit"; +import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; +import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022"; import chalk from "chalk"; import type { Command } from "commander"; +import type { JupiterApi, JupiterDatapi } from "jupiter-api-ts"; import { - DatapiClient, - type SpotTrade, + Asset, + getTokensByMints, + resolveToken, + searchTokens, type Token, -} from "../clients/DatapiClient.ts"; -import { - UltraClient, - type GetHoldingsResponse, - type HoldingsTokenAccount, -} from "../clients/UltraClient.ts"; -import { Asset, resolveWalletAsset } from "../lib/Asset.ts"; +} from "../lib/Asset.ts"; import { Config } from "../lib/Config.ts"; +import { requireJupiterApiKey } from "../lib/JupiterApi.ts"; import { NumberConverter } from "../lib/NumberConverter.ts"; import { Output } from "../lib/Output.ts"; import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; +import { + buildCloseTokenAccountsTransaction, + buildSolTransferTransaction, + buildTokenTransferTransaction, +} from "../lib/SolanaTransactions.ts"; import { Swap } from "../lib/Swap.ts"; export class SpotCommand { - public static register(program: Command): void { + static #datapi: JupiterDatapi; + static #api: JupiterApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + datapi: JupiterDatapi, + rpc: SolanaRpc, + api: JupiterApi + ): void { + this.#datapi = datapi; + this.#rpc = rpc; + this.#api = api; const spot = program .command("spot") - .description("Swaps, transfers, token and portfolio data"); + .description("Swaps, token discovery, and trade history"); spot .command("tokens") .description("Search for tokens by symbol or mint address") @@ -60,10 +82,14 @@ export class SpotCommand { "--slippage ", "Max slippage in basis points (leave empty to use Jupiter's Real-Time Slippage Estimation)" ) - .action((opts) => this.swap(opts)); - spot + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.swap(opts); + }); + program .command("portfolio") - .description("Show spot portfolio for a wallet") + .description("Show portfolio positions for a wallet") .option("--address
", "Wallet address to look up") .option("--key ", "Key to use (overrides active key)") .action((opts) => this.portfolio(opts)); @@ -81,7 +107,7 @@ export class SpotCommand { .option("--limit ", "Max number of results (max: 15)", "10") .option("--offset ", "Pagination offset for next page of results") .action((opts) => this.history(opts)); - spot + program .command("transfer") .description("Transfer tokens to another wallet") .requiredOption( @@ -95,26 +121,36 @@ export class SpotCommand { "Amount in on-chain units (no decimal conversion)" ) .option("--key ", "Key to use for signing") - .action((opts) => this.transfer(opts)); - spot + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.transfer(opts); + }); + program .command("reclaim") .description("Reclaim SOL rent from empty token accounts") .option("--key ", "Key to use for signing") .option("--token ", "Token symbol or mint address to reclaim") - .action((opts) => this.reclaim(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.reclaim(opts); + }); } private static async tokens(opts: { search: string; limit?: string; }): Promise { + requireJupiterApiKey(); if (opts.limit && isNaN(Number(opts.limit))) { throw new Error("--limit must be a number"); } - const tokens = await DatapiClient.getTokensSearch({ + const tokens = await searchTokens({ + api: this.#api, query: opts.search, - limit: opts.limit, + limit: opts.limit ? Number(opts.limit) : undefined, }); if (tokens.length === 0) { @@ -134,13 +170,11 @@ export class SpotCommand { name: "Name", price: "Price", mcap: "Market Cap", - verified: "Verified", }, rows: tokens.map((t) => ({ ...t, price: Output.formatDollar(t.usdPrice), mcap: Output.formatDollar(t.mcap), - verified: Output.formatBoolean(t.isVerified), })), }); } @@ -151,28 +185,35 @@ export class SpotCommand { amount?: string; rawAmount?: string; }): Promise { + requireJupiterApiKey(); Swap.validateAmountOpts(opts); - const [inputToken, outputToken] = await Promise.all([ - DatapiClient.resolveToken(opts.from), - DatapiClient.resolveToken(opts.to), + const resolved = await resolveToken(this.#api, [opts.from, opts.to]); + const inputToken = this.requireToken(resolved[0], opts.from); + const outputToken = this.requireToken(resolved[1], opts.to); + const [inputMultiplier, outputMultiplier] = await Promise.all([ + this.#rpc.getScaledUiMultiplier(inputToken.id), + this.#rpc.getScaledUiMultiplier(outputToken.id), ]); - const inputMultiplier = Swap.getScaledUiMultiplier(inputToken); - const outputMultiplier = Swap.getScaledUiMultiplier(outputToken); - - const order = await UltraClient.getOrder({ - inputMint: inputToken.id, - outputMint: outputToken.id, - amount: - opts.rawAmount ?? - NumberConverter.toChainAmount( - opts.amount!, - inputToken.decimals, - inputMultiplier - ), + const response = await this.#api.swap.v2.getOrder({ + query: { + inputMint: inputToken.id, + outputMint: outputToken.id, + amount: + opts.rawAmount ?? + NumberConverter.toChainAmount( + opts.amount!, + inputToken.decimals, + inputMultiplier + ), + }, }); - if (order.error) { - throw new Error(order.errorMessage ?? order.error); + if (response.error || !response.data) { + throw new Error("Unable to get a swap quote."); + } + const order = response.data; + if (!order.inAmount || !order.outAmount) { + throw new Error("Swap quote did not include token amounts."); } const inAmount = NumberConverter.fromChainAmount( @@ -234,6 +275,7 @@ export class SpotCommand { key?: string; slippage?: string; }): Promise { + requireJupiterApiKey(); Swap.validateAmountOpts(opts); if (opts.slippage !== undefined) { @@ -245,17 +287,18 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - const [inputToken, outputToken] = await Promise.all([ - resolveWalletAsset(signer.address, opts.from), - DatapiClient.resolveToken(opts.to), - ]); + const resolved = await resolveToken(this.#api, [opts.from, opts.to]); + const inputToken = this.requireToken(resolved[0], opts.from); + const outputToken = this.requireToken(resolved[1], opts.to); const swap = await Swap.execute({ signer, + api: this.#api, inputToken, outputToken, amount: opts.amount, rawAmount: opts.rawAmount, + rpc: this.#rpc, slippageBps: opts.slippage, }); @@ -286,7 +329,8 @@ export class SpotCommand { priceImpact: swap.order.priceImpact, networkFeeLamports: swap.networkFeeLamports, ...(Config.dryRun && { - transaction: swap.order.transaction, + serializedTransaction: swap.serializedTransaction, + simulation: swap.simulation, }), }); return; @@ -315,8 +359,20 @@ export class SpotCommand { value: `${networkFee} SOL`, }, ...(!Config.dryRun - ? [{ label: "Tx Signature", value: swap.result!.signature }] - : []), + ? [{ label: "Tx Signature", value: swap.result!.signature ?? "—" }] + : [ + { + label: "Serialized Transaction", + value: swap.serializedTransaction!, + }, + { + label: "Simulation", + value: swap.simulation!.success ? "Success" : "Failed", + }, + ...(swap.simulation!.error + ? [{ label: "Simulation Error", value: swap.simulation!.error }] + : []), + ]), ], }); } @@ -325,6 +381,7 @@ export class SpotCommand { address?: string; key?: string; }): Promise { + requireJupiterApiKey(); if (opts.address && opts.key) { throw new Error("Only one of --address or --key can be provided."); } @@ -333,126 +390,44 @@ export class SpotCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const SOL_DECIMALS = Asset.SOL.decimals; - - const raw = await UltraClient.getHoldings(address); - - // Preprocess: extract only ATA entries, keyed by mint - const ataByMint = new Map(); - for (const [mint, accounts] of Object.entries(raw.tokens)) { - const ata = accounts.find((acc) => acc.isAssociatedTokenAccount); - if (ata) { - ataByMint.set(mint, ata); - } - } - - // Resolve all token info via Datapi - const allMints = [...ataByMint.keys()]; - if (!ataByMint.has(Asset.SOL.id)) { - allMints.push(Asset.SOL.id); - } - - const tokenMap = new Map(); - const tokens = await DatapiClient.getTokensByMints(allMints); - for (const token of tokens) { - tokenMap.set(token.id, token); - } - - const outputTokens: { - id: string; - symbol: string; - decimals: number; - amount: number; - rawAmount: string; - value: number; - price: number; - priceChange: number; - isVerified?: boolean | undefined; - scaledUiMultiplier?: number | undefined; - reclaimableLamports?: string; - }[] = []; - - // Add combined SOL/WSOL entry - const wsolInfo = tokenMap.get(Asset.SOL.id); - const solLamports = BigInt(raw.amount); - const wsolLamports = BigInt(ataByMint.get(Asset.SOL.id)?.amount ?? "0"); - const combinedSolLamports = solLamports + wsolLamports; - if (wsolInfo && combinedSolLamports > 0n) { - const amount = Number( - NumberConverter.fromChainAmount(combinedSolLamports, SOL_DECIMALS) - ); - outputTokens.push({ - id: Asset.SOL.id, - symbol: wsolInfo.symbol, - decimals: SOL_DECIMALS, - amount, - rawAmount: combinedSolLamports.toString(), - value: amount * (wsolInfo.usdPrice ?? 0), - price: wsolInfo.usdPrice ?? 0, - priceChange: wsolInfo.stats24h?.priceChange ?? 0, - isVerified: wsolInfo.isVerified, - }); - } - - for (const [mint, ata] of ataByMint) { - if (mint === Asset.SOL.id) { - continue; - } - const info = tokenMap.get(mint); - if (!info) { - continue; - } - const multiplier = Swap.getScaledUiMultiplier(info); - const amount = Number( - NumberConverter.fromChainAmount(ata.amount, info.decimals, multiplier) - ); - outputTokens.push({ - id: mint, - symbol: info.symbol, - decimals: info.decimals, - amount, - rawAmount: ata.amount, - value: amount * (info.usdPrice ?? 0), - price: info.usdPrice ?? 0, - priceChange: info.stats24h?.priceChange ?? 0, - isVerified: info.isVerified, - scaledUiMultiplier: multiplier, - reclaimableLamports: ata.reclaimableLamports, - }); + const response = await this.#api.portfolio.getPortfolioV1PositionsAddress({ + path: { address }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get portfolio positions."); } - - // Sort by value descending - outputTokens.sort((a, b) => b.value - a.value); - const totalValue = outputTokens.reduce((sum, t) => sum + t.value, 0); + const positions = response.data.elements ?? []; + const totalValue = positions.reduce( + (total, position) => total + position.value, + 0 + ); if (Output.isJson()) { - Output.json({ totalValue, tokens: outputTokens }); + Output.json({ + owner: response.data.owner ?? address, + totalValue, + positions, + }); return; } - if (outputTokens.length === 0) { - throw new Error("No holdings found for this address."); + if (positions.length === 0) { + throw new Error("No portfolio positions found for this address."); } Output.table({ type: "horizontal", headers: { - symbol: "Token", - amount: "Amount", - price: "Price", + platform: "Platform", + label: "Type", + name: "Name", value: "Value", - priceChange: "24h Change", - verified: "Verified", }, - rows: outputTokens.map((t) => ({ - symbol: t.symbol, - amount: t.amount.toLocaleString("en-US", { - maximumFractionDigits: 6, - }), - price: Output.formatDollar(t.price), - value: Output.formatDollar(t.value), - priceChange: Output.formatPercentageChange(t.priceChange), - verified: Output.formatBoolean(t.isVerified), + rows: positions.map((position) => ({ + platform: position.platformId, + label: position.label, + name: position.name ?? "—", + value: Output.formatDollar(position.value), })), }); console.log(`\nTotal Value: ${Output.formatDollar(totalValue)}`); @@ -465,56 +440,58 @@ export class SpotCommand { to: string; key?: string; }): Promise { + requireJupiterApiKey(); Swap.validateAmountOpts(opts); + if (!isAddress(opts.to)) { + throw new Error("--to must be a valid Solana address."); + } const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - const token = await resolveWalletAsset(signer.address, opts.token); - const multiplier = Swap.getScaledUiMultiplier(token); + const token = this.requireToken( + (await resolveToken(this.#api, [opts.token]))[0], + opts.token + ); + const multiplier = await this.#rpc.getScaledUiMultiplier(token.id); const chainAmount = opts.rawAmount ?? NumberConverter.toChainAmount(opts.amount!, token.decimals, multiplier); - let txResponse; + const sender = signer.transactionSigner; + const recipient = address(opts.to); + let serializedTransaction; if (token.id === Asset.SOL.id) { - txResponse = await UltraClient.getTransferSolTx({ - senderAddress: signer.address as Address, - receiverAddress: opts.to as Address, - amount: chainAmount, - }); + serializedTransaction = await buildSolTransferTransaction( + this.#rpc, + sender, + recipient, + BigInt(chainAmount) + ); } else { - const [senderTokenAccountAddress] = await findAssociatedTokenPda({ - mint: token.id as Address, - owner: signer.address as Address, - tokenProgram: token.tokenProgram as Address, - }); - txResponse = await UltraClient.getTransferTokenTx({ - mint: token.id, - tokenDecimals: token.decimals.toString(), - tokenProgramId: token.tokenProgram, - senderAddress: signer.address as Address, - senderTokenAccountAddress, - receiverAddress: opts.to as Address, - amount: chainAmount, + const tokenProgram = await this.#rpc.getTokenProgram(token.id); + serializedTransaction = await buildTokenTransferTransaction({ + amount: BigInt(chainAmount), + decimals: token.decimals, + mint: address(token.id), + recipient, + rpc: this.#rpc, + sender, + tokenProgram, }); } - if ("error" in txResponse) { - throw new Error(txResponse.error); - } - const humanAmount = NumberConverter.fromChainAmount( chainAmount, token.decimals, multiplier ); const value = Number(humanAmount) * (token.usdPrice ?? 0); - const networkFee = NumberConverter.fromChainAmount( - txResponse.feeAmount?.toString() ?? 0n, - Asset.SOL.decimals + const signedTransaction = await signer.signTransaction( + serializedTransaction ); if (Config.dryRun) { + const simulation = await this.#rpc.simulate(signedTransaction); if (Output.isJson()) { Output.json({ dryRun: true, @@ -527,9 +504,9 @@ export class SpotCommand { }, amount: humanAmount, value: value, - networkFeeLamports: txResponse.feeAmount, signature: null, - transaction: txResponse.transaction, + serializedTransaction: simulation.serializedTransaction, + simulation, }); return; } @@ -545,21 +522,22 @@ export class SpotCommand { value: `${humanAmount} ${token.symbol} (${Output.formatDollar(value)})`, }, { - label: "Network Fee", - value: `${networkFee} SOL`, + label: "Serialized Transaction", + value: simulation.serializedTransaction, + }, + { + label: "Simulation", + value: simulation.success ? "Passed" : "Failed", }, + ...(simulation.error + ? [{ label: "Error", value: simulation.error }] + : []), ], }); return; } - const signedTx = await signer.signTransaction( - txResponse.transaction as Base64EncodedBytes - ); - const result = await UltraClient.postExecuteTransfer({ - requestId: txResponse.requestId, - signedTransaction: signedTx, - }); + const signature = await this.#rpc.send(signedTransaction); if (Output.isJson()) { Output.json({ @@ -572,8 +550,7 @@ export class SpotCommand { }, amount: humanAmount, value: value, - networkFeeLamports: txResponse.feeAmount, - signature: result.signature, + signature, }); return; } @@ -587,11 +564,7 @@ export class SpotCommand { label: "Amount", value: `${humanAmount} ${token.symbol} (${Output.formatDollar(value)})`, }, - { - label: "Network Fee", - value: `${networkFee} SOL`, - }, - { label: "Tx Signature", value: result.signature }, + { label: "Tx Signature", value: signature }, ], }); } @@ -605,6 +578,9 @@ export class SpotCommand { limit: string; offset?: string; }): Promise { + if (opts.token) { + requireJupiterApiKey(); + } if (opts.address && opts.key) { throw new Error("Only one of --address or --key can be provided."); } @@ -618,19 +594,48 @@ export class SpotCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); const targetAsset = opts.token - ? await DatapiClient.resolveToken(opts.token) + ? this.requireToken( + (await resolveToken(this.#api, [opts.token]))[0], + opts.token + ) : undefined; - const { userTrades, next } = await DatapiClient.getSwapsByAddress({ - address, - assetId: targetAsset?.id, - after: opts.after ? this.parseTimestamp(opts.after) : undefined, - before: opts.before ? this.parseTimestamp(opts.before) : undefined, - limit: opts.limit ? limit * 2 : undefined, // double bookkeeping - offset: opts.offset, + const response = await this.#datapi.getUserTransactions({ + query: { + addresses: address, + includeCapitalSide: true, + ...(targetAsset ? { assetId: targetAsset.id } : {}), + ...(opts.after ? { fromTs: this.parseTimestamp(opts.after) } : {}), + ...(opts.before ? { toTs: this.parseTimestamp(opts.before) } : {}), + limit: Math.min(limit * 2, 30), + ...(opts.offset ? { offset: Number(opts.offset) } : {}), + }, + }); + if (response.error || !response.data) { + throw new Error("Data API history request failed."); + } + const { next } = response.data; + const userTrades = response.data.userTrades.flatMap((trade) => { + const type = trade["type"]; + const usdVolume = trade["usdVolume"]; + const txHash = trade["txHash"]; + const assetId = trade["assetId"]; + const blockTime = trade["blockTime"]; + const amount = trade["amount"]; + if ( + (type !== "buy" && type !== "sell") || + typeof usdVolume !== "number" || + typeof txHash !== "string" || + typeof assetId !== "string" || + typeof blockTime !== "string" || + typeof amount !== "number" + ) { + return []; + } + return [{ type, usdVolume, txHash, assetId, blockTime, amount }]; }); // Group double-bookkeeping entries by txHash - const grouped = new Map(); + const grouped = new Map(); for (const t of userTrades) { const existing = grouped.get(t.txHash); if (existing) { @@ -643,7 +648,7 @@ export class SpotCommand { // Resolve token metadata for all unique mints const mints = [...new Set(userTrades.map((t) => t.assetId))]; const tokenMap = new Map(); - const tokens = await DatapiClient.getTokensByMints(mints); + const tokens = await getTokensByMints(this.#datapi, mints); for (const token of tokens) { tokenMap.set(token.id, token); } @@ -721,16 +726,26 @@ export class SpotCommand { token?: string; }): Promise { const signer = await Signer.load(opts.key ?? Config.load().activeKey); - const holdings = await UltraClient.getHoldings(signer.address); - - // Extract reclaimable mints from holdings ATAs - const reclaimableMints: string[] = []; - for (const [mint, accounts] of Object.entries(holdings.tokens)) { - const ata = accounts.find((acc) => acc.isAssociatedTokenAccount); - if (ata?.reclaimableLamports && BigInt(ata.reclaimableLamports) > 0n) { - reclaimableMints.push(mint); - } - } + const owner = signer.transactionSigner; + const accountsByProgram = await Promise.all( + [TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS].map( + async (tokenProgram) => ({ + tokenProgram, + accounts: await this.#rpc.getTokenAccountsByOwner( + owner.address, + tokenProgram + ), + }) + ) + ); + const candidates = accountsByProgram.flatMap(({ tokenProgram, accounts }) => + accounts + .filter((account) => account.amount === 0n && !account.isFrozen) + .map((account) => ({ ...account, tokenProgram })) + ); + const reclaimableMints: string[] = [ + ...new Set(candidates.map((candidate) => candidate.mint)), + ]; // Filter to single token if --token provided let mints = reclaimableMints; @@ -761,62 +776,54 @@ export class SpotCommand { throw new Error("No reclaimable token accounts found."); } - // Chunk mints into batches of 200 and craft - const MAX_MINTS_PER_REQUEST = 200; - const batches: string[][] = []; - for (let i = 0; i < mints.length; i += MAX_MINTS_PER_REQUEST) { - batches.push(mints.slice(i, i + MAX_MINTS_PER_REQUEST)); - } - const craftResponses = await Promise.all( - batches.map((batch) => - UltraClient.postReclaimCraft({ - owner: signer.address, - mints: batch, - }) - ) + const accounts = candidates.filter((candidate) => + mints.includes(candidate.mint) ); - - // Aggregate across chunks - const allTransactions: { requestId: string; transaction: string }[] = []; - let netLamportsReclaimed = 0n; - let networkFeeLamports = 0n; - let totalValueReclaimed = 0; - let skippedCount = 0; - - for (const r of craftResponses) { - if (r.error) { - throw new Error(r.error); - } - allTransactions.push(...r.transactions); - netLamportsReclaimed += BigInt(r.netLamportsReclaimed); - networkFeeLamports += BigInt(r.gasCostLamports); - totalValueReclaimed += r.netReclaimedUsdAmount ?? 0; - skippedCount += r.skippedMints?.length ?? 0; + if (accounts.length === 0) { + throw new Error("No reclaimable token accounts found."); } - if (allTransactions.length === 0) { - throw new Error("No reclaimable token accounts found."); + const grouped = new Map(); + for (const account of accounts) { + const group = grouped.get(account.tokenProgram) ?? []; + group.push(account); + grouped.set(account.tokenProgram, group); + } + const serializedTransactions: Base64EncodedWireTransaction[] = []; + for (const [tokenProgram, group] of grouped) { + serializedTransactions.push( + await buildCloseTokenAccountsTransaction({ + accounts: group.map((account) => account.address), + owner, + rpc: this.#rpc, + tokenProgram, + }) + ); } - const reclaimedSol = NumberConverter.fromChainAmount( - netLamportsReclaimed, - Asset.SOL.decimals + const rentLamports = accounts.reduce( + (total, account) => total + account.lamports, + 0n ); - const networkFee = NumberConverter.fromChainAmount( - networkFeeLamports, + const reclaimedSol = NumberConverter.fromChainAmount( + rentLamports, Asset.SOL.decimals ); - const accountCount = mints.length - skippedCount; + const accountCount = accounts.length; if (Config.dryRun) { + const simulations = []; + for (const transaction of serializedTransactions) { + simulations.push( + await this.#rpc.simulate(await signer.signTransaction(transaction)) + ); + } if (Output.isJson()) { Output.json({ dryRun: true, - totalLamportsReclaimed: Number(netLamportsReclaimed), - totalValueReclaimed: totalValueReclaimed, - networkFeeLamports: Number(networkFeeLamports), + totalLamportsReclaimed: rentLamports.toString(), signatures: null, - transactions: allTransactions.map((tx) => tx.transaction), + transactions: simulations, }); return; } @@ -827,42 +834,46 @@ export class SpotCommand { rows: [ { label: "SOL Reclaimed", - value: `${reclaimedSol} SOL (${Output.formatDollar(totalValueReclaimed)})`, + value: `${reclaimedSol} SOL before transaction fees`, }, { label: "Accounts Reclaimed", value: `${accountCount} token account${accountCount !== 1 ? "s" : ""}`, }, - { - label: "Network Fee", - value: `${networkFee} SOL`, - }, + ...simulations.flatMap((simulation, index) => [ + { + label: + simulations.length === 1 + ? "Serialized Transaction" + : `Serialized Transaction ${index + 1}`, + value: simulation.serializedTransaction, + }, + { + label: + simulations.length === 1 + ? "Simulation" + : `Simulation ${index + 1}`, + value: simulation.success ? "Passed" : "Failed", + }, + ...(simulation.error + ? [{ label: "Error", value: simulation.error }] + : []), + ]), ], }); return; } - // Sign and execute each transaction sequentially const signatures: string[] = []; - for (const tx of allTransactions) { - const signedTx = await signer.signTransaction( - tx.transaction as Base64EncodedBytes + for (const transaction of serializedTransactions) { + signatures.push( + await this.#rpc.send(await signer.signTransaction(transaction)) ); - const result = await UltraClient.postReclaimExecute({ - requestId: tx.requestId, - signedTransaction: signedTx, - }); - if (result.status === "Failed") { - throw new Error(result.error ?? "Reclaim transaction failed."); - } - signatures.push(result.signature); } if (Output.isJson()) { Output.json({ - totalLamportsReclaimed: Number(netLamportsReclaimed), - totalValueReclaimed: totalValueReclaimed, - networkFeeLamports: Number(networkFeeLamports), + totalLamportsReclaimed: rentLamports.toString(), signatures, }); return; @@ -873,16 +884,12 @@ export class SpotCommand { rows: [ { label: "SOL Reclaimed", - value: `${reclaimedSol} SOL (${Output.formatDollar(totalValueReclaimed)})`, + value: `${reclaimedSol} SOL before transaction fees`, }, { label: "Accounts Reclaimed", value: `${accountCount} token account${accountCount !== 1 ? "s" : ""}`, }, - { - label: "Network Fee", - value: `${networkFee} SOL`, - }, ...signatures.map((sig, i) => ({ label: signatures.length === 1 ? "Tx Signature" : `Tx Signature ${i + 1}`, @@ -900,10 +907,14 @@ export class SpotCommand { if (!holdingsMints.includes(input)) { throw new Error(`Token ${input} not found in wallet.`); } - return DatapiClient.resolveToken(input); + const [token] = await getTokensByMints(this.#datapi, [input]); + if (!token) { + throw new Error(`Token ${input} not found in wallet.`); + } + return token; } - const tokens = await DatapiClient.getTokensByMints(holdingsMints); + const tokens = await getTokensByMints(this.#datapi, holdingsMints); const query = input.toLowerCase(); const matches = tokens.filter((t) => t.symbol.toLowerCase() === query); @@ -919,22 +930,21 @@ export class SpotCommand { ); } - private static getHoldingsMints(holdings: GetHoldingsResponse): string[] { - const mints = Object.keys(holdings.tokens); - if (BigInt(holdings.amount) > 0n && !mints.includes(Asset.SOL.id)) { - mints.push(Asset.SOL.id); + private static requireToken(token: Token | undefined, query: string): Token { + if (!token) { + throw new Error(`Token not found: ${query}`); } - return mints; + return token; } - private static parseTimestamp(value: string): string { + private static parseTimestamp(value: string): number { if (/^\d+$/.test(value)) { - return new Date(Number(value) * 1000).toISOString(); + return Number(value) * 1000; } const ms = new Date(value).getTime(); if (isNaN(ms)) { throw new Error(`Invalid date: ${value}`); } - return new Date(ms).toISOString(); + return ms; } } diff --git a/src/commands/VrfdCommand.ts b/src/commands/VrfdCommand.ts index 65fa08b..fc1b999 100644 --- a/src/commands/VrfdCommand.ts +++ b/src/commands/VrfdCommand.ts @@ -2,18 +2,33 @@ import { isAddress, type Base64EncodedBytes } from "@solana/kit"; import type { Command } from "commander"; -import { - VrfdClient, - type ExecuteResponse, - type TokenMetadata, -} from "../clients/VrfdClient.ts"; +import type { JupiterApi, JupiterApiTypes } from "jupiter-api-ts"; + import { Config } from "../lib/Config.ts"; +import { requireJupiterApiKey } from "../lib/JupiterApi.ts"; import { NumberConverter } from "../lib/NumberConverter.ts"; import { Output } from "../lib/Output.ts"; import { Signer } from "../lib/Signer.ts"; +import type { SolanaRpc } from "../lib/SolanaRpc.ts"; + +type TokenMetadata = JupiterApiTypes.TokensV2VerificationTokenMetadataInput; +type VrfdSubmission = { + result?: JupiterApiTypes.TokensV2VerificationExpressExecuteResponse; + serializedTransaction?: string; + simulation?: Awaited>; +}; export class VrfdCommand { - public static register(program: Command): void { + static #api: JupiterApi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + rpc: SolanaRpc, + api: JupiterApi + ): void { + this.#rpc = rpc; + this.#api = api; const vrfd = program.command("vrfd").description("Token verification"); vrfd .command("check") @@ -58,14 +73,28 @@ export class VrfdCommand { "Enable circulating supply URL" ) .option("--key ", "Key to use for signing") - .action((opts) => this.submit(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.submit(opts); + }); } private static async check(opts: { token: string }): Promise { + requireJupiterApiKey(); if (!isAddress(opts.token)) { throw new Error("Invalid token mint address."); } - const eligibility = await VrfdClient.checkEligibility(opts.token); + const response = + await this.#api.tokens.v2.verification.getTokensV2VerifyExpressCheckEligibility( + { + query: { tokenId: opts.token }, + } + ); + if (response.error || !response.data) { + throw new Error("Unable to check token verification eligibility."); + } + const eligibility = response.data; if (Output.isJson()) { Output.json(eligibility); @@ -110,15 +139,28 @@ export class VrfdCommand { private static async signAndExecute( signer: Signer, transaction: string, - req: Omit[0], "transaction"> - ): Promise { - if (Config.dryRun) { - return null; - } + req: Omit< + JupiterApiTypes.TokensV2VerificationExpressExecuteBody, + "transaction" + > + ): Promise { const signedTx = await signer.signTransaction( transaction as Base64EncodedBytes ); - return VrfdClient.execute({ ...req, transaction: signedTx }); + if (Config.dryRun) { + return { + serializedTransaction: signedTx, + simulation: await this.#rpc.simulate(signedTx), + }; + } + const response = + await this.#api.tokens.v2.verification.postTokensV2VerifyExpressExecute({ + body: { ...req, transaction: signedTx }, + }); + if (response.error || !response.data) { + throw new Error("Unable to execute the verification transaction."); + } + return { result: response.data }; } private static async submit(opts: { @@ -146,6 +188,7 @@ export class VrfdCommand { metaUseCoingeckoCoinId?: boolean; metaUseCirculatingSupplyUrl?: boolean; }): Promise { + requireJupiterApiKey(); const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); @@ -154,7 +197,16 @@ export class VrfdCommand { } // Check eligibility before crafting transaction - const eligibility = await VrfdClient.checkEligibility(opts.token); + const eligibilityResponse = + await this.#api.tokens.v2.verification.getTokensV2VerifyExpressCheckEligibility( + { + query: { tokenId: opts.token }, + } + ); + if (eligibilityResponse.error || !eligibilityResponse.data) { + throw new Error("Unable to check token verification eligibility."); + } + const eligibility = eligibilityResponse.data; if (!eligibility.tokenExists) { throw new Error("Token not found."); } @@ -180,7 +232,14 @@ export class VrfdCommand { } // Craft the payment transaction - const craftResult = await VrfdClient.craftTxn(signer.address); + const craftResponse = + await this.#api.tokens.v2.verification.getTokensV2VerifyExpressCraftTxn({ + query: { senderAddress: signer.address }, + }); + if (craftResponse.error || !craftResponse.data) { + throw new Error("Unable to build the verification transaction."); + } + const craftResult = craftResponse.data; if (craftResult.error) { throw new Error(craftResult.error); } @@ -203,8 +262,8 @@ export class VrfdCommand { tokenMetadata, }); - if (result?.status === "Failed") { - throw new Error(result.error ?? "Verification submission failed."); + if (result.result?.status === "Failed") { + throw new Error(result.result.error ?? "Verification submission failed."); } if (Output.isJson()) { @@ -212,15 +271,18 @@ export class VrfdCommand { ...(Config.dryRun && { dryRun: true }), sender: signer.address, tokenId: opts.token, - status: result?.status ?? null, - signature: result?.signature ?? null, + status: result.result?.status ?? null, + signature: result.result?.signature ?? null, paymentAmount, paymentMint: craftResult.mint, feeUsd: craftResult.feeUsdAmount ?? null, - verificationCreated: result?.verificationCreated ?? null, - metadataCreated: result?.metadataCreated ?? null, + verificationCreated: result.result?.verificationCreated ?? null, + metadataCreated: result.result?.metadataCreated ?? null, metadata: tokenMetadata ?? null, - ...(Config.dryRun && { transaction: craftResult.transaction }), + ...(Config.dryRun && { + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, + }), }); return; } @@ -246,20 +308,39 @@ export class VrfdCommand { }, { label: "Verification Created", - value: result - ? Output.formatBoolean(result.verificationCreated) + value: result.result + ? Output.formatBoolean(result.result.verificationCreated) : "\u2014", }, { label: "Metadata Created", - value: result - ? Output.formatBoolean(result.metadataCreated) + value: result.result + ? Output.formatBoolean(result.result.metadataCreated) : "\u2014", }, ...this.metadataRows(tokenMetadata), - ...(!Config.dryRun && result?.signature - ? [{ label: "Tx Signature", value: result.signature }] - : []), + ...(!Config.dryRun && result.result?.signature + ? [{ label: "Tx Signature", value: result.result.signature }] + : Config.dryRun + ? [ + { + label: "Serialized Transaction", + value: result.serializedTransaction!, + }, + { + label: "Simulation", + value: result.simulation!.success ? "Success" : "Failed", + }, + ...(result.simulation!.error + ? [ + { + label: "Simulation Error", + value: result.simulation!.error, + }, + ] + : []), + ] + : []), ], }); } diff --git a/src/index.ts b/src/index.ts index 107ba33..2dc462b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,13 @@ import { Command } from "commander"; import { ConfigCommand } from "./commands/ConfigCommand.ts"; +import { InviteCommand } from "./commands/InviteCommand.ts"; +import { JlpCommand } from "./commands/JlpCommand.ts"; import { KeysCommand } from "./commands/KeysCommand.ts"; import { LendCommand } from "./commands/LendCommand.ts"; import { PerpsCommand } from "./commands/PerpsCommand.ts"; import { PredictionsCommand } from "./commands/PredictionsCommand.ts"; +import { RecurringCommand } from "./commands/RecurringCommand.ts"; import { SignCommand } from "./commands/SignCommand.ts"; import { SpotCommand } from "./commands/SpotCommand.ts"; import { UpdateCommand } from "./commands/UpdateCommand.ts"; @@ -13,9 +16,13 @@ import { VrfdCommand } from "./commands/VrfdCommand.ts"; import { version } from "../package.json"; import { Config } from "./lib/Config.ts"; +import { createJupiterClients } from "./lib/JupiterApi.ts"; import { Output } from "./lib/Output.ts"; +import { SolanaRpc } from "./lib/SolanaRpc.ts"; Config.init(); +const jupiterClients = createJupiterClients(); +const solanaRpc = new SolanaRpc(); const program = new Command(); program @@ -23,7 +30,10 @@ program .description("Jupiter CLI for agentic workflows") .version(version) .option("-f, --format ", "Output format ('table' or 'json')") - .option("--dry-run", "Preview the transaction without signing or submitting") + .option( + "--dry-run", + "Sign and simulate the transaction without submitting it" + ) .hook("preAction", (thisCommand) => { const opts = thisCommand.opts(); if (opts.format) { @@ -39,13 +49,25 @@ program ConfigCommand.register(program); KeysCommand.register(program); -LendCommand.register(program); -PerpsCommand.register(program); -PredictionsCommand.register(program); +InviteCommand.register(program, solanaRpc, jupiterClients.api); +JlpCommand.register(program, jupiterClients.perps, solanaRpc); +LendCommand.register(program, solanaRpc, jupiterClients.api); +PerpsCommand.register(program, jupiterClients.perps, solanaRpc); +PredictionsCommand.register( + program, + solanaRpc, + jupiterClients.api +); +RecurringCommand.register(program, solanaRpc, jupiterClients.api); SignCommand.register(program); -SpotCommand.register(program); +SpotCommand.register( + program, + jupiterClients.datapi, + solanaRpc, + jupiterClients.api +); UpdateCommand.register(program); -VrfdCommand.register(program); +VrfdCommand.register(program, solanaRpc, jupiterClients.api); program.parseAsync().catch(async (err: unknown) => { await Output.error(err); diff --git a/src/lib/Asset.test.ts b/src/lib/Asset.test.ts new file mode 100644 index 0000000..d9df6dc --- /dev/null +++ b/src/lib/Asset.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from "bun:test"; +import type { JupiterApi } from "jupiter-api-ts"; + +import { resolveToken, searchTokens } from "./Asset.ts"; + +const tokenA = { + id: "mint-a", + name: "Token A", + symbol: "A", + decimals: 6, +}; +const tokenB = { + id: "mint-b", + name: "Token B", + symbol: "B", + decimals: 9, +}; + +function mockSearchApi(handler: (request: unknown) => unknown): JupiterApi { + return { + tokens: { + v2: { + getTokensV2Search: async (request: unknown) => handler(request), + }, + }, + } as unknown as JupiterApi; +} + +test("searchTokens uses Tokens V2 for a comma-separated mint search", async () => { + const calls: unknown[] = []; + const api = mockSearchApi((request) => { + calls.push(request); + return { data: [tokenA, tokenB] }; + }); + + const tokens = await searchTokens({ + api, + query: "mint-a,mint-b", + }); + + expect(calls).toEqual([{ query: { query: "mint-a,mint-b" } }]); + expect(tokens.map((token) => token.id)).toEqual(["mint-a", "mint-b"]); +}); + +test("resolveToken looks up a single query as the top search result", async () => { + const calls: unknown[] = []; + const api = mockSearchApi((request) => { + calls.push(request); + return { data: [tokenA, tokenB] }; + }); + + const [token] = await resolveToken(api, ["A"]); + + expect(calls).toEqual([{ query: { query: "A" } }]); + expect(token).toEqual(tokenA); +}); + +test("resolveToken looks up multiple queries in one search call", async () => { + const calls: unknown[] = []; + const api = mockSearchApi((request) => { + calls.push(request); + return { data: [tokenB, tokenA] }; + }); + + const [inputToken, outputToken] = await resolveToken(api, [ + "mint-a", + "mint-b", + ]); + + expect(calls).toEqual([{ query: { query: "mint-a,mint-b" } }]); + expect(inputToken).toEqual(tokenA); + expect(outputToken).toEqual(tokenB); +}); + +test("resolveToken matches multi-query results by symbol", async () => { + const api = mockSearchApi(() => ({ data: [tokenA, tokenB] })); + + const [inputToken, outputToken] = await resolveToken(api, ["a", "B"]); + + expect(inputToken).toEqual(tokenA); + expect(outputToken).toEqual(tokenB); +}); + +test("resolveToken throws when a multi-query token is missing", async () => { + const api = mockSearchApi(() => ({ data: [tokenA] })); + + await expect(resolveToken(api, ["mint-a", "mint-b"])).rejects.toThrow( + "Token not found: mint-b" + ); +}); diff --git a/src/lib/Asset.ts b/src/lib/Asset.ts index dceb3fd..4619b69 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -1,7 +1,5 @@ -import { address, isAddress } from "@solana/kit"; - -import { DatapiClient, type Token } from "../clients/DatapiClient.ts"; -import { UltraClient } from "../clients/UltraClient.ts"; +import { address } from "@solana/kit"; +import type { JupiterApi, JupiterDatapi } from "jupiter-api-ts"; type AssetInfo = { readonly id: ReturnType; @@ -29,46 +27,101 @@ export const Asset = { export type Asset = (typeof Asset)[keyof typeof Asset]; -export function resolveAsset(name: string): (typeof Asset)[keyof typeof Asset] { - const key = name.toUpperCase(); - const asset = Asset[key as keyof typeof Asset]; - if (!asset) { - throw new Error(`Unknown asset: ${name}`); +export type Token = { + id: string; + name: string; + symbol: string; + decimals: number; + usdPrice?: number; + mcap?: number; + [key: string]: unknown; +}; + +export async function searchTokens(params: { + api: JupiterApi; + query: string; + limit?: number; +}): Promise { + const { api, query, limit } = params; + const response = await api.tokens.v2.getTokensV2Search({ + query: { query }, + }); + if (response.error || !response.data) { + throw new Error("Tokens V2 search failed."); } - return asset; + const tokens = response.data.filter( + (token): token is Token => + typeof token.id === "string" && + typeof token.name === "string" && + typeof token.symbol === "string" && + typeof token.decimals === "number" + ); + return limit === undefined ? tokens : tokens.slice(0, limit); } -export async function resolveWalletAsset( - walletAddress: string, - asset: string -): Promise { - if (isAddress(asset)) { - return DatapiClient.resolveToken(asset); +export async function getTokensByMints( + datapi: JupiterDatapi, + mints: string[] +): Promise { + if (mints.length === 0) { + return []; } - - const key = asset.toUpperCase(); - // Prevent full holdings lookup for well-known tokens - if (key in Asset) { - return DatapiClient.resolveToken(Asset[key as keyof typeof Asset].id); + const batches: string[][] = []; + for (let i = 0; i < mints.length; i += 100) { + batches.push(mints.slice(i, i + 100)); } + const results = await Promise.all( + batches.map(async (batch) => { + const response = await datapi.getAssetsByIds({ + query: { ids: batch.join(",") }, + }); + if (response.error || !response.data) { + throw new Error("Data API asset lookup failed."); + } + return response.data.assets.filter( + (asset): asset is Token => typeof asset.decimals === "number" + ); + }) + ); + return results.flat(); +} - const holdings = await UltraClient.getHoldings(walletAddress); - const mints = Object.keys(holdings.tokens); - if (BigInt(holdings.amount) > 0n && !mints.includes(Asset.SOL.id)) { - mints.push(Asset.SOL.id); +export async function resolveToken( + api: JupiterApi, + queries: string[] +): Promise { + if (queries.length === 0) { + throw new Error("Token not found."); } - const tokens = await DatapiClient.getTokensByMints(mints); - const query = asset.toLowerCase(); - const matches = tokens.filter((t) => t.symbol.toLowerCase() === query); - if (matches.length === 0) { - throw new Error(`Token "${asset}" not found in wallet.`); - } - if (matches.length === 1) { - return matches[0]!; - } - const options = matches.map((t) => ` - ${t.symbol} (${t.id})`).join("\n"); - throw new Error( - `Multiple tokens matching "${asset}" found in wallet. Use the mint address instead:\n${options}` + const tokens = await searchTokens({ + api, + query: queries.join(","), + ...(queries.length === 1 ? { limit: 1 } : {}), + }); + + return queries.map((query) => { + const token = queries.length === 1 ? tokens[0] : matchToken(tokens, query); + if (!token) { + throw new Error(`Token not found: ${query}`); + } + return token; + }); +} + +function matchToken(tokens: Token[], query: string): Token | undefined { + const lower = query.toLowerCase(); + return ( + tokens.find((token) => token.id === query) ?? + tokens.find((token) => token.symbol.toLowerCase() === lower) ); } + +export function resolveAsset(name: string): (typeof Asset)[keyof typeof Asset] { + const key = name.toUpperCase(); + const asset = Asset[key as keyof typeof Asset]; + if (!asset) { + throw new Error(`Unknown asset: ${name}`); + } + return asset; +} diff --git a/src/lib/Config.test.ts b/src/lib/Config.test.ts index 6564b48..7fe7777 100644 --- a/src/lib/Config.test.ts +++ b/src/lib/Config.test.ts @@ -30,6 +30,16 @@ describe("Config.load", () => { expect(s.apiKey).toBeUndefined(); }); + test("uses the mainnet RPC fallback when rpcUrl is unset", () => { + expect(Config.getRpcUrl()).toBe("https://api.mainnet.solana.com"); + }); + + test("uses a command dry-run flag when present", () => { + Config.dryRun = false; + expect(Config.resolveDryRun(true)).toBe(true); + expect(Config.resolveDryRun(undefined)).toBe(false); + }); + test("throws a clear error when settings.json is not valid json", () => { writeFileSync( join(TEST_DIR, "settings.json"), @@ -57,6 +67,27 @@ describe("Config.load", () => { }); }); + test("persists a configured HTTPS RPC URL", () => { + Config.set({ rpcUrl: "https://rpc.example.com" }); + + expect(Config.load().rpcUrl).toBe("https://rpc.example.com"); + expect(Config.getRpcUrl()).toBe("https://rpc.example.com"); + }); + + test("clears an RPC URL back to the mainnet fallback", () => { + Config.set({ rpcUrl: "https://rpc.example.com" }); + Config.set({ rpcUrl: undefined }); + + expect(Config.load().rpcUrl).toBeUndefined(); + expect(Config.getRpcUrl()).toBe("https://api.mainnet.solana.com"); + }); + + test("rejects non-HTTPS RPC URLs", () => { + expect(() => Config.set({ rpcUrl: "http://localhost:8899" })).toThrow( + "Invalid RPC URL. It must be an HTTPS URL." + ); + }); + test("treats non-object root as empty object for fields", () => { writeFileSync(join(TEST_DIR, "settings.json"), "[]"); const s = Config.load(); diff --git a/src/lib/Config.ts b/src/lib/Config.ts index 8f0708b..fa72151 100644 --- a/src/lib/Config.ts +++ b/src/lib/Config.ts @@ -6,8 +6,11 @@ type Settings = { activeKey: "default" | string; output: "table" | "json"; apiKey?: string | undefined; + rpcUrl?: string | undefined; }; +const DEFAULT_RPC_URL = "https://api.mainnet.solana.com"; + const DEFAULT_SETTINGS: Settings = { activeKey: "default", output: "table", @@ -56,13 +59,34 @@ export class Config { ? o.output : DEFAULT_SETTINGS.output, apiKey: typeof o?.apiKey === "string" ? o.apiKey : undefined, + rpcUrl: typeof o?.rpcUrl === "string" ? o.rpcUrl : undefined, }; } + public static getRpcUrl(): string { + return this.load().rpcUrl ?? DEFAULT_RPC_URL; + } + + public static resolveDryRun(commandDryRun?: true): boolean { + return commandDryRun ?? this.dryRun; + } + public static set(settings: Partial): void { if (Object.keys(settings).length === 0) { throw new Error("No settings provided to update."); } + if (settings.rpcUrl !== undefined) { + let url: URL; + try { + url = new URL(settings.rpcUrl); + } catch { + throw new Error("Invalid RPC URL. It must be an HTTPS URL."); + } + if (url.protocol !== "https:") { + throw new Error("Invalid RPC URL. It must be an HTTPS URL."); + } + } + const currentSettings = this.load(); this.save({ ...currentSettings, @@ -72,8 +96,12 @@ export class Config { public static save(settings: Settings): void { // Strip undefined optional fields so settings.json stays clean - const { apiKey, ...rest } = settings; - const out = apiKey ? { ...rest, apiKey } : rest; + const { apiKey, rpcUrl, ...rest } = settings; + const out = { + ...rest, + ...(apiKey ? { apiKey } : {}), + ...(rpcUrl ? { rpcUrl } : {}), + }; writeFileSync(this.SETTINGS_FILE, JSON.stringify(out, null, 2) + "\n"); } } diff --git a/src/lib/JupiterApi.ts b/src/lib/JupiterApi.ts new file mode 100644 index 0000000..5136d87 --- /dev/null +++ b/src/lib/JupiterApi.ts @@ -0,0 +1,57 @@ +import { + createJupiterApiClient, + createJupiterDatapiClient, + createJupiterPerpsApiClient, + JupiterApi, + JupiterDatapi, + JupiterPerpsApi, +} from "jupiter-api-ts"; + +import { Config } from "./Config.ts"; + +const CLIENT_PLATFORM_HEADER = "jupiter.cli"; + +type JupiterClientOptions = { + apiKey?: string; + fetch?: typeof fetch; +}; + +export type JupiterClients = { + api: JupiterApi; + datapi: JupiterDatapi; + perps: JupiterPerpsApi; +}; + +export function createJupiterClients( + options: JupiterClientOptions = {} +): JupiterClients { + const apiKey = options.apiKey ?? Config.load().apiKey; + const fetch = options.fetch; + const coreClient = createJupiterApiClient({ + auth: apiKey, + headers: { "x-client-platform": CLIENT_PLATFORM_HEADER }, + ...(fetch ? { fetch } : {}), + }); + const perpsClient = createJupiterPerpsApiClient({ + auth: apiKey, + headers: { "x-client-platform": CLIENT_PLATFORM_HEADER }, + ...(fetch ? { fetch } : {}), + }); + const datapiClient = createJupiterDatapiClient(fetch ? { fetch } : {}); + + return { + api: new JupiterApi({ client: coreClient }), + datapi: new JupiterDatapi({ client: datapiClient }), + perps: new JupiterPerpsApi({ client: perpsClient }), + }; +} + +export function requireJupiterApiKey(): string { + const apiKey = Config.load().apiKey; + if (!apiKey) { + throw new Error( + "A Jupiter API key is required. Run jup config set api-key ." + ); + } + return apiKey; +} diff --git a/src/lib/NumberConverter.test.ts b/src/lib/NumberConverter.test.ts index 7255b46..2409db3 100644 --- a/src/lib/NumberConverter.test.ts +++ b/src/lib/NumberConverter.test.ts @@ -79,6 +79,12 @@ describe("fromChainAmount", () => { expect(NumberConverter.fromChainAmount("1000000", 6, 0.5)).toBe("0.5"); }); + test("signed amounts", () => { + expect(NumberConverter.fromChainAmount("-1500000", 6)).toBe("-1.5"); + expect(NumberConverter.fromChainAmount("-30000000", 9)).toBe("-0.03"); + expect(NumberConverter.fromChainAmount("-0", 6)).toBe("0"); + }); + test("multiplier of 1 is no-op", () => { expect(NumberConverter.fromChainAmount("1500000", 6, 1)).toBe("1.5"); }); @@ -124,6 +130,12 @@ describe("toChainAmount", () => { expect(NumberConverter.toChainAmount("1.5", 6, 1)).toBe("1500000"); }); + test("signed amounts", () => { + expect(NumberConverter.toChainAmount("-1.5", 6)).toBe("-1500000"); + expect(NumberConverter.toChainAmount("-0.03", 9)).toBe("-30000000"); + expect(NumberConverter.toChainAmount("-0", 6)).toBe("0"); + }); + test("round-trip with fromChainAmount", () => { const decimals = 6; const cases = ["1000000", "1500000", "500000", "100000000", "1"]; diff --git a/src/lib/NumberConverter.ts b/src/lib/NumberConverter.ts index 3232c81..6771691 100644 --- a/src/lib/NumberConverter.ts +++ b/src/lib/NumberConverter.ts @@ -38,6 +38,14 @@ export class NumberConverter { multiplier?: number | undefined ): string { const amountString = amount.toString(); + if (amountString.startsWith("-")) { + const converted = this.fromChainAmount( + amountString.slice(1), + decimals, + multiplier + ); + return converted === "0" ? "0" : `-${converted}`; + } let postDecimals: string; if (amountString.length <= decimals) { @@ -67,6 +75,14 @@ export class NumberConverter { decimals: number, multiplier?: number | undefined ): string { + if (amount.startsWith("-")) { + const converted = this.toChainAmount( + amount.slice(1), + decimals, + multiplier + ); + return converted === "0" ? "0" : `-${converted}`; + } const [integerPart, fractionalPart] = amount.split("."); const postDecimals = this.removeInsignificantZeros( (integerPart ?? "") + diff --git a/src/lib/Signer.test.ts b/src/lib/Signer.test.ts index 44cd4c5..3f6d140 100644 --- a/src/lib/Signer.test.ts +++ b/src/lib/Signer.test.ts @@ -58,4 +58,16 @@ describe("signTransaction", () => { "Attempted to sign a transaction with an address that is not a signer for it" ); }); + + test("signs with an explicit signer set", async () => { + const signer = await Signer.generate(); + const unsignedTx = createUnsignedTransaction(signer.address); + + const signedTx = await Signer.signTransactionWith([signer], unsignedTx); + const decodedTx = getTransactionCodec().decode( + getBase64Codec().encode(signedTx) + ); + + expect(decodedTx.signatures[address(signer.address)]).not.toBeNull(); + }); }); diff --git a/src/lib/Signer.ts b/src/lib/Signer.ts index 2b58c92..b791b93 100644 --- a/src/lib/Signer.ts +++ b/src/lib/Signer.ts @@ -90,13 +90,25 @@ export class Signer { return this.#signer.address; } + /** The underlying Solana Kit signer for instruction builders. */ + public get transactionSigner(): TransactionPartialSigner { + return this.#signer; + } + public async signTransaction( - transaction: Base64EncodedBytes + transaction: Base64EncodedBytes | Base64EncodedWireTransaction + ): Promise { + return Signer.signTransactionWith([this], transaction); + } + + public static async signTransactionWith( + signers: readonly Signer[], + transaction: Base64EncodedBytes | Base64EncodedWireTransaction ): Promise { - const txBytes = getBase64Codec().encode(transaction); + const txBytes = getBase64Codec().encode(transaction as Base64EncodedBytes); const decodedTx = getTransactionCodec().decode(txBytes); const signedTx = await partiallySignTransactionWithSigners( - [this.#signer], + signers.map((signer) => signer.#signer), decodedTx ); return getBase64EncodedWireTransaction(signedTx); diff --git a/src/lib/SolanaRpc.test.ts b/src/lib/SolanaRpc.test.ts new file mode 100644 index 0000000..1f1457b --- /dev/null +++ b/src/lib/SolanaRpc.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from "bun:test"; +import type { + Base64EncodedWireTransaction, + Blockhash, + GetAccountInfoApi, + GetLatestBlockhashApi, + GetMultipleAccountsApi, + GetTokenAccountsByOwnerApi, + Rpc, + SendTransactionApi, + SimulateTransactionApi, +} from "@solana/kit"; + +import { SolanaRpc } from "./SolanaRpc.ts"; + +const signedTransaction = "c2lnbmVk" as Base64EncodedWireTransaction; +type SolanaRpcApi = GetAccountInfoApi & + GetLatestBlockhashApi & + GetMultipleAccountsApi & + GetTokenAccountsByOwnerApi & + SendTransactionApi & + SimulateTransactionApi; + +test("simulates a signed transaction with signature verification", async () => { + let args: unknown[] | undefined; + const rpc = new SolanaRpc({ + rpcClient: { + simulateTransaction: (...params: unknown[]) => { + args = params; + return { send: async () => ({ value: { err: null } }) }; + }, + } as unknown as Rpc, + }); + + const result = await rpc.simulate(signedTransaction); + + expect(args).toEqual(["c2lnbmVk", { encoding: "base64", sigVerify: true }]); + expect(result).toEqual({ + serializedTransaction: signedTransaction, + success: true, + }); +}); + +test("sends a signed transaction through the configured RPC", async () => { + let args: unknown[] | undefined; + const rpc = new SolanaRpc({ + rpcClient: { + sendTransaction: (...params: unknown[]) => { + args = params; + return { send: async () => "signature" }; + }, + } as unknown as Rpc, + }); + + await expect(rpc.send(signedTransaction)).resolves.toBe("signature"); + expect(args).toEqual(["c2lnbmVk", { encoding: "base64" }]); +}); + +test("gets the latest blockhash through Solana Kit's RPC method", async () => { + const blockhash = "11111111111111111111111111111111" as Blockhash; + let commitment: string | undefined; + const rpcClient = { + getLatestBlockhash: (config?: { commitment?: string }) => { + commitment = config?.commitment; + return { + send: async () => ({ + context: { slot: 1n }, + value: { blockhash, lastValidBlockHeight: 123n }, + }), + }; + }, + } as unknown as Rpc; + const rpc = new SolanaRpc({ rpcClient }); + + await expect(rpc.getLatestBlockhash()).resolves.toEqual({ + blockhash, + lastValidBlockHeight: 123n, + }); + expect(commitment).toBe("confirmed"); +}); diff --git a/src/lib/SolanaRpc.ts b/src/lib/SolanaRpc.ts new file mode 100644 index 0000000..4c74dd7 --- /dev/null +++ b/src/lib/SolanaRpc.ts @@ -0,0 +1,235 @@ +import type { + Address, + Base64EncodedBytes, + Base64EncodedWireTransaction, + BlockhashLifetimeConstraint, + GetAccountInfoApi, + GetLatestBlockhashApi, + GetMultipleAccountsApi, + GetTokenAccountsByOwnerApi, + Rpc, + SendTransactionApi, + SimulateTransactionApi, +} from "@solana/kit"; +import { + address, + createSolanaRpc, + parseBase64RpcAccount, + unwrapOption, +} from "@solana/kit"; +import { + decodeToken as decodeToken2022, + fetchMint, + TOKEN_2022_PROGRAM_ADDRESS, +} from "@solana-program/token-2022"; +import { decodeToken, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; + +import { Config } from "./Config.ts"; +import { Signer } from "./Signer.ts"; + +type SimulationResult = { + serializedTransaction: Base64EncodedWireTransaction; + success: boolean; + error?: string; +}; + +export type ClosableTokenAccount = { + address: Address; + amount: bigint; + isFrozen: boolean; + lamports: bigint; + mint: Address; + owner: Address; +}; + +type SolanaRpcOptions = { + rpcClient?: Rpc< + GetAccountInfoApi & + GetLatestBlockhashApi & + GetMultipleAccountsApi & + GetTokenAccountsByOwnerApi & + SendTransactionApi & + SimulateTransactionApi + >; + url?: string; +}; + +export class SolanaRpc { + readonly #rpcClient: Rpc< + GetAccountInfoApi & + GetLatestBlockhashApi & + GetMultipleAccountsApi & + GetTokenAccountsByOwnerApi & + SendTransactionApi & + SimulateTransactionApi + >; + readonly #mints = new Map>(); + readonly #scaledUiMultipliers = new Map< + string, + Promise + >(); + + public constructor(options: SolanaRpcOptions = {}) { + this.#rpcClient = + options.rpcClient ?? createSolanaRpc(options.url ?? Config.getRpcUrl()); + } + + public getScaledUiMultiplier(mint: string): Promise { + const cached = this.#scaledUiMultipliers.get(mint); + if (cached) { + return cached; + } + + const multiplier = this.#fetchScaledUiMultiplier(address(mint)); + this.#scaledUiMultipliers.set(mint, multiplier); + return multiplier; + } + + public async getTokenProgram(mint: string): Promise
{ + const account = await this.#fetchMint(address(mint)); + if ( + account.programAddress !== TOKEN_PROGRAM_ADDRESS && + account.programAddress !== TOKEN_2022_PROGRAM_ADDRESS + ) { + throw new Error(`Mint ${mint} is not owned by an SPL Token program.`); + } + return account.programAddress; + } + + public async getTokenPrograms( + mints: readonly string[] + ): Promise> { + const response = await this.#rpcClient + .getMultipleAccounts(mints.map(address)) + .send(); + const tokenPrograms = new Map(); + + for (const [index, account] of response.value.entries()) { + const mint = mints[index]!; + if ( + account?.owner !== TOKEN_PROGRAM_ADDRESS && + account?.owner !== TOKEN_2022_PROGRAM_ADDRESS + ) { + throw new Error(`Mint ${mint} is not owned by an SPL Token program.`); + } + tokenPrograms.set(mint, account.owner); + } + + return tokenPrograms; + } + + public async getTokenAccountsByOwner( + owner: Address, + tokenProgram: Address + ): Promise { + const response = await this.#rpcClient + .getTokenAccountsByOwner( + owner, + { programId: tokenProgram }, + { encoding: "base64" } + ) + .send(); + + return response.value.map(({ account, pubkey }) => { + const decoded = + tokenProgram === TOKEN_2022_PROGRAM_ADDRESS + ? decodeToken2022(parseBase64RpcAccount(pubkey, account)) + : decodeToken(parseBase64RpcAccount(pubkey, account)); + return { + address: pubkey, + amount: decoded.data.amount, + isFrozen: decoded.data.state === 2, + lamports: decoded.lamports, + mint: decoded.data.mint, + owner: decoded.data.owner, + }; + }); + } + + public async getLatestBlockhash(): Promise { + const { value } = await this.#rpcClient + .getLatestBlockhash({ commitment: "confirmed" }) + .send(); + return value; + } + + async #fetchScaledUiMultiplier(mint: Address): Promise { + const account = await this.#fetchMint(mint); + if (account.programAddress !== TOKEN_2022_PROGRAM_ADDRESS) { + return undefined; + } + + const config = unwrapOption(account.data.extensions)?.find( + (extension) => extension.__kind === "ScaledUiAmountConfig" + ); + if (!config || config.__kind !== "ScaledUiAmountConfig") { + return undefined; + } + + const timestamp = BigInt(Math.floor(Date.now() / 1_000)); + return timestamp >= config.newMultiplierEffectiveTimestamp + ? config.newMultiplier + : config.multiplier; + } + + #fetchMint(mint: Address): ReturnType { + const cached = this.#mints.get(mint); + if (cached) { + return cached; + } + + const account = fetchMint(this.#rpcClient, mint); + this.#mints.set(mint, account); + return account; + } + + public async simulate( + serializedTransaction: Base64EncodedWireTransaction + ): Promise { + const { value } = await this.#rpcClient + .simulateTransaction(serializedTransaction, { + encoding: "base64", + sigVerify: true, + }) + .send(); + const error = this.#formatSimulationError(value.err); + + return { + serializedTransaction, + success: error === undefined, + ...(error ? { error } : {}), + }; + } + + public async send( + serializedTransaction: Base64EncodedWireTransaction + ): Promise { + return this.#rpcClient + .sendTransaction(serializedTransaction, { encoding: "base64" }) + .send(); + } + + public async signAndSimulate( + signer: Signer, + transaction: Base64EncodedBytes + ): Promise { + return this.simulate(await signer.signTransaction(transaction)); + } + + public async signAndSend( + signer: Signer, + transaction: Base64EncodedBytes + ): Promise { + return this.send(await signer.signTransaction(transaction)); + } + + #formatSimulationError(error: unknown): string | undefined { + if (error === null || error === undefined) { + return undefined; + } + if (typeof error === "string") { + return error; + } + return JSON.stringify(error); + } +} diff --git a/src/lib/SolanaTransactions.test.ts b/src/lib/SolanaTransactions.test.ts new file mode 100644 index 0000000..000832d --- /dev/null +++ b/src/lib/SolanaTransactions.test.ts @@ -0,0 +1,94 @@ +import { + address, + getBase64Codec, + getTransactionCodec, + type Blockhash, +} from "@solana/kit"; +import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; +import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022"; +import { describe, expect, test } from "bun:test"; + +import type { SolanaRpc } from "./SolanaRpc.ts"; +import { Signer } from "./Signer.ts"; +import { + buildCloseTokenAccountsTransaction, + buildSolTransferTransaction, + buildTokenTransferTransaction, +} from "./SolanaTransactions.ts"; + +const RECIPIENT = address("7YttLkHdoCtAnBPD22dAvHaLLtH9FJ4XhJvR8H4zhTnx"); +const USDC = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + +const rpc = { + getLatestBlockhash: async () => ({ + blockhash: "11111111111111111111111111111111" as Blockhash, + lastValidBlockHeight: 1n, + }), +} as SolanaRpc; + +function expectSenderSignature(transaction: string, sender: string): void { + const decoded = getTransactionCodec().decode( + getBase64Codec().encode(transaction) + ); + expect(decoded.signatures[address(sender)]).toBeNull(); +} + +describe("native spot transactions", () => { + test("builds a SOL transfer requiring the sender signature", async () => { + const signer = await Signer.generate(); + expectSenderSignature( + await buildSolTransferTransaction( + rpc, + signer.transactionSigner, + RECIPIENT, + 1_000n + ), + signer.address + ); + }); + + test("builds a checked token transfer with idempotent recipient ATA creation", async () => { + const signer = await Signer.generate(); + expectSenderSignature( + await buildTokenTransferTransaction({ + amount: 1_000_000n, + decimals: 6, + mint: USDC, + recipient: RECIPIENT, + rpc, + sender: signer.transactionSigner, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }), + signer.address + ); + }); + + test("builds a batched token-account close instruction requiring the owner signature", async () => { + const signer = await Signer.generate(); + expectSenderSignature( + await buildCloseTokenAccountsTransaction({ + accounts: [ + address("9xQeWvG816bUx9EPfEZ3rJCeU5B7T6Zpw6wS2pFvZp7B"), + address("Vote111111111111111111111111111111111111111"), + ], + owner: signer.transactionSigner, + rpc, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }), + signer.address + ); + }); + + test("builds a separate Token-2022 batch close instruction", async () => { + const signer = await Signer.generate(); + expectSenderSignature( + await buildCloseTokenAccountsTransaction({ + accounts: [address("9xQeWvG816bUx9EPfEZ3rJCeU5B7T6Zpw6wS2pFvZp7B")], + owner: signer.transactionSigner, + rpc, + tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + }), + signer.address + ); + }); +}); diff --git a/src/lib/SolanaTransactions.ts b/src/lib/SolanaTransactions.ts new file mode 100644 index 0000000..f1f0926 --- /dev/null +++ b/src/lib/SolanaTransactions.ts @@ -0,0 +1,119 @@ +import { + appendTransactionMessageInstructions, + compileTransaction, + createTransactionMessage, + getBase64EncodedWireTransaction, + pipe, + setTransactionMessageFeePayer, + setTransactionMessageLifetimeUsingBlockhash, + type Address, + type Base64EncodedWireTransaction, + type Instruction, + type TransactionSigner, +} from "@solana/kit"; +import { getTransferSolInstruction } from "@solana-program/system"; +import { + findAssociatedTokenPda, + getBatchInstruction, + getCloseAccountInstruction, + getCreateAssociatedTokenIdempotentInstruction, + getTransferCheckedInstruction, +} from "@solana-program/token"; + +import type { SolanaRpc } from "./SolanaRpc.ts"; + +async function compileInstructions( + rpc: SolanaRpc, + feePayer: Address, + instructions: Instruction[] +): Promise { + const latestBlockhash = await rpc.getLatestBlockhash(); + const message = pipe( + createTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageFeePayer(feePayer, tx), + (tx) => appendTransactionMessageInstructions(instructions, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx) + ); + return getBase64EncodedWireTransaction(compileTransaction(message)); +} + +export async function buildSolTransferTransaction( + rpc: SolanaRpc, + sender: TransactionSigner, + recipient: Address, + amount: bigint +): Promise { + return compileInstructions(rpc, sender.address, [ + getTransferSolInstruction({ + source: sender, + destination: recipient, + amount, + }), + ]); +} + +export async function buildTokenTransferTransaction(params: { + amount: bigint; + decimals: number; + mint: Address; + recipient: Address; + rpc: SolanaRpc; + sender: TransactionSigner; + tokenProgram: Address; +}): Promise { + const { mint, recipient, rpc, sender, tokenProgram } = params; + const [senderAta] = await findAssociatedTokenPda({ + mint, + owner: sender.address, + tokenProgram, + }); + const [recipientAta] = await findAssociatedTokenPda({ + mint, + owner: recipient, + tokenProgram, + }); + + return compileInstructions(rpc, sender.address, [ + getCreateAssociatedTokenIdempotentInstruction({ + payer: sender, + ata: recipientAta, + owner: recipient, + mint, + tokenProgram, + }), + getTransferCheckedInstruction( + { + source: senderAta, + mint, + destination: recipientAta, + authority: sender, + amount: params.amount, + decimals: params.decimals, + }, + { programAddress: tokenProgram } + ), + ]); +} + +export async function buildCloseTokenAccountsTransaction(params: { + accounts: Address[]; + owner: TransactionSigner; + rpc: SolanaRpc; + tokenProgram: Address; +}): Promise { + const closeInstructions = params.accounts.map((account) => + getCloseAccountInstruction( + { + account, + destination: params.owner.address, + owner: params.owner, + }, + { programAddress: params.tokenProgram } + ) + ); + return compileInstructions(params.rpc, params.owner.address, [ + getBatchInstruction(closeInstructions, { + programAddress: params.tokenProgram, + }), + ]); +} diff --git a/src/lib/Swap.test.ts b/src/lib/Swap.test.ts new file mode 100644 index 0000000..bba5590 --- /dev/null +++ b/src/lib/Swap.test.ts @@ -0,0 +1,94 @@ +import { afterEach, expect, test } from "bun:test"; +import type { Base64EncodedBytes } from "@solana/kit"; +import type { JupiterApi } from "jupiter-api-ts"; + +import type { Token } from "./Asset.ts"; +import { Config } from "./Config.ts"; +import type { Signer } from "./Signer.ts"; +import type { SolanaRpc } from "./SolanaRpc.ts"; +import { Swap } from "./Swap.ts"; + +const inputToken = { + id: "So11111111111111111111111111111111111111112", + symbol: "SOL", + name: "Solana", + decimals: 9, +} as Token; +const outputToken = { + id: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + symbol: "USDC", + name: "USD Coin", + decimals: 6, +} as Token; + +afterEach(() => { + Config.dryRun = false; +}); + +test("executes a Swap V2 order through its execute route", async () => { + const calls: unknown[] = []; + const api = { + swap: { + v2: { + getOrder: async (request: unknown) => { + calls.push(request); + return { + data: { + inAmount: "1000000000", + outAmount: "150000000", + requestId: "request-id", + lastValidBlockHeight: "123", + transaction: "unsigned-transaction", + }, + }; + }, + postExecute: async (request: unknown) => { + calls.push(request); + return { + data: { + status: "Success", + signature: "signature", + inputAmountResult: "1000000000", + outputAmountResult: "150000000", + }, + }; + }, + }, + }, + } as unknown as JupiterApi; + const signer = { + address: "wallet", + signTransaction: async () => "signed-transaction" as Base64EncodedBytes, + } as unknown as Signer; + const rpc = { + getScaledUiMultiplier: async () => undefined, + } as unknown as SolanaRpc; + + const result = await Swap.execute({ + api, + signer, + rpc, + inputToken, + outputToken, + amount: "1", + }); + + expect(calls).toEqual([ + { + query: { + inputMint: inputToken.id, + outputMint: outputToken.id, + amount: "1000000000", + taker: "wallet", + }, + }, + { + body: { + requestId: "request-id", + lastValidBlockHeight: "123", + signedTransaction: "signed-transaction", + }, + }, + ]); + expect(result.result?.signature).toBe("signature"); +}); diff --git a/src/lib/Swap.ts b/src/lib/Swap.ts index 2c27b82..0c6bff6 100644 --- a/src/lib/Swap.ts +++ b/src/lib/Swap.ts @@ -1,24 +1,23 @@ import type { Base64EncodedBytes } from "@solana/kit"; +import type { JupiterApi, JupiterApiTypes } from "jupiter-api-ts"; -import type { Token } from "../clients/DatapiClient.ts"; -import { - UltraClient, - type GetOrderResponse, - type PostExecuteResponse, -} from "../clients/UltraClient.ts"; +import type { Token } from "./Asset.ts"; import { Config } from "./Config.ts"; import { NumberConverter } from "./NumberConverter.ts"; import type { Signer } from "./Signer.ts"; +import type { SolanaRpc } from "./SolanaRpc.ts"; export type SwapResult = { signer: Signer; - order: GetOrderResponse; - result: PostExecuteResponse | null; + order: JupiterApiTypes.GetSwapV2OrderResponse; + result: JupiterApiTypes.PostSwapV2ExecuteResponse | null; inputToken: Token; outputToken: Token; inAmount: string; outAmount: string; networkFeeLamports: number; + serializedTransaction?: string; + simulation?: Awaited>; }; export class Swap { @@ -35,36 +34,42 @@ export class Swap { } public static async execute(opts: { + api: JupiterApi; signer: Signer; inputToken: Token; outputToken: Token; amount?: string; rawAmount?: string; + rpc: SolanaRpc; slippageBps?: string; }): Promise { - const { signer, inputToken, outputToken } = opts; - const inputMultiplier = this.getScaledUiMultiplier(inputToken); - const outputMultiplier = this.getScaledUiMultiplier(outputToken); + const { api, signer, inputToken, outputToken, rpc } = opts; + const [inputMultiplier, outputMultiplier] = await Promise.all([ + rpc.getScaledUiMultiplier(inputToken.id), + rpc.getScaledUiMultiplier(outputToken.id), + ]); - const order = await UltraClient.getOrder({ - inputMint: inputToken.id, - outputMint: outputToken.id, - amount: - opts.rawAmount ?? - NumberConverter.toChainAmount( - opts.amount!, - inputToken.decimals, - inputMultiplier - ), - taker: signer.address, - slippageBps: opts.slippageBps, + const orderResponse = await api.swap.v2.getOrder({ + query: { + inputMint: inputToken.id, + outputMint: outputToken.id, + amount: + opts.rawAmount ?? + NumberConverter.toChainAmount( + opts.amount!, + inputToken.decimals, + inputMultiplier + ), + taker: signer.address, + ...(opts.slippageBps ? { slippageBps: Number(opts.slippageBps) } : {}), + }, }); - - if (order.error) { - throw new Error(order.errorMessage ?? order.error); + if (orderResponse.error || !orderResponse.data) { + throw new Error("Unable to get a swap order."); } - if (!order.transaction) { - throw new Error("No valid routes found."); + const order = orderResponse.data; + if (!order.transaction || !order.inAmount || !order.outAmount) { + throw new Error(order.errorMessage ?? "No valid routes found."); } let networkFeeLamports = 0; @@ -72,18 +77,22 @@ export class Swap { order.prioritizationFeePayer === signer.address && order.prioritizationFeeLamports ) { - networkFeeLamports = order.prioritizationFeeLamports; + networkFeeLamports = Number(order.prioritizationFeeLamports); } if (order.rentFeePayer === signer.address && order.rentFeeLamports) { - networkFeeLamports += order.rentFeeLamports; + networkFeeLamports += Number(order.rentFeeLamports); } if ( order.signatureFeePayer === signer.address && order.signatureFeeLamports ) { - networkFeeLamports += order.signatureFeeLamports; + networkFeeLamports += Number(order.signatureFeeLamports); } + const signedTx = await signer.signTransaction( + order.transaction as Base64EncodedBytes + ); + if (Config.dryRun) { const inAmount = NumberConverter.fromChainAmount( order.inAmount, @@ -104,16 +113,35 @@ export class Swap { inAmount, outAmount, networkFeeLamports, + serializedTransaction: signedTx, + simulation: await rpc.simulate(signedTx), }; } - const signedTx = await signer.signTransaction( - order.transaction as Base64EncodedBytes - ); - const result = await UltraClient.postExecute({ - requestId: order.requestId, - signedTransaction: signedTx, + if (!order.requestId) { + throw new Error("Swap order did not include a request ID."); + } + + const executionResponse = await api.swap.v2.postExecute({ + body: { + requestId: order.requestId, + ...(order.lastValidBlockHeight + ? { lastValidBlockHeight: order.lastValidBlockHeight } + : {}), + signedTransaction: signedTx, + }, }); + if (executionResponse.error || !executionResponse.data) { + throw new Error("Unable to execute the swap."); + } + const result = executionResponse.data; + if ( + result.status === "Failed" || + !result.inputAmountResult || + !result.outputAmountResult + ) { + throw new Error(result.error ?? "Swap execution failed."); + } const inAmount = NumberConverter.fromChainAmount( result.inputAmountResult, @@ -137,15 +165,4 @@ export class Swap { networkFeeLamports, }; } - - public static getScaledUiMultiplier(token: Token): number | undefined { - if (!token.scaledUiConfig) { - return undefined; - } - const isNewMultiplierActive = - new Date() >= new Date(token.scaledUiConfig.newMultiplierEffectiveAt); - return isNewMultiplierActive - ? token.scaledUiConfig.newMultiplier - : token.scaledUiConfig.multiplier; - } }