diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 99c16bd..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: 2 -updates: - - package-ecosystem: 'npm' - directory: '/' - schedule: - interval: 'weekly' - open-pull-requests-limit: 10 - groups: - dev-dependencies: - dependency-type: 'development' - update-types: - - 'minor' - - 'patch' - - - package-ecosystem: 'github-actions' - directory: '/' - schedule: - interval: 'weekly' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e6f927..ecc5ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,8 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + # Use the latest patches of the supported LTS release lines. + node-version: [22.x, 24.x] steps: - name: Checkout repository @@ -22,6 +23,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + check-latest: true cache: 'npm' - name: Install dependencies @@ -43,7 +45,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '24.x' + check-latest: true cache: 'npm' - name: Install dependencies @@ -51,3 +54,6 @@ jobs: - name: TypeScript type check run: npx tsc --noEmit + + - name: Lint + run: npm run lint diff --git a/README.md b/README.md index 4397ba6..bd274ed 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) ![Network](https://img.shields.io/badge/Network-TRON-red) -![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933) +![Node.js](https://img.shields.io/badge/Node.js-22%20%7C%2024%20LTS-339933) ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6) > A CLI for AI-driven and human-operated DeFi workflows on TRON via the **SUN.IO / SUNSWAP** ecosystem. @@ -57,7 +57,7 @@ npm install -g @sun-protocol/sun-cli ``` -Requires Node.js **20+**. +Requires Node.js **22.23.4+ (22.x)** or **24.22.1+ (24.x)**. Use the latest patch release of either supported LTS line. --- @@ -317,12 +317,17 @@ sun liquidity v3:collect --token-id 123 ```bash sun liquidity v4:mint --token0 TRX --token1 USDT --amount0 1000000 sun liquidity v4:mint --token0 TRX --token1 USDT --amount0 1000000 --create-pool -sun liquidity v4:increase --token-id 123 --token0 TRX --token1 USDT --amount0 500000 -sun liquidity v4:decrease --token-id 123 --liquidity 1000 --token0 TRX --token1 USDT +sun liquidity v4:increase --token-id 123 --amount0 500000 +sun liquidity v4:decrease --token-id 123 --liquidity 1000 sun liquidity v4:collect --token-id 123 sun liquidity v4:info --pm --token-id 123 ``` +For existing V4 positions, the CLI loads the authoritative pool currencies and fee from +`--token-id`. Optional `--token0/--token1` (supplied together) and `--fee` values are treated as +assertions. If the supplied token order is reversed, the CLI maps amount and minimum fields to the +position's on-chain currency order before execution. + ### Protocol & History **Protocol-wide analytics:** diff --git a/package-lock.json b/package-lock.json index 54a6fd5..ee6f73b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,30 @@ { "name": "@sun-protocol/sun-cli", - "version": "1.2.2", + "version": "2.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sun-protocol/sun-cli", - "version": "1.2.2", + "version": "2.0.0-beta.0", "license": "MIT", "dependencies": { "@bankofai/agent-wallet": "^2.3.0", "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", - "@sun-protocol/sun-kit": "^1.2.0", + "@sun-protocol/sun-sdk-api": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-protocols": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-runtime": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v3": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v4": "0.1.1-beta.1", "chalk": "^4.1.2", "cli-table3": "^0.6.5", "commander": "^12.1.0", "dotenv": "^16.4.5", "ora": "^5.4.1", - "tronweb": "^6.2.1" + "tronweb": "^6.5.1" }, "bin": { "sun": "bin/sun" @@ -36,22 +42,25 @@ "ts-node": "^10.9.2", "typescript": "^5.0.0", "typescript-eslint": "^8.56.1" + }, + "engines": { + "node": "^22.23.4 || ^24.22.1" } }, "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -60,9 +69,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -70,21 +79,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -101,14 +110,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -118,14 +127,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -135,9 +144,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -145,29 +154,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -187,9 +196,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -197,9 +206,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -207,9 +216,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -217,27 +226,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -498,33 +507,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -532,14 +541,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -707,16 +716,16 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@eslint/config-array/node_modules/minimatch": { @@ -806,30 +815,583 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/address/node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash/node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/hash/node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", + "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions/node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/web/node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2027,21 +2589,109 @@ "node": ">=20" } }, - "node_modules/@sun-protocol/sun-kit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@sun-protocol/sun-kit/-/sun-kit-1.2.0.tgz", - "integrity": "sha512-UH5LIxC6WRno+OYVHSyCACqyujh8de0PQYRRzQV/IUxXxpsQADIVnO8w1Awn7ENbkN4imZh+9O2IkT+l4AP6qA==", - "license": "MIT", + "node_modules/@sun-protocol/sun-sdk-api": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-api/-/sun-sdk-api-0.1.1-beta.1.tgz", + "integrity": "sha512-kGxmCxe1pSSOtb4Zm1cuqrT/hRjIhao3dOPsM5shea/8Y0ZsPeggQFPH9zEcXa3/3gBIoSDvssqfH9ORkfdYzw==", + "dependencies": { + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-chains": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-chains/-/sun-sdk-chains-0.1.1-beta.1.tgz", + "integrity": "sha512-4ywzbFJksW1U7zgb7/MRxmj8CKnEgFRiaGobigfjV9BAeCIdPk6zNxrTs8z/z/p5iswXJgLAR5IsQvIFd1pT5w==", + "dependencies": { + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-core": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-core/-/sun-sdk-core-0.1.1-beta.1.tgz", + "integrity": "sha512-WzQLbOtLJA7vwt3IGpMVi6PjYV1mdNtUkxMHSlscAICnQ41MErhkDuTqpK77ixVALKXHq9PemKdZk04NmacpYA==", + "dependencies": { + "@ethersproject/address": "^5.0.2", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/strings": "5.7.0", + "big.js": "^5.2.2", + "decimal.js-light": "^2.5.0", + "jsbi": "^3.1.4", + "tiny-invariant": "^1.1.0", + "toformat": "^2.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-launchpad": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-launchpad/-/sun-sdk-launchpad-0.1.1-beta.1.tgz", + "integrity": "sha512-COt7mTPixaOAGcBYqrrcqupqipOOvunyhGYhRl5J5Qv23/oaxXvitILANGug70osXb+JJYFS/4vru+xGIGfgVQ==", + "dependencies": { + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-runtime": "0.1.1-beta.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-protocols": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-protocols/-/sun-sdk-protocols-0.1.1-beta.1.tgz", + "integrity": "sha512-hrz9+fdGyU2DUVmurx3JhS280ZYVR9QVquzo2q4NV+at6xyJCFAB6tq8t0lmkNocC8p4v0hcyFbciUmIVOq81w==", "dependencies": { "@sun-protocol/permit2-sdk": "^1.0.0", - "@sun-protocol/universal-router-sdk": "^1.0.1", - "viem": "^2.0.0" + "@sun-protocol/sun-sdk-api": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-launchpad": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-psm": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-runtime": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v2": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v3": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v4": "0.1.1-beta.1", + "@sun-protocol/universal-router-sdk": "^1.1.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-psm": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-psm/-/sun-sdk-psm-0.1.1-beta.1.tgz", + "integrity": "sha512-3CIgb2/4p8wOZPsy6wXayqVa19QWtTZ4awhhzVcU/6rcPaLtcKGjfC5+m4m5VoICtEGhnWpbZ7IniRp31i4nxg==", + "dependencies": { + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-runtime": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-runtime/-/sun-sdk-runtime-0.1.1-beta.1.tgz", + "integrity": "sha512-NzvEZfwEEkg/1RtX5y93rhahGjRmAYWWVwPpkskk4v7xZVt5nT3eDtceoWA3aTUniy0K5UgUB0Fq8vQh6reVBg==", + "dependencies": { + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "tronweb": "^6.2.1" + "tronweb": "*" }, "peerDependenciesMeta": { "tronweb": { @@ -2049,16 +2699,182 @@ } } }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v2": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-sunswap-v2/-/sun-sdk-sunswap-v2-0.1.1-beta.1.tgz", + "integrity": "sha512-eoaiz/c9feModOLXfvkX7MqWtUVghE/d4LyyHM/C5oIxjCMniNfz1TZC0CMw3NyhrEVlyfFNDj2SeBOqn7UBpQ==", + "dependencies": { + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@noble/hashes": "1.4.0", + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1", + "jsbi": "^3.1.4", + "tiny-invariant": "^1.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v2/node_modules/@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v2/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v3": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-sunswap-v3/-/sun-sdk-sunswap-v3-0.1.1-beta.1.tgz", + "integrity": "sha512-ohcclFvUXC61NfzPzaSkm4hBP2Xacwm5bZdkDkyUPrpenOPZPFzQTHPXtoRAxPz7ptHDj+pUaF+2mt96VNjcuQ==", + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@noble/hashes": "1.4.0", + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1", + "jsbi": "^3.1.4", + "tiny-invariant": "^1.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v3/node_modules/@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v3/node_modules/@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v3/node_modules/@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v3/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v4": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/sun-sdk-sunswap-v4/-/sun-sdk-sunswap-v4-0.1.1-beta.1.tgz", + "integrity": "sha512-c9ioHOfLYy/9BHmP1H/psQdmeejOcykJaWRkl3nSYWrMxMhEOlfR13vzHyd24jlrR5N/L+wDHGQHLvbNM7kNyA==", + "dependencies": { + "@ethersproject/keccak256": "5.7.0", + "@noble/hashes": "1.4.0", + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sun-protocol/sun-sdk-sunswap-v4/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sun-protocol/universal-router-sdk": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@sun-protocol/universal-router-sdk/-/universal-router-sdk-1.0.1.tgz", - "integrity": "sha512-DdwRXUZjzYQctjoxmnogjdSeyJhCo0X9aO39ntS14nR8xuqrJcQPg4DYrcaM+RlHDRXyWA/XGnr2jahV91wDnw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@sun-protocol/universal-router-sdk/-/universal-router-sdk-1.1.1.tgz", + "integrity": "sha512-9OSxI7UG17KL81xs8XNObQ7WRKjM/DSf7hoIrtr+lxAMVTr+FxSWTHang3q58nmEZB2yEAf9Cb5kfGkRx0bJoQ==", "license": "MIT", "dependencies": { "@pancakeswap/swap-sdk-core": "^1.1.0", "@sun-protocol/permit2-sdk": "^1.0.0", - "decimal.js": "^10.6.0", - "tronweb": "^6.0.4", "viem": "^2.37.5" }, "engines": { @@ -2479,16 +3295,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -2901,6 +3717,18 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -2989,9 +3817,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3025,14 +3853,15 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-jest": { @@ -3168,9 +3997,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", - "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3245,19 +4074,25 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -3275,11 +4110,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -3409,9 +4244,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001778", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", - "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", "dev": true, "funding": [ { @@ -3694,7 +4529,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3708,12 +4542,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -3839,12 +4667,33 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.313", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", - "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "version": "1.5.451", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.451.tgz", + "integrity": "sha512-vUNKPrUzw8ByEMOxloq0a71iD/mPuPxVkbDm3G5YhnxTzOw/I6qAg2MUS0cYj/c267lWmw26yW4Vv7foxTxPLg==", "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -3893,9 +4742,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4100,16 +4949,16 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/eslint/node_modules/escape-string-regexp": { @@ -4341,9 +5190,9 @@ } }, "node_modules/ethers": { - "version": "6.13.5", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz", - "integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", "funding": [ { "type": "individual", @@ -4356,13 +5205,13 @@ ], "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "1.10.1", + "@adraffy/ens-normalize": "1.11.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", - "ws": "8.17.1" + "ws": "8.21.0" }, "engines": { "node": ">=14.0.0" @@ -4608,16 +5457,16 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "funding": [ { "type": "individual", @@ -4652,16 +5501,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -4839,13 +5688,13 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", + "minimist": "^1.2.8", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" @@ -4896,10 +5745,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4908,6 +5767,17 @@ "node": ">= 0.4" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4915,6 +5785,19 @@ "dev": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5281,9 +6164,9 @@ "license": "MIT" }, "node_modules/jayson/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -5893,6 +6776,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5901,9 +6790,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -5914,6 +6803,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", + "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==", + "license": "Apache-2.0" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6153,6 +7048,18 @@ "node": ">=6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -6274,11 +7181,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -6391,9 +7301,9 @@ } }, "node_modules/ox": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.0.tgz", - "integrity": "sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.54.tgz", + "integrity": "sha512-52PGH6ldJmWY9+iy/TBSJUsCSy5NmLvkVBYTINmO8srsjRrCmCdO575kGizmZ1jpNQ1gnlpVwiciIpU37fcULA==", "funding": [ { "type": "github", @@ -6420,12 +7330,6 @@ } } }, - "node_modules/ox/node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", - "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", - "license": "MIT" - }, "node_modules/ox/node_modules/@noble/curves": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", @@ -6620,9 +7524,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -6723,10 +7627,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -6891,9 +7798,9 @@ } }, "node_modules/rpc-websockets/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -7286,9 +8193,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -7385,19 +8292,19 @@ "license": "MIT" }, "node_modules/tronweb": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/tronweb/-/tronweb-6.2.2.tgz", - "integrity": "sha512-jRBf4+7fJ0HUVzveBi0tE21r3EygCNtbYE92T38Sxlwr/x320W2vz+dvGLOIpp4kW/CvJ4HLvtnb6U30A0V2eA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/tronweb/-/tronweb-6.5.1.tgz", + "integrity": "sha512-mqa3WIBq7sYtkkIywa2Jop3UwJX6EgIrBkFpm3sSjppStTOTtYeMJyeIHJVvFFCaYN4vjU1FDjQPSvHFTcuzEw==", "license": "MIT", "dependencies": { - "@babel/runtime": "7.26.10", - "axios": "1.13.5", + "axios": "1.18.0", "bignumber.js": "9.1.2", "ethereum-cryptography": "2.2.1", - "ethers": "6.13.5", + "ethers": "6.17.0", "eventemitter3": "5.0.1", "google-protobuf": "3.21.4", "semver": "7.7.1", + "tslib": "2.7.0", "validator": "13.15.23" } }, @@ -7684,9 +8591,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dev": true, "funding": [ { @@ -7785,9 +8692,9 @@ } }, "node_modules/viem": { - "version": "2.47.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.2.tgz", - "integrity": "sha512-etDIwDgmDiGaPg8rUbJtUFuC3/nAJCbhMYyfh5dOcqNNkzBWTNcS2VluPSM5JVo+9U3b2hle2RkBEq3+xyvlvg==", + "version": "2.57.4", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.57.4.tgz", + "integrity": "sha512-ro+8AKrcGU6tPfou0gBRYv6bxnU3tPxxJGY4qpJwjiR40eHCWzeGhrAgYMlIncFtlnry2/yGjz57WOC67oshCQ==", "funding": [ { "type": "github", @@ -7802,8 +8709,8 @@ "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", - "ox": "0.14.0", - "ws": "8.18.3" + "ox": "0.14.54", + "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" @@ -7865,27 +8772,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -8074,9 +8960,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 94be3e2..03dcfab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sun-protocol/sun-cli", - "version": "1.2.2", + "version": "2.0.0-beta.0", "description": "CLI tool for SUN.IO / SUNSWAP on TRON — for humans and AI agents", "main": "dist/bin.js", "type": "commonjs", @@ -8,7 +8,7 @@ "sun": "./bin/sun" }, "scripts": { - "build": "tsc", + "build": "npm run clean && tsc", "dev": "ts-node src/bin.ts", "start": "node dist/bin.js", "clean": "rm -rf dist", @@ -17,7 +17,7 @@ "lint:fix": "eslint . --fix", "format": "prettier --write .", "format:check": "prettier --check .", - "prepublishOnly": "npm run clean && npm run build", + "prepublishOnly": "npm run build", "publish:beta": "npm publish --tag beta --access public", "publish:latest": "npm publish --access public" }, @@ -37,6 +37,9 @@ "ai-agent" ], "license": "MIT", + "engines": { + "node": "^22.23.4 || ^24.22.1" + }, "overrides": { "utf-8-validate": "5.0.10" }, @@ -44,13 +47,19 @@ "@bankofai/agent-wallet": "^2.3.0", "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", - "@sun-protocol/sun-kit": "^1.2.0", + "@sun-protocol/sun-sdk-api": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-chains": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-core": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-protocols": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-runtime": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v3": "0.1.1-beta.1", + "@sun-protocol/sun-sdk-sunswap-v4": "0.1.1-beta.1", "chalk": "^4.1.2", "cli-table3": "^0.6.5", "commander": "^12.1.0", "dotenv": "^16.4.5", "ora": "^5.4.1", - "tronweb": "^6.2.1" + "tronweb": "^6.5.1" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/commands/contract.ts b/src/commands/contract.ts index 05c9548..cf4e4dc 100644 --- a/src/commands/contract.ts +++ b/src/commands/contract.ts @@ -1,6 +1,15 @@ import { Command } from 'commander' import { getNetwork } from '../lib/context' -import { writeAction, readAction } from '../lib/command' +import { writeRuntimeAction as writeAction, readRuntimeAction as readAction } from '../lib/command' +import { toCliTxResult } from '../lib/sdk/compat' + +function parseAbi(value: string | undefined): any[] | undefined { + return value === undefined ? undefined : JSON.parse(value) +} + +function parseValue(value: string | undefined): bigint | undefined { + return value === undefined ? undefined : BigInt(value) +} export function registerContractCommands(program: Command) { const contract = program @@ -16,16 +25,13 @@ export function registerContractCommands(program: Command) { await readAction({ spinnerLabel: `Reading ${functionName}...`, errorLabel: 'Contract read failed', - execute: (kit) => - kit.readContract( - { - address, - functionName, - args: JSON.parse(opts.args), - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - }, - getNetwork(), - ), + execute: (sdk) => + sdk.runtime.contract.read({ + address, + functionName, + args: JSON.parse(opts.args), + abi: parseAbi(opts.abi), + }), transform: (result) => ({ result }), }) }) @@ -50,14 +56,15 @@ export function registerContractCommands(program: Command) { spinnerLabel: `Sending ${functionName}...`, errorLabel: 'Contract send failed', execute: (kit) => - kit.sendContractTx({ - address, - functionName, - args: JSON.parse(opts.args), - value: opts.value, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - network: getNetwork(), - }), + kit.runtime.contract + .write({ + address, + functionName, + args: JSON.parse(opts.args), + callValue: parseValue(opts.value), + abi: parseAbi(opts.abi), + }) + .then(toCliTxResult), }) }) } diff --git a/src/commands/liquidity.ts b/src/commands/liquidity.ts index cd14636..2bd8255 100644 --- a/src/commands/liquidity.ts +++ b/src/commands/liquidity.ts @@ -1,56 +1,225 @@ import { Command } from 'commander' import { getNetwork } from '../lib/context' import { writeAction, readAction } from '../lib/command' -import { withSpinner } from '../lib/output' +import { outputError, withSpinner } from '../lib/output' import { resolveTokenAddress, getSymbolOrAddress } from '../lib/tokens' +import { getContractAddress } from '@sun-protocol/sun-sdk-chains' +import type { Network } from '@sun-protocol/sun-sdk-core' +import type { V4PositionSnapshot } from '@sun-protocol/sun-sdk-protocols' +import type { V4PoolKey } from '@sun-protocol/sun-sdk-sunswap-v4' +import { TronWeb } from 'tronweb' import { - SUNSWAP_V2_MAINNET_ROUTER, - SUNSWAP_V2_NILE_ROUTER, - SUNSWAP_V2_MAINNET_FACTORY, - SUNSWAP_V2_NILE_FACTORY, - SUNSWAP_V2_FACTORY_MIN_ABI, - SUNSWAP_V2_PAIR_MIN_ABI, - SUNSWAP_V3_MAINNET_POSITION_MANAGER, - SUNSWAP_V3_NILE_POSITION_MANAGER, + DEFAULT_V3_FEE, + DEFAULT_V4_FEE, TRX_ADDRESS, WTRX_MAINNET, WTRX_NILE, - createReadonlyTronWeb, -} from '@sun-protocol/sun-kit' +} from '../lib/sdk/constants' +import { createReadonlyTronWeb, withLiquidityAbi } from '../lib/sdk/factory' +import { toCliTxResult } from '../lib/sdk/compat' +import { resolveLegacyV3Pool } from '../lib/sdk/liquidity-v3' +import { resolveV4MintTicks, v4TickSpacingForFee } from '../lib/sdk/liquidity-v4' +import type { CliSunSDK } from '../lib/sdk/types' +import { parseDecimalSlippage, type ParsedDecimalSlippage } from '../lib/slippage' + +const SUNSWAP_V2_FACTORY_MIN_ABI = [ + { + constant: true, + inputs: [ + { name: 'tokenA', type: 'address' }, + { name: 'tokenB', type: 'address' }, + ], + name: 'getPair', + outputs: [{ name: 'pair', type: 'address' }], + type: 'function', + }, +] + +const SUNSWAP_V2_PAIR_MIN_ABI = [ + { + constant: true, + inputs: [], + name: 'getReserves', + outputs: [ + { name: '_reserve0', type: 'uint112' }, + { name: '_reserve1', type: 'uint112' }, + { name: '_blockTimestampLast', type: 'uint32' }, + ], + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'token0', + outputs: [{ name: '', type: 'address' }], + type: 'function', + }, +] + +function assertSdkNetwork(network: string): Network { + if (network === 'mainnet' || network === 'nile') return network + throw new Error(`Unsupported SDK network: ${network}`) +} -const V2_ROUTERS: Record = { - mainnet: SUNSWAP_V2_MAINNET_ROUTER, - nile: SUNSWAP_V2_NILE_ROUTER, +function getV2Router(network: string, override?: string): string { + if (override) return override + return getContractAddress(assertSdkNetwork(network), 'sunswapV2Router') } -const V2_FACTORIES: Record = { - mainnet: SUNSWAP_V2_MAINNET_FACTORY, - nile: SUNSWAP_V2_NILE_FACTORY, +function getV3PositionManager(network: string, override?: string): string { + if (override) return override + return getContractAddress(assertSdkNetwork(network), 'sunswapV3PositionManager') } -const V3_POSITION_MANAGERS: Record = { - mainnet: SUNSWAP_V3_MAINNET_POSITION_MANAGER, - nile: SUNSWAP_V3_NILE_POSITION_MANAGER, +function getV2Factory(network: string): string { + return getContractAddress(assertSdkNetwork(network), 'sunswapV2Factory') } -function getV2Router(network: string, override?: string): string { - if (override) return override - const router = V2_ROUTERS[network] - if (!router) { - throw new Error(`V2 router not configured for network: ${network}. Use --router to specify.`) +function parseOptionalSlippage(value: string | undefined): ParsedDecimalSlippage | undefined { + return value === undefined ? undefined : parseDecimalSlippage(value) +} + +function invalidParamsError(message: string): Error & { code: string } { + return Object.assign(new Error(message), { code: 'INVALID_PARAMS' }) +} + +function assertLiquidityAmounts(amount0: string | undefined, amount1: string | undefined): void { + if (amount0 === undefined && amount1 === undefined) { + throw invalidParamsError('At least one of --amount0 or --amount1 is required') + } + + for (const [name, value] of [ + ['--amount0', amount0], + ['--amount1', amount1], + ] as const) { + if (value !== undefined && !/^\d+$/.test(value)) { + throw invalidParamsError(`${name} must be a non-negative integer in base units`) + } + } + + if (BigInt(amount0 ?? '0') === 0n && BigInt(amount1 ?? '0') === 0n) { + throw invalidParamsError('At least one of --amount0 or --amount1 must be greater than zero') } - return router } -function getV3PositionManager(network: string, override?: string): string { - if (override) return override - const pm = V3_POSITION_MANAGERS[network] - if (!pm) { - throw new Error( - `V3 position manager not configured for network: ${network}. Use --pm to specify.`, - ) +interface OptionalV4Pair { + token0?: string + token1?: string +} + +interface PreparedV4Position { + poolKey: V4PoolKey + reversed: boolean +} + +function resolveOptionalV4Pair( + token0Input: string | undefined, + token1Input: string | undefined, + network: string, +): OptionalV4Pair { + if ((token0Input === undefined) !== (token1Input === undefined)) { + throw invalidParamsError('--token0 and --token1 must be provided together or both omitted') + } + if (token0Input === undefined || token1Input === undefined) return {} + return { + token0: resolveTokenAddress(token0Input, network), + token1: resolveTokenAddress(token1Input, network), + } +} + +function parseOptionalV4Fee(value: string | undefined): number | undefined { + if (value === undefined) return undefined + if (!/^\d+$/.test(value)) throw invalidParamsError('--fee must be a non-negative integer') + const fee = Number(value) + if (!Number.isSafeInteger(fee) || fee > 0xffffff) { + throw invalidParamsError('--fee must fit in an unsigned 24-bit integer') + } + return fee +} + +function prepareV4Position( + snapshot: V4PositionSnapshot, + expectedPair: OptionalV4Pair, + expectedFee: number | undefined, +): PreparedV4Position { + const poolKey = snapshot.poolKey + if (!poolKey) { + throw Object.assign(new Error('V4 position does not include its pool information'), { + code: 'SDK_CONTRACT_ERROR', + }) + } + + if (expectedFee !== undefined && poolKey.fee !== expectedFee) { + throw invalidParamsError(`--fee ${expectedFee} does not match position pool fee ${poolKey.fee}`) + } + + if (expectedPair.token0 === undefined || expectedPair.token1 === undefined) { + return { poolKey, reversed: false } + } + if (expectedPair.token0 === expectedPair.token1) { + throw invalidParamsError('--token0 and --token1 must be different currencies') + } + if (expectedPair.token0 === poolKey.currency0 && expectedPair.token1 === poolKey.currency1) { + return { poolKey, reversed: false } + } + if (expectedPair.token0 === poolKey.currency1 && expectedPair.token1 === poolKey.currency0) { + return { poolKey, reversed: true } + } + throw invalidParamsError('Provided --token0/--token1 do not match the tokenId position') +} + +function v4PositionTokenSummary(poolKey: V4PoolKey, network: string) { + return { + Token0: `${getSymbolOrAddress(poolKey.currency0, network)} (${poolKey.currency0})`, + Token1: `${getSymbolOrAddress(poolKey.currency1, network)} (${poolKey.currency1})`, + Fee: poolKey.fee, + } +} + +function orderV4PositionValues(reversed: boolean, value0: T, value1: T): [T, T] { + return reversed ? [value1, value0] : [value0, value1] +} + +function createV4PositionLoader( + tokenId: string, + expectedPair: OptionalV4Pair, + expectedFee: number | undefined, +): (sdk: CliSunSDK) => Promise { + let pending: Promise | undefined + return (sdk) => { + pending ??= sdk.positions.v4 + .read({ tokenId }) + .then((snapshot) => prepareV4Position(snapshot, expectedPair, expectedFee)) + return pending + } +} + +function toV3PoolOrder(token0: string, token1: string, value0: T, value1: T): [T, T] { + const token0Hex = TronWeb.address.toHex(token0).toLowerCase() + const token1Hex = TronWeb.address.toHex(token1).toLowerCase() + return token0Hex < token1Hex ? [value0, value1] : [value1, value0] +} + +function withContracts( + overrides: Record, +): Record | undefined { + const entries = Object.entries(overrides).filter((entry): entry is [string, string] => !!entry[1]) + return entries.length ? Object.fromEntries(entries) : undefined +} + +function mapTx(result: unknown, reverseAmounts = false): unknown { + const mapped = toCliTxResult(result) as Record + const amounts = mapped?.computedAmounts as + | { amount0Desired: string; amount1Desired: string } + | undefined + if (!reverseAmounts || !amounts) return mapped + return { + ...mapped, + computedAmounts: { + amount0Desired: amounts.amount1Desired, + amount1Desired: amounts.amount0Desired, + }, } - return pm } function toWTRXIfNative(tokenAddress: string, network: string): string { @@ -74,23 +243,40 @@ interface PairReserves { pairExists: boolean } +export function unwrapContractAddressResult(result: unknown, label: string): string { + const value = Array.isArray(result) + ? result[0] + : result && typeof result === 'object' + ? Object.values(result as Record)[0] + : result + if (typeof value !== 'string') throw new Error(`${label} did not return an address`) + return value +} + +function toBase58ContractAddress( + tronWeb: ReturnType, + result: unknown, + label: string, +): string { + const value = unwrapContractAddressResult(result, label) + if (value.startsWith('T')) return value + return tronWeb.address.fromHex(value.startsWith('0x') ? `41${value.slice(2)}` : value) +} + async function getV2PairReserves( network: string, tokenA: string, tokenB: string, ): Promise { - const tronWeb = await createReadonlyTronWeb(network) - const factoryAddress = V2_FACTORIES[network] - if (!factoryAddress) { - throw new Error(`V2 factory not configured for network: ${network}`) - } + const tronWeb = createReadonlyTronWeb({ network }) + const factoryAddress = getV2Factory(network) const lookupA = getLookupToken(tokenA, network) const lookupB = getLookupToken(tokenB, network) const factory = await tronWeb.contract(SUNSWAP_V2_FACTORY_MIN_ABI as any, factoryAddress) - const pairHex = await factory.getPair(lookupA, lookupB).call() - const pairBase58 = tronWeb.address.fromHex(pairHex) + const pairResult = await factory.getPair(lookupA, lookupB).call() + const pairBase58 = toBase58ContractAddress(tronWeb, pairResult, 'V2 factory getPair') const zeroBase58 = tronWeb.address.fromHex('410000000000000000000000000000000000000000') if (!pairBase58 || pairBase58 === zeroBase58) { @@ -99,9 +285,9 @@ async function getV2PairReserves( const pair = await tronWeb.contract(SUNSWAP_V2_PAIR_MIN_ABI as any, pairBase58) const reserves = await pair.getReserves().call() - const token0Hex = await pair.token0().call() + const token0Result = await pair.token0().call() - const token0 = tronWeb.address.fromHex(token0Hex) + const token0 = toBase58ContractAddress(tronWeb, token0Result, 'V2 pair token0') const reserve0 = (reserves._reserve0 ?? reserves[0]).toString() const reserve1 = (reserves._reserve1 ?? reserves[1]).toString() @@ -212,20 +398,21 @@ export function registerLiquidityCommands(program: Command) { confirmMsg: 'Add V2 liquidity?', spinnerLabel: 'Adding V2 liquidity...', errorLabel: 'V2 add liquidity failed', - execute: (kit) => - kit.addLiquidityV2({ - network, - routerAddress: router, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - tokenA, - tokenB, - amountADesired: amountA, - amountBDesired: amountB, - amountAMin: opts.minA, - amountBMin: opts.minB, - to: opts.to, - deadline: opts.deadline, - }), + execute: async (client) => + (await withLiquidityAbi(client, router, opts.abi)).liquidity.v2.add + .execute({ + contracts: withContracts({ sunswapV2Router: router }), + tokenA, + tokenB, + amountADesired: amountA, + amountBDesired: amountB, + amountAMin: opts.minA, + amountBMin: opts.minB, + recipient: opts.to, + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then((result) => mapTx(result)), }) }) @@ -270,19 +457,20 @@ export function registerLiquidityCommands(program: Command) { confirmMsg: 'Remove V2 liquidity?', spinnerLabel: 'Removing V2 liquidity...', errorLabel: 'V2 remove liquidity failed', - execute: (kit) => - kit.removeLiquidityV2({ - network, - routerAddress: router, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - tokenA, - tokenB, - liquidity: opts.liquidity, - amountAMin: opts.minA, - amountBMin: opts.minB, - to: opts.to, - deadline: opts.deadline, - }), + execute: async (sdk) => + (await withLiquidityAbi(sdk, router, opts.abi)).liquidity.v2.remove + .execute({ + contracts: withContracts({ sunswapV2Router: router }), + tokenA, + tokenB, + liquidity: opts.liquidity, + amountAMin: opts.minA, + amountBMin: opts.minB, + recipient: opts.to, + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then((result) => mapTx(result)), }) }) @@ -296,7 +484,7 @@ export function registerLiquidityCommands(program: Command) { .option('--pm
', 'V3 NonfungiblePositionManager address (auto-detected by network)') .requiredOption('--token0 ', 'Token0 (symbol like TRX/USDT or address)') .requiredOption('--token1 ', 'Token1 (symbol like TRX/USDT or address)') - .option('--fee ', 'Pool fee tier (default: 3000)') + .option('--fee ', `Pool fee tier (default: ${DEFAULT_V3_FEE})`) .option('--tick-lower ', 'Lower tick (auto-computed if omitted)') .option('--tick-upper ', 'Upper tick (auto-computed if omitted)') .option('--amount0 ', 'Desired amount of token0') @@ -323,9 +511,10 @@ export function registerLiquidityCommands(program: Command) { const token0 = toWTRXIfNative(token0Input, network) const token1 = toWTRXIfNative(token1Input, network) - if (!opts.amount0 && !opts.amount1) { - console.error('Error: At least one of --amount0 or --amount1 is required') - process.exitCode = 1 + try { + assertLiquidityAmounts(opts.amount0, opts.amount1) + } catch (err) { + outputError('Invalid liquidity amounts', err) return } @@ -336,6 +525,8 @@ export function registerLiquidityCommands(program: Command) { const tickRange = opts.tickLower && opts.tickUpper ? `[${opts.tickLower}, ${opts.tickUpper}]` : '(auto)' + const [amount0, amount1] = toV3PoolOrder(token0, token1, opts.amount0, opts.amount1) + const [amount0Min, amount1Min] = toV3PoolOrder(token0, token1, opts.min0, opts.min1) await writeAction({ title: 'V3 Mint Position', @@ -343,32 +534,42 @@ export function registerLiquidityCommands(program: Command) { 'Position Manager': pm, Token0: `${token0Display}${token0Note} (${token0})`, Token1: `${token1Display}${token1Note} (${token1})`, - Fee: opts.fee || '3000 (default)', + Fee: opts.fee || `${DEFAULT_V3_FEE} (default)`, 'Tick Range': tickRange, - Amount0: opts.amount0 || '(auto)', - Amount1: opts.amount1 || '(auto)', + Amount0: opts.amount0 ?? '(not provided)', + Amount1: opts.amount1 ?? '(not provided)', Network: network, }, confirmMsg: 'Mint V3 position?', spinnerLabel: 'Minting V3 position...', errorLabel: 'V3 mint failed', - execute: (kit) => - kit.mintPositionV3({ - network, - positionManagerAddress: pm, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - token0, - token1, - fee: opts.fee ? parseInt(opts.fee) : undefined, - tickLower: opts.tickLower ? parseInt(opts.tickLower) : undefined, - tickUpper: opts.tickUpper ? parseInt(opts.tickUpper) : undefined, - amount0Desired: opts.amount0, - amount1Desired: opts.amount1, - amount0Min: opts.min0, - amount1Min: opts.min1, - recipient: opts.recipient, - deadline: opts.deadline, - }), + execute: async (sdk) => + (await withLiquidityAbi(sdk, pm, opts.abi)).liquidity.v3.add + .execute({ + contracts: withContracts({ sunswapV3PositionManager: pm }), + tokenA: token0, + tokenB: token1, + fee: opts.fee ? parseInt(opts.fee) : DEFAULT_V3_FEE, + pool: await resolveLegacyV3Pool(sdk, { + token0, + token1, + fee: opts.fee ? parseInt(opts.fee) : DEFAULT_V3_FEE, + }), + tickLower: opts.tickLower ? parseInt(opts.tickLower) : undefined, + tickUpper: opts.tickUpper ? parseInt(opts.tickUpper) : undefined, + tickRangeMode: 'legacy', + includeComputedMetadata: true, + tickRangeWidth: + opts.tickLower === undefined || opts.tickUpper === undefined ? 100 : undefined, + amount0, + amount1, + amount0Min, + amount1Min, + recipient: opts.recipient, + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then((result) => mapTx(result, toV3PoolOrder(token0, token1, false, true)[0])), summarizeResult: (result: any) => { const out: Record = {} if (result?.computedTicks) { @@ -400,9 +601,10 @@ export function registerLiquidityCommands(program: Command) { const network = getNetwork() const pm = getV3PositionManager(network, opts.pm) - if (!opts.amount0 && !opts.amount1) { - console.error('Error: At least one of --amount0 or --amount1 is required') - process.exitCode = 1 + try { + assertLiquidityAmounts(opts.amount0, opts.amount1) + } catch (err) { + outputError('Invalid liquidity amounts', err) return } @@ -411,25 +613,44 @@ export function registerLiquidityCommands(program: Command) { summary: { 'Position Manager': pm, 'Token ID': opts.tokenId, - Amount0: opts.amount0 || '(auto)', - Amount1: opts.amount1 || '(auto)', + Amount0: opts.amount0 ?? '(not provided)', + Amount1: opts.amount1 ?? '(not provided)', Network: network, }, confirmMsg: 'Increase V3 liquidity?', spinnerLabel: 'Increasing V3 liquidity...', errorLabel: 'V3 increase liquidity failed', - execute: (kit) => - kit.increaseLiquidityV3({ - network, - positionManagerAddress: pm, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - tokenId: opts.tokenId, - amount0Desired: opts.amount0, - amount1Desired: opts.amount1, - amount0Min: opts.min0, - amount1Min: opts.min1, - deadline: opts.deadline, - }), + execute: async (sdk) => + ( + await withLiquidityAbi( + sdk, + pm, + opts.abi, + BigInt(opts.amount0 ?? '0') === 0n || BigInt(opts.amount1 ?? '0') === 0n + ? ['positions(uint256)'] + : [], + ) + ).liquidity.v3.increase + .execute({ + contracts: withContracts({ sunswapV3PositionManager: pm }), + tokenId: opts.tokenId, + amount0: opts.amount0, + amount1: opts.amount1, + includeComputedMetadata: true, + pool: + BigInt(opts.amount0 ?? '0') === 0n || BigInt(opts.amount1 ?? '0') === 0n + ? await resolveLegacyV3Pool(sdk, { + tokenId: opts.tokenId, + positionManager: pm, + positionAbi: opts.abi, + }) + : undefined, + amount0Min: opts.min0, + amount1Min: opts.min1, + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then(mapTx), summarizeResult: (result: any) => { const out: Record = {} if (result?.computedAmounts) { @@ -467,17 +688,19 @@ export function registerLiquidityCommands(program: Command) { confirmMsg: 'Decrease V3 liquidity?', spinnerLabel: 'Decreasing V3 liquidity...', errorLabel: 'V3 decrease liquidity failed', - execute: (kit) => - kit.decreaseLiquidityV3({ - network, - positionManagerAddress: pm, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - tokenId: opts.tokenId, - liquidity: opts.liquidity, - amount0Min: opts.min0, - amount1Min: opts.min1, - deadline: opts.deadline, - }), + execute: async (sdk) => + (await withLiquidityAbi(sdk, pm, opts.abi)).positions.v3.remove + .execute({ + contracts: withContracts({ sunswapV3PositionManager: pm }), + tokenId: opts.tokenId, + liquidity: opts.liquidity, + amount0Min: opts.min0, + amount1Min: opts.min1, + collectFees: false, + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then(mapTx), }) }) @@ -503,14 +726,15 @@ export function registerLiquidityCommands(program: Command) { confirmMsg: 'Collect V3 fees?', spinnerLabel: 'Collecting V3 fees...', errorLabel: 'V3 collect failed', - execute: (kit) => - kit.collectPositionV3({ - network, - positionManagerAddress: pm, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - tokenId: opts.tokenId, - recipient: opts.recipient, - }), + execute: async (sdk) => + (await withLiquidityAbi(sdk, pm, opts.abi)).positions.v3.collect + .execute({ + contracts: withContracts({ sunswapV3PositionManager: pm }), + tokenId: opts.tokenId, + recipient: opts.recipient, + estimateFees: true, + }) + .then(mapTx), summarizeResult: (result: any) => { const out: Record = {} if (result?.estimatedFees) { @@ -532,7 +756,7 @@ export function registerLiquidityCommands(program: Command) { .description('Mint a new SUNSWAP V4 concentrated liquidity position') .requiredOption('--token0 ', 'Token0 (symbol like TRX/USDT or address)') .requiredOption('--token1 ', 'Token1 (symbol like TRX/USDT or address)') - .option('--fee ', 'Pool fee tier (e.g. 500)') + .option('--fee ', `Pool fee tier (default: ${DEFAULT_V4_FEE})`) .option('--tick-lower ', 'Lower tick (auto-computed if omitted)') .option('--tick-upper ', 'Upper tick (auto-computed if omitted)') .option('--amount0 ', 'Desired amount of token0') @@ -555,49 +779,84 @@ export function registerLiquidityCommands(program: Command) { return } - const token0 = toWTRXIfNative(token0Input, network) - const token1 = toWTRXIfNative(token1Input, network) + // V4 identifies native TRX with the zero address. WTRX is a different pool currency. + const token0 = token0Input + const token1 = token1Input const token0Display = getSymbolOrAddress(token0Input, network) const token1Display = getSymbolOrAddress(token1Input, network) - const token0Note = token0 !== token0Input ? ` → WTRX` : '' - const token1Note = token1 !== token1Input ? ` → WTRX` : '' + const fee = opts.fee ? parseInt(opts.fee) : DEFAULT_V4_FEE + const tickLower = opts.tickLower !== undefined ? parseInt(opts.tickLower) : undefined + const tickUpper = opts.tickUpper !== undefined ? parseInt(opts.tickUpper) : undefined + + let slippage: ParsedDecimalSlippage | undefined + try { + slippage = parseOptionalSlippage(opts.slippage) + } catch (err) { + outputError('Invalid --slippage', err) + return + } + + try { + assertLiquidityAmounts(opts.amount0, opts.amount1) + } catch (err) { + outputError('Invalid liquidity amounts', err) + return + } await writeAction({ title: 'V4 Mint Position', summary: { - Token0: `${token0Display}${token0Note} (${token0})`, - Token1: `${token1Display}${token1Note} (${token1})`, - Fee: opts.fee || '(auto)', + Token0: `${token0Display} (${token0})`, + Token1: `${token1Display} (${token1})`, + Fee: opts.fee || `${DEFAULT_V4_FEE} (default)`, 'Tick Range': - opts.tickLower && opts.tickUpper ? `[${opts.tickLower}, ${opts.tickUpper}]` : '(auto)', - Amount0: opts.amount0 || '(auto)', - Amount1: opts.amount1 || '(auto)', - Slippage: opts.slippage - ? `${(parseFloat(opts.slippage) * 100).toFixed(2)}%` - : '(default)', + tickLower !== undefined && tickUpper !== undefined + ? `[${tickLower}, ${tickUpper}]` + : '(auto)', + Amount0: opts.amount0 ?? '(not provided)', + Amount1: opts.amount1 ?? '(not provided)', + Slippage: slippage?.display ?? '(default)', 'Create Pool': opts.createPool ? 'Yes' : 'No', Network: network, }, confirmMsg: 'Mint V4 position?', spinnerLabel: 'Minting V4 position...', errorLabel: 'V4 mint failed', - execute: (kit) => - kit.mintPositionV4({ - network, + execute: async (sdk) => { + const ticks = await resolveV4MintTicks(sdk, { token0, token1, - fee: opts.fee ? parseInt(opts.fee) : undefined, - tickLower: opts.tickLower ? parseInt(opts.tickLower) : undefined, - tickUpper: opts.tickUpper ? parseInt(opts.tickUpper) : undefined, - amount0Desired: opts.amount0, - amount1Desired: opts.amount1, - slippage: opts.slippage ? parseFloat(opts.slippage) : undefined, - recipient: opts.recipient, - deadline: opts.deadline, + fee, + tickLower, + tickUpper, sqrtPriceX96: opts.sqrtPrice, - createPoolIfNeeded: opts.createPool, - }), + createPool: opts.createPool, + }) + return sdk.liquidity.v4.add + .execute({ + token0, + token1, + fee, + ...ticks, + amount0: opts.amount0, + amount1: opts.amount1, + slippage: slippage?.sdk, + includeComputedMetadata: true, + transactionMode: 'multicall', + recipient: opts.recipient, + sweepRecipient: opts.recipient, + ttlSeconds: 1800, + deadline: opts.deadline, + sqrtPriceX96: opts.sqrtPrice, + }) + .then((result) => { + const mapped = mapTx(result) as Record + return tickLower === undefined || tickUpper === undefined + ? { ...mapped, computedTicks: ticks } + : mapped + }) + }, summarizeResult: (result: any) => { const out: Record = {} if (result?.poolCreated !== undefined) out['Pool Created'] = result.poolCreated @@ -619,9 +878,9 @@ export function registerLiquidityCommands(program: Command) { .command('v4:increase') .description('Increase liquidity of an existing V4 position') .requiredOption('--token-id ', 'Position NFT token ID') - .requiredOption('--token0 ', 'Token0 (symbol like TRX/USDT or address)') - .requiredOption('--token1 ', 'Token1 (symbol like TRX/USDT or address)') - .option('--fee ', 'Pool fee tier') + .option('--token0 ', 'Expected token0; provide together with --token1') + .option('--token1 ', 'Expected token1; provide together with --token0') + .option('--fee ', 'Expected pool fee tier (validated against the position)') .option('--amount0 ', 'Additional amount of token0') .option('--amount1 ', 'Additional amount of token1') .option('--slippage ', 'Slippage tolerance as decimal') @@ -629,47 +888,81 @@ export function registerLiquidityCommands(program: Command) { .action(async (opts) => { const network = getNetwork() - let token0Input: string, token1Input: string + let expectedPair: OptionalV4Pair, expectedFee: number | undefined try { - token0Input = resolveTokenAddress(opts.token0, network) - token1Input = resolveTokenAddress(opts.token1, network) - } catch (err: any) { - console.error(err.message) - process.exitCode = 1 + expectedPair = resolveOptionalV4Pair(opts.token0, opts.token1, network) + expectedFee = parseOptionalV4Fee(opts.fee) + } catch (err) { + outputError('Invalid V4 position assertions', err) return } - const token0 = toWTRXIfNative(token0Input, network) - const token1 = toWTRXIfNative(token1Input, network) + let slippage: ParsedDecimalSlippage | undefined + try { + slippage = parseOptionalSlippage(opts.slippage) + } catch (err) { + outputError('Invalid --slippage', err) + return + } - const token0Display = getSymbolOrAddress(token0Input, network) - const token1Display = getSymbolOrAddress(token1Input, network) + try { + assertLiquidityAmounts(opts.amount0, opts.amount1) + } catch (err) { + outputError('Invalid liquidity amounts', err) + return + } + + const summary: Record = { + 'Token ID': opts.tokenId, + Token0: expectedPair.token0 ?? '(from tokenId)', + Token1: expectedPair.token1 ?? '(from tokenId)', + Fee: expectedFee ?? '(from tokenId)', + Amount0: opts.amount0 ?? '(not provided)', + Amount1: opts.amount1 ?? '(not provided)', + Slippage: slippage?.display ?? '(default)', + Network: network, + } + const loadPosition = createV4PositionLoader(opts.tokenId, expectedPair, expectedFee) await writeAction({ title: 'V4 Increase Liquidity', - summary: { - 'Token ID': opts.tokenId, - Token0: `${token0Display} (${token0})`, - Token1: `${token1Display} (${token1})`, - Amount0: opts.amount0 || '(auto)', - Amount1: opts.amount1 || '(auto)', - Network: network, + summary, + prepareLabel: 'Loading V4 position...', + prepare: async (sdk) => { + const prepared = await loadPosition(sdk) + const [amount0, amount1] = orderV4PositionValues( + prepared.reversed, + opts.amount0, + opts.amount1, + ) + Object.assign(summary, v4PositionTokenSummary(prepared.poolKey, network), { + Amount0: amount0 ?? '(not provided)', + Amount1: amount1 ?? '(not provided)', + }) }, confirmMsg: 'Increase V4 liquidity?', spinnerLabel: 'Increasing V4 liquidity...', errorLabel: 'V4 increase liquidity failed', - execute: (kit) => - kit.increaseLiquidityV4({ - network, - tokenId: opts.tokenId, - token0, - token1, - fee: opts.fee ? parseInt(opts.fee) : undefined, - amount0Desired: opts.amount0, - amount1Desired: opts.amount1, - slippage: opts.slippage ? parseFloat(opts.slippage) : undefined, - deadline: opts.deadline, - }), + execute: async (sdk) => { + const prepared = await loadPosition(sdk) + const [amount0, amount1] = orderV4PositionValues( + prepared.reversed, + opts.amount0, + opts.amount1, + ) + return sdk.liquidity.v4.increase + .execute({ + tokenId: opts.tokenId, + amount0, + amount1, + slippage: slippage?.sdk, + includeComputedMetadata: true, + transactionMode: 'multicall', + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then(mapTx) + }, summarizeResult: (result: any) => { const out: Record = {} if (result?.computedAmounts) { @@ -687,9 +980,9 @@ export function registerLiquidityCommands(program: Command) { .description('Decrease liquidity of an existing V4 position') .requiredOption('--token-id ', 'Position NFT token ID') .requiredOption('--liquidity ', 'Amount of liquidity to remove') - .requiredOption('--token0 ', 'Token0 (symbol like TRX/USDT or address)') - .requiredOption('--token1 ', 'Token1 (symbol like TRX/USDT or address)') - .option('--fee ', 'Pool fee tier') + .option('--token0 ', 'Expected token0; provide together with --token1') + .option('--token1 ', 'Expected token1; provide together with --token0') + .option('--fee ', 'Expected pool fee tier (validated against the position)') .option('--min0 ', 'Minimum amount of token0 to receive') .option('--min1 ', 'Minimum amount of token1 to receive') .option('--slippage ', 'Slippage tolerance as decimal') @@ -697,47 +990,73 @@ export function registerLiquidityCommands(program: Command) { .action(async (opts) => { const network = getNetwork() - let token0Input: string, token1Input: string + let expectedPair: OptionalV4Pair, expectedFee: number | undefined try { - token0Input = resolveTokenAddress(opts.token0, network) - token1Input = resolveTokenAddress(opts.token1, network) - } catch (err: any) { - console.error(err.message) - process.exitCode = 1 + expectedPair = resolveOptionalV4Pair(opts.token0, opts.token1, network) + expectedFee = parseOptionalV4Fee(opts.fee) + } catch (err) { + outputError('Invalid V4 position assertions', err) return } - const token0 = toWTRXIfNative(token0Input, network) - const token1 = toWTRXIfNative(token1Input, network) + let slippage: ParsedDecimalSlippage | undefined + try { + slippage = parseOptionalSlippage(opts.slippage) + } catch (err) { + outputError('Invalid --slippage', err) + return + } - const token0Display = getSymbolOrAddress(token0Input, network) - const token1Display = getSymbolOrAddress(token1Input, network) + const summary: Record = { + 'Token ID': opts.tokenId, + Liquidity: opts.liquidity, + Token0: expectedPair.token0 ?? '(from tokenId)', + Token1: expectedPair.token1 ?? '(from tokenId)', + Fee: expectedFee ?? '(from tokenId)', + Min0: opts.min0 ?? '(SDK derived)', + Min1: opts.min1 ?? '(SDK derived)', + Slippage: slippage?.display ?? '(default)', + Network: network, + } + const loadPosition = createV4PositionLoader(opts.tokenId, expectedPair, expectedFee) await writeAction({ title: 'V4 Decrease Liquidity', - summary: { - 'Token ID': opts.tokenId, - Liquidity: opts.liquidity, - Token0: `${token0Display} (${token0})`, - Token1: `${token1Display} (${token1})`, - Network: network, + summary, + prepareLabel: 'Loading V4 position...', + prepare: async (sdk) => { + const prepared = await loadPosition(sdk) + const [min0, min1] = orderV4PositionValues(prepared.reversed, opts.min0, opts.min1) + Object.assign(summary, v4PositionTokenSummary(prepared.poolKey, network), { + Min0: min0 ?? '(SDK derived)', + Min1: min1 ?? '(SDK derived)', + }) }, confirmMsg: 'Decrease V4 liquidity?', spinnerLabel: 'Decreasing V4 liquidity...', errorLabel: 'V4 decrease liquidity failed', - execute: (kit) => - kit.decreaseLiquidityV4({ - network, - tokenId: opts.tokenId, - liquidity: opts.liquidity, - token0, - token1, - fee: opts.fee ? parseInt(opts.fee) : undefined, - amount0Min: opts.min0, - amount1Min: opts.min1, - slippage: opts.slippage ? parseFloat(opts.slippage) : undefined, - deadline: opts.deadline, - }), + execute: async (sdk) => { + const prepared = await loadPosition(sdk) + const [amount0Min, amount1Min] = orderV4PositionValues( + prepared.reversed, + opts.min0, + opts.min1, + ) + return sdk.positions.v4.remove + .execute({ + tokenId: opts.tokenId, + liquidity: opts.liquidity, + amount0Min, + amount1Min, + slippage: slippage?.sdk, + includeComputedMetadata: true, + transactionMode: 'multicall', + settlementMode: 'close-currency', + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then(mapTx) + }, summarizeResult: (result: any) => { const out: Record = {} if (result?.computedAmountMin) { @@ -754,55 +1073,54 @@ export function registerLiquidityCommands(program: Command) { .command('v4:collect') .description('Collect accrued fees from an existing V4 position') .requiredOption('--token-id ', 'Position NFT token ID') - .option('--token0 ', 'Token0 (symbol like TRX/USDT or address)') - .option('--token1 ', 'Token1 (symbol like TRX/USDT or address)') - .option('--fee ', 'Pool fee tier') + .option('--token0 ', 'Expected token0; provide together with --token1') + .option('--token1 ', 'Expected token1; provide together with --token0') + .option('--fee ', 'Expected pool fee tier (validated against the position)') .option('--deadline ', 'Unix timestamp deadline') .action(async (opts) => { const network = getNetwork() - let token0: string | undefined, token1: string | undefined - if (opts.token0) { - try { - const resolved = resolveTokenAddress(opts.token0, network) - token0 = toWTRXIfNative(resolved, network) - } catch (err: any) { - console.error(err.message) - process.exitCode = 1 - return - } + let expectedPair: OptionalV4Pair, expectedFee: number | undefined + try { + expectedPair = resolveOptionalV4Pair(opts.token0, opts.token1, network) + expectedFee = parseOptionalV4Fee(opts.fee) + } catch (err) { + outputError('Invalid V4 position assertions', err) + return } - if (opts.token1) { - try { - const resolved = resolveTokenAddress(opts.token1, network) - token1 = toWTRXIfNative(resolved, network) - } catch (err: any) { - console.error(err.message) - process.exitCode = 1 - return - } + + const summary: Record = { + 'Token ID': opts.tokenId, + Token0: expectedPair.token0 ?? '(from tokenId)', + Token1: expectedPair.token1 ?? '(from tokenId)', + Fee: expectedFee ?? '(from tokenId)', + Network: network, } + const loadPosition = createV4PositionLoader(opts.tokenId, expectedPair, expectedFee) await writeAction({ title: 'V4 Collect Fees', - summary: { - 'Token ID': opts.tokenId, - Token0: token0 || '(auto)', - Token1: token1 || '(auto)', - Network: network, + summary, + prepareLabel: 'Loading V4 position...', + prepare: async (sdk) => { + const prepared = await loadPosition(sdk) + Object.assign(summary, v4PositionTokenSummary(prepared.poolKey, network)) }, confirmMsg: 'Collect V4 fees?', spinnerLabel: 'Collecting V4 fees...', errorLabel: 'V4 collect failed', - execute: (kit) => - kit.collectPositionV4({ - network, - tokenId: opts.tokenId, - token0, - token1, - fee: opts.fee ? parseInt(opts.fee) : undefined, - deadline: opts.deadline, - }), + execute: async (sdk) => { + await loadPosition(sdk) + return sdk.positions.v4.collect + .execute({ + tokenId: opts.tokenId, + transactionMode: 'multicall', + settlementMode: 'close-currency', + ttlSeconds: 1800, + deadline: opts.deadline, + }) + .then(mapTx) + }, }) }) @@ -815,14 +1133,21 @@ export function registerLiquidityCommands(program: Command) { await readAction({ spinnerLabel: 'Fetching V4 position info...', errorLabel: 'V4 position info failed', - execute: (kit) => kit.getV4PositionInfo(opts.pm, opts.tokenId, getNetwork()), - transform: (result) => { + execute: (sdk) => + sdk.positions.v4.read({ + tokenId: opts.tokenId, + contracts: withContracts({ sunswapV4PositionManager: opts.pm }), + }), + transform: (result: any) => { if (!result) return { error: 'Position not found' } + const fee = result.poolKey?.fee return { - currency0: result.poolKey.currency0, - currency1: result.poolKey.currency1, - fee: result.poolKey.fee, - tickSpacing: result.poolKey.tickSpacing, + currency0: result.poolKey?.currency0, + currency1: result.poolKey?.currency1, + fee: fee === undefined ? undefined : String(fee), + tickSpacing: + result.poolKey?.tickSpacing ?? + (Number.isFinite(Number(fee)) ? v4TickSpacingForFee(Number(fee)) : undefined), tickLower: result.tickLower, tickUpper: result.tickUpper, liquidity: result.liquidity, diff --git a/src/commands/pool.ts b/src/commands/pool.ts index 491d4de..56752bf 100644 --- a/src/commands/pool.ts +++ b/src/commands/pool.ts @@ -3,7 +3,7 @@ import { getNetwork } from '../lib/context' import { readApiAction } from '../lib/command' import { formatUsd, formatPct, formatTime } from '../lib/output' import { tryResolveTokenAddress } from '../lib/tokens' -import { TRX_ADDRESS, WTRX_MAINNET, WTRX_NILE } from '@sun-protocol/sun-kit' +import { TRX_ADDRESS, WTRX_MAINNET, WTRX_NILE } from '../lib/sdk/constants' function resolveTokenForPoolQuery(input: string | undefined, network: string): string | undefined { if (!input) return undefined diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 1516c79..e8d561e 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -2,6 +2,9 @@ import { Command } from 'commander' import { isDryRun, parseApiResponse, writeAction } from '../lib/command' import { confirm, printSummary } from '../lib/confirm' import { getNetwork, getKit } from '../lib/context' +import { SunPumpApiClient } from '@sun-protocol/sun-sdk-api' +import { toCliTxResult } from '../lib/sdk/compat' +import { parseDecimalSlippage } from '../lib/slippage' import { output, outputError, @@ -15,7 +18,6 @@ import { formatAmount, formatPct, } from '../lib/output' -import { getSunPump, SunPump } from '../lib/sunpump' // --------------------------------------------------------------------------- // pumpAction — local mirror of readApiAction but for the SunPump client. @@ -26,7 +28,7 @@ import { getSunPump, SunPump } from '../lib/sunpump' interface PumpActionOpts { spinnerLabel: string errorLabel: string - execute: (client: SunPump) => Promise + execute: (client: SunPumpApiClient) => Promise tableConfig?: { headers: string[]; toRow: (item: any) => string[] } transform?: (data: any) => unknown parseEnvelope?: boolean @@ -34,18 +36,24 @@ interface PumpActionOpts { } function assertMainnet(): void { - const n = getNetwork() - if (n && n !== 'mainnet') { - throw new Error( - `SunPump is only available on mainnet (got "${n}"). Drop --network or pass --network mainnet.`, + const network = getNetwork() + if (network !== 'mainnet') { + const error = new Error( + `SunPump is only available on mainnet (got "${network}"). Drop --network or pass --network mainnet.`, ) + ;(error as Error & { code: string }).code = 'SDK_UNSUPPORTED_NETWORK' + throw error } } +function getSunPumpApi(): SunPumpApiClient { + assertMainnet() + return new SunPumpApiClient({ network: 'mainnet', baseUrl: process.env.SUNPUMP_API_BASE_URL }) +} + async function pumpAction(opts: PumpActionOpts): Promise { try { - assertMainnet() - const client = getSunPump() + const client = getSunPumpApi() const raw = await withSpinner(opts.spinnerLabel, () => opts.execute(client)) let data: unknown = raw @@ -237,7 +245,7 @@ function baseUnitsToDecimal(raw: string | bigint, decimals: number): string { return negative ? `-${out}` : out } -// SunKit's enum lists 0/1/2 but the contract uses extra states (e.g. 3 = launched-on-DEX). +// The contract uses extra states (e.g. 3 = launched-on-DEX). // Map what we know; surface raw values verbatim otherwise. const SUNPUMP_STATE_NAME: Record = { 0: 'NOT_EXIST', @@ -669,8 +677,7 @@ export function registerSunpumpCommands(program: Command) { } try { - assertMainnet() - const client = getSunPump() + const client = getSunPumpApi() const raw = await withSpinner('Launching token...', () => client.agentTokenLaunch(params)) const { data } = parseApiResponse(raw) if (isJsonMode()) { @@ -706,11 +713,10 @@ export function registerSunpumpCommands(program: Command) { .description('Show SunPump token state (0 NOT_EXIST, 1 TRADING, 2 READY_TO_LAUNCH, 3 LAUNCHED)') .action(async (tokenAddress: string) => { try { - const network = getNetwork() const result = await withSpinner('Fetching token state...', async () => { const kit = await getKit() - const state = await kit.getSunPumpTokenState(tokenAddress, network) - const info = await kit.getSunPumpTokenInfo(tokenAddress, network).catch(() => null) + const state = await kit.pump.state({ token: tokenAddress }) + const info = await kit.pump.info({ token: tokenAddress }).catch(() => null) return { state, info } }) if (isJsonMode()) { @@ -747,10 +753,9 @@ export function registerSunpumpCommands(program: Command) { return } try { - const network = getNetwork() const quote: any = await withSpinner('Fetching buy quote...', async () => { const kit = await getKit() - return kit.sunpumpQuoteBuy(tokenAddress, trxSun, network) + return kit.pump.buy.quote({ token: tokenAddress, trxAmount: trxSun, launchpadOnly: true }) }) if (isJsonMode()) { output({ ...quote, tokenAddress, trxSun, trxIn: opts.trx }) @@ -781,10 +786,13 @@ export function registerSunpumpCommands(program: Command) { return } try { - const network = getNetwork() const quote: any = await withSpinner('Fetching sell quote...', async () => { const kit = await getKit() - return kit.sunpumpQuoteSell(tokenAddress, tokenRaw, network) + return kit.pump.sell.quote({ + token: tokenAddress, + tokenAmount: tokenRaw, + launchpadOnly: true, + }) }) if (isJsonMode()) { output({ ...quote, tokenAddress, tokenRaw, amountIn: opts.amount }) @@ -814,19 +822,25 @@ export function registerSunpumpCommands(program: Command) { outputError('Invalid --trx', err) return } - const slippage = parseFloat(opts.slippage) + let slippage + try { + slippage = parseDecimalSlippage(opts.slippage) + } catch (err) { + outputError('Invalid --slippage', err) + return + } const network = getNetwork() const summary: Record = { Token: tokenAddress, 'TRX In': `${opts.trx} TRX (${trxSun} Sun)`, - Slippage: `${(slippage * 100).toFixed(2)}%`, + Slippage: slippage.display, Network: network, } try { const quote = await withSpinner('Fetching buy quote...', async () => { const kit = await getKit() - return kit.sunpumpQuoteBuy(tokenAddress, trxSun, network) + return kit.pump.buy.quote({ token: tokenAddress, trxAmount: trxSun, launchpadOnly: true }) }) summary['Tokens Out (expected)'] = baseUnitsToDecimal((quote as any).tokenAmount, 18) summary['Fee'] = `${baseUnitsToDecimal((quote as any).fee, 6)} TRX` @@ -842,13 +856,15 @@ export function registerSunpumpCommands(program: Command) { spinnerLabel: 'Submitting buy...', errorLabel: 'Buy failed', execute: (kit) => - kit.sunpumpBuy({ - tokenAddress, - trxAmount: trxSun, - minTokenOut: opts.minOut, - slippage, - network, - }), + kit.pump.buy + .execute({ + token: tokenAddress, + trxAmount: trxSun, + minTokenOut: opts.minOut, + launchpadOnly: true, + slippage: slippage.sdk, + }) + .then(toCliTxResult), onSuccess: async (result: any) => { if (isJsonMode()) return const chalk = (await import('chalk')).default @@ -885,19 +901,29 @@ export function registerSunpumpCommands(program: Command) { outputError('Invalid --amount', err) return } - const slippage = parseFloat(opts.slippage) + let slippage + try { + slippage = parseDecimalSlippage(opts.slippage) + } catch (err) { + outputError('Invalid --slippage', err) + return + } const network = getNetwork() const summary: Record = { Token: tokenAddress, 'Tokens In': `${opts.amount} (${tokenRaw} raw)`, - Slippage: `${(slippage * 100).toFixed(2)}%`, + Slippage: slippage.display, Network: network, } try { const quote = await withSpinner('Fetching sell quote...', async () => { const kit = await getKit() - return kit.sunpumpQuoteSell(tokenAddress, tokenRaw, network) + return kit.pump.sell.quote({ + token: tokenAddress, + tokenAmount: tokenRaw, + launchpadOnly: true, + }) }) summary['TRX Out (expected)'] = `${baseUnitsToDecimal((quote as any).trxAmount, 6)} TRX` summary['Fee'] = `${baseUnitsToDecimal((quote as any).fee, 6)} TRX` @@ -913,13 +939,15 @@ export function registerSunpumpCommands(program: Command) { spinnerLabel: 'Submitting sell...', errorLabel: 'Sell failed', execute: (kit) => - kit.sunpumpSell({ - tokenAddress, - tokenAmount: tokenRaw, - minTrxOut: opts.minOut, - slippage, - network, - }), + kit.pump.sell + .execute({ + token: tokenAddress, + tokenAmount: tokenRaw, + minTrxOut: opts.minOut, + launchpadOnly: true, + slippage: slippage.sdk, + }) + .then(toCliTxResult), onSuccess: async (result: any) => { if (isJsonMode()) return const chalk = (await import('chalk')).default diff --git a/src/commands/swap.ts b/src/commands/swap.ts index 49a79c7..9d5f25a 100644 --- a/src/commands/swap.ts +++ b/src/commands/swap.ts @@ -1,8 +1,10 @@ import { Command } from 'commander' import { getNetwork } from '../lib/context' -import { writeAction, readAction } from '../lib/command' +import { writeAction, writeRuntimeAction, readRuntimeAction as readAction } from '../lib/command' import { isJsonMode, output, outputError, withSpinner } from '../lib/output' import { resolveTokenAddress, getSymbolOrAddress } from '../lib/tokens' +import { toCliTxResult } from '../lib/sdk/compat' +import { parseDecimalSlippage } from '../lib/slippage' const ROUTER_API: Record = { mainnet: 'https://rot.endjgfsv.link', @@ -17,6 +19,14 @@ interface RouteData { impact: string } +function selectSwapRoute(quote: any): any { + return quote.bestRoute ?? quote.route ?? quote.routes?.[0] +} + +function parseAbi(value: string | undefined): any[] | undefined { + return value === undefined ? undefined : JSON.parse(value) +} + async function fetchSwapQuote( tokenIn: string, tokenOut: string, @@ -60,7 +70,13 @@ export function registerSwapCommands(program: Command) { .option('--slippage ', 'Slippage tolerance as decimal (e.g. 0.005 for 0.5%)', '0.005') .action(async (tokenInArg: string, tokenOutArg: string, amountIn: string, opts) => { const network = getNetwork() - const slippage = parseFloat(opts.slippage) + let slippage + try { + slippage = parseDecimalSlippage(opts.slippage) + } catch (err) { + outputError('Invalid --slippage', err) + return + } let tokenIn: string, tokenOut: string try { @@ -81,13 +97,34 @@ export function registerSwapCommands(program: Command) { 'Token In': `${tokenInDisplay} (${tokenIn})`, 'Token Out': `${tokenOutDisplay} (${tokenOut})`, 'Amount In': amountIn, - Slippage: `${(slippage * 100).toFixed(2)}%`, + Slippage: slippage.display, Network: network, }, confirmMsg: 'Execute this swap?', spinnerLabel: 'Executing swap...', errorLabel: 'Swap failed', - execute: (kit) => kit.swap({ tokenIn, tokenOut, amountIn, slippage, network }), + execute: async (sdk) => { + const quote = await sdk.swap.router.quote({ tokenIn, tokenOut, amountIn }) + const route = selectSwapRoute(quote) + if (!route) throw new Error('No route available for this token pair') + const result = await sdk.swap.router.execute({ + quote, + route, + slippage: slippage.sdk, + // SunKit router swaps used a one-hour transaction deadline. + transactionValidForSeconds: 3600, + }) + return { + txid: (toCliTxResult(result) as { txid: string }).txid, + route: { + amountIn: route.amountIn, + amountOut: route.amountOut, + symbols: route.symbols, + poolVersions: route.poolVersions, + impact: route.impact, + }, + } + }, onSuccess: async (result: any) => { if (!isJsonMode()) { const chalk = (await import('chalk')).default @@ -179,13 +216,12 @@ export function registerSwapCommands(program: Command) { await readAction({ spinnerLabel: 'Quoting swap...', errorLabel: 'Quote failed', - execute: (kit) => - kit.quoteExactInput({ - network: getNetwork(), - routerAddress: opts.router, + execute: (sdk) => + sdk.runtime.contract.read({ + address: opts.router, functionName: opts.fn, args: JSON.parse(opts.args), - abi: opts.abi ? JSON.parse(opts.abi) : undefined, + abi: parseAbi(opts.abi), }), transform: (result) => ({ result }), }) @@ -200,7 +236,7 @@ export function registerSwapCommands(program: Command) { .option('--value ', 'TRX call value in Sun') .option('--abi ', 'Optional router ABI as JSON array') .action(async (opts) => { - await writeAction({ + await writeRuntimeAction({ title: 'SwapExactInput', summary: { Router: opts.router, @@ -212,14 +248,15 @@ export function registerSwapCommands(program: Command) { spinnerLabel: 'Executing swap...', errorLabel: 'SwapExactInput failed', execute: (kit) => - kit.swapExactInput({ - network: getNetwork(), - routerAddress: opts.router, - functionName: opts.fn, - args: JSON.parse(opts.args), - value: opts.value, - abi: opts.abi ? JSON.parse(opts.abi) : undefined, - }), + kit.runtime.contract + .write({ + address: opts.router, + functionName: opts.fn, + args: JSON.parse(opts.args), + callValue: opts.value === undefined ? undefined : BigInt(opts.value), + abi: parseAbi(opts.abi), + }) + .then(toCliTxResult), }) }) } diff --git a/src/commands/wallet.ts b/src/commands/wallet.ts index 5ee57a4..68e7dda 100644 --- a/src/commands/wallet.ts +++ b/src/commands/wallet.ts @@ -1,8 +1,9 @@ import { Command } from 'commander' import { getNetwork } from '../lib/context' -import { readAction } from '../lib/command' +import { readRuntimeAction as readAction } from '../lib/command' import { getWalletAddress, initWallet } from '../lib/wallet' import { output, outputError } from '../lib/output' +import { readBalances } from '@sun-protocol/sun-sdk-runtime' export function registerWalletCommands(program: Command) { const wallet = program.command('wallet').description('Wallet management') @@ -27,23 +28,28 @@ export function registerWalletCommands(program: Command) { .option('--owner
', 'Wallet address (default: active wallet)') .option('--tokens ', 'Comma-separated: TRX,,...', 'TRX') .action(async (opts) => { - const tokenList = opts.tokens.split(',').map((t: string) => { - const trimmed = t.trim() - if (trimmed.toUpperCase() === 'TRX') { - return { address: opts.owner || '', type: 'TRX' as const } - } - return { address: opts.owner || '', type: 'TRC20' as const, tokenAddress: trimmed } - }) + const tokenList = opts.tokens.split(',').map((t: string) => t.trim()) await readAction({ spinnerLabel: 'Fetching balances...', errorLabel: 'Failed to get balances', - execute: (kit) => - kit.getBalances({ - network: getNetwork(), - ownerAddress: opts.owner, + execute: async (sdk) => { + const owner = opts.owner || (await sdk.runtime.getAddress()) + if (!owner) + throw new Error( + 'Wallet required. Set agent-wallet credentials before running this command.', + ) + const balances = await readBalances(sdk.runtime.tronClient, { + owner, tokens: tokenList, - }), + }) + return balances.map((item) => ({ + address: owner, + type: String(item.token).toUpperCase() === 'TRX' ? 'TRX' : 'TRC20', + tokenAddress: String(item.token).toUpperCase() === 'TRX' ? undefined : item.token, + balance: item.balance, + })) + }, tableConfig: { headers: ['Type', 'Token Address', 'Balance'], toRow: (item: any) => [item.type, item.tokenAddress || 'TRX (native)', item.balance], diff --git a/src/lib/command.ts b/src/lib/command.ts index 15fb19e..bd601fe 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -2,8 +2,8 @@ * Command helpers — eliminate boilerplate in write/read command handlers. */ -import type { SunKit, SunAPI } from '@sun-protocol/sun-kit' -import { getKit, getApi, ensureWallet } from './context' +import { getKit, getApi, getRuntime, ensureWallet } from './context' +import type { ContractRuntime } from '@sun-protocol/sun-sdk-runtime' import { output, outputError, @@ -13,6 +13,8 @@ import { printKeyValue, } from './output' import { confirm, printSummary } from './confirm' +import type { SunApiClient } from '@sun-protocol/sun-sdk-api' +import type { CliSunSDK } from './sdk/types' // --------------------------------------------------------------------------- // Dry-run state @@ -167,7 +169,7 @@ export function parseApiResponse( // writeAction — for state-changing commands (swap, liquidity, contract:send) // --------------------------------------------------------------------------- -export interface WriteActionOpts { +export interface WriteActionOpts { /** Title shown in the pre-execution summary */ title: string /** Key-value pairs displayed in summary */ @@ -177,7 +179,11 @@ export interface WriteActionOpts { /** Spinner label during execution */ spinnerLabel: string /** The async operation that calls kit methods */ - execute: (kit: SunKit) => Promise + execute: (kit: Client) => Promise + /** Optional read-only preparation performed before the summary and confirmation. */ + prepare?: (kit: Client) => Promise + /** Spinner label for the optional preparation step. */ + prepareLabel?: string /** Error label prefix for outputError */ errorLabel: string /** Optional post-success callback (e.g. extra human-friendly output) */ @@ -187,6 +193,19 @@ export interface WriteActionOpts { } export async function writeAction(opts: WriteActionOpts): Promise { + return runWriteAction(opts, getKit) +} + +export async function writeRuntimeAction( + opts: WriteActionOpts, +): Promise { + return runWriteAction(opts, async () => ({ runtime: await getRuntime() })) +} + +async function runWriteAction( + opts: WriteActionOpts, + getClient: () => Promise, +): Promise { try { if (_dryRun) { output({ dryRun: true, action: opts.title, params: opts.summary }) @@ -194,10 +213,14 @@ export async function writeAction(opts: WriteActionOpts): Promise { } await ensureWallet() - const kit = await getKit() + const kit = await getClient() const network = typeof opts.summary.Network === 'string' ? String(opts.summary.Network) : undefined + if (opts.prepare) { + await withSpinner(opts.prepareLabel ?? 'Preparing transaction...', () => opts.prepare!(kit)) + } + printSummary(opts.title, opts.summary) const confirmed = await confirm(opts.confirmMsg) @@ -227,14 +250,14 @@ export async function writeAction(opts: WriteActionOpts): Promise { } // --------------------------------------------------------------------------- -// readAction — for read commands that need SunKit (balances, contract:read) +// readAction — for read commands that need the protocol SDK runtime (balances, contract:read) // --------------------------------------------------------------------------- -export interface ReadActionOpts { +export interface ReadActionOpts { /** Spinner label */ spinnerLabel: string /** The async operation */ - execute: (kit: SunKit) => Promise + execute: (kit: Client) => Promise /** Table config for human-readable output */ tableConfig?: { headers: string[]; toRow: (item: any) => string[] } /** Error label prefix */ @@ -244,8 +267,21 @@ export interface ReadActionOpts { } export async function readAction(opts: ReadActionOpts): Promise { + return runReadAction(opts, getKit) +} + +export async function readRuntimeAction( + opts: ReadActionOpts, +): Promise { + return runReadAction(opts, async () => ({ runtime: await getRuntime() })) +} + +async function runReadAction( + opts: ReadActionOpts, + getClient: () => Promise, +): Promise { try { - const kit = await getKit() + const kit = await getClient() const result = await withSpinner(opts.spinnerLabel, () => opts.execute(kit)) const data = opts.transform ? opts.transform(result) : result @@ -256,14 +292,14 @@ export async function readAction(opts: ReadActionOpts): Promise { } // --------------------------------------------------------------------------- -// readApiAction — for read commands using SunAPI only (no wallet init) +// readApiAction — for read commands using the SUN API client only (no wallet init) // --------------------------------------------------------------------------- -export interface ReadApiActionOpts { +export interface ReadApiActionOpts { /** Spinner label */ spinnerLabel: string /** The async operation */ - execute: (api: SunAPI) => Promise + execute: (api: SunApiClient) => Promise /** Table config for human-readable output */ tableConfig?: { headers: string[]; toRow: (item: any) => string[] } /** Error label prefix */ diff --git a/src/lib/context.ts b/src/lib/context.ts index cf35af3..dd85fa8 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -1,34 +1,38 @@ /** - * Global application context — lazily initializes SunAPI, SunKit, and wallet. + * Global application context — lazily initializes SUN API, SunSDK, and wallet. */ -import { SunAPI, SunKit } from '@sun-protocol/sun-kit' +import type { SunApiClient } from '@sun-protocol/sun-sdk-api' import { initWallet, getWallet, isWalletConfigured } from './wallet' +import { createSunApiClient, createSunSDK, createGenericRuntime } from './sdk/factory' +import type { ContractRuntime } from '@sun-protocol/sun-sdk-runtime' +import type { CliSunSDK } from './sdk/types' -let _api: SunAPI | null = null -let _kit: SunKit | null = null +let _api: SunApiClient | null = null +let _sdk: CliSunSDK | null = null +let _runtime: ContractRuntime | null = null let _initialized = false function getNetwork(): string { return process.env.TRON_NETWORK || 'mainnet' } -export function getApi(): SunAPI { +export function getApi(): SunApiClient { if (!_api) { - _api = new SunAPI() + _api = createSunApiClient() } return _api } -export async function getKit(): Promise { +export async function getSdk(): Promise { if (!_initialized) { await initWallet() _initialized = true } - if (!_kit) { + if (!_sdk) { const wallet = isWalletConfigured() ? getWallet() : undefined - _kit = new SunKit({ + _sdk = await createSunSDK({ wallet, network: getNetwork(), tronGridApiKey: process.env.TRONGRID_API_KEY || process.env.TRON_GRID_API_KEY, @@ -36,7 +40,28 @@ export async function getKit(): Promise { }) } - return _kit + return _sdk +} + +export async function getRuntime(): Promise { + if (!_initialized) { + await initWallet() + _initialized = true + } + if (!_runtime) { + _runtime = await createGenericRuntime({ + wallet: isWalletConfigured() ? getWallet() : undefined, + network: getNetwork(), + tronGridApiKey: process.env.TRONGRID_API_KEY || process.env.TRON_GRID_API_KEY, + rpcUrl: process.env.TRON_RPC_URL, + }) + } + return _runtime +} + +/** @deprecated Use getSdk(). Kept for the existing sun-cli command API. */ +export async function getKit(): Promise { + return getSdk() } export async function ensureWallet(): Promise { diff --git a/src/lib/output.ts b/src/lib/output.ts index 626f24b..d2546a0 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -74,7 +74,7 @@ export function outputJson(data: unknown) { } function classifyError(message: string, error?: unknown): string { - // Prefer error object's own code (e.g. SunKitError.code) + // Prefer error object's own code from SDK/API errors. if ( error && typeof error === 'object' && diff --git a/src/lib/sdk/compat.ts b/src/lib/sdk/compat.ts new file mode 100644 index 0000000..e61f154 --- /dev/null +++ b/src/lib/sdk/compat.ts @@ -0,0 +1,62 @@ +export function toCliTxResult(result: T): unknown { + if (!result || typeof result !== 'object' || Array.isArray(result)) { + return result + } + + const record = result as Record + const metadata = (record.plan as { metadata?: Record } | undefined)?.metadata + const finalResult = record.finalResult as Record | undefined + if (finalResult && finalResult.type === 'transaction' && typeof finalResult.txid === 'string') { + const txResult = { result: true, txid: finalResult.txid } + if (metadata?.operation === 'launchpad-buy-exact-trx') { + return { + txResult, + tokenAddress: metadata.token, + trxSpent: metadata.trxAmount, + expectedTokens: metadata.quotedTokenAmount, + minTokenOut: metadata.minTokenOut, + } + } + if (metadata?.operation === 'launchpad-sell-exact-token') { + return { + txResult, + tokenAddress: metadata.token, + tokensSold: metadata.tokenAmount, + expectedTrx: metadata.quotedTrxAmount, + minTrxOut: metadata.minTrxOut, + } + } + if (metadata?.protocol === 'sunswap-v3' || metadata?.protocol === 'sunswap-v4') { + if (metadata.protocol === 'sunswap-v3' && metadata.operation === 'remove-position') + return txResult + const mapped: Record = { txResult } + for (const key of [ + 'computedAmounts', + 'computedTicks', + 'computedAmountMin', + 'estimatedFees', + ]) { + if (metadata[key] !== undefined) mapped[key] = metadata[key] + } + if (metadata.operation === 'mint-position') mapped.poolCreated = metadata.createPool + return mapped + } + return txResult + } + + if (Array.isArray(record.txids) && typeof record.txids[record.txids.length - 1] === 'string') { + return { + txid: record.txids[record.txids.length - 1], + result: true, + } + } + + if (typeof record.txid === 'string') { + return 'raw' in record ? { result: true, txid: record.txid } : result + } + if (typeof record.txID !== 'string') return result + + const { txID, ...rest } = record + void txID + return { txid: record.txID, ...rest } as T & { txid: string } +} diff --git a/src/lib/sdk/constants.ts b/src/lib/sdk/constants.ts new file mode 100644 index 0000000..10b8499 --- /dev/null +++ b/src/lib/sdk/constants.ts @@ -0,0 +1,17 @@ +import { getWrappedNativeAddress } from '@sun-protocol/sun-sdk-chains' +import { TRON_ZERO_ADDRESS } from '@sun-protocol/sun-sdk-core' + +export const TRX_ADDRESS = TRON_ZERO_ADDRESS +export const WTRX_MAINNET = getWrappedNativeAddress('mainnet') +export const WTRX_NILE = getWrappedNativeAddress('nile') + +// Preserve the fee defaults of the pre-migration SunKit implementation. +// Fees are expressed in millionths: 3000 = 0.3%, 500 = 0.05%. +export const DEFAULT_V3_FEE = 3000 +export const DEFAULT_V4_FEE = 500 + +// The CLI's legacy Nile V3 deployment differs from the SUN.io SDK default. +export const CLI_V3_FACTORY = { + mainnet: 'TThJt8zaJzJMhCEScH7zWKnp5buVZqys9x', + nile: 'TLJWAScHZ4Qmk1axyKMzrnoYuu2pSLer1F', +} as const diff --git a/src/lib/sdk/errors.ts b/src/lib/sdk/errors.ts new file mode 100644 index 0000000..d956d49 --- /dev/null +++ b/src/lib/sdk/errors.ts @@ -0,0 +1,21 @@ +import type { CliError } from './types' + +function hasStringCode(error: unknown): error is Error & { code: string } { + return error instanceof Error && typeof (error as any).code === 'string' +} + +export function mapSdkError(error: unknown, fallbackCode = 'UNKNOWN'): CliError { + if (error instanceof Error && /wallet required|agent-wallet|no wallet/i.test(error.message)) { + return Object.assign(error, { code: 'NO_WALLET' }) + } + + if (hasStringCode(error)) { + return error + } + + if (error instanceof Error) { + return Object.assign(error, { code: fallbackCode }) + } + + return Object.assign(new Error(String(error)), { code: fallbackCode, detail: error }) +} diff --git a/src/lib/sdk/factory.ts b/src/lib/sdk/factory.ts new file mode 100644 index 0000000..9c6f112 --- /dev/null +++ b/src/lib/sdk/factory.ts @@ -0,0 +1,137 @@ +import { SunApiClient } from '@sun-protocol/sun-sdk-api' +import type { Network } from '@sun-protocol/sun-sdk-core' +import { SunSDK } from '@sun-protocol/sun-sdk-protocols' +import { + createRuntime, + createContractRuntime, + withContractAbi, + type ContractRuntime, + type RuntimeNetwork, + type AbiItem, + type Runtime, + type TronClient, + type WalletAdapter, +} from '@sun-protocol/sun-sdk-runtime' +import { TronWeb } from 'tronweb' +import { TRX_ADDRESS } from './constants' +import type { CliSunSDK } from './types' + +export interface SdkRuntimeOptions { + network?: string + wallet?: WalletAdapter + tronGridApiKey?: string + rpcUrl?: string +} + +export function getNetworkFromEnv(env: NodeJS.ProcessEnv = process.env): Network { + const network = env.TRON_NETWORK || 'mainnet' + if (network === 'mainnet' || network === 'nile') return network + throw new Error(`Unsupported SDK network: ${network}`) +} + +function getDefaultRpcUrl(network: RuntimeNetwork): string { + if (network === 'shasta') return 'https://api.shasta.trongrid.io' + return network === 'nile' ? 'https://nile.trongrid.io' : 'https://api.trongrid.io' +} + +export function createReadonlyTronWeb(options: SdkRuntimeOptions = {}): TronWeb { + const network = options.network ?? 'mainnet' + const fullHost = options.rpcUrl || getDefaultRpcUrl(assertRuntimeNetwork(network)) + const headers = options.tronGridApiKey + ? { 'TRON-PRO-API-KEY': options.tronGridApiKey } + : undefined + const tronWeb = new TronWeb({ fullHost, headers }) + // TronGrid requires owner_address even for constant calls. Set the zero address + // as a read-only caller without introducing a signing key. + tronWeb.setAddress(TRX_ADDRESS) + return tronWeb +} + +function assertSdkNetwork(network: string): Network { + if (network === 'mainnet' || network === 'nile') return network + throw new Error(`Unsupported SDK network: ${network}`) +} + +function assertRuntimeNetwork(network: string): RuntimeNetwork { + if (network === 'mainnet' || network === 'nile' || network === 'shasta') return network + throw new Error(`Unsupported SDK network: ${network}`) +} + +export async function createGenericRuntime( + options: SdkRuntimeOptions = {}, +): Promise { + const network = assertRuntimeNetwork(options.network ?? 'mainnet') + const tronWeb = createReadonlyTronWeb(options) + const { createTronWebClient } = require('@sun-protocol/sun-sdk-runtime/node') as { + createTronWebClient(input: { + network: RuntimeNetwork + tronWeb: TronWeb + }): TronClient + } + return createContractRuntime({ + network, + tronClient: createTronWebClient({ network, tronWeb }), + wallet: options.wallet, + }) +} + +/** A fresh runtime confines an explicit ABI to this command, without mutating shared SDK state. */ +export async function withLiquidityAbi( + sdk: CliSunSDK, + address: string, + json?: string, + readSelectors: readonly string[] = [], +): Promise { + if (json === undefined) return sdk + const abi = JSON.parse(json) as readonly AbiItem[] + const scopedClient = withContractAbi(sdk.runtime.tronClient, address, abi) + const runtime = createRuntime({ + network: sdk.network, + tronClient: { + ...scopedClient, + // Legacy PM overrides applied to writes and only to the positions read + // used by single-side increase. Extra SDK planning views use their own ABI. + readContract: (call) => + readSelectors.includes(call.functionSelector) + ? scopedClient.readContract(call) + : sdk.runtime.tronClient.readContract(call), + }, + getWallet: () => sdk.runtime.getWallet(), + }) + const scoped = await SunSDK.create({ network: sdk.network, runtime }) + return scoped as CliSunSDK +} + +export async function createSdkRuntime(options: SdkRuntimeOptions = {}): Promise { + const network = assertSdkNetwork(options.network ?? 'mainnet') + const tronWeb = createReadonlyTronWeb(options) + const { createTronWebClient } = require('@sun-protocol/sun-sdk-runtime/node') as { + createTronWebClient(input: { network: Network; tronWeb: TronWeb }): TronClient + } + const tronClient = createTronWebClient({ + network, + tronWeb, + }) + return createRuntime({ + network, + tronClient, + wallet: options.wallet, + }) +} + +export function createSunApiClient(): SunApiClient { + return new SunApiClient() +} + +export async function createSunSDK(options: SdkRuntimeOptions = {}): Promise { + const network = assertSdkNetwork(options.network ?? 'mainnet') + const runtime = await createSdkRuntime(options) + const sdk = await SunSDK.create({ + network, + runtime, + }) + if (!sdk.runtime) { + throw new Error('SUN SDK runtime initialization failed') + } + return sdk as CliSunSDK +} diff --git a/src/lib/sdk/index.ts b/src/lib/sdk/index.ts new file mode 100644 index 0000000..6a71eec --- /dev/null +++ b/src/lib/sdk/index.ts @@ -0,0 +1,6 @@ +export * from './types' +export * from './errors' +export * from './compat' +export * from './constants' +export * from './wallet' +export * from './factory' diff --git a/src/lib/sdk/liquidity-v3.ts b/src/lib/sdk/liquidity-v3.ts new file mode 100644 index 0000000..d98a25d --- /dev/null +++ b/src/lib/sdk/liquidity-v3.ts @@ -0,0 +1,71 @@ +import { assertTronAddress } from '@sun-protocol/sun-sdk-core' +import { buildV3PoolStateReads, buildV3PositionRead } from '@sun-protocol/sun-sdk-sunswap-v3' +import { TronWeb } from 'tronweb' +import { CLI_V3_FACTORY, TRX_ADDRESS } from './constants' +import type { CliSunSDK } from './types' + +function address(value: unknown): string { + const raw = String(Array.isArray(value) ? value[0] : value) + return raw.startsWith('T') + ? raw + : TronWeb.address.fromHex(raw.startsWith('0x') ? `41${raw.slice(2)}` : raw) +} + +/** Look up the exact legacy CLI pool on-chain; do not assume the SDK's factory/init-code hash. */ +export async function resolveLegacyV3Pool( + sdk: CliSunSDK, + input: { + token0?: string + token1?: string + fee?: number + tokenId?: string + positionManager?: string + positionAbi?: string + }, +) { + let { token0, token1, fee } = input + if (token0 === undefined || token1 === undefined || fee === undefined) { + if (!input.tokenId || !input.positionManager) throw new Error('V3 position context is required') + const position = (await (input.positionAbi === undefined + ? sdk.runtime.readContract( + buildV3PositionRead({ + network: sdk.network, + tokenId: input.tokenId, + positionManager: assertTronAddress(input.positionManager), + }), + ) + : sdk.runtime.contract.read({ + address: input.positionManager, + abi: JSON.parse(input.positionAbi), + functionName: 'positions', + args: [input.tokenId], + }))) as Record | readonly unknown[] + const values = position as Record + token0 = address(values.token0 ?? values[2]) + token1 = address(values.token1 ?? values[3]) + fee = Number(values.fee ?? values[4]) + } + const pool = address( + await sdk.runtime.contract.read({ + address: CLI_V3_FACTORY[sdk.network], + functionSelector: 'getPool(address,address,uint24)', + parameters: [ + { type: 'address', value: token0 }, + { type: 'address', value: token1 }, + { type: 'uint24', value: fee }, + ], + outputs: [{ type: 'address' }], + }), + ) + if (pool === TRX_ADDRESS) throw new Error(`V3 pool not found for ${token0}/${token1} fee=${fee}`) + const reads = buildV3PoolStateReads({ pool: assertTronAddress(pool) }) + const results = await sdk.runtime.multicall([reads[0]!, reads[1]!, reads[3]!]) + if (results.some((result) => !result.success)) throw new Error('V3 legacy pool state read failed') + const slot = results[0]!.value as Record + return { + sqrtRatioX96: String(slot.sqrtPriceX96 ?? slot[0]), + tickCurrent: Number(slot.tick ?? slot[1]), + liquidity: String(results[1]!.value), + tickSpacing: Number(results[2]!.value), + } +} diff --git a/src/lib/sdk/liquidity-v4.ts b/src/lib/sdk/liquidity-v4.ts new file mode 100644 index 0000000..634cd1a --- /dev/null +++ b/src/lib/sdk/liquidity-v4.ts @@ -0,0 +1,92 @@ +import { Fraction, type Network } from '@sun-protocol/sun-sdk-core' +import { nearestUsableTick, TickMath } from '@sun-protocol/sun-sdk-sunswap-v3' +import { + computeV4PoolId, + createV4PoolKey, + encodeV4PoolParameters, +} from '@sun-protocol/sun-sdk-sunswap-v4' +import type { CliSunSDK } from './types' + +const DEFAULT_TICK_RANGE_FACTOR = 100 + +const TICK_SPACING_BY_FEE: Record = { + 100: 1, + 500: 10, + 3000: 60, + 10000: 200, +} + +export function v4TickSpacingForFee(fee: number): number { + return TICK_SPACING_BY_FEE[fee] ?? 10 +} + +export function v4PoolKeyFromCli(network: Network, token0: string, token1: string, fee: number) { + return createV4PoolKey({ + network, + currencyA: token0, + currencyB: token1, + fee, + parameters: encodeV4PoolParameters({ tickSpacing: v4TickSpacingForFee(fee) }), + }) +} + +interface V4MintTickInput { + token0: string + token1: string + fee: number + tickLower?: number + tickUpper?: number + sqrtPriceX96?: string + createPool?: boolean +} + +/** Resolve omitted V4 mint ticks from the pool state, as SunKit did. */ +export async function resolveV4MintTicks(sdk: CliSunSDK, input: V4MintTickInput) { + const tickSpacing = v4TickSpacingForFee(input.fee) + if (input.tickLower !== undefined && input.tickUpper !== undefined) { + return { + tickSpacing, + tickLower: input.tickLower, + tickUpper: input.tickUpper, + createPool: input.createPool, + } + } + + const poolKey = createV4PoolKey({ + network: sdk.network, + currencyA: input.token0, + currencyB: input.token1, + fee: input.fee, + parameters: encodeV4PoolParameters({ tickSpacing }), + }) + + let currentTick: number + let createPool = input.createPool + try { + const pool = await sdk.liquidity.v4.read({ poolId: computeV4PoolId(poolKey) }) + if (BigInt(pool.sqrtPriceX96) === 0n) throw new Error('V4 pool is not initialized') + currentTick = pool.tick + } catch (cause) { + if (input.sqrtPriceX96 === undefined) { + throw Object.assign( + new Error( + `V4 pool not found for ${input.token0}/${input.token1} fee=${input.fee}. Provide sqrtPriceX96 to create a new pool.`, + ), + { code: 'POOL_NOT_FOUND', cause }, + ) + } + currentTick = TickMath.getTickAtSqrtRatio(new Fraction(input.sqrtPriceX96).quotient) + createPool = true + } + + return { + tickSpacing, + tickLower: + input.tickLower ?? + nearestUsableTick(currentTick - DEFAULT_TICK_RANGE_FACTOR * tickSpacing, tickSpacing), + tickUpper: + input.tickUpper ?? + nearestUsableTick(currentTick + DEFAULT_TICK_RANGE_FACTOR * tickSpacing, tickSpacing), + createPool, + } +} diff --git a/src/lib/sdk/types.ts b/src/lib/sdk/types.ts new file mode 100644 index 0000000..600cbd3 --- /dev/null +++ b/src/lib/sdk/types.ts @@ -0,0 +1,16 @@ +import type { SunSDK } from '@sun-protocol/sun-sdk-protocols' +import type { Runtime } from '@sun-protocol/sun-sdk-runtime' + +export type CliNetwork = 'mainnet' | 'nile' | 'shasta' + +export type CliError = Error & { + code?: string + detail?: unknown +} + +/** + * The CLI always creates SunSDK with a runtime. Keep that invariant visible to + * command implementations even though the public SDK also supports read-only + * construction without one. + */ +export type CliSunSDK = SunSDK & { readonly runtime: Runtime } diff --git a/src/lib/sdk/wallet.ts b/src/lib/sdk/wallet.ts new file mode 100644 index 0000000..988a160 --- /dev/null +++ b/src/lib/sdk/wallet.ts @@ -0,0 +1,37 @@ +import type { + SignedTronTransaction, + TypedDataPayload, + UnsignedTronTransaction, + WalletAdapter, +} from '@sun-protocol/sun-sdk-runtime' +import type { Wallet as AgentWallet, Eip712Capable } from '@bankofai/agent-wallet' + +export class AgentWalletSdkAdapter implements WalletAdapter { + readonly kind = 'custom' as const + readonly capabilities = { + signTransaction: true, + signAndSendTransaction: false, + signTypedData: true, + userInteractive: false, + } + + constructor(private readonly agentWallet: AgentWallet) {} + + async getAddress(): Promise { + return this.agentWallet.getAddress() + } + + async signTransaction(tx: UnsignedTronTransaction): Promise { + const signedJson = await this.agentWallet.signTransaction(tx as Record) + return JSON.parse(signedJson) + } + + async signMessage(message: string): Promise { + return this.agentWallet.signMessage(Buffer.from(message, 'utf-8')) + } + + async signTypedData(payload: TypedDataPayload): Promise { + const sig = await (this.agentWallet as unknown as Eip712Capable).signTypedData(payload as any) + return sig.startsWith('0x') ? sig.slice(2) : sig + } +} diff --git a/src/lib/slippage.ts b/src/lib/slippage.ts new file mode 100644 index 0000000..6127901 --- /dev/null +++ b/src/lib/slippage.ts @@ -0,0 +1,28 @@ +export interface ParsedDecimalSlippage { + /** Slippage in whole basis points. */ + bips: number + /** Percentage string accepted by the public SDK. */ + sdk: `${number}%` + /** Stable human-readable percentage. */ + display: string +} + +/** + * Parse the CLI's decimal-ratio syntax exactly: 0.01 means 1%. + * Four decimal places are the smallest value representable as whole basis points. + */ +export function parseDecimalSlippage(value: string, label = '--slippage'): ParsedDecimalSlippage { + const match = /^(0|1)(?:\.(\d+))?$/.exec(value) + const fraction = match?.[2] ?? '' + if (!match || fraction.length > 4 || (match[1] === '1' && /[1-9]/.test(fraction))) { + throw new Error(`${label} must be a decimal between 0 and 1 with at most 4 decimal places`) + } + + const bips = Number(match[1]) * 10_000 + Number(fraction.padEnd(4, '0')) + const percent = bips / 100 + return { + bips, + sdk: `${percent}%`, + display: `${percent.toFixed(2)}%`, + } +} diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts deleted file mode 100644 index 7044c58..0000000 --- a/src/lib/sunpump.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * SunPump API client — read-only GET endpoints. - * - * Mainnet only: https://api-v2.sunpump.meme/pump-api - * - * Standalone from sun-kit's SunAPI: SunPump is a separate service with its own - * base URL and schema. Uses Node's global fetch (>=18). - * - * Methods mirror the OpenAPI operationIds documented in docs/sunpump-api.md. - */ - -export const SUNPUMP_DEFAULT_BASE_URL = 'https://api-v2.sunpump.meme/pump-api' - -export interface SunPumpClientOptions { - baseUrl?: string - fetchImpl?: typeof fetch -} - -type QueryValue = string | number | boolean | null | undefined -export type Query = Record - -export interface AgentTokenLaunchParams { - name: string - symbol: string - description: string - /** Logo image content as a base64 string (no data-URI prefix). */ - imageBase64?: string - twitterUrl?: string - telegramUrl?: string - websiteUrl?: string - tweetUsername?: string -} - -export class SunPumpHttpError extends Error { - readonly code = 'SUNPUMP_HTTP_ERROR' - constructor( - message: string, - readonly status: number, - readonly body?: string, - ) { - super(message) - this.name = 'SunPumpHttpError' - } -} - -function buildQueryString(query?: Query): string { - if (!query) return '' - const usp = new URLSearchParams() - for (const [key, value] of Object.entries(query)) { - if (value === undefined || value === null || value === '') continue - usp.append(key, String(value)) - } - const s = usp.toString() - return s ? `?${s}` : '' -} - -export class SunPump { - private readonly baseUrl: string - private readonly fetchImpl: typeof fetch - - constructor(opts: SunPumpClientOptions = {}) { - const base = opts.baseUrl ?? process.env.SUNPUMP_API_BASE_URL ?? SUNPUMP_DEFAULT_BASE_URL - this.baseUrl = base.replace(/\/+$/, '') - this.fetchImpl = opts.fetchImpl ?? fetch - } - - async request(path: string, query?: Query): Promise { - return this.send(path, buildQueryString(query), { - method: 'GET', - headers: { Accept: 'application/json' }, - }) - } - - async post(path: string, body: unknown): Promise { - return this.send(path, '', { - method: 'POST', - headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - } - - private async send(path: string, queryString: string, init: RequestInit): Promise { - const url = `${this.baseUrl}${path}${queryString}` - const res = await this.fetchImpl(url, init) - const text = await res.text() - if (!res.ok) { - const excerpt = text.length > 500 ? text.slice(0, 500) + '…' : text - let apiMsg: string | undefined - try { - const parsed = JSON.parse(text) - if (parsed && typeof parsed === 'object' && typeof parsed.msg === 'string') { - apiMsg = parsed.msg - } - } catch { - // body not JSON; fall through - } - const suffix = apiMsg ? ` — ${apiMsg}` : '' - throw new SunPumpHttpError( - `SunPump request failed: ${res.status} ${res.statusText} (${path})${suffix}`, - res.status, - excerpt, - ) - } - if (!text) return undefined as unknown as T - try { - return JSON.parse(text) as T - } catch { - throw new SunPumpHttpError(`SunPump returned non-JSON body (${path})`, res.status, text) - } - } - - // --------------------------------------------------------------------------- - // Token Info - // --------------------------------------------------------------------------- - - listTokens( - query: { - contractAddress?: string - ownerAddress?: string - symbol?: string - name?: string - description?: string - page?: number - size?: number - sort?: string - } = {}, - ) { - return this.request('/token', query) - } - - getToken(contractAddress: string) { - return this.request(`/token/${encodeURIComponent(contractAddress)}`) - } - - searchTokens(query: { - query?: string - onSunSwap?: boolean - page?: number - size?: number - sort?: string - }) { - return this.request('/token/search', query) - } - - searchTokensV2(query: { - query?: string - onSunSwap?: boolean - filterDliveShowing?: boolean - filterAiHelper?: boolean - filterTwitterLaunch?: boolean - filterSunAgentLaunch?: boolean - page?: number - size?: number - sort?: string - }) { - return this.request('/token/searchV2', query) - } - - tokensByOwner(query: { address: string; page?: number; size?: number; sort?: string }) { - return this.request('/token/search/by_owner', query) - } - - tokenHolders(query: { - address: string - includeZeroBalance?: boolean - page?: number - size?: number - sort?: string - }) { - return this.request('/token/holders', query) - } - - tokenHoldersV2(query: { - address: string - includeZeroBalance?: boolean - page?: number - size?: number - sort?: string - }) { - return this.request('/token/holdersV2', query) - } - - userFavors(query: { - signature: string - signedMessage: string - userAddress: string - tokenAddress?: string - page?: number - size?: number - }) { - return this.request('/token/getUserFavors', query) - } - - ranking(query: { rankingType: string; size?: number }) { - return this.request('/token/getRanking', query) - } - - kingOfHill() { - return this.request('/token/getKingOfHill') - } - - pumpTokenList() { - return this.request('/token/SunPumpTokenList.json') - } - - // --------------------------------------------------------------------------- - // Transactions - // --------------------------------------------------------------------------- - - tokenTransactions( - contractAddress: string, - query: { - swapTranType?: string - swapPoolAddress?: string - txHash?: string - blockNum?: number - startTime?: number - endTime?: number - page?: number - size?: number - sort?: string - } = {}, - ) { - return this.request(`/transactions/token/${encodeURIComponent(contractAddress)}`, query) - } - - userTransactions( - ownerAddress: string, - query: { - swapTranType?: string - swapPoolAddress?: string - txHash?: string - blockNum?: number - startTime?: number - endTime?: number - page?: number - size?: number - sort?: string - } = {}, - ) { - return this.request(`/transactions/holder/${encodeURIComponent(ownerAddress)}`, query) - } - - // --------------------------------------------------------------------------- - // AI agent — token launch - // --------------------------------------------------------------------------- - - /** - * Launch a new token through the SunPump agent endpoint. The server creates - * the token on-chain itself — no local wallet or signing involved. Returns - * the full token object (contractAddress, createTxHash, …) in the envelope. - */ - agentTokenLaunch(params: AgentTokenLaunchParams) { - return this.post('/ai/agentTokenLaunch', params) - } - - // --------------------------------------------------------------------------- - // Holder portfolio - // --------------------------------------------------------------------------- - - portfolio( - address: string, - query: { - includeZeroBalance?: boolean - trxAmountMin?: number - page?: number - size?: number - sort?: string - } = {}, - ) { - return this.request(`/holders/${encodeURIComponent(address)}/tokens`, query) - } -} - -let _client: SunPump | null = null - -export function getSunPump(): SunPump { - if (!_client) _client = new SunPump() - return _client -} diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index 96e5549..9714220 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -3,7 +3,7 @@ * Provides a unified way to resolve token symbols to addresses across all commands. */ -import { TRX_ADDRESS, WTRX_MAINNET, WTRX_NILE } from '@sun-protocol/sun-kit' +import { TRX_ADDRESS, WTRX_MAINNET, WTRX_NILE } from './sdk/constants' export interface TokenInfo { symbol: string diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index e262ded..5add179 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -1,13 +1,8 @@ -import type { Wallet } from '@sun-protocol/sun-kit' -import { createReadonlyTronWeb } from '@sun-protocol/sun-kit' -import { - resolveWalletProvider, - type Wallet as BaseWallet, - type Eip712Capable, -} from '@bankofai/agent-wallet' -import { TronWeb } from 'tronweb' +import { resolveWalletProvider } from '@bankofai/agent-wallet' +import type { WalletAdapter } from '@sun-protocol/sun-sdk-runtime' +import { AgentWalletSdkAdapter } from './sdk/wallet' -export type { Wallet } +export type Wallet = WalletAdapter let _wallet: Wallet | null = null @@ -20,7 +15,7 @@ export async function initWallet(): Promise { _wallet = null return } - _wallet = new AgentWalletAdapter(activeWallet) + _wallet = new AgentWalletSdkAdapter(activeWallet) } catch { _wallet = null } @@ -42,87 +37,3 @@ export function isWalletConfigured(): boolean { export async function getWalletAddress(): Promise { return getWallet().getAddress() } - -class AgentWalletAdapter implements Wallet { - readonly type = 'agent-wallet' as const - - constructor(private readonly agentWallet: BaseWallet) {} - - async getAddress(): Promise { - return await this.agentWallet.getAddress() - } - - async getTronWeb(network = 'mainnet'): Promise { - const tronWeb = await createReadonlyTronWeb(network) - const ownerAddress = await this.agentWallet.getAddress() - const ownerHex = - typeof (tronWeb as any).address?.toHex === 'function' - ? (tronWeb as any).address.toHex(ownerAddress) - : ownerAddress - const ownerBase58 = - typeof (tronWeb as any).address?.fromHex === 'function' - ? (tronWeb as any).address.fromHex(ownerHex) - : ownerAddress - - const tronWebWithDefaultAddress = tronWeb as any - tronWebWithDefaultAddress.defaultAddress = { hex: ownerHex, base58: ownerBase58 } - return tronWeb - } - - async signAndBroadcast( - unsignedTx: Record, - network = 'mainnet', - ): Promise<{ result: boolean; txid: string }> { - const tx = (unsignedTx as any).transaction || unsignedTx - const txid = await this.buildSignBroadcast(tx, network) - return { result: true, txid } - } - - async buildSignBroadcast( - unsignedTx: Record, - network = 'mainnet', - ): Promise { - const signedTx = await this.signTransaction(unsignedTx) - const tronWeb = await createReadonlyTronWeb(network) - const result = await tronWeb.trx.sendRawTransaction(signedTx as any) - - if (result.result) { - return result.txid - } - - throw new Error(`Broadcast failed: ${JSON.stringify(result)}`) - } - - async signTransaction(unsignedTx: Record): Promise { - const signedJson = await this.agentWallet.signTransaction(unsignedTx) - return JSON.parse(signedJson) - } - - async signMessage(message: string): Promise { - return await this.agentWallet.signMessage(Buffer.from(message, 'utf-8')) - } - - async signTypedData( - primaryType: string, - domain: Record, - types: Record, - message: Record, - ): Promise { - const signer = this.agentWallet as unknown as Eip712Capable - const sig = await signer.signTypedData({ - domain, - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - ...types, - }, - primaryType, - message, - }) - - return sig.startsWith('0x') ? sig.slice(2) : sig - } -} diff --git a/test/commands/liquidity-sdk.test.ts b/test/commands/liquidity-sdk.test.ts new file mode 100644 index 0000000..4be2683 --- /dev/null +++ b/test/commands/liquidity-sdk.test.ts @@ -0,0 +1,638 @@ +import { Command } from 'commander' + +const nileSun = 'TWrZRHY9aKQZcyjpovdH6qeCEyYZrRQDZt' +const nileUsdt = 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf' + +function v4PositionSnapshot() { + return { + tokenId: '7', + poolKey: { currency0: nileSun, currency1: nileUsdt, fee: 500 }, + liquidity: '1000', + } +} + +describe('liquidity SDK integration', () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + }) + + it('unwraps TronWeb single-output address arrays', () => { + const { unwrapContractAddressResult } = require('../../src/commands/liquidity') + expect(unwrapContractAddressResult(['41abc'], 'getPair')).toBe('41abc') + expect(unwrapContractAddressResult({ pair: '41def' }, 'getPair')).toBe('41def') + expect(() => unwrapContractAddressResult([], 'getPair')).toThrow( + 'getPair did not return an address', + ) + }) + + async function captureWriteAction(args: string[]) { + jest.resetModules() + const writeAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/command', () => ({ + writeAction, + readAction: jest.fn().mockResolvedValue(undefined), + })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + jest.doMock('../../src/lib/sdk/liquidity-v3', () => ({ + resolveLegacyV3Pool: async () => ({ + sqrtRatioX96: '79228162514264337593543950336', + liquidity: '1000000', + tickCurrent: 0, + tickSpacing: 60, + }), + })) + + const { registerLiquidityCommands } = require('../../src/commands/liquidity') + const program = new Command() + program.exitOverride() + registerLiquidityCommands(program) + await program.parseAsync(['node', 'sun', 'liquidity', ...args], { from: 'node' }) + + return writeAction.mock.calls[0][0] + } + + it.each([ + ['mint', ['v4:mint', '--token0', 'SUN', '--token1', 'USDT']], + ['increase', ['v4:increase', '--token-id', '7', '--token0', 'SUN', '--token1', 'USDT']], + ])( + 'returns a structured invalid-params error when V4 %s omits both amounts', + async (_name, args) => { + const outputError = jest.fn() + const writeAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/output', () => ({ + outputError, + withSpinner: async (_label: string, run: () => Promise) => run(), + })) + jest.doMock('../../src/lib/command', () => ({ + writeAction, + readAction: jest.fn().mockResolvedValue(undefined), + })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + + const { registerLiquidityCommands } = require('../../src/commands/liquidity') + const program = new Command() + program.exitOverride() + registerLiquidityCommands(program) + await program.parseAsync(['node', 'sun', 'liquidity', ...args], { from: 'node' }) + + expect(outputError).toHaveBeenCalledWith( + 'Invalid liquidity amounts', + expect.objectContaining({ + code: 'INVALID_PARAMS', + message: 'At least one of --amount0 or --amount1 is required', + }), + ) + expect(writeAction).not.toHaveBeenCalled() + }, + ) + + it.each([ + [ + 'V3 mint', + ['v3:mint', '--token0', 'SUN', '--token1', 'USDT', '--amount0', '0', '--amount1', '0'], + ], + ['V3 increase', ['v3:increase', '--token-id', '7', '--amount0', '0', '--amount1', '0']], + [ + 'V4 mint', + ['v4:mint', '--token0', 'SUN', '--token1', 'USDT', '--amount0', '0', '--amount1', '0'], + ], + ['V4 increase', ['v4:increase', '--token-id', '7', '--amount0', '0', '--amount1', '0']], + ])( + 'rejects explicit zero-only amounts before %s reaches dry-run or the SDK', + async (_name, args) => { + const outputError = jest.fn() + const writeAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/output', () => ({ + outputError, + withSpinner: async (_label: string, run: () => Promise) => run(), + })) + jest.doMock('../../src/lib/command', () => ({ + writeAction, + readAction: jest.fn().mockResolvedValue(undefined), + })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + + const { registerLiquidityCommands } = require('../../src/commands/liquidity') + const program = new Command() + program.exitOverride() + registerLiquidityCommands(program) + await program.parseAsync(['node', 'sun', 'liquidity', ...args], { from: 'node' }) + + expect(outputError).toHaveBeenCalledWith( + 'Invalid liquidity amounts', + expect.objectContaining({ + code: 'INVALID_PARAMS', + message: 'At least one of --amount0 or --amount1 must be greater than zero', + }), + ) + expect(writeAction).not.toHaveBeenCalled() + }, + ) + + it('rejects a single V4 position token assertion before loading the SDK', async () => { + const outputError = jest.fn() + const writeAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/output', () => ({ + outputError, + withSpinner: async (_label: string, run: () => Promise) => run(), + })) + jest.doMock('../../src/lib/command', () => ({ + writeAction, + readAction: jest.fn().mockResolvedValue(undefined), + })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + + const { registerLiquidityCommands } = require('../../src/commands/liquidity') + const program = new Command() + program.exitOverride() + registerLiquidityCommands(program) + await program.parseAsync( + [ + 'node', + 'sun', + 'liquidity', + 'v4:increase', + '--token-id', + '7', + '--token0', + 'SUN', + '--amount0', + '1', + ], + { from: 'node' }, + ) + + expect(outputError).toHaveBeenCalledWith( + 'Invalid V4 position assertions', + expect.objectContaining({ code: 'INVALID_PARAMS' }), + ) + expect(writeAction).not.toHaveBeenCalled() + }) + + it('retains partial V3 tick ranges and lets the SDK derive omitted minima', async () => { + const action = await captureWriteAction([ + 'v3:mint', + '--token0', + 'SUN', + '--token1', + 'USDT', + '--tick-lower', + '-600', + '--amount0', + '1000', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['mint-tx'] }) + await action.execute({ liquidity: { v3: { add: { execute } } } }) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + tickLower: -600, + tickUpper: undefined, + tickRangeMode: 'legacy', + tickRangeWidth: 100, + ttlSeconds: 1800, + amount0Min: undefined, + amount1Min: undefined, + }), + ) + expect(execute.mock.calls[0][0]).not.toHaveProperty('amountInputMode') + }) + + it('passes V2 remove minimum amounts to the SDK', async () => { + const action = await captureWriteAction([ + 'v2:remove', + '--token-a', + 'SUN', + '--token-b', + 'USDT', + '--liquidity', + '100', + '--min-a', + '11', + '--min-b', + '22', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['v2-remove-tx'] }) + + await action.execute({ liquidity: { v2: { remove: { execute } } } }) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + liquidity: '100', + amountAMin: '11', + amountBMin: '22', + }), + ) + }) + + it('passes V3 increase minimum amounts to the SDK', async () => { + const action = await captureWriteAction([ + 'v3:increase', + '--token-id', + '7', + '--amount0', + '100', + '--min0', + '11', + '--min1', + '22', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['v3-increase-tx'] }) + + await action.execute({ liquidity: { v3: { increase: { execute } } } }) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + tokenId: '7', + amount0: '100', + amount0Min: '11', + amount1Min: '22', + }), + ) + expect(execute.mock.calls[0][0]).not.toHaveProperty('amountInputMode') + }) + + it('preserves automatic V3 mint ticks through the SDK tick range width', async () => { + const action = await captureWriteAction([ + 'v3:mint', + '--token0', + 'TRX', + '--token1', + 'USDT', + '--amount0', + '100', + '--amount1', + '200', + '--min0', + '11', + '--min1', + '22', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['v3-mint-tx'] }) + + await action.execute({ liquidity: { v3: { add: { execute } } } }) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + tickLower: undefined, + tickUpper: undefined, + tickRangeWidth: 100, + tokenA: 'TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a', + tokenB: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', + amount0: '200', + amount1: '100', + amount0Min: '22', + amount1Min: '11', + }), + ) + }) + + it('passes V3 decrease minimum amounts to the SDK', async () => { + const action = await captureWriteAction([ + 'v3:decrease', + '--token-id', + '7', + '--liquidity', + '100', + '--min0', + '11', + '--min1', + '22', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['v3-decrease-tx'] }) + + await action.execute({ positions: { v3: { remove: { execute } } } }) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + tokenId: '7', + liquidity: '100', + amount0Min: '11', + amount1Min: '22', + collectFees: false, + }), + ) + }) + + it('maps V4 mint defaults and recipient to the public SDK input', async () => { + const action = await captureWriteAction([ + 'v4:mint', + '--token0', + 'SUN', + '--token1', + 'USDT', + '--amount0', + '100', + '--recipient', + 'TOwner', + ]) + const execute = jest.fn().mockResolvedValue({ + finalResult: { type: 'transaction', txid: 'mint-tx', raw: { result: true } }, + }) + const read = jest + .fn() + .mockResolvedValue({ sqrtPriceX96: '79228162514264337593543950336', tick: 12347 }) + + await expect( + action.execute({ network: 'nile', liquidity: { v4: { read, add: { execute } } } }), + ).resolves.toMatchObject({ txid: 'mint-tx' }) + expect(action.summary.Fee).toBe('500 (default)') + expect(action.summary['Tick Range']).toBe('(auto)') + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + fee: 500, + tickSpacing: 10, + tickLower: 11350, + tickUpper: 13350, + amount0: '100', + recipient: 'TOwner', + sweepRecipient: 'TOwner', + }), + ) + expect(execute.mock.calls[0][0]).not.toHaveProperty('owner') + expect(execute.mock.calls[0][0]).not.toHaveProperty('amountInputMode') + }) + + it('preserves explicit V4 fees and tick zero without an automatic pool read', async () => { + const action = await captureWriteAction([ + 'v4:mint', + '--token0', + 'SUN', + '--token1', + 'USDT', + '--amount0', + '100', + '--amount1', + '200', + '--fee', + '3000', + '--tick-lower', + '0', + '--tick-upper', + '120', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['mint-explicit-tx'] }) + const read = jest.fn() + + await action.execute({ network: 'nile', liquidity: { v4: { read, add: { execute } } } }) + + expect(read).not.toHaveBeenCalled() + expect(action.summary.Fee).toBe('3000') + expect(action.summary['Tick Range']).toBe('[0, 120]') + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + fee: 3000, + tickSpacing: 60, + tickLower: 0, + tickUpper: 120, + }), + ) + }) + + it('does not submit V4 mint when automatic ticks cannot be resolved', async () => { + const action = await captureWriteAction([ + 'v4:mint', + '--token0', + 'SUN', + '--token1', + 'USDT', + '--amount0', + '100', + ]) + const execute = jest.fn() + const read = jest.fn().mockRejectedValue(new Error('Pool unavailable')) + + await expect( + action.execute({ + network: 'nile', + liquidity: { v4: { read, add: { execute } } }, + }), + ).rejects.toMatchObject({ code: 'POOL_NOT_FOUND' }) + expect(execute).not.toHaveBeenCalled() + }) + + it('preserves the V4 info output shape while using corrected SDK values', async () => { + const readAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/command', () => ({ + writeAction: jest.fn().mockResolvedValue(undefined), + readAction, + })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + + const { registerLiquidityCommands } = require('../../src/commands/liquidity') + const program = new Command() + program.exitOverride() + registerLiquidityCommands(program) + await program.parseAsync( + ['node', 'sun', 'liquidity', 'v4:info', '--pm', 'TPositionManager', '--token-id', '7'], + { from: 'node' }, + ) + + const action = readAction.mock.calls[0][0] + expect( + action.transform({ + poolKey: { currency0: 'TToken0', currency1: 'TToken1', fee: 500 }, + tickLower: -120, + tickUpper: 120, + liquidity: '199999', + }), + ).toEqual({ + currency0: 'TToken0', + currency1: 'TToken1', + fee: '500', + tickSpacing: 10, + tickLower: -120, + tickUpper: 120, + liquidity: '199999', + }) + }) + + it('uses tokenId-derived V4 pool data and standard SDK amount semantics', async () => { + const action = await captureWriteAction([ + 'v4:increase', + '--token-id', + '7', + '--token0', + 'SUN', + '--token1', + 'USDT', + '--amount1', + '200', + '--slippage', + '0.01', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['increase-tx'] }) + const read = jest.fn().mockResolvedValue(v4PositionSnapshot()) + const sdk = { + positions: { v4: { read } }, + liquidity: { v4: { increase: { execute } } }, + } + + await action.prepare(sdk) + await action.execute(sdk) + expect(read).toHaveBeenCalledTimes(1) + expect(action.summary).toMatchObject({ + Token0: `SUN (${nileSun})`, + Token1: `USDT (${nileUsdt})`, + Fee: 500, + }) + expect(execute).toHaveBeenCalledWith({ + tokenId: '7', + amount0: undefined, + amount1: '200', + slippage: '1%', + includeComputedMetadata: true, + transactionMode: 'multicall', + ttlSeconds: 1800, + deadline: undefined, + }) + }) + + it('lets the SDK derive the V4 pool key for remove and collect', async () => { + const removeAction = await captureWriteAction([ + 'v4:decrease', + '--token-id', + '7', + '--liquidity', + '10', + '--min0', + '1', + ]) + const remove = jest.fn().mockResolvedValue({ txids: ['remove-tx'] }) + const removeRead = jest.fn().mockResolvedValue(v4PositionSnapshot()) + const removeSdk = { positions: { v4: { read: removeRead, remove: { execute: remove } } } } + await removeAction.prepare(removeSdk) + await removeAction.execute(removeSdk) + expect(removeRead).toHaveBeenCalledTimes(1) + expect(remove).toHaveBeenCalledWith( + expect.objectContaining({ + tokenId: '7', + liquidity: '10', + amount0Min: '1', + amount1Min: undefined, + slippage: undefined, + transactionMode: 'multicall', + settlementMode: 'close-currency', + deadline: undefined, + }), + ) + expect(remove.mock.calls[0][0]).not.toHaveProperty('poolKey') + + const collectAction = await captureWriteAction(['v4:collect', '--token-id', '7']) + const collect = jest.fn().mockResolvedValue({ txids: ['collect-tx'] }) + const collectRead = jest.fn().mockResolvedValue(v4PositionSnapshot()) + const collectSdk = { + positions: { v4: { read: collectRead, collect: { execute: collect } } }, + } + await collectAction.prepare(collectSdk) + await collectAction.execute(collectSdk) + expect(collectRead).toHaveBeenCalledTimes(1) + expect(collect).toHaveBeenCalledWith({ + tokenId: '7', + deadline: undefined, + transactionMode: 'multicall', + settlementMode: 'close-currency', + ttlSeconds: 1800, + }) + }) + + it('maps reversed V4 token inputs and corresponding amounts to position order', async () => { + const increaseAction = await captureWriteAction([ + 'v4:increase', + '--token-id', + '7', + '--token0', + 'USDT', + '--token1', + 'SUN', + '--amount0', + '200', + '--amount1', + '100', + ]) + const increase = jest.fn().mockResolvedValue({ txids: ['increase-reversed-tx'] }) + const increaseSdk = { + positions: { v4: { read: jest.fn().mockResolvedValue(v4PositionSnapshot()) } }, + liquidity: { v4: { increase: { execute: increase } } }, + } + await increaseAction.prepare(increaseSdk) + await increaseAction.execute(increaseSdk) + expect(increase).toHaveBeenCalledWith( + expect.objectContaining({ amount0: '100', amount1: '200' }), + ) + + const decreaseAction = await captureWriteAction([ + 'v4:decrease', + '--token-id', + '7', + '--liquidity', + '10', + '--token0', + 'USDT', + '--token1', + 'SUN', + '--min0', + '22', + '--min1', + '11', + ]) + const remove = jest.fn().mockResolvedValue({ txids: ['remove-reversed-tx'] }) + const decreaseSdk = { + positions: { + v4: { + read: jest.fn().mockResolvedValue(v4PositionSnapshot()), + remove: { execute: remove }, + }, + }, + } + await decreaseAction.prepare(decreaseSdk) + await decreaseAction.execute(decreaseSdk) + expect(remove).toHaveBeenCalledWith( + expect.objectContaining({ amount0Min: '11', amount1Min: '22' }), + ) + }) + + it('rejects V4 position token and fee assertions that do not match tokenId', async () => { + const tokenAction = await captureWriteAction([ + 'v4:collect', + '--token-id', + '7', + '--token0', + 'TRX', + '--token1', + 'USDT', + ]) + const tokenSdk = { + positions: { v4: { read: jest.fn().mockResolvedValue(v4PositionSnapshot()) } }, + } + await expect(tokenAction.prepare(tokenSdk)).rejects.toMatchObject({ code: 'INVALID_PARAMS' }) + + const feeAction = await captureWriteAction(['v4:collect', '--token-id', '7', '--fee', '3000']) + const feeSdk = { + positions: { v4: { read: jest.fn().mockResolvedValue(v4PositionSnapshot()) } }, + } + await expect(feeAction.prepare(feeSdk)).rejects.toMatchObject({ code: 'INVALID_PARAMS' }) + }) + + it('keeps native TRX unwrapped for V4 SDK calls', async () => { + const action = await captureWriteAction([ + 'v4:mint', + '--token0', + 'TRX', + '--token1', + 'USDT', + '--amount0', + '100', + '--tick-lower', + '-100', + '--tick-upper', + '100', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['mint-native-tx'] }) + + await action.execute({ network: 'nile', liquidity: { v4: { add: { execute } } } }) + + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + token0: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb', + token1: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', + }), + ) + }) +}) diff --git a/test/commands/sdk-write-migration.test.ts b/test/commands/sdk-write-migration.test.ts new file mode 100644 index 0000000..ff99564 --- /dev/null +++ b/test/commands/sdk-write-migration.test.ts @@ -0,0 +1,155 @@ +import { Command } from 'commander' + +describe('SDK command migrations', () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + }) + + function makeProgram() { + const program = new Command() + program.exitOverride() + return program + } + + it.each(['read', 'send'])('keeps contract %s ABI optional for SDK discovery', async (verb) => { + const capture = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/command', () => ({ + readRuntimeAction: capture, + writeRuntimeAction: capture, + })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'shasta' })) + const { registerContractCommands } = require('../../src/commands/contract') + const program = makeProgram() + registerContractCommands(program) + await program.parseAsync(['node', 'sun', 'contract', verb, 'TContract', 'totalSupply']) + const call = jest.fn().mockResolvedValue('42') + await capture.mock.calls[0][0].execute({ runtime: { contract: { read: call, write: call } } }) + expect(call).toHaveBeenCalledWith( + expect.objectContaining({ functionName: 'totalSupply', abi: undefined, args: [] }), + ) + }) + + it('wallet balances calls SDK readBalances with string tokens', async () => { + const readAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/command', () => ({ readRuntimeAction: readAction })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + const readBalances = jest + .fn() + .mockResolvedValue([{ token: 'TRX', balance: '100', decimals: 6 }]) + jest.doMock('@sun-protocol/sun-sdk-runtime', () => ({ readBalances })) + + const { registerWalletCommands } = require('../../src/commands/wallet') + const program = makeProgram() + registerWalletCommands(program) + + await program.parseAsync( + ['node', 'sun', 'wallet', 'balances', '--owner', 'TOwner', '--tokens', 'TRX,TToken'], + { from: 'node' }, + ) + + const opts = readAction.mock.calls[0][0] + const tronClient = { kind: 'tron-client' } + const runtime = { tronClient } + await expect(opts.execute({ runtime })).resolves.toEqual([ + { address: 'TOwner', type: 'TRX', tokenAddress: undefined, balance: '100' }, + ]) + expect(readBalances).toHaveBeenCalledWith(tronClient, { + owner: 'TOwner', + tokens: ['TRX', 'TToken'], + }) + }) + + it('contract send calls SDK runtime contract client and maps tx result', async () => { + const writeAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/command', () => ({ writeRuntimeAction: writeAction })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + const write = jest.fn().mockResolvedValue({ txid: 'tx1', raw: { result: true } }) + + const { registerContractCommands } = require('../../src/commands/contract') + const program = makeProgram() + registerContractCommands(program) + + await program.parseAsync( + [ + 'node', + 'sun', + 'contract', + 'send', + 'TContract', + 'transfer', + '--args', + '["TA","1"]', + '--abi', + '[{"type":"function","name":"transfer","inputs":[]}]', + '--value', + '10', + ], + { from: 'node' }, + ) + + const opts = writeAction.mock.calls[0][0] + const sdk = { runtime: { contract: { write } } } + await expect(opts.execute(sdk)).resolves.toEqual({ txid: 'tx1', result: true }) + expect(write).toHaveBeenCalledWith({ + address: 'TContract', + functionName: 'transfer', + args: ['TA', '1'], + abi: [{ type: 'function', name: 'transfer', inputs: [] }], + callValue: 10n, + }) + }) + + it('swap execute quotes, executes, and maps SDK plan result', async () => { + const writeAction = jest.fn().mockResolvedValue(undefined) + jest.doMock('../../src/lib/command', () => ({ writeAction })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'nile' })) + + const { registerSwapCommands } = require('../../src/commands/swap') + const program = makeProgram() + registerSwapCommands(program) + + await program.parseAsync(['node', 'sun', 'swap', 'SUN', 'USDT', '1', '--slippage', '0.005'], { + from: 'node', + }) + + const opts = writeAction.mock.calls[0][0] + const quote = { + kind: 'router', + bestRoute: { amountIn: '1', amountOut: '2', symbols: ['SUN', 'USDT'] }, + } + const sdk = { + swap: { + router: { + quote: jest.fn().mockResolvedValue(quote), + execute: jest.fn().mockResolvedValue({ + txids: ['swap-tx'], + finalResult: { type: 'transaction', txid: 'swap-tx', raw: { result: true } }, + }), + }, + }, + } + + await expect(opts.execute(sdk)).resolves.toEqual({ + txid: 'swap-tx', + route: { + amountIn: '1', + amountOut: '2', + symbols: ['SUN', 'USDT'], + poolVersions: undefined, + impact: undefined, + }, + }) + expect(sdk.swap.router.quote).toHaveBeenCalledWith({ + tokenIn: 'TWrZRHY9aKQZcyjpovdH6qeCEyYZrRQDZt', + tokenOut: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', + amountIn: '1', + }) + expect(sdk.swap.router.execute).toHaveBeenCalledWith({ + quote, + route: quote.bestRoute, + slippage: '0.5%', + transactionValidForSeconds: 3600, + }) + }) +}) diff --git a/test/commands/sunpump-sdk.test.ts b/test/commands/sunpump-sdk.test.ts new file mode 100644 index 0000000..a202bfc --- /dev/null +++ b/test/commands/sunpump-sdk.test.ts @@ -0,0 +1,140 @@ +import { Command } from 'commander' + +describe('sunpump SDK integration', () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + }) + + it.each(['buy', 'sell'])( + 'forwards %s minimum and preserves launchpad-only trading', + async (direction) => { + const writeAction = jest.fn().mockResolvedValue(undefined) + const quote = jest + .fn() + .mockResolvedValue({ tokenAmount: '1000', trxAmount: '1000', fee: '0' }) + jest.doMock('../../src/lib/command', () => ({ writeAction, isDryRun: () => true })) + jest.doMock('../../src/lib/context', () => ({ + getNetwork: () => 'mainnet', + getKit: async () => ({ pump: { [direction]: { quote } } }), + })) + jest.doMock('../../src/lib/output', () => ({ + isJsonMode: () => true, + withSpinner: async (_label: string, run: () => Promise) => run(), + })) + const { registerSunpumpCommands } = require('../../src/commands/sunpump') + const program = new Command() + program.exitOverride() + registerSunpumpCommands(program) + await program.parseAsync([ + 'node', + 'sun', + 'sunpump', + direction, + 'TToken', + direction === 'buy' ? '--trx' : '--amount', + '1', + '--min-out', + '990', + ]) + const execute = jest.fn().mockResolvedValue({ txids: ['pump-tx'] }) + await writeAction.mock.calls[0][0].execute({ pump: { [direction]: { execute } } }) + expect(quote).toHaveBeenCalledWith(expect.objectContaining({ launchpadOnly: true })) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ + token: 'TToken', + launchpadOnly: true, + slippage: '5%', + [direction === 'buy' ? 'minTokenOut' : 'minTrxOut']: '990', + }), + ) + }, + ) + + it('passes the existing custom API endpoint through to the public SDK client', async () => { + const previous = process.env.SUNPUMP_API_BASE_URL + process.env.SUNPUMP_API_BASE_URL = 'https://example.invalid/custom-pump' + try { + const SunPumpApiClient = jest + .fn() + .mockImplementation(() => ({ searchTokensV2: async () => ({ code: 0, data: [] }) })) + jest.doMock('@sun-protocol/sun-sdk-api', () => ({ SunPumpApiClient })) + jest.doMock('../../src/lib/context', () => ({ getNetwork: () => 'mainnet' })) + jest.doMock('../../src/lib/command', () => ({ + parseApiResponse: (raw: any) => ({ data: raw.data }), + })) + jest.doMock('../../src/lib/output', () => ({ + output: jest.fn(), + outputError: jest.fn(), + isJsonMode: () => true, + withSpinner: async (_label: string, execute: () => Promise) => execute(), + })) + const { registerSunpumpCommands } = require('../../src/commands/sunpump') + const program = new Command() + program.exitOverride() + registerSunpumpCommands(program) + await program.parseAsync(['node', 'sun', 'sunpump', 'token', 'search-v2', 'SUN']) + expect(SunPumpApiClient).toHaveBeenCalledWith({ + network: 'mainnet', + baseUrl: 'https://example.invalid/custom-pump', + }) + } finally { + if (previous === undefined) delete process.env.SUNPUMP_API_BASE_URL + else process.env.SUNPUMP_API_BASE_URL = previous + } + }) + + it('rejects every SunPump subcommand on Nile before initializing an SDK client', async () => { + const searchTokensV2 = jest.fn().mockResolvedValue({ code: 0, data: { list: [] } }) + const SunPumpApiClient = jest.fn().mockImplementation(() => ({ + searchTokensV2, + })) + const output = jest.fn() + const printPaginationFooter = jest.fn() + + jest.doMock('@sun-protocol/sun-sdk-api', () => ({ SunPumpApiClient })) + const getKit = jest.fn() + jest.doMock('../../src/lib/context', () => ({ + getNetwork: () => 'nile', + getKit, + })) + jest.doMock('../../src/lib/output', () => ({ + output, + outputError: jest.fn(), + withSpinner: jest.fn().mockImplementation(async (_label, fn) => fn()), + printPaginationFooter, + printKeyValue: jest.fn(), + isJsonMode: jest.fn().mockReturnValue(true), + info: jest.fn(), + formatUsd: (v: unknown) => String(v ?? '-'), + formatTime: (v: unknown) => String(v ?? '-'), + formatAmount: (v: unknown) => String(v ?? '-'), + formatPct: (v: unknown) => String(v ?? '-'), + })) + + const { registerSunpumpCommands } = require('../../src/commands/sunpump') + const commands = [ + ['sunpump', 'token', 'search-v2', 'SUN', '--page', '1', '--size', '36'], + ['sunpump', 'quote-buy', 'TTestToken', '--trx', '1'], + ['sunpump', 'buy', 'TTestToken', '--trx', '1'], + ] + + for (const args of commands) { + const program = new Command() + program.exitOverride() + registerSunpumpCommands(program) + + await expect( + program.parseAsync(['node', 'sun', ...args], { from: 'node' }), + ).rejects.toMatchObject({ + code: 'SDK_UNSUPPORTED_NETWORK', + message: expect.stringContaining('SunPump is only available on mainnet'), + }) + } + + expect(SunPumpApiClient).not.toHaveBeenCalled() + expect(searchTokensV2).not.toHaveBeenCalled() + expect(getKit).not.toHaveBeenCalled() + expect(output).not.toHaveBeenCalled() + }) +}) diff --git a/test/contract/command-manifest.test.ts b/test/contract/command-manifest.test.ts new file mode 100644 index 0000000..123d572 --- /dev/null +++ b/test/contract/command-manifest.test.ts @@ -0,0 +1,22 @@ +import { commandHelpFixtures, rootHelpFixture } from './fixtures' +import { runCli } from '../helpers/run-cli' + +describe('CLI command manifest contract', () => { + it('keeps the root command groups and global options visible', async () => { + const result = await runCli(rootHelpFixture.args) + + expect(result.code).toBe(0) + for (const text of rootHelpFixture.includes) { + expect(result.stdout).toContain(text) + } + }) + + it.each(commandHelpFixtures)('keeps $name help compatible', async (fixture) => { + const result = await runCli(fixture.args) + + expect(result.code).toBe(0) + for (const text of fixture.includes) { + expect(result.stdout).toContain(text) + } + }) +}) diff --git a/test/contract/fixtures.ts b/test/contract/fixtures.ts new file mode 100644 index 0000000..d49790c --- /dev/null +++ b/test/contract/fixtures.ts @@ -0,0 +1,103 @@ +interface HelpFixture { + name: string + args: readonly string[] + includes: readonly string[] +} + +interface GoldenOutputFixture { + name: string + args: readonly string[] + expectCode: number + expectJson?: Record + stdoutIncludes?: readonly string[] + stderrIncludes?: readonly string[] +} + +export const rootHelpFixture: HelpFixture = { + name: 'root', + args: ['--help'], + includes: [ + 'Usage: sun [options] [command]', + '--network ', + '--dry-run', + 'wallet', + 'token', + 'pool', + 'protocol', + 'liquidity', + 'sunpump', + ], +} + +export const commandHelpFixtures: readonly HelpFixture[] = [ + { + name: 'token list', + args: ['token', 'list', '--help'], + includes: [ + 'Usage: sun token list [options]', + '--address ', + '--protocol ', + ], + }, + { + name: 'pool list', + args: ['pool', 'list', '--help'], + includes: ['Usage: sun pool list [options]', '--token ', '--no-blacklist'], + }, + { + name: 'protocol info', + args: ['protocol', 'info', '--help'], + includes: ['Usage: sun protocol info [options]', '--protocol '], + }, + { + name: 'wallet address', + args: ['wallet', 'address', '--help'], + includes: ['Usage: sun wallet address [options]', 'Show the active TRON wallet address'], + }, + { + name: 'contract read', + args: ['contract', 'read', '--help'], + includes: ['Usage: sun contract read [options]', '--abi ', '--args '], + }, + { + name: 'swap quote', + args: ['swap:quote', '--help'], + includes: ['Usage: sun swap:quote [options]', '--all'], + }, + { + name: 'liquidity', + args: ['liquidity', '--help'], + includes: ['v2:add', 'v3:mint', 'v4:mint', 'v4:info'], + }, + { + name: 'sunpump search v2', + args: ['sunpump', 'token', 'search-v2', '--help'], + includes: ['Usage: sun sunpump token search-v2 [options]', '--page ', '--size '], + }, +] + +export const goldenOutputFixtures: readonly GoldenOutputFixture[] = [ + { + name: 'dry-run swap output', + args: ['--json', '--network', 'nile', '--dry-run', 'swap', 'SUN', 'USDT', '1'], + expectCode: 0, + expectJson: { + dryRun: true, + action: 'Swap Preview', + params: { + 'Amount In': '1', + Network: 'nile', + }, + }, + }, + { + name: 'missing wallet error output', + args: ['--json', '--network', 'nile', 'wallet', 'address'], + expectCode: 1, + expectJson: { + error: 'Failed to get wallet address', + code: 'WALLET_NOT_CONFIGURED', + }, + stdoutIncludes: ['No wallet configured'], + }, +] diff --git a/test/contract/golden-output.test.ts b/test/contract/golden-output.test.ts new file mode 100644 index 0000000..d481192 --- /dev/null +++ b/test/contract/golden-output.test.ts @@ -0,0 +1,20 @@ +import { goldenOutputFixtures } from './fixtures' +import { runCli } from '../helpers/run-cli' + +describe('CLI golden output contract', () => { + it.each(goldenOutputFixtures)('$name', async (fixture) => { + const result = await runCli(fixture.args) + + expect(result.code).toBe(fixture.expectCode) + if (fixture.expectJson) { + expect(result.stdout).toContain('\n') + expect(JSON.parse(result.stdout)).toMatchObject(fixture.expectJson) + } + for (const text of fixture.stdoutIncludes ?? []) { + expect(result.stdout).toContain(text) + } + for (const text of fixture.stderrIncludes ?? []) { + expect(result.stderr).toContain(text) + } + }) +}) diff --git a/test/contract/sdk-dependency-boundary.test.ts b/test/contract/sdk-dependency-boundary.test.ts new file mode 100644 index 0000000..bdb7c4e --- /dev/null +++ b/test/contract/sdk-dependency-boundary.test.ts @@ -0,0 +1,42 @@ +import fs from 'node:fs' +import path from 'node:path' + +const root = path.resolve(__dirname, '../..') + +function sourceFiles(dir: string): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(dir, entry.name) + return entry.isDirectory() ? sourceFiles(entryPath) : [entryPath] + }) +} + +describe('public SUN SDK dependency boundary', () => { + it('uses only public @sun-protocol SDK packages in production code', () => { + const packageJson = fs.readFileSync(path.join(root, 'package.json'), 'utf8') + const source = sourceFiles(path.join(root, 'src')) + .map((file) => fs.readFileSync(file, 'utf8')) + .join('\n') + + expect(packageJson).not.toContain('@sun-protocol/sun-kit') + expect(packageJson).not.toContain('@sun-sdk/') + expect(source).not.toContain('@sun-sdk/') + }) + + it('pins the public SDK packages to the tested release', () => { + const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) + const dependencies = packageJson.dependencies as Record + const sdkPackages = [ + '@sun-protocol/sun-sdk-api', + '@sun-protocol/sun-sdk-chains', + '@sun-protocol/sun-sdk-core', + '@sun-protocol/sun-sdk-protocols', + '@sun-protocol/sun-sdk-runtime', + '@sun-protocol/sun-sdk-sunswap-v3', + '@sun-protocol/sun-sdk-sunswap-v4', + ] + + for (const name of sdkPackages) { + expect(dependencies[name]).toBe('0.1.1-beta.1') + } + }) +}) diff --git a/test/helpers/run-cli.test.ts b/test/helpers/run-cli.test.ts new file mode 100644 index 0000000..05acf16 --- /dev/null +++ b/test/helpers/run-cli.test.ts @@ -0,0 +1,44 @@ +import { buildCliEnv } from './run-cli' + +describe('buildCliEnv', () => { + const secretAliases = ['TRON_PRIVATE_KEY', 'TRON_MNEMONIC', 'TRON_ACCOUNT_INDEX'] as const + const originalValues = new Map() + + beforeEach(() => { + for (const key of secretAliases) { + originalValues.set(key, process.env[key]) + process.env[key] = `parent-${key}` + } + }) + + afterEach(() => { + for (const key of secretAliases) { + const value = originalValues.get(key) + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + originalValues.clear() + }) + + it('removes inherited wallet aliases and forces an isolated wallet directory', () => { + const env = buildCliEnv({}, '/tmp/sun-cli-isolated-wallet') + + for (const key of secretAliases) expect(env[key]).toBeUndefined() + expect(env.AGENT_WALLET_DIR).toBe('/tmp/sun-cli-isolated-wallet') + }) + + it('preserves wallet values explicitly supplied by the test', () => { + const env = buildCliEnv( + { + env: { + TRON_PRIVATE_KEY: 'explicit-test-key', + AGENT_WALLET_DIR: '/tmp/explicit-test-wallet', + }, + }, + '/tmp/sun-cli-isolated-wallet', + ) + + expect(env.TRON_PRIVATE_KEY).toBe('explicit-test-key') + expect(env.AGENT_WALLET_DIR).toBe('/tmp/explicit-test-wallet') + }) +}) diff --git a/test/helpers/run-cli.ts b/test/helpers/run-cli.ts new file mode 100644 index 0000000..e9f6e64 --- /dev/null +++ b/test/helpers/run-cli.ts @@ -0,0 +1,108 @@ +import { spawn } from 'child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'path' + +export interface CliResult { + code: number | null + signal: NodeJS.Signals | null + stdout: string + stderr: string +} + +export interface RunCliOptions { + env?: NodeJS.ProcessEnv + timeoutMs?: number +} + +const secretEnvKeys = [ + 'AGENT_WALLET_PRIVATE_KEY', + 'AGENT_WALLET_MNEMONIC', + 'AGENT_WALLET_PASSWORD', + 'AGENT_WALLET_DIR', + 'AGENT_WALLET_MNEMONIC_ACCOUNT_INDEX', + 'TRON_PRIVATE_KEY', + 'TRON_MNEMONIC', + 'TRON_ACCOUNT_INDEX', + 'TRON_NETWORK', + 'TRON_RPC_URL', + 'TRONGRID_API_KEY', + 'TRON_GRID_API_KEY', +] + +export function buildCliEnv(options: RunCliOptions, isolatedWalletDir: string): NodeJS.ProcessEnv { + const rootDir = path.resolve(__dirname, '..', '..') + const env: NodeJS.ProcessEnv = { + ...process.env, + FORCE_COLOR: '0', + NO_COLOR: '1', + DOTENV_CONFIG_PATH: path.join(rootDir, '.codex-test-empty.env'), + ...options.env, + } + + for (const key of secretEnvKeys) { + if (!options.env || !(key in options.env)) delete env[key] + } + if (!options.env || !('AGENT_WALLET_DIR' in options.env)) { + env.AGENT_WALLET_DIR = isolatedWalletDir + } + return env +} + +export function runCli(args: readonly string[], options: RunCliOptions = {}): Promise { + const rootDir = path.resolve(__dirname, '..', '..') + const isolatedWalletDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sun-cli-test-wallet-')) + const env = buildCliEnv(options, isolatedWalletDir) + + return new Promise((resolve, reject) => { + let cleaned = false + const cleanup = () => { + if (cleaned) return + cleaned = true + fs.rmSync(isolatedWalletDir, { recursive: true, force: true }) + } + let child + try { + child = spawn( + process.execPath, + ['-r', 'ts-node/register/transpile-only', path.join(rootDir, 'src/bin.ts'), ...args], + { + cwd: rootDir, + env, + stdio: ['ignore', 'pipe', 'pipe'], + }, + ) + } catch (error) { + cleanup() + reject(error) + return + } + + let stdout = '' + let stderr = '' + const timeout = setTimeout(() => { + child.kill('SIGTERM') + cleanup() + reject(new Error(`CLI timed out after ${options.timeoutMs ?? 10000}ms: ${args.join(' ')}`)) + }, options.timeoutMs ?? 10000) + + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => { + stdout += chunk + }) + child.stderr.on('data', (chunk) => { + stderr += chunk + }) + child.on('error', (error) => { + clearTimeout(timeout) + cleanup() + reject(error) + }) + child.on('close', (code, signal) => { + clearTimeout(timeout) + cleanup() + resolve({ code, signal, stdout, stderr }) + }) + }) +} diff --git a/test/lib/command.test.ts b/test/lib/command.test.ts index adf7c4e..4afd5fc 100644 --- a/test/lib/command.test.ts +++ b/test/lib/command.test.ts @@ -142,10 +142,68 @@ describe('command helpers', () => { }) }) + it('prepares read-only context before displaying the confirmation summary', async () => { + const { commandModule, printSummary, confirm, withSpinner } = loadCommandModule() + const summary: Record = { Network: 'nile', Position: '(loading)' } + const prepare = jest.fn().mockImplementation(async () => { + summary.Position = 'tokenId 7: SUN/USDT' + }) + const execute = jest.fn().mockResolvedValue({ txid: 'prepared-tx' }) + + await commandModule.writeAction({ + title: 'V4 Increase', + summary, + prepareLabel: 'Loading V4 position...', + prepare, + confirmMsg: 'Execute?', + spinnerLabel: 'Executing...', + errorLabel: 'Increase failed', + execute, + }) + + expect(prepare).toHaveBeenCalledWith({ kind: 'kit' }) + expect(printSummary).toHaveBeenCalledWith('V4 Increase', { + Network: 'nile', + Position: 'tokenId 7: SUN/USDT', + }) + expect(withSpinner.mock.calls.map(([label]) => label)).toEqual([ + 'Loading V4 position...', + 'Executing...', + ]) + expect(prepare.mock.invocationCallOrder[0]).toBeLessThan(confirm.mock.invocationCallOrder[0]) + expect(confirm.mock.invocationCallOrder[0]).toBeLessThan(execute.mock.invocationCallOrder[0]) + }) + + it('stops before confirmation when transaction preparation fails', async () => { + const { commandModule, outputError, confirm, printSummary } = loadCommandModule() + const error = Object.assign(new Error('Position token pair mismatch'), { + code: 'INVALID_PARAMS', + }) + const execute = jest.fn() + + await commandModule.writeAction({ + title: 'V4 Increase', + summary: { Network: 'nile' }, + prepare: async () => { + throw error + }, + confirmMsg: 'Execute?', + spinnerLabel: 'Executing...', + errorLabel: 'V4 increase liquidity failed', + execute, + }) + + expect(outputError).toHaveBeenCalledWith('V4 increase liquidity failed', error) + expect(printSummary).not.toHaveBeenCalled() + expect(confirm).not.toHaveBeenCalled() + expect(execute).not.toHaveBeenCalled() + }) + it('does not require a wallet for dry-run write actions', async () => { const { commandModule, ensureWallet, getKit, output, confirm, withSpinner } = loadCommandModule() const execute = jest.fn() + const prepare = jest.fn() commandModule.setDryRun(true) @@ -155,6 +213,7 @@ describe('command helpers', () => { confirmMsg: 'Execute?', spinnerLabel: 'Executing...', errorLabel: 'Swap failed', + prepare, execute, }) @@ -167,6 +226,7 @@ describe('command helpers', () => { expect(getKit).not.toHaveBeenCalled() expect(confirm).not.toHaveBeenCalled() expect(withSpinner).not.toHaveBeenCalled() + expect(prepare).not.toHaveBeenCalled() expect(execute).not.toHaveBeenCalled() }) }) diff --git a/test/lib/context.test.ts b/test/lib/context.test.ts index eb25d26..2e6c233 100644 --- a/test/lib/context.test.ts +++ b/test/lib/context.test.ts @@ -12,13 +12,13 @@ describe('context', () => { walletConfigured?: boolean wallet?: Record }) { - const wallet = options?.wallet ?? { type: 'agent-wallet' } + const wallet = options?.wallet ?? { kind: 'custom' } const initWallet = jest.fn().mockResolvedValue(undefined) const getWallet = jest.fn().mockReturnValue(wallet) const isWalletConfigured = jest.fn().mockReturnValue(options?.walletConfigured ?? true) - const SunAPI = jest.fn().mockImplementation(() => ({ kind: 'api-instance' })) - const SunKit = jest.fn().mockImplementation((config) => ({ kind: 'kit-instance', config })) + const createSunApiClient = jest.fn().mockReturnValue({ kind: 'api-instance' }) + const createSunSDK = jest.fn().mockResolvedValue({ kind: 'sdk-instance' }) jest.doMock('../../src/lib/wallet', () => ({ initWallet, @@ -26,9 +26,9 @@ describe('context', () => { isWalletConfigured, })) - jest.doMock('@sun-protocol/sun-kit', () => ({ - SunAPI, - SunKit, + jest.doMock('../../src/lib/sdk/factory', () => ({ + createSunApiClient, + createSunSDK, })) const contextModule = require('../../src/lib/context') @@ -37,54 +37,50 @@ describe('context', () => { initWallet, getWallet, isWalletConfigured, - SunAPI, - SunKit, + createSunApiClient, + createSunSDK, wallet, } } - it('lazily initializes SunKit once and reuses the same instance', async () => { + it('lazily initializes SunSDK once and reuses the same instance', async () => { process.env.TRON_NETWORK = 'nile' process.env.TRONGRID_API_KEY = 'grid-key' process.env.TRON_RPC_URL = 'https://rpc.local' - const { contextModule, initWallet, getWallet, SunKit, wallet } = loadContextModule() + const { contextModule, initWallet, getWallet, createSunSDK, wallet } = loadContextModule() - const first = await contextModule.getKit() + const first = await contextModule.getSdk() const second = await contextModule.getKit() expect(initWallet).toHaveBeenCalledTimes(1) expect(getWallet).toHaveBeenCalledTimes(1) - expect(SunKit).toHaveBeenCalledTimes(1) + expect(createSunSDK).toHaveBeenCalledTimes(1) expect(first).toBe(second) - expect(first).toEqual({ - kind: 'kit-instance', - config: { - wallet, - network: 'nile', - tronGridApiKey: 'grid-key', - rpcUrl: 'https://rpc.local', - }, + expect(createSunSDK).toHaveBeenCalledWith({ + wallet, + network: 'nile', + tronGridApiKey: 'grid-key', + rpcUrl: 'https://rpc.local', }) }) - it('creates SunKit without a wallet when none is configured', async () => { + it('creates SunSDK without a wallet when none is configured', async () => { process.env.TRON_GRID_API_KEY = 'fallback-key' - const { contextModule, getWallet, SunKit } = loadContextModule({ + const { contextModule, getWallet, createSunSDK } = loadContextModule({ walletConfigured: false, }) - const kit = await contextModule.getKit() + await contextModule.getSdk() expect(getWallet).not.toHaveBeenCalled() - expect(SunKit).toHaveBeenCalledWith({ + expect(createSunSDK).toHaveBeenCalledWith({ wallet: undefined, network: 'mainnet', tronGridApiKey: 'fallback-key', rpcUrl: undefined, }) - expect(kit.config.wallet).toBeUndefined() }) it('throws from ensureWallet when initialization completes without a wallet', async () => { @@ -99,13 +95,13 @@ describe('context', () => { expect(isWalletConfigured).toHaveBeenCalledTimes(1) }) - it('caches SunAPI instances', () => { - const { contextModule, SunAPI } = loadContextModule() + it('caches SUN API client instances', () => { + const { contextModule, createSunApiClient } = loadContextModule() const first = contextModule.getApi() const second = contextModule.getApi() - expect(SunAPI).toHaveBeenCalledTimes(1) + expect(createSunApiClient).toHaveBeenCalledTimes(1) expect(first).toBe(second) }) }) diff --git a/test/lib/sdk/compat.test.ts b/test/lib/sdk/compat.test.ts new file mode 100644 index 0000000..532c736 --- /dev/null +++ b/test/lib/sdk/compat.test.ts @@ -0,0 +1,109 @@ +import { toCliTxResult } from '../../../src/lib/sdk/compat' + +describe('SDK result compatibility', () => { + const execution = (metadata: Record) => ({ + plan: { metadata }, + txids: ['approval', 'tx'], + finalResult: { type: 'transaction', txid: 'tx', raw: { result: true } }, + }) + + it('restores legacy Pump business fields rather than exposing the internal execution plan', () => { + expect( + toCliTxResult( + execution({ + operation: 'launchpad-buy-exact-trx', + token: 'token', + trxAmount: '100', + quotedTokenAmount: '1000', + minTokenOut: '0', + }), + ), + ).toEqual({ + txResult: { result: true, txid: 'tx' }, + tokenAddress: 'token', + trxSpent: '100', + expectedTokens: '1000', + minTokenOut: '0', + }) + expect( + toCliTxResult( + execution({ + operation: 'launchpad-sell-exact-token', + token: 'token', + tokenAmount: '100', + quotedTrxAmount: '900', + minTrxOut: '800', + }), + ), + ).toEqual({ + txResult: { result: true, txid: 'tx' }, + tokenAddress: 'token', + tokensSold: '100', + expectedTrx: '900', + minTrxOut: '800', + }) + }) + + it('restores liquidity output fields without leaking other SDK metadata', () => { + const computedTicks = { tickLower: -1000, tickUpper: 1000 } + const computedAmounts = { amount0Desired: '100', amount1Desired: '200' } + expect( + toCliTxResult( + execution({ + protocol: 'sunswap-v4', + operation: 'mint-position', + computedTicks, + computedAmounts, + createPool: false, + deadline: '123', + }), + ), + ).toEqual({ + txResult: { result: true, txid: 'tx' }, + computedTicks, + computedAmounts, + poolCreated: false, + }) + expect( + toCliTxResult( + execution({ + protocol: 'sunswap-v3', + operation: 'collect-position', + estimatedFees: { amount0: '1', amount1: '2' }, + }), + ), + ).toEqual({ + txResult: { result: true, txid: 'tx' }, + estimatedFees: { amount0: '1', amount1: '2' }, + }) + expect( + toCliTxResult(execution({ protocol: 'sunswap-v3', operation: 'remove-position' })), + ).toEqual({ result: true, txid: 'tx' }) + }) + + it('keeps nested txResult txid shape for explorer enrichment', () => { + expect(toCliTxResult({ txResult: { txid: 'abc' }, raw: { result: true } })).toEqual({ + txResult: { txid: 'abc' }, + raw: { result: true }, + }) + }) + + it('maps SDK txID to existing txid shape', () => { + expect(toCliTxResult({ txID: 'abc', result: true })).toEqual({ + txid: 'abc', + result: true, + }) + }) + + it('maps SDK transaction plan execution to txid shape', () => { + expect( + toCliTxResult({ + txids: ['first', 'last'], + finalResult: { type: 'transaction', txid: 'last', raw: { result: true } }, + }), + ).toEqual({ + txid: 'last', + result: true, + }) + }) +}) diff --git a/test/lib/sdk/constants.test.ts b/test/lib/sdk/constants.test.ts new file mode 100644 index 0000000..618f72b --- /dev/null +++ b/test/lib/sdk/constants.test.ts @@ -0,0 +1,9 @@ +import { TRX_ADDRESS, WTRX_MAINNET, WTRX_NILE } from '../../../src/lib/sdk/constants' + +describe('SDK compatibility constants', () => { + it('keeps TRX and WTRX addresses compatible with the old CLI registry', () => { + expect(TRX_ADDRESS).toBe('T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb') + expect(WTRX_MAINNET).toBe('TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR') + expect(WTRX_NILE).toBe('TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a') + }) +}) diff --git a/test/lib/sdk/errors.test.ts b/test/lib/sdk/errors.test.ts new file mode 100644 index 0000000..c25bbe3 --- /dev/null +++ b/test/lib/sdk/errors.test.ts @@ -0,0 +1,18 @@ +import { mapSdkError } from '../../../src/lib/sdk/errors' + +describe('SDK error compatibility', () => { + it('preserves existing wallet error code', () => { + const err = mapSdkError( + new Error('Wallet required. Set agent-wallet credentials before running this command.'), + ) + + expect((err as any).code).toBe('NO_WALLET') + }) + + it('preserves SDK error codes when present', () => { + const err = mapSdkError(Object.assign(new Error('bad params'), { code: 'INVALID_ARGUMENT' })) + + expect(err.message).toBe('bad params') + expect((err as any).code).toBe('INVALID_ARGUMENT') + }) +}) diff --git a/test/lib/sdk/factory.test.ts b/test/lib/sdk/factory.test.ts new file mode 100644 index 0000000..650206e --- /dev/null +++ b/test/lib/sdk/factory.test.ts @@ -0,0 +1,41 @@ +describe('SDK factory', () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + }) + + it('defaults to mainnet and accepts nile', () => { + const { getNetworkFromEnv } = require('../../../src/lib/sdk/factory') + + expect(getNetworkFromEnv({})).toBe('mainnet') + expect(getNetworkFromEnv({ TRON_NETWORK: 'nile' })).toBe('nile') + }) + + it('sets a non-signing caller address for TronGrid constant calls', () => { + const { createReadonlyTronWeb } = require('../../../src/lib/sdk/factory') + const tronWeb = createReadonlyTronWeb({ network: 'nile' }) + + expect(tronWeb.defaultAddress).toEqual({ + base58: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb', + hex: '410000000000000000000000000000000000000000', + }) + expect(tronWeb.defaultPrivateKey).toBe(false) + }) + + it('rejects SDK-unsupported networks before runtime creation', async () => { + const { createSdkRuntime } = require('../../../src/lib/sdk/factory') + + await expect(createSdkRuntime({ network: 'shasta' })).rejects.toThrow( + 'Unsupported SDK network: shasta', + ) + }) + + it('uses the legacy Shasta RPC for generic calls without configuring SUN protocol contracts', async () => { + const { createGenericRuntime, createReadonlyTronWeb } = require('../../../src/lib/sdk/factory') + const tronWeb = createReadonlyTronWeb({ network: 'shasta' }) + expect(tronWeb.fullNode.host).toBe('https://api.shasta.trongrid.io') + const runtime = await createGenericRuntime({ network: 'shasta' }) + expect(runtime.network).toBe('shasta') + expect(runtime.chain).toBeUndefined() + }) +}) diff --git a/test/lib/sdk/liquidity-v3.test.ts b/test/lib/sdk/liquidity-v3.test.ts new file mode 100644 index 0000000..da713fe --- /dev/null +++ b/test/lib/sdk/liquidity-v3.test.ts @@ -0,0 +1,72 @@ +import { resolveLegacyV3Pool } from '../../../src/lib/sdk/liquidity-v3' + +describe('legacy CLI V3 deployment', () => { + it('uses the supplied PM ABI for the legacy single-side positions read', async () => { + const abi = [{ type: 'function', name: 'positions', inputs: [{ type: 'uint256' }] }] + const pm = 'TPQzqHbCzQfoVdAV6bLwGDos8Lk2UjXz2R' + const token0 = 'TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a' + const token1 = 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf' + const read = jest + .fn() + .mockResolvedValueOnce({ token0, token1, fee: '500' }) + .mockResolvedValueOnce('TE6MBGu85TcLvDpxMM9PjVobWi1kxDkjcW') + const multicall = jest.fn().mockResolvedValue([ + { success: true, value: ['123', 25700] }, + { success: true, value: '456' }, + { success: true, value: '10' }, + ]) + await resolveLegacyV3Pool( + { network: 'nile', runtime: { contract: { read }, multicall } } as never, + { + tokenId: '706', + positionManager: pm, + positionAbi: JSON.stringify(abi), + }, + ) + expect(read).toHaveBeenNthCalledWith(1, { + address: pm, + abi, + functionName: 'positions', + args: ['706'], + }) + }) + + it('rejects a missing legacy pool without substituting an SDK deployment', async () => { + const read = jest.fn().mockResolvedValue('T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb') + await expect( + resolveLegacyV3Pool({ network: 'nile', runtime: { contract: { read } } } as never, { + token0: 'TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a', + token1: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', + fee: 500, + }), + ).rejects.toThrow('V3 pool not found') + }) + + it('uses the legacy factory getPool result instead of deriving a different pool from SDK defaults', async () => { + const token0 = 'TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a' + const token1 = 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf' + const pool = 'TE6MBGu85TcLvDpxMM9PjVobWi1kxDkjcW' + const read = jest.fn().mockResolvedValue(pool) + const multicall = jest.fn().mockResolvedValue([ + { success: true, value: { sqrtPriceX96: '123', tick: 25700 } }, + { success: true, value: '456' }, + { success: true, value: '10' }, + ]) + const sdk = { network: 'nile', runtime: { contract: { read }, multicall } } + await expect(resolveLegacyV3Pool(sdk as never, { token0, token1, fee: 500 })).resolves.toEqual({ + sqrtRatioX96: '123', + tickCurrent: 25700, + liquidity: '456', + tickSpacing: 10, + }) + expect(read).toHaveBeenCalledWith( + expect.objectContaining({ + address: 'TLJWAScHZ4Qmk1axyKMzrnoYuu2pSLer1F', + functionSelector: 'getPool(address,address,uint24)', + }), + ) + expect( + multicall.mock.calls[0][0].every((call: { target: string }) => call.target === pool), + ).toBe(true) + }) +}) diff --git a/test/lib/sdk/liquidity-v4.test.ts b/test/lib/sdk/liquidity-v4.test.ts new file mode 100644 index 0000000..ba09399 --- /dev/null +++ b/test/lib/sdk/liquidity-v4.test.ts @@ -0,0 +1,128 @@ +import { TickMath } from '@sun-protocol/sun-sdk-sunswap-v3' +import { + computeV4PoolId, + createV4PoolKey, + encodeV4PoolParameters, +} from '@sun-protocol/sun-sdk-sunswap-v4' +import { + resolveV4MintTicks, + v4PoolKeyFromCli, + v4TickSpacingForFee, +} from '../../../src/lib/sdk/liquidity-v4' +import type { CliSunSDK } from '../../../src/lib/sdk/types' + +const token0 = 'TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a' +const token1 = 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf' +const input = { token0, token1, fee: 500 } + +function mockSdk(read: jest.Mock): CliSunSDK { + return { network: 'nile', liquidity: { v4: { read } } } as unknown as CliSunSDK +} + +describe('V4 mint automatic ticks', () => { + it.each([ + [500, 10, 12347, 11350, 13350], + [500, 10, -12347, -13350, -11350], + [3000, 60, 12000, 6000, 18000], + [100, 1, 0, -100, 100], + [10000, 200, 4000, -16000, 24000], + [2500, 10, 12347, 11350, 13350], + ])( + 'derives the legacy 100-spacing range for fee %i at tick %i', + async (fee, tickSpacing, tick, tickLower, tickUpper) => { + const read = jest.fn().mockResolvedValue({ sqrtPriceX96: '1', tick }) + const result = await resolveV4MintTicks(mockSdk(read), { ...input, fee }) + + expect(result).toEqual({ tickSpacing, tickLower, tickUpper, createPool: undefined }) + expect(read).toHaveBeenCalledWith({ + poolId: computeV4PoolId( + createV4PoolKey({ + network: 'nile', + currencyA: token0, + currencyB: token1, + fee, + parameters: encodeV4PoolParameters({ tickSpacing }), + }), + ), + }) + }, + ) + + it('derives only the omitted bound and preserves an explicit zero', async () => { + const read = jest.fn().mockResolvedValue({ sqrtPriceX96: '1', tick: 1500 }) + expect(await resolveV4MintTicks(mockSdk(read), { ...input, tickLower: 0 })).toMatchObject({ + tickLower: 0, + tickUpper: 2500, + }) + expect(await resolveV4MintTicks(mockSdk(read), { ...input, tickUpper: 3000 })).toMatchObject({ + tickLower: 500, + tickUpper: 3000, + }) + }) + + it('uses the existing pool tick even when an initial price is also supplied', async () => { + const read = jest.fn().mockResolvedValue({ sqrtPriceX96: '1', tick: 1500 }) + expect( + await resolveV4MintTicks(mockSdk(read), { + ...input, + sqrtPriceX96: TickMath.getSqrtRatioAtTick(0).toString(), + createPool: true, + }), + ).toMatchObject({ tickLower: 500, tickUpper: 2500, createPool: true }) + }) + + it.each(['missing', 'uninitialized'])('uses the initial price for a %s pool', async (state) => { + const read = + state === 'missing' + ? jest.fn().mockRejectedValue(new Error('Pool unavailable')) + : jest.fn().mockResolvedValue({ sqrtPriceX96: '0', tick: 0 }) + const result = await resolveV4MintTicks(mockSdk(read), { + ...input, + sqrtPriceX96: TickMath.getSqrtRatioAtTick(1200).toString(), + }) + expect(result).toEqual({ tickSpacing: 10, tickLower: 200, tickUpper: 2200, createPool: true }) + }) + + it('fails if the pool is uninitialized and no initial price is available', async () => { + const read = jest.fn().mockResolvedValue({ sqrtPriceX96: '0', tick: 0 }) + await expect(resolveV4MintTicks(mockSdk(read), input)).rejects.toMatchObject({ + code: 'POOL_NOT_FOUND', + }) + }) + + it('does not turn a pool read failure into a static tick range', async () => { + const cause = new Error('RPC unavailable') + const read = jest.fn().mockRejectedValue(cause) + await expect(resolveV4MintTicks(mockSdk(read), input)).rejects.toMatchObject({ + code: 'POOL_NOT_FOUND', + cause, + }) + }) + + it('rejects an invalid initial sqrt price', async () => { + const read = jest.fn().mockRejectedValue(new Error('Pool unavailable')) + await expect( + resolveV4MintTicks(mockSdk(read), { ...input, sqrtPriceX96: '0' }), + ).rejects.toThrow() + }) + + it('keeps the legacy V4 tick-spacing fallback', () => { + expect(v4TickSpacingForFee(2500)).toBe(10) + }) + + it('derives the native TRX pool instead of the WTRX pool', () => { + const nativeTrx = 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb' + const usdt = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' + const wtrx = 'TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR' + const nativePoolKey = v4PoolKeyFromCli('mainnet', nativeTrx, usdt, 500) + const wrappedPoolKey = v4PoolKeyFromCli('mainnet', wtrx, usdt, 500) + + expect(nativePoolKey.currency0).toBe(nativeTrx) + expect(computeV4PoolId(nativePoolKey)).toBe( + '0xdda1d5819853f19f3e952da5d93aa2d572d95c72a8e6e4c2acab65384fd2557e', + ) + expect(computeV4PoolId(wrappedPoolKey)).toBe( + '0xc6e07bc4418402f27a3af8bcd349dacb1515ba038ad7189cf0e1bbf689fc082e', + ) + }) +}) diff --git a/test/lib/sdk/wallet.test.ts b/test/lib/sdk/wallet.test.ts new file mode 100644 index 0000000..c652df7 --- /dev/null +++ b/test/lib/sdk/wallet.test.ts @@ -0,0 +1,34 @@ +import { AgentWalletSdkAdapter } from '../../../src/lib/sdk/wallet' + +describe('AgentWalletSdkAdapter', () => { + it('adapts agent-wallet signing to SDK WalletAdapter', async () => { + const agentWallet = { + getAddress: jest.fn().mockResolvedValue('TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX'), + signTransaction: jest + .fn() + .mockResolvedValue(JSON.stringify({ txID: 'abc', signature: ['sig'] })), + signMessage: jest.fn().mockResolvedValue('signed'), + signTypedData: jest.fn().mockResolvedValue('0xtyped'), + } + + const wallet = new AgentWalletSdkAdapter(agentWallet as any) + + expect(wallet.kind).toBe('custom') + expect(wallet.capabilities).toEqual({ + signTransaction: true, + signAndSendTransaction: false, + signTypedData: true, + userInteractive: false, + }) + expect(await wallet.getAddress()).toBe('TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX') + expect(await wallet.signTransaction!({ txID: 'abc' })).toEqual({ + txID: 'abc', + signature: ['sig'], + }) + expect(await wallet.signMessage!('hello')).toBe('signed') + expect( + await wallet.signTypedData!({ domain: {}, types: {}, primaryType: 'Message', message: {} }), + ).toBe('typed') + expect(agentWallet.signMessage).toHaveBeenCalledWith(Buffer.from('hello', 'utf-8')) + }) +}) diff --git a/test/lib/slippage.test.ts b/test/lib/slippage.test.ts new file mode 100644 index 0000000..c08f062 --- /dev/null +++ b/test/lib/slippage.test.ts @@ -0,0 +1,21 @@ +import { parseDecimalSlippage } from '../../src/lib/slippage' + +describe('parseDecimalSlippage', () => { + it.each([ + ['0', 0, '0%', '0.00%'], + ['0.005', 50, '0.5%', '0.50%'], + ['0.01', 100, '1%', '1.00%'], + ['1', 10000, '100%', '100.00%'], + ])('parses %s as a decimal ratio', (value, bips, sdk, display) => { + expect(parseDecimalSlippage(value)).toEqual({ bips, sdk, display }) + }) + + it.each(['', '-0.01', '0.00001', '0.01junk', '1.0001', '1e-2', '2', 'abc'])( + 'rejects invalid input %p', + (value) => { + expect(() => parseDecimalSlippage(value)).toThrow( + '--slippage must be a decimal between 0 and 1 with at most 4 decimal places', + ) + }, + ) +}) diff --git a/test/lib/sunpump.test.ts b/test/lib/sunpump.test.ts deleted file mode 100644 index 183e783..0000000 --- a/test/lib/sunpump.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { SunPump, SunPumpHttpError, SUNPUMP_DEFAULT_BASE_URL } from '../../src/lib/sunpump' - -describe('SunPump', () => { - it('uses default base URL', () => { - const client = new SunPump() - expect(SUNPUMP_DEFAULT_BASE_URL).toBe('https://api-v2.sunpump.meme/pump-api') - }) - - it('accepts custom base URL', () => { - const client = new SunPump({ baseUrl: 'https://custom.example.com/api' }) - expect(client).toBeDefined() - }) - - it('accepts custom fetch implementation', () => { - const mockFetch = jest.fn() - const client = new SunPump({ fetchImpl: mockFetch as unknown as typeof fetch }) - expect(client).toBeDefined() - }) -}) - -describe('SunPumpHttpError', () => { - it('has correct error code', () => { - const err = new SunPumpHttpError('test error', 404, 'not found') - expect(err.code).toBe('SUNPUMP_HTTP_ERROR') - expect(err.status).toBe(404) - expect(err.body).toBe('not found') - expect(err.message).toBe('test error') - }) - - it('is an instance of Error', () => { - const err = new SunPumpHttpError('test', 500) - expect(err).toBeInstanceOf(Error) - }) -}) diff --git a/test/lib/wallet.test.ts b/test/lib/wallet.test.ts index d94e33b..f8a811a 100644 --- a/test/lib/wallet.test.ts +++ b/test/lib/wallet.test.ts @@ -20,7 +20,6 @@ describe('wallet', () => { function loadWalletModule(options?: { activeWallet?: Record - tronWeb?: Record getActiveWalletError?: Error getActiveWalletValue?: Record | null }) { @@ -31,16 +30,6 @@ describe('wallet', () => { signTypedData: jest.fn().mockResolvedValue('0xsignedtypeddata'), } - const tronWeb = options?.tronWeb ?? { - address: { - toHex: jest.fn().mockReturnValue('41abc'), - fromHex: jest.fn().mockReturnValue('TWalletAddress'), - }, - trx: { - sendRawTransaction: jest.fn().mockResolvedValue({ result: true, txid: 'tx-123' }), - }, - } - const provider = { getActiveWallet: options?.getActiveWalletError ? jest.fn().mockRejectedValue(options.getActiveWalletError) @@ -54,24 +43,17 @@ describe('wallet', () => { } const resolveWalletProvider = jest.fn().mockReturnValue(provider) - const createReadonlyTronWeb = jest.fn().mockResolvedValue(tronWeb) jest.doMock('@bankofai/agent-wallet', () => ({ resolveWalletProvider, })) - jest.doMock('@sun-protocol/sun-kit', () => ({ - createReadonlyTronWeb, - })) - const walletModule = require('../../src/lib/wallet') return { walletModule, activeWallet, - tronWeb, provider, resolveWalletProvider, - createReadonlyTronWeb, } } @@ -116,78 +98,36 @@ describe('wallet', () => { await expect(walletModule.getWalletAddress()).resolves.toBe('TWalletAddress') }) - it('builds tronWeb with the active wallet as default address', async () => { + it('returns an SDK wallet adapter that signs and normalizes typed-data signatures', async () => { process.env.AGENT_WALLET_PASSWORD = 'secret' - const { walletModule, tronWeb, createReadonlyTronWeb } = loadWalletModule() - - await walletModule.initWallet() - const wallet = walletModule.getWallet() as any - const resolvedTronWeb = await wallet.getTronWeb('nile') - - expect(createReadonlyTronWeb).toHaveBeenCalledWith('nile') - expect(resolvedTronWeb).toBe(tronWeb) - expect(tronWeb.address.toHex).toHaveBeenCalledWith('TWalletAddress') - expect(tronWeb.address.fromHex).toHaveBeenCalledWith('41abc') - expect((tronWeb as any).defaultAddress).toEqual({ - hex: '41abc', - base58: 'TWalletAddress', - }) - }) - - it('signs, broadcasts, and normalizes typed-data signatures', async () => { - process.env.AGENT_WALLET_PASSWORD = 'secret' - - const { walletModule, activeWallet, tronWeb } = loadWalletModule({ + const { walletModule, activeWallet } = loadWalletModule({ activeWallet: { getAddress: jest.fn().mockResolvedValue('TWalletAddress'), signTransaction: jest.fn().mockResolvedValue('{"txID":"signed","raw_data":{"x":1}}'), signMessage: jest.fn().mockResolvedValue('signed-message'), signTypedData: jest.fn().mockResolvedValue('0xdeadbeef'), }, - tronWeb: { - address: { - toHex: jest.fn().mockReturnValue('41abc'), - fromHex: jest.fn().mockReturnValue('TWalletAddress'), - }, - trx: { - sendRawTransaction: jest.fn().mockResolvedValue({ result: true, txid: 'tx-999' }), - }, - }, }) await walletModule.initWallet() const wallet = walletModule.getWallet() as any - await expect( - wallet.signAndBroadcast({ transaction: { raw_data: { contract: [] } } }, 'mainnet'), - ).resolves.toEqual({ result: true, txid: 'tx-999' }) - expect(activeWallet.signTransaction).toHaveBeenCalledWith({ raw_data: { contract: [] } }) - expect(tronWeb.trx.sendRawTransaction).toHaveBeenCalledWith({ + expect(wallet.kind).toBe('custom') + await expect(wallet.signTransaction({ raw_data: { contract: [] } })).resolves.toEqual({ txID: 'signed', raw_data: { x: 1 }, }) + expect(activeWallet.signTransaction).toHaveBeenCalledWith({ raw_data: { contract: [] } }) - await expect( - wallet.signTypedData( - 'Permit', - { name: 'Sun', chainId: 1, verifyingContract: 'TContract' }, - { Permit: [{ name: 'owner', type: 'address' }] }, - { owner: 'TWalletAddress' }, - ), - ).resolves.toBe('deadbeef') - expect(activeWallet.signTypedData).toHaveBeenCalledWith({ + const payload = { domain: { name: 'Sun', chainId: 1, verifyingContract: 'TContract' }, - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - Permit: [{ name: 'owner', type: 'address' }], - }, - primaryType: 'Permit', + types: { Permit: [{ name: 'owner', type: 'address' }] }, message: { owner: 'TWalletAddress' }, + } + await expect(wallet.signTypedData(payload)).resolves.toBe('deadbeef') + expect(activeWallet.signTypedData).toHaveBeenCalledWith({ + ...payload, }) }) })