From eac2a4d2cef96f0b7e1610bfbb2fa79da22ea47a Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Sun, 30 Aug 2026 13:33:18 +0800 Subject: [PATCH 01/32] chore: update oxlint config to ts --- .oxlintrc.json | 10 ---------- oxlint.config.ts | 11 +++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 .oxlintrc.json create mode 100644 oxlint.config.ts 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/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", + }, +}); From 0c8d81cef4567c819c8f1d30edc827f88050785f Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Sun, 30 Aug 2026 13:46:45 +0800 Subject: [PATCH 02/32] chore: replace prettier with oxfmt --- .prettierignore | 1 - .prettierrc | 11 ----------- CLAUDE.md | 4 ++-- bun.lock | 46 +++++++++++++++++++++++++++++++++++++++++++--- oxfmt.config.ts | 13 +++++++++++++ package.json | 6 +++--- 6 files changed, 61 insertions(+), 20 deletions(-) delete mode 100644 .prettierignore delete mode 100644 .prettierrc create mode 100644 oxfmt.config.ts 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..1addcb5 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 @@ -87,7 +87,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/bun.lock b/bun.lock index bbe79eb..7f2312d 100644 --- a/bun.lock +++ b/bun.lock @@ -18,8 +18,8 @@ }, "devDependencies": { "@types/bun": "latest", + "oxfmt": "^0.65.0", "oxlint": "^1.53.0", - "prettier": "^3.8.1", "typescript": "^5.9.3", }, }, @@ -89,6 +89,44 @@ "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.65.0", "", { "os": "android", "cpu": "arm" }, "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.65.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.65.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.65.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.65.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.65.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.65.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q=="], + + "@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=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.65.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA=="], + + "@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=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.65.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.65.0", "", { "os": "none", "cpu": "arm64" }, "sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.65.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.65.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.65.0", "", { "os": "win32", "cpu": "x64" }, "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.53.0", "", { "os": "android", "cpu": "arm" }, "sha512-JC89/jAx4d2zhDIbK8MC4L659FN1WiMXMBkNg7b33KXSkYpUgcbf+0nz7+EPRg+VwWiZVfaoFkNHJ7RXYb5Neg=="], "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.53.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CY+pZfi+uyeU7AwFrEnjsNT+VfxYmKLMuk7bVxArd8f+09hQbJb8f7C7EpvTfNqrCK1J8zZlaYI4LltmEctgbQ=="], @@ -397,12 +435,12 @@ "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=="], + "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=="], + "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=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -411,6 +449,8 @@ "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=="], 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/package.json b/package.json index 8e6a0f8..6ce69ee 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", @@ -46,8 +46,8 @@ }, "devDependencies": { "@types/bun": "latest", + "oxfmt": "^0.65.0", "oxlint": "^1.53.0", - "prettier": "^3.8.1", "typescript": "^5.9.3" } } From 96264590a6120102bfa1e928a7041f8bd9c77653 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 01:15:02 +0800 Subject: [PATCH 03/32] feat: add jupiter-api-ts --- bun.lock | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index 7f2312d..40f4c8e 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^14.0.3", + "jupiter-api-ts": "^0.4.0", "ky": "^1.14.3", "micro-key-producer": "^0.8.5", }, @@ -419,6 +420,8 @@ "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.0", "", {}, "sha512-marIqjN3Zyw4erg2HpLiQbk9f/forMYAmGXcOba6xKHe6kw2m1BYXAa9I2i9bVZsjb0lRVgdzpmwc/WsmQzT2A=="], + "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=="], diff --git a/package.json b/package.json index 6ce69ee..c194da9 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^14.0.3", + "jupiter-api-ts": "^0.4.0", "ky": "^1.14.3", "micro-key-producer": "^0.8.5" }, From 3354c41d92b7f9597f5a1d2813d43a89235aa45b Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 01:17:12 +0800 Subject: [PATCH 04/32] feat: add rpcUrl field in config --- docs/config.md | 12 ++++ src/commands/ConfigCommand.test.ts | 93 ++++++++++++++++++++++++++++++ src/commands/ConfigCommand.ts | 28 ++++++--- src/lib/Config.test.ts | 25 ++++++++ src/lib/Config.ts | 28 ++++++++- 5 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/commands/ConfigCommand.test.ts 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/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/lib/Config.test.ts b/src/lib/Config.test.ts index 6564b48..711f023 100644 --- a/src/lib/Config.test.ts +++ b/src/lib/Config.test.ts @@ -30,6 +30,10 @@ 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("throws a clear error when settings.json is not valid json", () => { writeFileSync( join(TEST_DIR, "settings.json"), @@ -57,6 +61,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..1a5a400 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,30 @@ 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 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 +92,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"); } } From 1a13d9fe05d1cdc23f4bca970e943beff1b24129 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 01:41:24 +0800 Subject: [PATCH 05/32] feat: add SolanaRpc lib --- src/lib/SolanaRpc.test.ts | 53 ++++++++++++++++++ src/lib/SolanaRpc.ts | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/lib/SolanaRpc.test.ts create mode 100644 src/lib/SolanaRpc.ts diff --git a/src/lib/SolanaRpc.test.ts b/src/lib/SolanaRpc.test.ts new file mode 100644 index 0000000..636e6ec --- /dev/null +++ b/src/lib/SolanaRpc.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from "bun:test"; +import type { Base64EncodedWireTransaction } from "@solana/kit"; + +import { SolanaRpc } from "./SolanaRpc.ts"; + +const signedTransaction = "c2lnbmVk" as Base64EncodedWireTransaction; + +test("simulates a signed transaction with signature verification", async () => { + let body: unknown; + const rpc = new SolanaRpc({ + fetch: (async (_input, init) => { + body = JSON.parse(String(init?.body)); + return Response.json({ + jsonrpc: "2.0", + id: 1, + result: { value: { err: null } }, + }); + }) as typeof fetch, + url: "https://rpc.example.com", + }); + + const result = await rpc.simulate(signedTransaction); + + expect(body).toEqual({ + jsonrpc: "2.0", + id: 1, + method: "simulateTransaction", + params: ["c2lnbmVk", { encoding: "base64", sigVerify: true }], + }); + expect(result).toEqual({ + serializedTransaction: signedTransaction, + success: true, + }); +}); + +test("sends a signed transaction through the configured RPC", async () => { + let body: unknown; + const rpc = new SolanaRpc({ + fetch: (async (_input, init) => { + body = JSON.parse(String(init?.body)); + return Response.json({ jsonrpc: "2.0", id: 1, result: "signature" }); + }) as typeof fetch, + url: "https://rpc.example.com", + }); + + await expect(rpc.send(signedTransaction)).resolves.toBe("signature"); + expect(body).toEqual({ + jsonrpc: "2.0", + id: 1, + method: "sendTransaction", + params: ["c2lnbmVk", { encoding: "base64" }], + }); +}); diff --git a/src/lib/SolanaRpc.ts b/src/lib/SolanaRpc.ts new file mode 100644 index 0000000..1f08400 --- /dev/null +++ b/src/lib/SolanaRpc.ts @@ -0,0 +1,114 @@ +import type { + Base64EncodedBytes, + Base64EncodedWireTransaction, +} from "@solana/kit"; + +import { Config } from "./Config.ts"; +import { Signer } from "./Signer.ts"; + +type RpcError = { + message?: string; +}; + +type RpcResponse = { + result?: T; + error?: RpcError; +}; + +type SimulationValue = { + err: unknown; +}; + +type SimulationResult = { + serializedTransaction: Base64EncodedWireTransaction; + success: boolean; + error?: string; +}; + +type SolanaRpcOptions = { + fetch?: typeof fetch; + url?: string; +}; + +export class SolanaRpc { + readonly #fetch: typeof fetch; + readonly #url: string; + + public constructor(options: SolanaRpcOptions = {}) { + this.#fetch = options.fetch ?? fetch; + this.#url = options.url ?? Config.getRpcUrl(); + } + + public async simulate( + serializedTransaction: Base64EncodedWireTransaction + ): Promise { + const result = await this.#request<{ value: SimulationValue }>( + "simulateTransaction", + [serializedTransaction, { encoding: "base64", sigVerify: true }] + ); + const error = this.#formatSimulationError(result.value.err); + + return { + serializedTransaction, + success: error === undefined, + ...(error ? { error } : {}), + }; + } + + public async send( + serializedTransaction: Base64EncodedWireTransaction + ): Promise { + return this.#request("sendTransaction", [ + serializedTransaction, + { + encoding: "base64", + }, + ]); + } + + 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)); + } + + async #request(method: string, params: unknown[]): Promise { + const response = await this.#fetch(this.#url, { + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + headers: { "content-type": "application/json" }, + method: "POST", + }); + const payload = (await response.json()) as RpcResponse; + + if (!response.ok) { + throw new Error(`RPC ${method} failed with HTTP ${response.status}.`); + } + if (payload.error) { + throw new Error( + `RPC ${method} failed: ${payload.error.message ?? "unknown error"}` + ); + } + if (payload.result === undefined) { + throw new Error(`RPC ${method} returned no result.`); + } + return payload.result; + } + + #formatSimulationError(error: unknown): string | undefined { + if (error === null || error === undefined) { + return undefined; + } + if (typeof error === "string") { + return error; + } + return JSON.stringify(error); + } +} From 1d3eb40480c6d33af3de116820c7affca1c49bf3 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 01:41:47 +0800 Subject: [PATCH 06/32] feat: add dry-run flag --- src/commands/LendCommand.ts | 12 ++++++++++-- src/commands/PerpsCommand.ts | 18 +++++++++++++++--- src/commands/PredictionsCommand.ts | 12 ++++++++++-- src/commands/SpotCommand.ts | 18 +++++++++++++++--- src/commands/VrfdCommand.ts | 6 +++++- src/index.ts | 5 ++++- src/lib/Config.test.ts | 6 ++++++ src/lib/Config.ts | 4 ++++ 8 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/commands/LendCommand.ts b/src/commands/LendCommand.ts index d5fd99b..303fc33 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -40,7 +40,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,7 +58,11 @@ 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); + }); } private static async tokens(): Promise { diff --git a/src/commands/PerpsCommand.ts b/src/commands/PerpsCommand.ts index 313872c..480c88d 100644 --- a/src/commands/PerpsCommand.ts +++ b/src/commands/PerpsCommand.ts @@ -41,7 +41,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 +55,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 +73,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") diff --git a/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index 201b599..8bd8a1a 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -59,7 +59,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") @@ -68,7 +72,11 @@ export class PredictionsCommand { "Position pubkey to close, or 'all'" ) .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") diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index ab28856..8cdcc19 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -60,7 +60,11 @@ export class SpotCommand { "--slippage ", "Max slippage in basis points (leave empty to use Jupiter's Real-Time Slippage Estimation)" ) - .action((opts) => this.swap(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.swap(opts); + }); spot .command("portfolio") .description("Show spot portfolio for a wallet") @@ -95,13 +99,21 @@ export class SpotCommand { "Amount in on-chain units (no decimal conversion)" ) .option("--key ", "Key to use for signing") - .action((opts) => this.transfer(opts)); + .option("--dry-run", "Preview without submitting the transaction") + .action((opts) => { + Config.dryRun = Config.resolveDryRun(opts.dryRun); + return this.transfer(opts); + }); spot .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: { diff --git a/src/commands/VrfdCommand.ts b/src/commands/VrfdCommand.ts index 65fa08b..e04c42b 100644 --- a/src/commands/VrfdCommand.ts +++ b/src/commands/VrfdCommand.ts @@ -58,7 +58,11 @@ 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 { diff --git a/src/index.ts b/src/index.ts index 107ba33..ba2c320 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,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) { diff --git a/src/lib/Config.test.ts b/src/lib/Config.test.ts index 711f023..7fe7777 100644 --- a/src/lib/Config.test.ts +++ b/src/lib/Config.test.ts @@ -34,6 +34,12 @@ describe("Config.load", () => { 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"), diff --git a/src/lib/Config.ts b/src/lib/Config.ts index 1a5a400..fa72151 100644 --- a/src/lib/Config.ts +++ b/src/lib/Config.ts @@ -67,6 +67,10 @@ export class Config { 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."); From e38c1380f49b49bfeec7d86e07766f0e6e3878e1 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 01:42:21 +0800 Subject: [PATCH 07/32] feat: add JupiterApi lib --- src/index.ts | 2 ++ src/lib/JupiterApi.ts | 57 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/lib/JupiterApi.ts diff --git a/src/index.ts b/src/index.ts index ba2c320..4f3f8e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,9 +13,11 @@ 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"; Config.init(); +const jupiterClients = createJupiterClients(); const program = new Command(); program 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; +} From 995c432df21358f505088f3db88f53debe120576 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 01:56:17 +0800 Subject: [PATCH 08/32] refactor: replace asset helpers using JupiterDatapi --- src/lib/Asset.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/lib/Asset.ts b/src/lib/Asset.ts index dceb3fd..9326808 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -1,6 +1,6 @@ import { address, isAddress } from "@solana/kit"; +import type { JupiterDatapi, JupiterDatapiTypes } from "jupiter-api-ts"; -import { DatapiClient, type Token } from "../clients/DatapiClient.ts"; import { UltraClient } from "../clients/UltraClient.ts"; type AssetInfo = { @@ -29,6 +29,61 @@ export const Asset = { export type Asset = (typeof Asset)[keyof typeof Asset]; +export type Token = JupiterDatapiTypes.Asset & { decimals: number }; + +export async function searchTokens(params: { + datapi: JupiterDatapi; + query: string; + limit?: number; +}): Promise { + const { datapi, ...query } = params; + const response = await datapi.searchAssets({ query }); + if (response.error || !response.data) { + throw new Error("Data API token search failed."); + } + return response.data.filter( + (asset): asset is Token => typeof asset.decimals === "number" + ); +} + +export async function getTokensByMints( + datapi: JupiterDatapi, + mints: string[] +): Promise { + if (mints.length === 0) { + return []; + } + 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(); +} + +export async function resolveToken( + datapi: JupiterDatapi, + input: string +): Promise { + const [token] = await searchTokens({ datapi, query: input, limit: 1 }); + if (!token) { + throw new Error(`Token not found: ${input}`); + } + return token; +} + export function resolveAsset(name: string): (typeof Asset)[keyof typeof Asset] { const key = name.toUpperCase(); const asset = Asset[key as keyof typeof Asset]; @@ -39,17 +94,18 @@ export function resolveAsset(name: string): (typeof Asset)[keyof typeof Asset] { } export async function resolveWalletAsset( + datapi: JupiterDatapi, walletAddress: string, asset: string ): Promise { if (isAddress(asset)) { - return DatapiClient.resolveToken(asset); + return resolveToken(datapi, asset); } 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); + return resolveToken(datapi, Asset[key as keyof typeof Asset].id); } const holdings = await UltraClient.getHoldings(walletAddress); @@ -57,7 +113,7 @@ export async function resolveWalletAsset( if (BigInt(holdings.amount) > 0n && !mints.includes(Asset.SOL.id)) { mints.push(Asset.SOL.id); } - const tokens = await DatapiClient.getTokensByMints(mints); + const tokens = await getTokensByMints(datapi, mints); const query = asset.toLowerCase(); const matches = tokens.filter((t) => t.symbol.toLowerCase() === query); From 66594a267de4e0856cf61a325a356ff3b5cec82f Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 02:21:02 +0800 Subject: [PATCH 09/32] feat: add @solana-program/token-2022 --- bun.lock | 17 +++++++++++++++++ package.json | 1 + 2 files changed, 18 insertions(+) diff --git a/bun.lock b/bun.lock index 40f4c8e..0005302 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", "@solana-program/token": "^0.12.0", + "@solana-program/token-2022": "^0.16.0", "@solana/keychain": "^1.1.0", "@solana/kit": "^6.7.0", "chalk": "^5.6.2", @@ -252,10 +253,16 @@ "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + "@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=="], + "@solana-program/system": ["@solana-program/system@0.12.0", "", { "peerDependencies": { "@solana/kit": "^6.1.0" } }, "sha512-ZnAAWeGVMWNtJhw3GdifI2HnhZ0A0H0qs8tBkcFvxp/8wIavvO+GOM4Jd0N22u2+Lni2zcwvcrxrsxj6Mjphng=="], "@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-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=="], + + "@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=="], + "@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/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=="], @@ -468,12 +475,22 @@ "@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/record/@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], + + "@solana-program/token-2022/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@solana-program/token-2022/@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], + + "@solana-program/zk-elgamal-proof/@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], + "@solana/rpc-transport-http/undici-types": ["undici-types@7.22.0", "", {}, "sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw=="], "@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=="], "@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-program/token-2022/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@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=="], "@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=="], diff --git a/package.json b/package.json index c194da9..00d3759 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", "@solana-program/token": "^0.12.0", + "@solana-program/token-2022": "^0.16.0", "@solana/keychain": "^1.1.0", "@solana/kit": "^6.7.0", "chalk": "^5.6.2", From 23938225b0edf37d8fef5ad9a5e4583a08f8a7e1 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 02:29:52 +0800 Subject: [PATCH 10/32] feat: add getScaledUiMultiplier method to SolanaRpc --- src/lib/SolanaRpc.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/lib/SolanaRpc.ts b/src/lib/SolanaRpc.ts index 1f08400..816e647 100644 --- a/src/lib/SolanaRpc.ts +++ b/src/lib/SolanaRpc.ts @@ -1,7 +1,15 @@ import type { + Address, Base64EncodedBytes, Base64EncodedWireTransaction, + GetAccountInfoApi, + Rpc, } from "@solana/kit"; +import { address, createSolanaRpc, unwrapOption } from "@solana/kit"; +import { + fetchMint, + TOKEN_2022_PROGRAM_ADDRESS, +} from "@solana-program/token-2022"; import { Config } from "./Config.ts"; import { Signer } from "./Signer.ts"; @@ -27,16 +35,53 @@ type SimulationResult = { type SolanaRpcOptions = { fetch?: typeof fetch; + rpcClient?: Rpc; url?: string; }; export class SolanaRpc { readonly #fetch: typeof fetch; + readonly #rpcClient: Rpc; readonly #url: string; + readonly #scaledUiMultipliers = new Map< + string, + Promise + >(); public constructor(options: SolanaRpcOptions = {}) { this.#fetch = options.fetch ?? fetch; this.#url = options.url ?? Config.getRpcUrl(); + this.#rpcClient = options.rpcClient ?? createSolanaRpc(this.#url); + } + + 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; + } + + async #fetchScaledUiMultiplier(mint: Address): Promise { + const account = await fetchMint(this.#rpcClient, 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; } public async simulate( From b8f7b1a63db69d00a6a8f1e2d53a3ce645785802 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 02:39:41 +0800 Subject: [PATCH 11/32] feat: add getTokenProgram method to SolanaRpc --- src/lib/SolanaRpc.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/lib/SolanaRpc.ts b/src/lib/SolanaRpc.ts index 816e647..f8afe3f 100644 --- a/src/lib/SolanaRpc.ts +++ b/src/lib/SolanaRpc.ts @@ -10,6 +10,7 @@ import { fetchMint, TOKEN_2022_PROGRAM_ADDRESS, } from "@solana-program/token-2022"; +import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; import { Config } from "./Config.ts"; import { Signer } from "./Signer.ts"; @@ -43,6 +44,7 @@ export class SolanaRpc { readonly #fetch: typeof fetch; readonly #rpcClient: Rpc; readonly #url: string; + readonly #mints = new Map>(); readonly #scaledUiMultipliers = new Map< string, Promise @@ -65,8 +67,19 @@ export class SolanaRpc { 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; + } + async #fetchScaledUiMultiplier(mint: Address): Promise { - const account = await fetchMint(this.#rpcClient, mint); + const account = await this.#fetchMint(mint); if (account.programAddress !== TOKEN_2022_PROGRAM_ADDRESS) { return undefined; } @@ -84,6 +97,17 @@ export class SolanaRpc { : 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 { From d89bc77c6dc77efc7e1d2bde6297ff22e2fd7c7c Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 02:39:24 +0800 Subject: [PATCH 12/32] refactor: use Datapi client and SolanaRpc --- src/clients/DatapiClient.ts | 188 ----------------------------- src/commands/LendCommand.ts | 25 +++- src/commands/PredictionsCommand.ts | 12 +- src/commands/SpotCommand.ts | 135 +++++++++++++-------- src/index.ts | 8 +- src/lib/Swap.ts | 23 ++-- 6 files changed, 126 insertions(+), 265 deletions(-) delete mode 100644 src/clients/DatapiClient.ts 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/commands/LendCommand.ts b/src/commands/LendCommand.ts index 303fc33..e8a729e 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -1,15 +1,26 @@ import type { Command } from "commander"; +import type { JupiterDatapi } from "jupiter-api-ts"; -import { DatapiClient } from "../clients/DatapiClient.ts"; import { LendClient, type LendToken } from "../clients/LendClient.ts"; +import { resolveToken } 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"; export class LendCommand { - public static register(program: Command): void { + static #datapi: JupiterDatapi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + datapi: JupiterDatapi, + rpc: SolanaRpc + ): void { + this.#datapi = datapi; + this.#rpc = rpc; const lend = program .command("lend") .description("Lending and earning yield"); @@ -232,8 +243,8 @@ export class LendCommand { 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), + resolveToken(this.#datapi, lendToken.assetAddress), + resolveToken(this.#datapi, lendToken.address), ]); const swap = await Swap.execute({ @@ -242,6 +253,7 @@ export class LendCommand { outputToken, amount: opts.amount, rawAmount: opts.rawAmount, + rpc: this.#rpc, }); const apyPct = this.rateToPct(lendToken.totalRate); @@ -369,8 +381,8 @@ export class LendCommand { } const [inputToken, outputToken] = await Promise.all([ - DatapiClient.resolveToken(lendToken.address), - DatapiClient.resolveToken(lendToken.assetAddress), + resolveToken(this.#datapi, lendToken.address), + resolveToken(this.#datapi, lendToken.assetAddress), ]); const swap = await Swap.execute({ @@ -378,6 +390,7 @@ export class LendCommand { inputToken, outputToken, rawAmount, + rpc: this.#rpc, }); const apyPct = this.rateToPct(lendToken.totalRate); diff --git a/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index 8bd8a1a..e3bfe3a 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -1,6 +1,7 @@ import type { Base64EncodedBytes } from "@solana/kit"; import chalk from "chalk"; import type { Command } from "commander"; +import type { JupiterDatapi } from "jupiter-api-ts"; import { PredictionsClient, @@ -15,7 +16,10 @@ import { Output } from "../lib/Output.ts"; import { Signer } from "../lib/Signer.ts"; export class PredictionsCommand { - public static register(program: Command): void { + static #datapi: JupiterDatapi; + + public static register(program: Command, datapi: JupiterDatapi): void { + this.#datapi = datapi; const predictions = program .command("predictions") .description("Prediction markets"); @@ -353,7 +357,11 @@ export class PredictionsCommand { 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 resolveWalletAsset( + this.#datapi, + signer.address, + opts.input + ); const depositAmount = NumberConverter.toChainAmount( opts.amount, token.decimals diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index 8cdcc19..4ef5d50 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -2,26 +2,39 @@ import { findAssociatedTokenPda } from "@solana-program/token"; import { isAddress, type Address, type Base64EncodedBytes } from "@solana/kit"; import chalk from "chalk"; import type { Command } from "commander"; +import type { JupiterDatapi } from "jupiter-api-ts"; -import { - DatapiClient, - type SpotTrade, - type Token, -} from "../clients/DatapiClient.ts"; import { UltraClient, type GetHoldingsResponse, type HoldingsTokenAccount, } from "../clients/UltraClient.ts"; -import { Asset, resolveWalletAsset } from "../lib/Asset.ts"; +import { + Asset, + getTokensByMints, + resolveToken, + resolveWalletAsset, + searchTokens, + type Token, +} 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"; export class SpotCommand { - public static register(program: Command): void { + static #datapi: JupiterDatapi; + static #rpc: SolanaRpc; + + public static register( + program: Command, + datapi: JupiterDatapi, + rpc: SolanaRpc + ): void { + this.#datapi = datapi; + this.#rpc = rpc; const spot = program .command("spot") .description("Swaps, transfers, token and portfolio data"); @@ -124,9 +137,10 @@ export class SpotCommand { throw new Error("--limit must be a number"); } - const tokens = await DatapiClient.getTokensSearch({ + const tokens = await searchTokens({ + datapi: this.#datapi, query: opts.search, - limit: opts.limit, + limit: opts.limit ? Number(opts.limit) : undefined, }); if (tokens.length === 0) { @@ -146,13 +160,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), })), }); } @@ -166,12 +178,13 @@ export class SpotCommand { Swap.validateAmountOpts(opts); const [inputToken, outputToken] = await Promise.all([ - DatapiClient.resolveToken(opts.from), - DatapiClient.resolveToken(opts.to), + resolveToken(this.#datapi, opts.from), + resolveToken(this.#datapi, 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, @@ -258,8 +271,8 @@ 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), + resolveWalletAsset(this.#datapi, signer.address, opts.from), + resolveToken(this.#datapi, opts.to), ]); const swap = await Swap.execute({ @@ -268,6 +281,7 @@ export class SpotCommand { outputToken, amount: opts.amount, rawAmount: opts.rawAmount, + rpc: this.#rpc, slippageBps: opts.slippage, }); @@ -365,7 +379,7 @@ export class SpotCommand { } const tokenMap = new Map(); - const tokens = await DatapiClient.getTokensByMints(allMints); + const tokens = await getTokensByMints(this.#datapi, allMints); for (const token of tokens) { tokenMap.set(token.id, token); } @@ -378,9 +392,6 @@ export class SpotCommand { rawAmount: string; value: number; price: number; - priceChange: number; - isVerified?: boolean | undefined; - scaledUiMultiplier?: number | undefined; reclaimableLamports?: string; }[] = []; @@ -401,8 +412,6 @@ export class SpotCommand { rawAmount: combinedSolLamports.toString(), value: amount * (wsolInfo.usdPrice ?? 0), price: wsolInfo.usdPrice ?? 0, - priceChange: wsolInfo.stats24h?.priceChange ?? 0, - isVerified: wsolInfo.isVerified, }); } @@ -414,7 +423,7 @@ export class SpotCommand { if (!info) { continue; } - const multiplier = Swap.getScaledUiMultiplier(info); + const multiplier = await this.#rpc.getScaledUiMultiplier(info.id); const amount = Number( NumberConverter.fromChainAmount(ata.amount, info.decimals, multiplier) ); @@ -426,9 +435,6 @@ export class SpotCommand { 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, }); } @@ -453,8 +459,6 @@ export class SpotCommand { amount: "Amount", price: "Price", value: "Value", - priceChange: "24h Change", - verified: "Verified", }, rows: outputTokens.map((t) => ({ symbol: t.symbol, @@ -463,8 +467,6 @@ export class SpotCommand { }), price: Output.formatDollar(t.price), value: Output.formatDollar(t.value), - priceChange: Output.formatPercentageChange(t.priceChange), - verified: Output.formatBoolean(t.isVerified), })), }); console.log(`\nTotal Value: ${Output.formatDollar(totalValue)}`); @@ -481,8 +483,12 @@ export class SpotCommand { 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 = await resolveWalletAsset( + this.#datapi, + signer.address, + opts.token + ); + const multiplier = await this.#rpc.getScaledUiMultiplier(token.id); const chainAmount = opts.rawAmount ?? NumberConverter.toChainAmount(opts.amount!, token.decimals, multiplier); @@ -495,15 +501,16 @@ export class SpotCommand { amount: chainAmount, }); } else { + const tokenProgram = await this.#rpc.getTokenProgram(token.id); const [senderTokenAccountAddress] = await findAssociatedTokenPda({ mint: token.id as Address, owner: signer.address as Address, - tokenProgram: token.tokenProgram as Address, + tokenProgram, }); txResponse = await UltraClient.getTransferTokenTx({ mint: token.id, tokenDecimals: token.decimals.toString(), - tokenProgramId: token.tokenProgram, + tokenProgramId: tokenProgram, senderAddress: signer.address as Address, senderTokenAccountAddress, receiverAddress: opts.to as Address, @@ -630,19 +637,45 @@ export class SpotCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); const targetAsset = opts.token - ? await DatapiClient.resolveToken(opts.token) + ? await resolveToken(this.#datapi, 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) { @@ -655,7 +688,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); } @@ -912,10 +945,10 @@ export class SpotCommand { if (!holdingsMints.includes(input)) { throw new Error(`Token ${input} not found in wallet.`); } - return DatapiClient.resolveToken(input); + return resolveToken(this.#datapi, input); } - 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); @@ -939,14 +972,14 @@ export class SpotCommand { return mints; } - 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/index.ts b/src/index.ts index 4f3f8e2..1e797b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,9 +15,11 @@ 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 @@ -44,11 +46,11 @@ program ConfigCommand.register(program); KeysCommand.register(program); -LendCommand.register(program); +LendCommand.register(program, jupiterClients.datapi, solanaRpc); PerpsCommand.register(program); -PredictionsCommand.register(program); +PredictionsCommand.register(program, jupiterClients.datapi); SignCommand.register(program); -SpotCommand.register(program); +SpotCommand.register(program, jupiterClients.datapi, solanaRpc); UpdateCommand.register(program); VrfdCommand.register(program); diff --git a/src/lib/Swap.ts b/src/lib/Swap.ts index 2c27b82..1001e8f 100644 --- a/src/lib/Swap.ts +++ b/src/lib/Swap.ts @@ -1,14 +1,15 @@ import type { Base64EncodedBytes } from "@solana/kit"; -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; @@ -40,11 +41,14 @@ export class Swap { 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 { 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, @@ -137,15 +141,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; - } } From a1215a8fc92fca06596d21034f41e07608196e30 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 02:40:28 +0800 Subject: [PATCH 13/32] docs: update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2d695da..3b17f0d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ jup sign --tx # View your spot portfolio jup spot 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 From 21f789993bd6aeb8d80a0a6d262aacff5834352b Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 03:27:36 +0800 Subject: [PATCH 14/32] refactor: replace handrolled clients with API-backed clients --- src/commands/LendCommand.ts | 277 ++++++++++++----- src/commands/PerpsCommand.ts | 454 ++++++++++++++++++++-------- src/commands/PredictionsCommand.ts | 461 ++++++++++++++++++++++------- src/commands/SpotCommand.ts | 76 +++-- src/commands/VrfdCommand.ts | 127 ++++++-- src/index.ts | 20 +- src/lib/Asset.ts | 17 +- src/lib/Swap.ts | 73 +++-- 8 files changed, 1110 insertions(+), 395 deletions(-) diff --git a/src/commands/LendCommand.ts b/src/commands/LendCommand.ts index e8a729e..8f50621 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -1,26 +1,36 @@ +import type { Base64EncodedBytes } from "@solana/kit"; import type { Command } from "commander"; -import type { JupiterDatapi } from "jupiter-api-ts"; +import type { JupiterApi, JupiterApiTypes } from "jupiter-api-ts"; -import { LendClient, type LendToken } from "../clients/LendClient.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 LendToken = JupiterApiTypes.LendTokenInfo; + +type LendTransactionSubmission = { + serializedTransaction: string; + signature?: string; + simulation?: { + success: boolean; + error?: string; + }; +}; export class LendCommand { - static #datapi: JupiterDatapi; + static #api: JupiterApi; static #rpc: SolanaRpc; public static register( program: Command, - datapi: JupiterDatapi, - rpc: SolanaRpc + rpc: SolanaRpc, + api: JupiterApi ): void { - this.#datapi = datapi; this.#rpc = rpc; + this.#api = api; const lend = program .command("lend") .description("Lending and earning yield"); @@ -77,7 +87,8 @@ export class LendCommand { } 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); @@ -138,6 +149,7 @@ export class LendCommand { address?: string; token?: string; }): Promise { + requireJupiterApiKey(); if (opts.address && opts.key) { throw new Error("Only one of --address or --key can be provided."); } @@ -146,7 +158,7 @@ export class LendCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const allPositions = await LendClient.getPositions(address); + const allPositions = await this.#getPositions(address); let activePositions = allPositions.filter((p) => p.shares !== "0"); if (opts.token) { @@ -167,7 +179,7 @@ export class LendCommand { } const jlAddresses = activePositions.map((p) => p.token.address).join(","); - const earnings = await LendClient.getEarnings({ + const earnings = await this.#getEarnings({ user: address, positions: jlAddresses, }); @@ -235,28 +247,37 @@ 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), - resolveToken(this.#datapi, lendToken.assetAddress), - resolveToken(this.#datapi, lendToken.address), - ]); - - const swap = await Swap.execute({ - signer, - inputToken, - outputToken, - amount: opts.amount, - rawAmount: opts.rawAmount, - rpc: this.#rpc, + 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.post({ + 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()) { @@ -267,11 +288,10 @@ export class LendCommand { 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; } @@ -282,12 +302,23 @@ export class LendCommand { rows: [ { 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; @@ -305,12 +336,11 @@ export class LendCommand { 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; } @@ -320,7 +350,7 @@ export class LendCommand { rows: [ { label: "Deposited", - value: `${swap.inAmount} ${lendToken.asset.symbol} (${Output.formatDollar(swap.order.inUsdValue)})`, + value: `${depositedAmount} ${lendToken.asset.symbol}`, }, { label: "Position", @@ -332,7 +362,7 @@ export class LendCommand { }, { label: "Tx Signature", - value: swap.result!.signature, + value: submission.signature!, }, ], }); @@ -344,21 +374,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 ); @@ -367,33 +396,44 @@ 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.post7({ + 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.post3({ + 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([ - resolveToken(this.#datapi, lendToken.address), - resolveToken(this.#datapi, lendToken.assetAddress), - ]); - - const swap = await Swap.execute({ - signer, - inputToken, - outputToken, - rawAmount, - rpc: this.#rpc, - }); - 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()) { @@ -404,11 +444,10 @@ export class LendCommand { 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; } @@ -419,12 +458,23 @@ export class LendCommand { rows: [ { 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; @@ -442,12 +492,11 @@ export class LendCommand { 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; } @@ -457,7 +506,7 @@ export class LendCommand { rows: [ { label: "Withdrawn", - value: `${swap.outAmount} ${lendToken.asset.symbol} (${Output.formatDollar(swap.order.outUsdValue)})`, + value: `${withdrawnAmount} ${lendToken.asset.symbol}`, }, { label: "Position", @@ -469,7 +518,7 @@ export class LendCommand { }, { label: "Tx Signature", - value: swap.result!.signature, + value: submission.signature!, }, ], }); @@ -479,7 +528,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 ); @@ -516,6 +565,78 @@ export class LendCommand { return match; } + static async #getTokens(): Promise { + const response = await this.#api.lend.lend.get(); + 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.get2({ 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.get3({ + 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 rateToPct(rate: string): number { return Number(rate) / 100; } diff --git a/src/commands/PerpsCommand.ts b/src/commands/PerpsCommand.ts index 480c88d..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"); @@ -97,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 normalizeSide(side: string): string { + 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 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"; @@ -120,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; @@ -133,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( @@ -266,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, @@ -330,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 @@ -347,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."); } @@ -379,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; @@ -407,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({ @@ -432,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, @@ -468,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; @@ -510,7 +627,7 @@ export class PerpsCommand { }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); } @@ -542,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."); } @@ -563,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; @@ -582,7 +705,7 @@ export class PerpsCommand { }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); return; @@ -590,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 ); @@ -603,7 +729,8 @@ export class PerpsCommand { action: string; triggerPriceUsd: number; signature: string | null; - transaction?: string; + serializedTransaction?: string; + simulation?: Awaited>; }[] = []; for (const [type, price] of [ @@ -620,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", @@ -635,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", @@ -663,7 +803,8 @@ export class PerpsCommand { triggerPriceUsd: Number(price), signature: result.txid, ...(Config.dryRun && { - transaction: res.serializedTxBase64, + serializedTransaction: result.serializedTransaction, + simulation: result.simulation, }), }); } @@ -683,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})` @@ -718,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", @@ -731,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})`); } @@ -747,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", @@ -760,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})`); } @@ -777,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; } @@ -826,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 ); @@ -837,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, @@ -872,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; @@ -910,7 +1109,7 @@ export class PerpsCommand { }, ...(!Config.dryRun ? [{ label: "Tx Signature", value: result.txid! }] - : []), + : this.dryRunRows(result)), ], }); } @@ -960,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 e3bfe3a..e937b22 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -1,25 +1,39 @@ import type { Base64EncodedBytes } from "@solana/kit"; import chalk from "chalk"; import type { Command } from "commander"; -import type { JupiterDatapi } from "jupiter-api-ts"; - -import { - PredictionsClient, - type ClaimPositionResponse, - type CreateOrderResponse, - type PredictionEvent, -} from "../clients/PredictionsClient.ts"; +import type { + JupiterApi, + JupiterApiTypes, + JupiterDatapi, +} from "jupiter-api-ts"; import { resolveWalletAsset } 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 { static #datapi: JupiterDatapi; - - public static register(program: Command, datapi: JupiterDatapi): void { + 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 predictions = program .command("predictions") .description("Prediction markets"); @@ -75,6 +89,11 @@ 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") .option("--dry-run", "Preview without submitting the transaction") .action((opts) => { @@ -100,6 +119,7 @@ export class PredictionsCommand { offset: string; limit: string; }): Promise { + requireJupiterApiKey(); const hasListingOpts = opts.filter || opts.sort !== "volume24h" || @@ -116,22 +136,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.get3({ + 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.get({ + 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); @@ -139,42 +164,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.get2({ + 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, @@ -237,14 +268,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: @@ -254,6 +285,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") { @@ -270,6 +351,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."); } @@ -279,28 +361,42 @@ export class PredictionsCommand { let data; if (opts.position) { - const p = await PredictionsClient.getPosition(opts.position); - data = [p]; + const response = await this.#api.prediction.get14({ + 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.get13({ + 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), })); @@ -339,7 +435,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, }; @@ -354,10 +450,12 @@ 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( + this.#api, this.#datapi, signer.address, opts.input @@ -367,16 +465,22 @@ export class PredictionsCommand { 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.post({ + 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); @@ -400,7 +504,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; } @@ -427,7 +534,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, + }, + ] + : []), + ]), ], }); } @@ -438,6 +562,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."); } @@ -450,11 +575,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.get15({ + 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(), @@ -522,7 +649,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 && @@ -533,25 +660,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.post2({ + 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.delete({ + 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."); @@ -561,20 +733,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, + } + : {}), }); } @@ -594,10 +772,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; @@ -616,15 +798,25 @@ export class PredictionsCommand { } // Single position - const position = await PredictionsClient.getPosition(opts.position); + const positionResponse = await this.#api.prediction.get14({ + 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.post3({ + 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( @@ -635,14 +827,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; } @@ -654,8 +849,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"), @@ -664,15 +865,36 @@ 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.delete2({ + 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); @@ -683,15 +905,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; } @@ -703,8 +928,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"), @@ -714,7 +945,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, + }, + ] + : []), + ]), ], }); } @@ -728,6 +976,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/SpotCommand.ts b/src/commands/SpotCommand.ts index 4ef5d50..eda4d4d 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -2,7 +2,11 @@ import { findAssociatedTokenPda } from "@solana-program/token"; import { isAddress, type Address, type Base64EncodedBytes } from "@solana/kit"; import chalk from "chalk"; import type { Command } from "commander"; -import type { JupiterDatapi } from "jupiter-api-ts"; +import type { + JupiterApi, + JupiterDatapi, + JupiterApiTypes, +} from "jupiter-api-ts"; import { UltraClient, @@ -18,6 +22,7 @@ import { type Token, } 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"; @@ -26,15 +31,18 @@ import { Swap } from "../lib/Swap.ts"; export class SpotCommand { static #datapi: JupiterDatapi; + static #api: JupiterApi; static #rpc: SolanaRpc; public static register( program: Command, datapi: JupiterDatapi, - rpc: SolanaRpc + 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"); @@ -175,6 +183,7 @@ export class SpotCommand { amount?: string; rawAmount?: string; }): Promise { + requireJupiterApiKey(); Swap.validateAmountOpts(opts); const [inputToken, outputToken] = await Promise.all([ @@ -185,20 +194,23 @@ export class SpotCommand { this.#rpc.getScaledUiMultiplier(inputToken.id), this.#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 - ), + const response = await this.#api.ultra.get({ + 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; const inAmount = NumberConverter.fromChainAmount( order.inAmount, @@ -259,6 +271,7 @@ export class SpotCommand { key?: string; slippage?: string; }): Promise { + requireJupiterApiKey(); Swap.validateAmountOpts(opts); if (opts.slippage !== undefined) { @@ -271,12 +284,13 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); const [inputToken, outputToken] = await Promise.all([ - resolveWalletAsset(this.#datapi, signer.address, opts.from), + resolveWalletAsset(this.#api, this.#datapi, signer.address, opts.from), resolveToken(this.#datapi, opts.to), ]); const swap = await Swap.execute({ signer, + api: this.#api, inputToken, outputToken, amount: opts.amount, @@ -312,7 +326,8 @@ export class SpotCommand { priceImpact: swap.order.priceImpact, networkFeeLamports: swap.networkFeeLamports, ...(Config.dryRun && { - transaction: swap.order.transaction, + serializedTransaction: swap.serializedTransaction, + simulation: swap.simulation, }), }); return; @@ -341,8 +356,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 }] + : []), + ]), ], }); } @@ -351,6 +378,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."); } @@ -361,10 +389,14 @@ export class SpotCommand { const SOL_DECIMALS = Asset.SOL.decimals; - const raw = await UltraClient.getHoldings(address); + const response = await this.#api.ultra.get3({ path: { address } }); + if (response.error || !response.data) { + throw new Error("Unable to get spot holdings."); + } + const raw = response.data; // Preprocess: extract only ATA entries, keyed by mint - const ataByMint = new Map(); + const ataByMint = new Map(); for (const [mint, accounts] of Object.entries(raw.tokens)) { const ata = accounts.find((acc) => acc.isAssociatedTokenAccount); if (ata) { @@ -435,7 +467,6 @@ export class SpotCommand { rawAmount: ata.amount, value: amount * (info.usdPrice ?? 0), price: info.usdPrice ?? 0, - reclaimableLamports: ata.reclaimableLamports, }); } @@ -479,11 +510,13 @@ export class SpotCommand { to: string; key?: string; }): Promise { + requireJupiterApiKey(); Swap.validateAmountOpts(opts); const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); const token = await resolveWalletAsset( + this.#api, this.#datapi, signer.address, opts.token @@ -765,6 +798,7 @@ export class SpotCommand { key?: string; token?: string; }): Promise { + requireJupiterApiKey(); const signer = await Signer.load(opts.key ?? Config.load().activeKey); const holdings = await UltraClient.getHoldings(signer.address); diff --git a/src/commands/VrfdCommand.ts b/src/commands/VrfdCommand.ts index e04c42b..c8fb151 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") @@ -66,10 +81,17 @@ export class VrfdCommand { } 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.get({ + 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); @@ -114,15 +136,27 @@ 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.post({ + 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: { @@ -150,6 +184,7 @@ export class VrfdCommand { metaUseCoingeckoCoinId?: boolean; metaUseCirculatingSupplyUrl?: boolean; }): Promise { + requireJupiterApiKey(); const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); @@ -158,7 +193,13 @@ export class VrfdCommand { } // Check eligibility before crafting transaction - const eligibility = await VrfdClient.checkEligibility(opts.token); + const eligibilityResponse = await this.#api.tokens.v2.verification.get({ + 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."); } @@ -184,7 +225,13 @@ export class VrfdCommand { } // Craft the payment transaction - const craftResult = await VrfdClient.craftTxn(signer.address); + const craftResponse = await this.#api.tokens.v2.verification.get2({ + 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); } @@ -207,8 +254,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()) { @@ -216,15 +263,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; } @@ -250,20 +300,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 1e797b3..e55b612 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,13 +46,23 @@ program ConfigCommand.register(program); KeysCommand.register(program); -LendCommand.register(program, jupiterClients.datapi, solanaRpc); -PerpsCommand.register(program); -PredictionsCommand.register(program, jupiterClients.datapi); +LendCommand.register(program, solanaRpc, jupiterClients.api); +PerpsCommand.register(program, jupiterClients.perps, solanaRpc); +PredictionsCommand.register( + program, + jupiterClients.datapi, + solanaRpc, + jupiterClients.api +); SignCommand.register(program); -SpotCommand.register(program, jupiterClients.datapi, solanaRpc); +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.ts b/src/lib/Asset.ts index 9326808..ef59242 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -1,7 +1,9 @@ import { address, isAddress } from "@solana/kit"; -import type { JupiterDatapi, JupiterDatapiTypes } from "jupiter-api-ts"; - -import { UltraClient } from "../clients/UltraClient.ts"; +import type { + JupiterApi, + JupiterDatapi, + JupiterDatapiTypes, +} from "jupiter-api-ts"; type AssetInfo = { readonly id: ReturnType; @@ -94,6 +96,7 @@ export function resolveAsset(name: string): (typeof Asset)[keyof typeof Asset] { } export async function resolveWalletAsset( + api: JupiterApi, datapi: JupiterDatapi, walletAddress: string, asset: string @@ -108,7 +111,13 @@ export async function resolveWalletAsset( return resolveToken(datapi, Asset[key as keyof typeof Asset].id); } - const holdings = await UltraClient.getHoldings(walletAddress); + const response = await api.ultra.get3({ + path: { address: walletAddress }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get wallet holdings."); + } + const holdings = response.data; const mints = Object.keys(holdings.tokens); if (BigInt(holdings.amount) > 0n && !mints.includes(Asset.SOL.id)) { mints.push(Asset.SOL.id); diff --git a/src/lib/Swap.ts b/src/lib/Swap.ts index 1001e8f..f5857c1 100644 --- a/src/lib/Swap.ts +++ b/src/lib/Swap.ts @@ -1,10 +1,6 @@ import type { Base64EncodedBytes } from "@solana/kit"; +import type { JupiterApi, JupiterApiTypes } from "jupiter-api-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"; @@ -13,13 +9,15 @@ import type { SolanaRpc } from "./SolanaRpc.ts"; export type SwapResult = { signer: Signer; - order: GetOrderResponse; - result: PostExecuteResponse | null; + order: JupiterApiTypes.GetUltraV1OrderResponse; + result: JupiterApiTypes.PostUltraV1ExecuteResponse | null; inputToken: Token; outputToken: Token; inAmount: string; outAmount: string; networkFeeLamports: number; + serializedTransaction?: string; + simulation?: Awaited>; }; export class Swap { @@ -36,6 +34,7 @@ export class Swap { } public static async execute(opts: { + api: JupiterApi; signer: Signer; inputToken: Token; outputToken: Token; @@ -44,31 +43,32 @@ export class Swap { rpc: SolanaRpc; slippageBps?: string; }): Promise { - const { signer, inputToken, outputToken, rpc } = opts; + 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.ultra.get({ + query: { + inputMint: inputToken.id, + outputMint: outputToken.id, + amount: + opts.rawAmount ?? + NumberConverter.toChainAmount( + opts.amount!, + inputToken.decimals, + inputMultiplier + ), + taker: signer.address, + }, }); - - if (order.error) { - throw new Error(order.errorMessage ?? order.error); + if (orderResponse.error || !orderResponse.data) { + throw new Error("Unable to get a swap order."); } + const order = orderResponse.data; if (!order.transaction) { - throw new Error("No valid routes found."); + throw new Error(order.errorMessage ?? "No valid routes found."); } let networkFeeLamports = 0; @@ -88,6 +88,10 @@ export class Swap { networkFeeLamports += order.signatureFeeLamports; } + const signedTx = await signer.signTransaction( + order.transaction as Base64EncodedBytes + ); + if (Config.dryRun) { const inAmount = NumberConverter.fromChainAmount( order.inAmount, @@ -108,16 +112,25 @@ 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, + const executionResponse = await api.ultra.post({ + body: { requestId: order.requestId, 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, From 622fd7cd37a58e111e16cc68f37efde34073f3bb Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 12:21:45 +0800 Subject: [PATCH 15/32] chore: use unambiguous fallback methods --- bun.lock | 4 +-- package.json | 2 +- src/commands/LendCommand.ts | 14 +++++---- src/commands/PredictionsCommand.ts | 50 +++++++++++++++++------------- src/commands/SpotCommand.ts | 6 ++-- src/commands/VrfdCommand.ts | 32 ++++++++++++------- src/lib/Asset.ts | 2 +- src/lib/Swap.ts | 4 +-- 8 files changed, 66 insertions(+), 48 deletions(-) diff --git a/bun.lock b/bun.lock index 0005302..e732eb9 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^14.0.3", - "jupiter-api-ts": "^0.4.0", + "jupiter-api-ts": "^0.4.1", "ky": "^1.14.3", "micro-key-producer": "^0.8.5", }, @@ -427,7 +427,7 @@ "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.0", "", {}, "sha512-marIqjN3Zyw4erg2HpLiQbk9f/forMYAmGXcOba6xKHe6kw2m1BYXAa9I2i9bVZsjb0lRVgdzpmwc/WsmQzT2A=="], + "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=="], diff --git a/package.json b/package.json index 00d3759..b61416a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^14.0.3", - "jupiter-api-ts": "^0.4.0", + "jupiter-api-ts": "^0.4.1", "ky": "^1.14.3", "micro-key-producer": "^0.8.5" }, diff --git a/src/commands/LendCommand.ts b/src/commands/LendCommand.ts index 8f50621..2ed1b42 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -258,7 +258,7 @@ export class LendCommand { const rawAmount = opts.rawAmount ?? NumberConverter.toChainAmount(opts.amount!, lendToken.asset.decimals); - const response = await this.#api.lend.lend.post({ + const response = await this.#api.lend.lend.postLendV1EarnDeposit({ body: { asset: lendToken.assetAddress, signer: signer.address, @@ -396,7 +396,7 @@ export class LendCommand { `No lending position found for ${lendToken.asset.symbol}.` ); } - const response = await this.#api.lend.lend.post7({ + const response = await this.#api.lend.lend.postLendV1EarnRedeem({ body: { asset: lendToken.assetAddress, signer: signer.address, @@ -415,7 +415,7 @@ export class LendCommand { withdrawnRawAmount = opts.rawAmount ?? NumberConverter.toChainAmount(opts.amount!, lendToken.asset.decimals); - const response = await this.#api.lend.lend.post3({ + const response = await this.#api.lend.lend.postLendV1EarnWithdraw({ body: { asset: lendToken.assetAddress, signer: signer.address, @@ -566,7 +566,7 @@ export class LendCommand { } static async #getTokens(): Promise { - const response = await this.#api.lend.lend.get(); + const response = await this.#api.lend.lend.getLendV1EarnTokens(); if (response.error || !response.data) { throw new Error("Unable to get Lend tokens."); } @@ -576,7 +576,9 @@ export class LendCommand { static async #getPositions( users: string ): Promise { - const response = await this.#api.lend.lend.get2({ query: { users } }); + const response = await this.#api.lend.lend.getLendV1EarnPositions({ + query: { users }, + }); if (response.error || !response.data) { throw new Error("Unable to get Lend positions."); } @@ -590,7 +592,7 @@ export class LendCommand { user: string; positions: string; }): Promise { - const response = await this.#api.lend.lend.get3({ + const response = await this.#api.lend.lend.getLendV1EarnEarnings({ query: { user, positions }, }); if (response.error || !response.data) { diff --git a/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index e937b22..635fc5d 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -141,7 +141,7 @@ export class PredictionsCommand { let nextOffset: number | undefined; if (opts.id) { - const response = await this.#api.prediction.get3({ + const response = await this.#api.prediction.getPredictionV1EventsEventId({ path: { eventId: opts.id }, query: { includeMarkets: true }, }); @@ -150,7 +150,7 @@ export class PredictionsCommand { } data = [response.data]; } else if (opts.search) { - const response = await this.#api.prediction.get({ + const response = await this.#api.prediction.getPredictionV1EventsSearch({ query: { query: opts.search, limit: Number(opts.limit) }, }); if (response.error || !response.data) { @@ -164,7 +164,7 @@ export class PredictionsCommand { const { sortBy, sortDirection } = this.normalizeEventSort(opts.sort); - const response = await this.#api.prediction.get2({ + const response = await this.#api.prediction.getPredictionV1Events({ query: { includeMarkets: true, filter: this.normalizeEventFilter(opts.filter), @@ -361,9 +361,10 @@ export class PredictionsCommand { let data; if (opts.position) { - const response = await this.#api.prediction.get14({ - path: { positionPubkey: opts.position }, - }); + 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."); } @@ -372,7 +373,7 @@ export class PredictionsCommand { const address = opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const response = await this.#api.prediction.get13({ + const response = await this.#api.prediction.getPredictionV1Positions({ query: { ownerPubkey: address }, }); if (response.error || !response.data) { @@ -465,7 +466,7 @@ export class PredictionsCommand { token.decimals ); - const response = await this.#api.prediction.post({ + const response = await this.#api.prediction.postPredictionV1Orders({ body: { isBuy: true, ownerPubkey: signer.address, @@ -575,7 +576,7 @@ export class PredictionsCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const response = await this.#api.prediction.get15({ + const response = await this.#api.prediction.getPredictionV1History({ query: { ownerPubkey: address, start, end }, }); if (response.error || !response.data) { @@ -677,7 +678,7 @@ export class PredictionsCommand { simulation: await this.#rpc.simulate(signedTx), }; } - const response = await this.#api.prediction.post2({ + const response = await this.#api.prediction.postPredictionV1Execute({ body: { signedTransaction: signedTx, ...("execution" in build && build.execution @@ -714,7 +715,7 @@ export class PredictionsCommand { const signer = await Signer.load(opts.key ?? Config.load().activeKey); if (opts.position === "all") { - const response = await this.#api.prediction.delete({ + const response = await this.#api.prediction.deletePredictionV1Positions({ body: { ownerPubkey: signer.address, minSellPriceSlippageBps, @@ -798,9 +799,10 @@ export class PredictionsCommand { } // Single position - const positionResponse = await this.#api.prediction.get14({ - path: { positionPubkey: 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."); } @@ -808,10 +810,13 @@ export class PredictionsCommand { const claimable = this.isClaimable(position); if (claimable) { - const response = await this.#api.prediction.post3({ - path: { positionPubkey: opts.position }, - body: { ownerPubkey: signer.address }, - }); + 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."); } @@ -886,10 +891,11 @@ export class PredictionsCommand { ], }); } else { - const response = await this.#api.prediction.delete2({ - path: { positionPubkey: opts.position }, - body: { ownerPubkey: signer.address }, - }); + 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."); } diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index eda4d4d..aecf0ca 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -194,7 +194,7 @@ export class SpotCommand { this.#rpc.getScaledUiMultiplier(inputToken.id), this.#rpc.getScaledUiMultiplier(outputToken.id), ]); - const response = await this.#api.ultra.get({ + const response = await this.#api.ultra.getUltraV1Order({ query: { inputMint: inputToken.id, outputMint: outputToken.id, @@ -389,7 +389,9 @@ export class SpotCommand { const SOL_DECIMALS = Asset.SOL.decimals; - const response = await this.#api.ultra.get3({ path: { address } }); + const response = await this.#api.ultra.getUltraV1HoldingsAddress({ + path: { address }, + }); if (response.error || !response.data) { throw new Error("Unable to get spot holdings."); } diff --git a/src/commands/VrfdCommand.ts b/src/commands/VrfdCommand.ts index c8fb151..fc1b999 100644 --- a/src/commands/VrfdCommand.ts +++ b/src/commands/VrfdCommand.ts @@ -85,9 +85,12 @@ export class VrfdCommand { if (!isAddress(opts.token)) { throw new Error("Invalid token mint address."); } - const response = await this.#api.tokens.v2.verification.get({ - query: { tokenId: 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."); } @@ -150,9 +153,10 @@ export class VrfdCommand { simulation: await this.#rpc.simulate(signedTx), }; } - const response = await this.#api.tokens.v2.verification.post({ - body: { ...req, transaction: 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."); } @@ -193,9 +197,12 @@ export class VrfdCommand { } // Check eligibility before crafting transaction - const eligibilityResponse = await this.#api.tokens.v2.verification.get({ - query: { tokenId: 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."); } @@ -225,9 +232,10 @@ export class VrfdCommand { } // Craft the payment transaction - const craftResponse = await this.#api.tokens.v2.verification.get2({ - query: { senderAddress: 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."); } diff --git a/src/lib/Asset.ts b/src/lib/Asset.ts index ef59242..fc083c2 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -111,7 +111,7 @@ export async function resolveWalletAsset( return resolveToken(datapi, Asset[key as keyof typeof Asset].id); } - const response = await api.ultra.get3({ + const response = await api.ultra.getUltraV1HoldingsAddress({ path: { address: walletAddress }, }); if (response.error || !response.data) { diff --git a/src/lib/Swap.ts b/src/lib/Swap.ts index f5857c1..cdc3e38 100644 --- a/src/lib/Swap.ts +++ b/src/lib/Swap.ts @@ -49,7 +49,7 @@ export class Swap { rpc.getScaledUiMultiplier(outputToken.id), ]); - const orderResponse = await api.ultra.get({ + const orderResponse = await api.ultra.getUltraV1Order({ query: { inputMint: inputToken.id, outputMint: outputToken.id, @@ -117,7 +117,7 @@ export class Swap { }; } - const executionResponse = await api.ultra.post({ + const executionResponse = await api.ultra.postUltraV1Execute({ body: { requestId: order.requestId, signedTransaction: signedTx }, }); if (executionResponse.error || !executionResponse.data) { From afc8dc4f9ca9b1e17595af584b11909095133c46 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 12:25:02 +0800 Subject: [PATCH 16/32] feat: add SolanaTransactions lib --- bun.lock | 359 ++++++++++------------------- package.json | 3 +- src/lib/Signer.ts | 5 + src/lib/SolanaRpc.test.ts | 38 ++- src/lib/SolanaRpc.ts | 19 +- src/lib/SolanaTransactions.test.ts | 94 ++++++++ src/lib/SolanaTransactions.ts | 119 ++++++++++ 7 files changed, 402 insertions(+), 235 deletions(-) create mode 100644 src/lib/SolanaTransactions.test.ts create mode 100644 src/lib/SolanaTransactions.ts diff --git a/bun.lock b/bun.lock index e732eb9..6a7e380 100644 --- a/bun.lock +++ b/bun.lock @@ -1,13 +1,14 @@ { - "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", @@ -27,69 +28,45 @@ }, }, "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/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="], - "@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], + "@noble/curves": ["@noble/curves@2.4.0", "", { "dependencies": { "@noble/hashes": "2.4.0" } }, "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew=="], - "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.65.0", "", { "os": "android", "cpu": "arm" }, "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg=="], @@ -129,249 +106,189 @@ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.65.0", "", { "os": "win32", "cpu": "x64" }, "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.53.0", "", { "os": "android", "cpu": "arm" }, "sha512-JC89/jAx4d2zhDIbK8MC4L659FN1WiMXMBkNg7b33KXSkYpUgcbf+0nz7+EPRg+VwWiZVfaoFkNHJ7RXYb5Neg=="], - - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.53.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CY+pZfi+uyeU7AwFrEnjsNT+VfxYmKLMuk7bVxArd8f+09hQbJb8f7C7EpvTfNqrCK1J8zZlaYI4LltmEctgbQ=="], - - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.53.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0aqsC4HDQ94oI6kMz64iaOJ1f3bCVArxvaHJGOScBvFz6CcQedXi5b70Xg09CYjKNaHA56dW0QJfoZ/111kz1A=="], - - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.53.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e+KvuaWtnisyWojO/t5qKDbp2dvVpg+1dl4MGnTb21QpY4+4+9Y1XmZPaztcA2XNvy4BIaXFW+9JH9tMpSBqUg=="], - - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.53.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hpU0ZHVeblFjmZDfgi9BxhhCpURh0KjoFy5V+Tvp9sg/fRcnMUEfaJrgz+jQfOX4jctlVWrAs1ANs91+5iV+zA=="], - - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.53.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ccKxOpw+X4xa2pO+qbTOpxQ2x1+Ag3ViRQMnWt3gHp1LcpNgS1xd6GYc3OvehmHtrXqEV3YGczZ0I1qpBB4/2A=="], - - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.53.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UBkBvmzSmlyH2ZObQMDKW/TuyTmUtP/XClPUyU2YLwj0qLopZTZxnDz4VG5d3wz1HQuZXO0o1QqsnQUW1v4a6Q=="], - - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.53.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PQJJ1izoH9p61las6rZ0BWOznAhTDMmdUPL2IEBLuXFwhy2mSloYHvRkk39PSYJ1DyG+trqU5Z9ZbtHSGH6plg=="], - - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.53.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GXI1o4Thn/rtnRIL38BwrDMwVcUbIHKCsOixIWf/CkU3fCG3MXFzFTtDMt+34ik0Qk452d8kcpksL0w/hUkMZA=="], - - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.53.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Uahk7IVs2yBamCgeJ3XKpKT9Vh+de0pDKISFKnjEcI3c/w2CFHk1+W6Q6G3KI56HGwE9PWCp6ayhA9whXWkNIQ=="], - - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.53.0", "", { "os": "linux", "cpu": "none" }, "sha512-sWtcU9UkrKMWsGKdFy8R6jkm9Q0VVG1VCpxVuh0HzRQQi3ENI1Nh5CkpsdfUs2MKRcOoHKbXqTscunuXjhxoxQ=="], - - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.53.0", "", { "os": "linux", "cpu": "none" }, "sha512-aXew1+HDvCdExijX/8NBVC854zJwxhKP3l9AHFSHQNo4EanlHtzDMIlIvP3raUkL0vXtFCkTFYezzU5HjstB8A=="], - - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.53.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-rVpyBSqPGou9sITcsoXqUoGBUH74bxYLYOAGUqN599Zu6BQBlBU9hh3bJQ/20D1xrhhrsbiCpVPvXpLPM5nL1w=="], - - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.53.0", "", { "os": "linux", "cpu": "x64" }, "sha512-eOyeQ8qFQ2geXmlWJuXAOaek0hFhbMLlYsU457NMLKDRoC43Xf+eDPZ9Yk0n9jDaGJ5zBl/3Dy8wo41cnIXuLA=="], - - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.53.0", "", { "os": "linux", "cpu": "x64" }, "sha512-S6rBArW/zD1tob8M9PwKYrRmz+j1ss1+wjbRAJCWKd7TC3JB6noDiA95pIj9zOZVVp04MIzy5qymnYusrEyXzg=="], - - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.53.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sd/A0Ny5sN0D/MJtlk7w2jGY4bJQou7gToa9WZF7Sj6HTyVzvlzKJWiOHfr4SulVk4ndiFQ8rKmF9rXP0EcF3A=="], - - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.53.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-QC3q7b51Er/ZurEFcFzc7RpQ/YEoEBLJuCp3WoOzhSHHH/nkUKFy+igOxlj1z3LayhEZPDQQ7sXvv2PM2cdG3Q=="], - - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.53.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3OvLgOqwd705hWHV2i8ni80pilvg6BUgpC2+xtVu++e/q28LKVohGh5J5QYJOrRMfWmxK0M/AUu43vUw62LAKQ=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.80.0", "", { "os": "android", "cpu": "arm" }, "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.53.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xTiOkntexCdJytZ7ArIIgl3vGW5ujMM3sJNM7/+iqGAVJagCqjFFWn68HRWRLeyT66c95uR+CeFmQFI6mLQqDw=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.80.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw=="], - "@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.80.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA=="], - "@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-darwin-x64": ["@oxlint/binding-darwin-x64@1.80.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA=="], - "@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-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.80.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg=="], - "@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-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA=="], - "@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-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ=="], - "@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-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A=="], - "@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-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ=="], - "@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-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.80.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A=="], - "@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-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA=="], - "@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-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.80.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw=="], - "@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-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A=="], - "@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-x64-musl": ["@oxlint/binding-linux-x64-musl@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ=="], - "@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-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.80.0", "", { "os": "none", "cpu": "arm64" }, "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA=="], - "@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-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.80.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.80.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw=="], - "@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-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.80.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q=="], - "@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=="], + "@scure/base": ["@scure/base@2.4.0", "", {}, "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + "@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/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="], + "@scure/bip39": ["@scure/bip39@2.4.0", "", { "dependencies": { "@noble/hashes": "2.4.0" } }, "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA=="], - "@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=="], + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], + "@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/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="], + "@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/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/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/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/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/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/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], - - "@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/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/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], - - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], - - "@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=="], - - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - - "@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=="], - - "@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=="], - - "@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=="], - - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], - - "@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=="], - - "@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-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-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + "@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], - "@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-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/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + "@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=="], - "@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=="], + "@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=="], - "@solana-program/system": ["@solana-program/system@0.12.0", "", { "peerDependencies": { "@solana/kit": "^6.1.0" } }, "sha512-ZnAAWeGVMWNtJhw3GdifI2HnhZ0A0H0qs8tBkcFvxp/8wIavvO+GOM4Jd0N22u2+Lni2zcwvcrxrsxj6Mjphng=="], + "@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=="], - "@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/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=="], - "@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=="], + "@solana/assertions": ["@solana/assertions@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-lKSAdVo+P/6Lp4vs6shstXmFOpvxrABwn4o1462tb7sKkNapk6o9pPFVPGw4DUgPS3WqWRs1j2tmpuVjhQRntg=="], - "@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=="], + "@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=="], - "@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/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - "@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/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=="], - "@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/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=="], - "@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/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=="], - "@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/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/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/fast-stable-stringify": ["@solana/fast-stable-stringify@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-iCNed27wk6PKSS3QUtHovRfMWF/jbVWogs2vB4tukKUCsqG4rDfDInIwZ6ur/nY6XTrgi2gMMdZq9GAUlWsbfw=="], - "@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/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/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/functional": ["@solana/functional@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-P8cevu4mAqHTXC37h1TVoOh8zhWB2tlOI/R9vWjYPpcLwcyWf8p2qq4LEGHl5kY+1C+4PNX39HsmCocXOPCDkQ=="], - "@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/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/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/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/functional": ["@solana/functional@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-RcR+RRJKH43Ko98ru+LKDkjtcs3sHGz26HWL2IyPujni8RH3VsK2hg7HiLqO8Y4R53yzwsn2iZ3D+q8SkYbTpg=="], + "@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/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-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/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-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/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-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/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-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/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-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/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-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/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-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/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/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/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/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-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/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-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/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-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/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-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/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-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/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/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/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/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/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/nominal-types": ["@solana/nominal-types@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-XY+6gF9TzonHiI6VBjtcrEfxT/0KlmF3KRFGSKM5JLEfhs3w+E26op3TOqHuOX7H6llZBgxybaZmXKrvo2rt/w=="], + "@solana/nominal-types": ["@solana/nominal-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-9ykyBBvnkInH7fCacjJi7zu2PJyd+OCt+VTjIISv070fHzKIMFqZqJJ/dJ0SRH2aHwfB3n86iVsmtBtuxi4KKA=="], - "@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/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/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/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/plugin-core": ["@solana/plugin-core@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-tO9PAa9nFxZdrlYYHuG8pP9WbO/Szo1rr2YT2Mpj8PKDDqLsrQ2cCC8GerHbdLOr1s+cE1O7fotbydGDqeC9yQ=="], + "@solana/plugin-core": ["@solana/plugin-core@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-JE70YTQOfFACVFGvoJon4Scc/eHUWjMu8Ovo35CcV2kHTAHYMCd4UkBd2gmlhK0vRMMomsQi1ZLPlAlTq0OoUQ=="], - "@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/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/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/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/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/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/promises": ["@solana/promises@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-LKdYoYr6KXqSsJ39VjlZ15l+QuCx/bzoug62CShJqud41BUFUTPm7mD5sbV+rc8k0W1+MY8OKFA8G1i7zljw7w=="], + "@solana/promises": ["@solana/promises@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-oJSIn+VBBMWDo8oqw7RV3tI6Jih+Ieup6FcQLYLDUriaeo7+8l1Zdezl8zh7SIfeU4lOfAbRg6mR0huaS/Lltg=="], - "@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": ["@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/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-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/rpc-parsed-types": ["@solana/rpc-parsed-types@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-DwX4O918uAodrJyMh5TmsovOGeXRfF46Qwgbh57ZknFk3whrnahAHnPV9H5uR41hBExfZ/RE4u4IvLySca3Y8g=="], + "@solana/rpc-parsed-types": ["@solana/rpc-parsed-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-5275mvSV1mxhwvrMVa+K7BU/nAetpHfcb+8Ql9rtA8RRf6DyiimFQFZUukE4Ez6XJihEpCHNy98yhkgai9wytQ=="], - "@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-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/rpc-spec-types": ["@solana/rpc-spec-types@6.7.0", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-ohameGNJmtjZm7bIIGSaRIRI2G6Ikx77MfCyi9egxZo+CSS6X8WYQyqibVFjq/5RuFz9KnQp6+o2wOn3UL78sg=="], + "@solana/rpc-spec-types": ["@solana/rpc-spec-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-NDZrKyZrJk4HaMFhTE/lAiMB824cWAodKqDHyKi0UteHU9pyRmil3BN1jt7e+j08mwMWwfklSgyrTaq52g6DIQ=="], - "@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/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/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/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-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/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-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/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-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/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-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/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-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=="], + "@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/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/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/subscribable": ["@solana/subscribable@6.7.0", "", { "dependencies": { "@solana/errors": "6.7.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-cAEM3u8Z9qTjWwMEZ75tMRoFWArPnseP3INz0FM8FxsjD5uxZ5rH6H0uPfUBBiI/n+pqdkmQMX39hmdXE39hXQ=="], + "@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/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/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/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-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/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/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/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=="], + "@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=="], - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], - "@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=="], @@ -385,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=="], @@ -395,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=="], @@ -403,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=="], @@ -423,7 +336,7 @@ "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=="], @@ -435,7 +348,7 @@ "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=="], @@ -447,9 +360,7 @@ "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=="], - "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=="], + "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=="], @@ -457,42 +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/record/@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], + "@solana-program/token/@solana-program/system": ["@solana-program/system@0.12.2", "", { "peerDependencies": { "@solana/kit": "^6.4.0" } }, "sha512-MaBeOxlvTruQhA7UYkOb3hVTEHPPagOtd+PvTm6a8rGgvEAP0kD4BbC37NceOaR4ABNqdaCmD5OMVRKgrE6KAg=="], "@solana-program/token-2022/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@solana-program/token-2022/@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], - - "@solana-program/zk-elgamal-proof/@solana-program/system": ["@solana-program/system@0.14.0", "", { "peerDependencies": { "@solana/kit": "^8.0.0" } }, "sha512-Pjs2RINZHYmk/pqWNBCuQmfNjZT9woPJ/w7QkzzCSwcqcokbARtxsnIdgj4lJVxvr1DuxG8JGIbKnq6z1QRY6Q=="], + "@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - "@solana/rpc-transport-http/undici-types": ["undici-types@7.22.0", "", {}, "sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw=="], + "@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/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - - "@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=="], + "micro-packed/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], "@solana-program/token-2022/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@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=="], - - "@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=="], } } diff --git a/package.json b/package.json index b61416a..0b434a4 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "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", diff --git a/src/lib/Signer.ts b/src/lib/Signer.ts index 2b58c92..6bcc4ed 100644 --- a/src/lib/Signer.ts +++ b/src/lib/Signer.ts @@ -90,6 +90,11 @@ 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 ): Promise { diff --git a/src/lib/SolanaRpc.test.ts b/src/lib/SolanaRpc.test.ts index 636e6ec..d26ef0d 100644 --- a/src/lib/SolanaRpc.test.ts +++ b/src/lib/SolanaRpc.test.ts @@ -1,5 +1,13 @@ import { expect, test } from "bun:test"; -import type { Base64EncodedWireTransaction } from "@solana/kit"; +import type { + Base64EncodedWireTransaction, + Blockhash, + GetAccountInfoApi, + GetLatestBlockhashApi, + GetMultipleAccountsApi, + GetTokenAccountsByOwnerApi, + Rpc, +} from "@solana/kit"; import { SolanaRpc } from "./SolanaRpc.ts"; @@ -51,3 +59,31 @@ test("sends a signed transaction through the configured RPC", async () => { params: ["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< + GetAccountInfoApi & + GetLatestBlockhashApi & + GetMultipleAccountsApi & + GetTokenAccountsByOwnerApi + >; + 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 index f8afe3f..8611c8b 100644 --- a/src/lib/SolanaRpc.ts +++ b/src/lib/SolanaRpc.ts @@ -2,7 +2,9 @@ import type { Address, Base64EncodedBytes, Base64EncodedWireTransaction, + BlockhashLifetimeConstraint, GetAccountInfoApi, + GetLatestBlockhashApi, Rpc, } from "@solana/kit"; import { address, createSolanaRpc, unwrapOption } from "@solana/kit"; @@ -36,13 +38,19 @@ type SimulationResult = { type SolanaRpcOptions = { fetch?: typeof fetch; - rpcClient?: Rpc; + rpcClient?: Rpc< + GetAccountInfoApi & + GetLatestBlockhashApi & + >; url?: string; }; export class SolanaRpc { readonly #fetch: typeof fetch; - readonly #rpcClient: Rpc; + readonly #rpcClient: Rpc< + GetAccountInfoApi & + GetLatestBlockhashApi & + >; readonly #url: string; readonly #mints = new Map>(); readonly #scaledUiMultipliers = new Map< @@ -78,6 +86,13 @@ export class SolanaRpc { return account.programAddress; } + 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) { 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, + }), + ]); +} From 15f2c89ccf69b64f64604c3de3ee48c656b5a3c5 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 12:43:06 +0800 Subject: [PATCH 17/32] chore: update signTransaction method to accept Base64EncodedWireTransaction --- src/lib/Signer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/Signer.ts b/src/lib/Signer.ts index 6bcc4ed..fd39b2b 100644 --- a/src/lib/Signer.ts +++ b/src/lib/Signer.ts @@ -96,9 +96,9 @@ export class Signer { } public async signTransaction( - transaction: Base64EncodedBytes + 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], From 178b55bafe0de34075433d99ea7fd57144907f00 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 12:55:20 +0800 Subject: [PATCH 18/32] feat: add getTokenPrograms and getTokenAccountsByOwner methods --- src/lib/SolanaRpc.ts | 78 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/lib/SolanaRpc.ts b/src/lib/SolanaRpc.ts index 8611c8b..f40cd93 100644 --- a/src/lib/SolanaRpc.ts +++ b/src/lib/SolanaRpc.ts @@ -5,14 +5,25 @@ import type { BlockhashLifetimeConstraint, GetAccountInfoApi, GetLatestBlockhashApi, + GetMultipleAccountsApi, + GetTokenAccountsByOwnerApi, Rpc, } from "@solana/kit"; -import { address, createSolanaRpc, unwrapOption } 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 { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; +import { + decodeToken, + TOKEN_PROGRAM_ADDRESS, +} from "@solana-program/token"; import { Config } from "./Config.ts"; import { Signer } from "./Signer.ts"; @@ -36,11 +47,22 @@ type SimulationResult = { error?: string; }; +export type ClosableTokenAccount = { + address: Address; + amount: bigint; + isFrozen: boolean; + lamports: bigint; + mint: Address; + owner: Address; +}; + type SolanaRpcOptions = { fetch?: typeof fetch; rpcClient?: Rpc< GetAccountInfoApi & GetLatestBlockhashApi & + GetMultipleAccountsApi & + GetTokenAccountsByOwnerApi >; url?: string; }; @@ -50,6 +72,8 @@ export class SolanaRpc { readonly #rpcClient: Rpc< GetAccountInfoApi & GetLatestBlockhashApi & + GetMultipleAccountsApi & + GetTokenAccountsByOwnerApi >; readonly #url: string; readonly #mints = new Map>(); @@ -86,6 +110,56 @@ export class SolanaRpc { 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" }) From 008e494c4a3dfffbceade7c0f822563fd87d5a9e Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 12:59:45 +0800 Subject: [PATCH 19/32] docs: update readme on dry-run --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3b17f0d..601fda0 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,6 @@ 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 spot trade history (does not require a Jupiter Core API key) @@ -73,7 +70,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: @@ -81,7 +78,7 @@ 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 - [Perps](docs/perps.md): Perps trading (leveraged longs/shorts) - [Lend](docs/lend.md): Lending and yield farming From 9a2ca84a2e8685dd6dc8af9d6ea04a9f70a62257 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 13:04:14 +0800 Subject: [PATCH 20/32] refactor: replace UltraClient and use primitives for transfer and reclaim --- docs/spot.md | 16 +-- src/commands/SpotCommand.ts | 269 ++++++++++++++++++------------------ 2 files changed, 142 insertions(+), 143 deletions(-) diff --git a/docs/spot.md b/docs/spot.md index 76322a0..1714be2 100644 --- a/docs/spot.md +++ b/docs/spot.md @@ -109,7 +109,7 @@ jup spot portfolio --address } ``` -### Reclaim rent from ATA +### Reclaim rent from token accounts ```bash jup spot reclaim @@ -118,15 +118,14 @@ jup spot reclaim --token USDC jup spot 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...", ] @@ -178,9 +177,9 @@ jup spot transfer --token SOL --to --amount 1 --key mykey jup spot 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. ```js // Example JSON response: @@ -190,7 +189,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 } ``` diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index aecf0ca..a091d9d 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -1,5 +1,9 @@ -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 chalk from "chalk"; import type { Command } from "commander"; import type { @@ -8,11 +12,6 @@ import type { JupiterApiTypes, } from "jupiter-api-ts"; -import { - UltraClient, - type GetHoldingsResponse, - type HoldingsTokenAccount, -} from "../clients/UltraClient.ts"; import { Asset, getTokensByMints, @@ -27,6 +26,11 @@ 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 { @@ -514,6 +518,9 @@ export class SpotCommand { }): 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); @@ -528,33 +535,27 @@ export class SpotCommand { 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 tokenProgram = await this.#rpc.getTokenProgram(token.id); - const [senderTokenAccountAddress] = await findAssociatedTokenPda({ - mint: token.id as Address, - owner: signer.address as Address, + serializedTransaction = await buildTokenTransferTransaction({ + amount: BigInt(chainAmount), + decimals: token.decimals, + mint: address(token.id), + recipient, + rpc: this.#rpc, + sender, tokenProgram, }); - txResponse = await UltraClient.getTransferTokenTx({ - mint: token.id, - tokenDecimals: token.decimals.toString(), - tokenProgramId: tokenProgram, - senderAddress: signer.address as Address, - senderTokenAccountAddress, - receiverAddress: opts.to as Address, - amount: chainAmount, - }); - } - - if ("error" in txResponse) { - throw new Error(txResponse.error); } const humanAmount = NumberConverter.fromChainAmount( @@ -563,12 +564,12 @@ export class SpotCommand { 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, @@ -581,9 +582,9 @@ export class SpotCommand { }, amount: humanAmount, value: value, - networkFeeLamports: txResponse.feeAmount, signature: null, - transaction: txResponse.transaction, + serializedTransaction: simulation.serializedTransaction, + simulation, }); return; } @@ -599,21 +600,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({ @@ -626,8 +628,7 @@ export class SpotCommand { }, amount: humanAmount, value: value, - networkFeeLamports: txResponse.feeAmount, - signature: result.signature, + signature, }); return; } @@ -641,11 +642,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 }, ], }); } @@ -802,17 +799,37 @@ export class SpotCommand { }): Promise { requireJupiterApiKey(); 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 response = await this.#api.ultra.getUltraV1HoldingsAddress({ + path: { address: signer.address }, + }); + if (response.error || !response.data) { + throw new Error("Unable to get spot holdings."); } + const owner = signer.transactionSigner; + const holdingMints = Object.keys(response.data.tokens); + const tokenPrograms = + holdingMints.length > 0 + ? await this.#rpc.getTokenPrograms(holdingMints) + : new Map(); + const accountsByProgram = await Promise.all( + [...new Set(tokenPrograms.values())].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; if (opts.token) { @@ -842,62 +859,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; } @@ -908,42 +917,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; @@ -954,16 +967,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}`, @@ -1000,14 +1009,6 @@ 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); - } - return mints; - } - private static parseTimestamp(value: string): number { if (/^\d+$/.test(value)) { return Number(value) * 1000; From 89b3f84276964741fdf416d1c55561b53569d5c6 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 13:04:53 +0800 Subject: [PATCH 21/32] chore: remove old clients --- src/clients/ClientConfig.ts | 16 -- src/clients/LendClient.ts | 83 ------- src/clients/PerpsClient.ts | 404 ------------------------------- src/clients/PredictionsClient.ts | 326 ------------------------- src/clients/UltraClient.ts | 223 ----------------- src/clients/VrfdClient.ts | 108 --------- 6 files changed, 1160 deletions(-) delete mode 100644 src/clients/ClientConfig.ts delete mode 100644 src/clients/LendClient.ts delete mode 100644 src/clients/PerpsClient.ts delete mode 100644 src/clients/PredictionsClient.ts delete mode 100644 src/clients/UltraClient.ts delete mode 100644 src/clients/VrfdClient.ts 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/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(); - } -} From f3522cd48f66ecf6b7591fc50544af956f092707 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 13:14:26 +0800 Subject: [PATCH 22/32] refactor: use RPC primitive and remove request builder --- src/lib/SolanaRpc.test.ts | 59 +++++++++++++---------------- src/lib/SolanaRpc.ts | 79 +++++++++++---------------------------- 2 files changed, 46 insertions(+), 92 deletions(-) diff --git a/src/lib/SolanaRpc.test.ts b/src/lib/SolanaRpc.test.ts index d26ef0d..1f1457b 100644 --- a/src/lib/SolanaRpc.test.ts +++ b/src/lib/SolanaRpc.test.ts @@ -7,34 +7,34 @@ import type { 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 body: unknown; + let args: unknown[] | undefined; const rpc = new SolanaRpc({ - fetch: (async (_input, init) => { - body = JSON.parse(String(init?.body)); - return Response.json({ - jsonrpc: "2.0", - id: 1, - result: { value: { err: null } }, - }); - }) as typeof fetch, - url: "https://rpc.example.com", + rpcClient: { + simulateTransaction: (...params: unknown[]) => { + args = params; + return { send: async () => ({ value: { err: null } }) }; + }, + } as unknown as Rpc, }); const result = await rpc.simulate(signedTransaction); - expect(body).toEqual({ - jsonrpc: "2.0", - id: 1, - method: "simulateTransaction", - params: ["c2lnbmVk", { encoding: "base64", sigVerify: true }], - }); + expect(args).toEqual(["c2lnbmVk", { encoding: "base64", sigVerify: true }]); expect(result).toEqual({ serializedTransaction: signedTransaction, success: true, @@ -42,22 +42,18 @@ test("simulates a signed transaction with signature verification", async () => { }); test("sends a signed transaction through the configured RPC", async () => { - let body: unknown; + let args: unknown[] | undefined; const rpc = new SolanaRpc({ - fetch: (async (_input, init) => { - body = JSON.parse(String(init?.body)); - return Response.json({ jsonrpc: "2.0", id: 1, result: "signature" }); - }) as typeof fetch, - url: "https://rpc.example.com", + rpcClient: { + sendTransaction: (...params: unknown[]) => { + args = params; + return { send: async () => "signature" }; + }, + } as unknown as Rpc, }); await expect(rpc.send(signedTransaction)).resolves.toBe("signature"); - expect(body).toEqual({ - jsonrpc: "2.0", - id: 1, - method: "sendTransaction", - params: ["c2lnbmVk", { encoding: "base64" }], - }); + expect(args).toEqual(["c2lnbmVk", { encoding: "base64" }]); }); test("gets the latest blockhash through Solana Kit's RPC method", async () => { @@ -73,12 +69,7 @@ test("gets the latest blockhash through Solana Kit's RPC method", async () => { }), }; }, - } as unknown as Rpc< - GetAccountInfoApi & - GetLatestBlockhashApi & - GetMultipleAccountsApi & - GetTokenAccountsByOwnerApi - >; + } as unknown as Rpc; const rpc = new SolanaRpc({ rpcClient }); await expect(rpc.getLatestBlockhash()).resolves.toEqual({ diff --git a/src/lib/SolanaRpc.ts b/src/lib/SolanaRpc.ts index f40cd93..4c74dd7 100644 --- a/src/lib/SolanaRpc.ts +++ b/src/lib/SolanaRpc.ts @@ -8,6 +8,8 @@ import type { GetMultipleAccountsApi, GetTokenAccountsByOwnerApi, Rpc, + SendTransactionApi, + SimulateTransactionApi, } from "@solana/kit"; import { address, @@ -20,27 +22,11 @@ import { fetchMint, TOKEN_2022_PROGRAM_ADDRESS, } from "@solana-program/token-2022"; -import { - decodeToken, - TOKEN_PROGRAM_ADDRESS, -} from "@solana-program/token"; +import { decodeToken, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; import { Config } from "./Config.ts"; import { Signer } from "./Signer.ts"; -type RpcError = { - message?: string; -}; - -type RpcResponse = { - result?: T; - error?: RpcError; -}; - -type SimulationValue = { - err: unknown; -}; - type SimulationResult = { serializedTransaction: Base64EncodedWireTransaction; success: boolean; @@ -57,25 +43,26 @@ export type ClosableTokenAccount = { }; type SolanaRpcOptions = { - fetch?: typeof fetch; rpcClient?: Rpc< GetAccountInfoApi & GetLatestBlockhashApi & GetMultipleAccountsApi & - GetTokenAccountsByOwnerApi + GetTokenAccountsByOwnerApi & + SendTransactionApi & + SimulateTransactionApi >; url?: string; }; export class SolanaRpc { - readonly #fetch: typeof fetch; readonly #rpcClient: Rpc< GetAccountInfoApi & GetLatestBlockhashApi & GetMultipleAccountsApi & - GetTokenAccountsByOwnerApi + GetTokenAccountsByOwnerApi & + SendTransactionApi & + SimulateTransactionApi >; - readonly #url: string; readonly #mints = new Map>(); readonly #scaledUiMultipliers = new Map< string, @@ -83,9 +70,8 @@ export class SolanaRpc { >(); public constructor(options: SolanaRpcOptions = {}) { - this.#fetch = options.fetch ?? fetch; - this.#url = options.url ?? Config.getRpcUrl(); - this.#rpcClient = options.rpcClient ?? createSolanaRpc(this.#url); + this.#rpcClient = + options.rpcClient ?? createSolanaRpc(options.url ?? Config.getRpcUrl()); } public getScaledUiMultiplier(mint: string): Promise { @@ -200,11 +186,13 @@ export class SolanaRpc { public async simulate( serializedTransaction: Base64EncodedWireTransaction ): Promise { - const result = await this.#request<{ value: SimulationValue }>( - "simulateTransaction", - [serializedTransaction, { encoding: "base64", sigVerify: true }] - ); - const error = this.#formatSimulationError(result.value.err); + const { value } = await this.#rpcClient + .simulateTransaction(serializedTransaction, { + encoding: "base64", + sigVerify: true, + }) + .send(); + const error = this.#formatSimulationError(value.err); return { serializedTransaction, @@ -216,12 +204,9 @@ export class SolanaRpc { public async send( serializedTransaction: Base64EncodedWireTransaction ): Promise { - return this.#request("sendTransaction", [ - serializedTransaction, - { - encoding: "base64", - }, - ]); + return this.#rpcClient + .sendTransaction(serializedTransaction, { encoding: "base64" }) + .send(); } public async signAndSimulate( @@ -238,28 +223,6 @@ export class SolanaRpc { return this.send(await signer.signTransaction(transaction)); } - async #request(method: string, params: unknown[]): Promise { - const response = await this.#fetch(this.#url, { - body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), - headers: { "content-type": "application/json" }, - method: "POST", - }); - const payload = (await response.json()) as RpcResponse; - - if (!response.ok) { - throw new Error(`RPC ${method} failed with HTTP ${response.status}.`); - } - if (payload.error) { - throw new Error( - `RPC ${method} failed: ${payload.error.message ?? "unknown error"}` - ); - } - if (payload.result === undefined) { - throw new Error(`RPC ${method} returned no result.`); - } - return payload.result; - } - #formatSimulationError(error: unknown): string | undefined { if (error === null || error === undefined) { return undefined; From 30339e44c88d975a554a9840cf072e8186e49843 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 14:03:12 +0800 Subject: [PATCH 23/32] feat: lift portfolio, transfer and reclaim to top level, update spot quote and swap to latest routes --- README.md | 6 +- docs/spot.md | 52 ++++----- src/commands/PredictionsCommand.ts | 9 +- src/commands/SpotCommand.test.ts | 29 +++++ src/commands/SpotCommand.ts | 182 +++++++---------------------- src/lib/Asset.ts | 51 +------- src/lib/Swap.test.ts | 94 +++++++++++++++ src/lib/Swap.ts | 29 +++-- 8 files changed, 217 insertions(+), 235 deletions(-) create mode 100644 src/commands/SpotCommand.test.ts create mode 100644 src/lib/Swap.test.ts diff --git a/README.md b/README.md index 601fda0..e263625 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,14 @@ 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 -# 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 # Open a 3x long SOL position with 10 USDC jup perps open --asset SOL --side long --amount 10 --input USDC --leverage 3 diff --git a/docs/spot.md b/docs/spot.md index 1714be2..35627f1 100644 --- a/docs/spot.md +++ b/docs/spot.md @@ -9,10 +9,8 @@ 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's global token search, which picks the top +result. Use a mint address whenever a symbol might be ambiguous. ## Commands @@ -59,7 +57,7 @@ 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. +- `--slippage` sets max slippage in basis points (e.g. `50` = 0.5%). Omit it to use Jupiter's default. - `--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. ```js @@ -81,29 +79,23 @@ 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 } ] } @@ -112,10 +104,10 @@ jup spot portfolio --address ### 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 every empty, non-frozen token account owned by the active key's wallet @@ -170,11 +162,11 @@ 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. The CLI builds the System Program or SPL Token transaction locally and sends it through the configured RPC URL. @@ -212,6 +204,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/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index 635fc5d..5e75bf2 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -6,7 +6,7 @@ import type { JupiterApiTypes, JupiterDatapi, } from "jupiter-api-ts"; -import { resolveWalletAsset } from "../lib/Asset.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"; @@ -455,12 +455,7 @@ export class PredictionsCommand { const side = this.normalizeSide(opts.side); const isYes = side === "yes"; const signer = await Signer.load(opts.key ?? Config.load().activeKey); - const token = await resolveWalletAsset( - this.#api, - this.#datapi, - signer.address, - opts.input - ); + const token = await resolveToken(this.#datapi, opts.input); const depositAmount = NumberConverter.toChainAmount( opts.amount, token.decimals 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 a091d9d..1e31f47 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -4,19 +4,16 @@ import { 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, - JupiterApiTypes, -} from "jupiter-api-ts"; +import type { JupiterApi, JupiterDatapi } from "jupiter-api-ts"; import { Asset, getTokensByMints, resolveToken, - resolveWalletAsset, searchTokens, type Token, } from "../lib/Asset.ts"; @@ -49,7 +46,7 @@ export class SpotCommand { 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") @@ -90,9 +87,9 @@ export class SpotCommand { Config.dryRun = Config.resolveDryRun(opts.dryRun); return this.swap(opts); }); - spot + 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)); @@ -110,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( @@ -129,7 +126,7 @@ export class SpotCommand { Config.dryRun = Config.resolveDryRun(opts.dryRun); return this.transfer(opts); }); - spot + program .command("reclaim") .description("Reclaim SOL rent from empty token accounts") .option("--key ", "Key to use for signing") @@ -198,7 +195,7 @@ export class SpotCommand { this.#rpc.getScaledUiMultiplier(inputToken.id), this.#rpc.getScaledUiMultiplier(outputToken.id), ]); - const response = await this.#api.ultra.getUltraV1Order({ + const response = await this.#api.swap.v2.getOrder({ query: { inputMint: inputToken.id, outputMint: outputToken.id, @@ -215,6 +212,9 @@ export class SpotCommand { 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( order.inAmount, @@ -288,7 +288,7 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); const [inputToken, outputToken] = await Promise.all([ - resolveWalletAsset(this.#api, this.#datapi, signer.address, opts.from), + resolveToken(this.#datapi, opts.from), resolveToken(this.#datapi, opts.to), ]); @@ -391,119 +391,44 @@ export class SpotCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); - const SOL_DECIMALS = Asset.SOL.decimals; - - const response = await this.#api.ultra.getUltraV1HoldingsAddress({ + const response = await this.#api.portfolio.getPortfolioV1PositionsAddress({ path: { address }, }); if (response.error || !response.data) { - throw new Error("Unable to get spot holdings."); + throw new Error("Unable to get portfolio positions."); } - const raw = response.data; - - // 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 getTokensByMints(this.#datapi, 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; - 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, - }); - } - - for (const [mint, ata] of ataByMint) { - if (mint === Asset.SOL.id) { - continue; - } - const info = tokenMap.get(mint); - if (!info) { - continue; - } - const multiplier = await this.#rpc.getScaledUiMultiplier(info.id); - 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, - }); - } - - // 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", }, - 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), + 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)}`); @@ -516,7 +441,6 @@ 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."); @@ -524,12 +448,7 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - const token = await resolveWalletAsset( - this.#api, - this.#datapi, - signer.address, - opts.token - ); + const token = await resolveToken(this.#datapi, opts.token); const multiplier = await this.#rpc.getScaledUiMultiplier(token.id); const chainAmount = opts.rawAmount ?? @@ -797,29 +716,18 @@ export class SpotCommand { key?: string; token?: string; }): Promise { - requireJupiterApiKey(); const signer = await Signer.load(opts.key ?? Config.load().activeKey); - const response = await this.#api.ultra.getUltraV1HoldingsAddress({ - path: { address: signer.address }, - }); - if (response.error || !response.data) { - throw new Error("Unable to get spot holdings."); - } - const owner = signer.transactionSigner; - const holdingMints = Object.keys(response.data.tokens); - const tokenPrograms = - holdingMints.length > 0 - ? await this.#rpc.getTokenPrograms(holdingMints) - : new Map(); const accountsByProgram = await Promise.all( - [...new Set(tokenPrograms.values())].map(async (tokenProgram) => ({ - tokenProgram, - accounts: await this.#rpc.getTokenAccountsByOwner( - owner.address, - tokenProgram - ), - })) + [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 diff --git a/src/lib/Asset.ts b/src/lib/Asset.ts index fc083c2..bf084d9 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -1,9 +1,5 @@ -import { address, isAddress } from "@solana/kit"; -import type { - JupiterApi, - JupiterDatapi, - JupiterDatapiTypes, -} from "jupiter-api-ts"; +import { address } from "@solana/kit"; +import type { JupiterDatapi, JupiterDatapiTypes } from "jupiter-api-ts"; type AssetInfo = { readonly id: ReturnType; @@ -94,46 +90,3 @@ export function resolveAsset(name: string): (typeof Asset)[keyof typeof Asset] { } return asset; } - -export async function resolveWalletAsset( - api: JupiterApi, - datapi: JupiterDatapi, - walletAddress: string, - asset: string -): Promise { - if (isAddress(asset)) { - return resolveToken(datapi, asset); - } - - const key = asset.toUpperCase(); - // Prevent full holdings lookup for well-known tokens - if (key in Asset) { - return resolveToken(datapi, Asset[key as keyof typeof Asset].id); - } - - const response = await api.ultra.getUltraV1HoldingsAddress({ - path: { address: walletAddress }, - }); - if (response.error || !response.data) { - throw new Error("Unable to get wallet holdings."); - } - const holdings = response.data; - const mints = Object.keys(holdings.tokens); - if (BigInt(holdings.amount) > 0n && !mints.includes(Asset.SOL.id)) { - mints.push(Asset.SOL.id); - } - const tokens = await getTokensByMints(datapi, 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}` - ); -} 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 cdc3e38..0c6bff6 100644 --- a/src/lib/Swap.ts +++ b/src/lib/Swap.ts @@ -9,8 +9,8 @@ import type { SolanaRpc } from "./SolanaRpc.ts"; export type SwapResult = { signer: Signer; - order: JupiterApiTypes.GetUltraV1OrderResponse; - result: JupiterApiTypes.PostUltraV1ExecuteResponse | null; + order: JupiterApiTypes.GetSwapV2OrderResponse; + result: JupiterApiTypes.PostSwapV2ExecuteResponse | null; inputToken: Token; outputToken: Token; inAmount: string; @@ -49,7 +49,7 @@ export class Swap { rpc.getScaledUiMultiplier(outputToken.id), ]); - const orderResponse = await api.ultra.getUltraV1Order({ + const orderResponse = await api.swap.v2.getOrder({ query: { inputMint: inputToken.id, outputMint: outputToken.id, @@ -61,13 +61,14 @@ export class Swap { inputMultiplier ), taker: signer.address, + ...(opts.slippageBps ? { slippageBps: Number(opts.slippageBps) } : {}), }, }); if (orderResponse.error || !orderResponse.data) { throw new Error("Unable to get a swap order."); } const order = orderResponse.data; - if (!order.transaction) { + if (!order.transaction || !order.inAmount || !order.outAmount) { throw new Error(order.errorMessage ?? "No valid routes found."); } @@ -76,16 +77,16 @@ 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( @@ -117,8 +118,18 @@ export class Swap { }; } - const executionResponse = await api.ultra.postUltraV1Execute({ - body: { 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."); From 833d2dc86ff7325ccfbccda3b47395e0d59c837e Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 15:52:53 +0800 Subject: [PATCH 24/32] refactor: replace Datapi searchAssets with getTokensV2Search --- docs/spot.md | 14 +++++++--- src/commands/PredictionsCommand.ts | 6 +---- src/commands/SpotCommand.ts | 25 ++++++++++++------ src/index.ts | 1 - src/lib/Asset.test.ts | 41 ++++++++++++++++++++++++++++++ src/lib/Asset.ts | 35 +++++++++++++++++-------- 6 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 src/lib/Asset.test.ts diff --git a/docs/spot.md b/docs/spot.md index 35627f1..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,8 +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 arguments resolve through Jupiter's global token search, which picks the top -result. Use a mint address whenever a symbol might be ambiguous. +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 @@ -33,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: @@ -58,7 +62,8 @@ 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%). Omit it to use Jupiter's default. -- `--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. +- `--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: @@ -172,6 +177,7 @@ jup transfer --token SOL --to --amount 1 --dry-run - 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` 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: diff --git a/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index 5e75bf2..c1747d6 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -4,7 +4,6 @@ import type { Command } from "commander"; import type { JupiterApi, JupiterApiTypes, - JupiterDatapi, } from "jupiter-api-ts"; import { resolveToken } from "../lib/Asset.ts"; import { Config } from "../lib/Config.ts"; @@ -21,17 +20,14 @@ type PredictionSubmission = { }; export class PredictionsCommand { - 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 predictions = program @@ -455,7 +451,7 @@ export class PredictionsCommand { const side = this.normalizeSide(opts.side); const isYes = side === "yes"; const signer = await Signer.load(opts.key ?? Config.load().activeKey); - const token = await resolveToken(this.#datapi, opts.input); + const token = await resolveToken(this.#api, opts.input); const depositAmount = NumberConverter.toChainAmount( opts.amount, token.decimals diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index 1e31f47..b13c0ff 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -142,12 +142,13 @@ export class SpotCommand { search: string; limit?: string; }): Promise { + requireJupiterApiKey(); if (opts.limit && isNaN(Number(opts.limit))) { throw new Error("--limit must be a number"); } const tokens = await searchTokens({ - datapi: this.#datapi, + api: this.#api, query: opts.search, limit: opts.limit ? Number(opts.limit) : undefined, }); @@ -188,8 +189,8 @@ export class SpotCommand { Swap.validateAmountOpts(opts); const [inputToken, outputToken] = await Promise.all([ - resolveToken(this.#datapi, opts.from), - resolveToken(this.#datapi, opts.to), + resolveToken(this.#api, opts.from), + resolveToken(this.#api, opts.to), ]); const [inputMultiplier, outputMultiplier] = await Promise.all([ this.#rpc.getScaledUiMultiplier(inputToken.id), @@ -288,8 +289,8 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); const [inputToken, outputToken] = await Promise.all([ - resolveToken(this.#datapi, opts.from), - resolveToken(this.#datapi, opts.to), + resolveToken(this.#api, opts.from), + resolveToken(this.#api, opts.to), ]); const swap = await Swap.execute({ @@ -441,6 +442,7 @@ 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."); @@ -448,7 +450,7 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - const token = await resolveToken(this.#datapi, opts.token); + const token = await resolveToken(this.#api, opts.token); const multiplier = await this.#rpc.getScaledUiMultiplier(token.id); const chainAmount = opts.rawAmount ?? @@ -575,6 +577,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."); } @@ -588,7 +593,7 @@ export class SpotCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); const targetAsset = opts.token - ? await resolveToken(this.#datapi, opts.token) + ? await resolveToken(this.#api, opts.token) : undefined; const response = await this.#datapi.getUserTransactions({ query: { @@ -898,7 +903,11 @@ export class SpotCommand { if (!holdingsMints.includes(input)) { throw new Error(`Token ${input} not found in wallet.`); } - return resolveToken(this.#datapi, input); + const [token] = await getTokensByMints(this.#datapi, [input]); + if (!token) { + throw new Error(`Token ${input} not found in wallet.`); + } + return token; } const tokens = await getTokensByMints(this.#datapi, holdingsMints); diff --git a/src/index.ts b/src/index.ts index e55b612..5f0553e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,7 +50,6 @@ LendCommand.register(program, solanaRpc, jupiterClients.api); PerpsCommand.register(program, jupiterClients.perps, solanaRpc); PredictionsCommand.register( program, - jupiterClients.datapi, solanaRpc, jupiterClients.api ); diff --git a/src/lib/Asset.test.ts b/src/lib/Asset.test.ts new file mode 100644 index 0000000..3328763 --- /dev/null +++ b/src/lib/Asset.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test"; +import type { JupiterApi } from "jupiter-api-ts"; + +import { searchTokens } from "./Asset.ts"; + +test("searchTokens uses Tokens V2 for a comma-separated mint search", async () => { + const calls: unknown[] = []; + const api = { + tokens: { + v2: { + getTokensV2Search: async (request: unknown) => { + calls.push(request); + return { + data: [ + { + id: "mint-a", + name: "Token A", + symbol: "A", + decimals: 6, + }, + { + id: "mint-b", + name: "Token B", + symbol: "B", + decimals: 9, + }, + ], + }; + }, + }, + }, + } as unknown as JupiterApi; + + const tokens = await searchTokens({ + api, + query: "mint-a,mint-b", + } as never); + + expect(calls).toEqual([{ query: { query: "mint-a,mint-b" } }]); + expect(tokens.map((token) => token.id)).toEqual(["mint-a", "mint-b"]); +}); diff --git a/src/lib/Asset.ts b/src/lib/Asset.ts index bf084d9..664c708 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -1,5 +1,5 @@ import { address } from "@solana/kit"; -import type { JupiterDatapi, JupiterDatapiTypes } from "jupiter-api-ts"; +import type { JupiterApi, JupiterDatapi } from "jupiter-api-ts"; type AssetInfo = { readonly id: ReturnType; @@ -27,21 +27,36 @@ export const Asset = { export type Asset = (typeof Asset)[keyof typeof Asset]; -export type Token = JupiterDatapiTypes.Asset & { decimals: number }; +export type Token = { + id: string; + name: string; + symbol: string; + decimals: number; + usdPrice?: number; + mcap?: number; + [key: string]: unknown; +}; export async function searchTokens(params: { - datapi: JupiterDatapi; + api: JupiterApi; query: string; limit?: number; }): Promise { - const { datapi, ...query } = params; - const response = await datapi.searchAssets({ query }); + const { api, query, limit } = params; + const response = await api.tokens.v2.getTokensV2Search({ + query: { query }, + }); if (response.error || !response.data) { - throw new Error("Data API token search failed."); + throw new Error("Tokens V2 search failed."); } - return response.data.filter( - (asset): asset is Token => typeof asset.decimals === "number" + 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 getTokensByMints( @@ -72,10 +87,10 @@ export async function getTokensByMints( } export async function resolveToken( - datapi: JupiterDatapi, + api: JupiterApi, input: string ): Promise { - const [token] = await searchTokens({ datapi, query: input, limit: 1 }); + const [token] = await searchTokens({ api, query: input, limit: 1 }); if (!token) { throw new Error(`Token not found: ${input}`); } From ddaa58529276bc72d521f4bd74ddbc098332d960 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 16:01:51 +0800 Subject: [PATCH 25/32] refactor: replace parallel API calls with single call --- src/commands/PredictionsCommand.ts | 2 +- src/commands/SpotCommand.ts | 31 +++++++--- src/lib/Asset.test.ts | 97 ++++++++++++++++++++++-------- src/lib/Asset.ts | 32 ++++++++-- 4 files changed, 121 insertions(+), 41 deletions(-) diff --git a/src/commands/PredictionsCommand.ts b/src/commands/PredictionsCommand.ts index c1747d6..d225fed 100644 --- a/src/commands/PredictionsCommand.ts +++ b/src/commands/PredictionsCommand.ts @@ -451,7 +451,7 @@ export class PredictionsCommand { const side = this.normalizeSide(opts.side); const isYes = side === "yes"; const signer = await Signer.load(opts.key ?? Config.load().activeKey); - const token = await resolveToken(this.#api, opts.input); + const token = (await resolveToken(this.#api, [opts.input]))[0]!; const depositAmount = NumberConverter.toChainAmount( opts.amount, token.decimals diff --git a/src/commands/SpotCommand.ts b/src/commands/SpotCommand.ts index b13c0ff..3cd9380 100644 --- a/src/commands/SpotCommand.ts +++ b/src/commands/SpotCommand.ts @@ -188,10 +188,9 @@ export class SpotCommand { requireJupiterApiKey(); Swap.validateAmountOpts(opts); - const [inputToken, outputToken] = await Promise.all([ - resolveToken(this.#api, opts.from), - resolveToken(this.#api, 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), @@ -288,10 +287,9 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - const [inputToken, outputToken] = await Promise.all([ - resolveToken(this.#api, opts.from), - resolveToken(this.#api, 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, @@ -450,7 +448,10 @@ export class SpotCommand { const settings = Config.load(); const signer = await Signer.load(opts.key ?? settings.activeKey); - const token = await resolveToken(this.#api, opts.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 ?? @@ -593,7 +594,10 @@ export class SpotCommand { opts.address ?? (await Signer.loadAddress(opts.key ?? Config.load().activeKey)); const targetAsset = opts.token - ? await resolveToken(this.#api, opts.token) + ? this.requireToken( + (await resolveToken(this.#api, [opts.token]))[0], + opts.token + ) : undefined; const response = await this.#datapi.getUserTransactions({ query: { @@ -926,6 +930,13 @@ export class SpotCommand { ); } + private static requireToken(token: Token | undefined, query: string): Token { + if (!token) { + throw new Error(`Token not found: ${query}`); + } + return token; + } + private static parseTimestamp(value: string): number { if (/^\d+$/.test(value)) { return Number(value) * 1000; diff --git a/src/lib/Asset.test.ts b/src/lib/Asset.test.ts index 3328763..d9df6dc 100644 --- a/src/lib/Asset.test.ts +++ b/src/lib/Asset.test.ts @@ -1,41 +1,90 @@ import { expect, test } from "bun:test"; import type { JupiterApi } from "jupiter-api-ts"; -import { searchTokens } from "./Asset.ts"; +import { resolveToken, searchTokens } from "./Asset.ts"; -test("searchTokens uses Tokens V2 for a comma-separated mint search", async () => { - const calls: unknown[] = []; - const api = { +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) => { - calls.push(request); - return { - data: [ - { - id: "mint-a", - name: "Token A", - symbol: "A", - decimals: 6, - }, - { - id: "mint-b", - name: "Token B", - symbol: "B", - decimals: 9, - }, - ], - }; - }, + 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", - } as never); + }); 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 664c708..4619b69 100644 --- a/src/lib/Asset.ts +++ b/src/lib/Asset.ts @@ -88,13 +88,33 @@ export async function getTokensByMints( export async function resolveToken( api: JupiterApi, - input: string -): Promise { - const [token] = await searchTokens({ api, query: input, limit: 1 }); - if (!token) { - throw new Error(`Token not found: ${input}`); + queries: string[] +): Promise { + if (queries.length === 0) { + throw new Error("Token not found."); } - return token; + + 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] { From 37473aa295f009ca5de11e61459135f49377a5a4 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 16:20:25 +0800 Subject: [PATCH 26/32] feat: add support for negative chain amount --- src/lib/NumberConverter.test.ts | 12 ++++++++++++ src/lib/NumberConverter.ts | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) 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 ?? "") + From c8d648e316dfdcd224c64a7cb48b34c241486b8b Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 16:38:56 +0800 Subject: [PATCH 27/32] chore: fallback to config active key if address is not passed for lend earn commands --- docs/lend.md | 3 +++ src/commands/LendCommand.ts | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/lend.md b/docs/lend.md index 6ee724f..8fb4a6b 100644 --- a/docs/lend.md +++ b/docs/lend.md @@ -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 diff --git a/src/commands/LendCommand.ts b/src/commands/LendCommand.ts index 2ed1b42..d1e37a0 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -150,13 +150,7 @@ export class LendCommand { token?: string; }): Promise { requireJupiterApiKey(); - 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)); + const address = await this.#resolveAddress(opts); const allPositions = await this.#getPositions(address); let activePositions = allPositions.filter((p) => p.shares !== "0"); @@ -172,7 +166,7 @@ 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."); @@ -220,10 +214,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: { @@ -283,6 +281,7 @@ export class LendCommand { if (Output.isJson()) { Output.json({ dryRun: true, + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, @@ -300,6 +299,7 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Deposited", value: `${depositedAmount} ${lendToken.asset.symbol}`, @@ -331,6 +331,7 @@ export class LendCommand { if (Output.isJson()) { Output.json({ + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, @@ -348,6 +349,7 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Deposited", value: `${depositedAmount} ${lendToken.asset.symbol}`, @@ -439,6 +441,7 @@ export class LendCommand { if (Output.isJson()) { Output.json({ dryRun: true, + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, @@ -456,6 +459,7 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Withdrawn", value: `${withdrawnAmount} ${lendToken.asset.symbol}`, @@ -487,6 +491,7 @@ export class LendCommand { if (Output.isJson()) { Output.json({ + address: signer.address, token: { id: lendToken.assetAddress, symbol: lendToken.asset.symbol, @@ -504,6 +509,7 @@ export class LendCommand { Output.table({ type: "vertical", rows: [ + { label: "Address", value: signer.address }, { label: "Withdrawn", value: `${withdrawnAmount} ${lendToken.asset.symbol}`, From be7312d7aa7d773b0bc96842513f2e9e1d2fc381 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 16:37:58 +0800 Subject: [PATCH 28/32] feat: add lend borrow commands --- CLAUDE.md | 6 +- README.md | 6 +- docs/lend.md | 156 ++++++++- llms.txt | 2 +- src/commands/LendCommand.test.ts | 32 ++ src/commands/LendCommand.ts | 554 ++++++++++++++++++++++++++++++- 6 files changed, 747 insertions(+), 9 deletions(-) create mode 100644 src/commands/LendCommand.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 1addcb5..16426c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index e263625..7cda7f0 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,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 @@ -81,7 +85,7 @@ jup vrfd submit --token --project-twitter @projecthandle --descri - [Sign](docs/sign.md): Sign an externally supplied transaction - [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/docs/lend.md b/docs/lend.md index 8fb4a6b..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 @@ -122,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 @@ -140,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/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/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 d1e37a0..4d25105 100644 --- a/src/commands/LendCommand.ts +++ b/src/commands/LendCommand.ts @@ -10,6 +10,12 @@ import { Signer } from "../lib/Signer.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; @@ -33,7 +39,7 @@ export class LendCommand { 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"); @@ -84,6 +90,48 @@ export class LendCommand { 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 { @@ -645,6 +693,510 @@ export class LendCommand { }; } + 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; } From 60fc9dfd1be75d0a3a2325fc7f67d3b1032921d0 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 17:12:05 +0800 Subject: [PATCH 29/32] feat: add dca commands --- README.md | 4 + docs/dca.md | 51 ++++ src/commands/RecurringCommand.test.ts | 40 +++ src/commands/RecurringCommand.ts | 350 ++++++++++++++++++++++++++ src/index.ts | 2 + 5 files changed, 447 insertions(+) create mode 100644 docs/dca.md create mode 100644 src/commands/RecurringCommand.test.ts create mode 100644 src/commands/RecurringCommand.ts diff --git a/README.md b/README.md index 7cda7f0..a5cef22 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ jup spot swap --from SOL --to USDC --amount 1 # Reclaim rent from empty token accounts 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 + # Open a 3x long SOL position with 10 USDC jup perps open --asset SOL --side long --amount 10 --input USDC --leverage 3 # View your perps positions @@ -84,6 +87,7 @@ jup vrfd submit --token --project-twitter @projecthandle --descri - [Keys](docs/keys.md): Private key management - [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 - [Perps](docs/perps.md): Perps trading (leveraged longs/shorts) - [Lend](docs/lend.md): Lending, borrowing, and yield farming - [Predictions](docs/predictions.md): Prediction markets 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/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/index.ts b/src/index.ts index 5f0553e..9bbeaf1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ 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"; @@ -53,6 +54,7 @@ PredictionsCommand.register( solanaRpc, jupiterClients.api ); +RecurringCommand.register(program, solanaRpc, jupiterClients.api); SignCommand.register(program); SpotCommand.register( program, From 44b63aaaf9bce6dc38c6e364fc6df78c6bc75045 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 18:02:07 +0800 Subject: [PATCH 30/32] feat: add signTransactionWith method --- src/lib/Signer.test.ts | 12 ++++++++++++ src/lib/Signer.ts | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) 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 fd39b2b..b791b93 100644 --- a/src/lib/Signer.ts +++ b/src/lib/Signer.ts @@ -97,11 +97,18 @@ export class Signer { public async signTransaction( 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 as Base64EncodedBytes); const decodedTx = getTransactionCodec().decode(txBytes); const signedTx = await partiallySignTransactionWithSigners( - [this.#signer], + signers.map((signer) => signer.#signer), decodedTx ); return getBase64EncodedWireTransaction(signedTx); From 28b659b9bee71b2d7a1940d08e519d8005e70133 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 18:05:05 +0800 Subject: [PATCH 31/32] feat: add invite commands --- README.md | 4 + docs/invite.md | 60 ++++++ src/commands/InviteCommand.test.ts | 52 +++++ src/commands/InviteCommand.ts | 314 +++++++++++++++++++++++++++++ src/index.ts | 2 + 5 files changed, 432 insertions(+) create mode 100644 docs/invite.md create mode 100644 src/commands/InviteCommand.test.ts create mode 100644 src/commands/InviteCommand.ts diff --git a/README.md b/README.md index a5cef22..92740bd 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ 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 + # Open a 3x long SOL position with 10 USDC jup perps open --asset SOL --side long --amount 10 --input USDC --leverage 3 # View your perps positions @@ -88,6 +91,7 @@ jup vrfd submit --token --project-twitter @projecthandle --descri - [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 - [Perps](docs/perps.md): Perps trading (leveraged longs/shorts) - [Lend](docs/lend.md): Lending, borrowing, and yield farming - [Predictions](docs/predictions.md): Prediction markets 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/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/index.ts b/src/index.ts index 9bbeaf1..a237825 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import { ConfigCommand } from "./commands/ConfigCommand.ts"; +import { InviteCommand } from "./commands/InviteCommand.ts"; import { KeysCommand } from "./commands/KeysCommand.ts"; import { LendCommand } from "./commands/LendCommand.ts"; import { PerpsCommand } from "./commands/PerpsCommand.ts"; @@ -47,6 +48,7 @@ program ConfigCommand.register(program); KeysCommand.register(program); +InviteCommand.register(program, solanaRpc, jupiterClients.api); LendCommand.register(program, solanaRpc, jupiterClients.api); PerpsCommand.register(program, jupiterClients.perps, solanaRpc); PredictionsCommand.register( From 70f0d0264dbd81b49436f4421cdcf8a90fe7cf47 Mon Sep 17 00:00:00 2001 From: Chii Yuen Date: Mon, 31 Aug 2026 18:24:28 +0800 Subject: [PATCH 32/32] feat: add jlp commands --- README.md | 4 + docs/jlp.md | 42 +++++ src/commands/JlpCommand.test.ts | 118 +++++++++++++ src/commands/JlpCommand.ts | 302 ++++++++++++++++++++++++++++++++ src/index.ts | 2 + 5 files changed, 468 insertions(+) create mode 100644 docs/jlp.md create mode 100644 src/commands/JlpCommand.test.ts create mode 100644 src/commands/JlpCommand.ts diff --git a/README.md b/README.md index 92740bd..faa6c20 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ 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 # View your perps positions @@ -92,6 +95,7 @@ jup vrfd submit --token --project-twitter @projecthandle --descri - [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, borrowing, and yield farming - [Predictions](docs/predictions.md): Prediction markets 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/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/index.ts b/src/index.ts index a237825..2dc462b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ 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"; @@ -49,6 +50,7 @@ program ConfigCommand.register(program); KeysCommand.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(