From 565f6cf3f5f80027b9349a1d1ea8ab9fd5ad5184 Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:39:20 -0500 Subject: [PATCH 1/7] feat: add @gridplus/cli package scaffold - Commander-based CLI with gridplus/gp commands - Placeholder commands: setup, address, pubkey, eth2 - ETH2 staking subcommands: deposit-data, bls-change - Build config following packages/btc patterns - Fix biome.json schema version for subpackage lint --- biome.json | 17 +- package.json | 2 +- packages/cli/bin/gridplus.ts | 3 + packages/cli/package.json | 35 ++ packages/cli/src/index.ts | 42 ++ packages/cli/tsconfig.json | 24 ++ packages/cli/tsup.config.ts | 28 ++ pnpm-lock.yaml | 803 +++++++++++++++++++++++++++++++++++ 8 files changed, 940 insertions(+), 14 deletions(-) create mode 100644 packages/cli/bin/gridplus.ts create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsup.config.ts diff --git a/biome.json b/biome.json index aed23215..c4bbd103 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -21,17 +21,9 @@ "noForEach": "warn" }, "correctness": { - "noUnusedImports": { - "level": "error" - }, - "noUnusedVariables": { - "level": "warn", - "fix": "none" - }, - "noUnusedFunctionParameters": { - "level": "warn", - "fix": "none" - } + "noUnusedImports": "error", + "noUnusedVariables": "warn", + "noUnusedFunctionParameters": "warn" }, "style": { "noParameterAssign": "warn", @@ -60,7 +52,6 @@ }, "files": { "ignoreUnknown": true, - "include": ["packages/*/src/**"], "ignore": ["**/dist/**", "**/node_modules/**", "**/coverage/**"] }, "javascript": { diff --git a/package.json b/package.json index d26cc23c..4f458017 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "packageManager": "pnpm@10.6.2", "scripts": { - "build": "turbo run build --filter=gridplus-sdk --filter=@gridplus/btc", + "build": "turbo run build --filter=gridplus-sdk --filter=@gridplus/btc --filter=@gridplus/cli", "test": "turbo run test --filter=gridplus-sdk --filter=@gridplus/btc", "test-unit": "turbo run test-unit --filter=gridplus-sdk --filter=@gridplus/btc", "lint": "turbo run lint --filter=gridplus-sdk --filter=@gridplus/btc", diff --git a/packages/cli/bin/gridplus.ts b/packages/cli/bin/gridplus.ts new file mode 100644 index 00000000..a205a796 --- /dev/null +++ b/packages/cli/bin/gridplus.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node +import { program } from '../src/index.js'; +program.parse(); diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..97c4d849 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,35 @@ +{ + "name": "@gridplus/cli", + "version": "0.1.0", + "type": "module", + "description": "CLI for GridPlus SDK", + "bin": { + "gridplus": "./dist/bin/gridplus.js", + "gp": "./dist/bin/gridplus.js" + }, + "scripts": { + "build": "tsup", + "lint": "biome check src bin", + "lint:fix": "biome check --write src bin", + "typecheck": "tsc --noEmit" + }, + "files": ["dist"], + "dependencies": { + "gridplus-sdk": "workspace:*", + "@gridplus/btc": "workspace:*", + "lattice-eth2-utils": "^0.5.1", + "commander": "^12.0.0", + "inquirer": "^12.0.0", + "chalk": "^5.0.0", + "ora": "^8.0.0", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^24.10.4", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + }, + "license": "MIT", + "engines": { "node": ">=20" } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000..c66d5bad --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,42 @@ +import { Command } from 'commander'; + +export const program = new Command() + .name('gridplus') + .description('CLI for GridPlus SDK - interact with Lattice hardware wallets') + .version('0.1.0'); + +// Placeholder commands - implementations in future phases +program + .command('setup') + .description('Interactive device setup and pairing') + .action(() => console.log('Not yet implemented')); + +program + .command('address [path]') + .description('Get ETH address at derivation path') + .option('-t, --type ', 'Address type', 'eth') + .action(() => console.log('Not yet implemented')); + +program + .command('pubkey [path]') + .description('Get public key at derivation path') + .option( + '-t, --type ', + 'Key type: secp256k1|ed25519|bls12_381', + 'secp256k1', + ) + .action(() => console.log('Not yet implemented')); + +const eth2 = program + .command('eth2') + .description('Ethereum 2.0 staking commands'); + +eth2 + .command('deposit-data') + .description('Export validator deposit data and keystores') + .action(() => console.log('Not yet implemented')); + +eth2 + .command('bls-change') + .description('Change BLS withdrawal credentials to ETH1 address') + .action(() => console.log('Not yet implemented')); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..794d6637 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2022"], + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": ".", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "exclude": ["node_modules", "dist"], + "include": ["src", "bin"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 00000000..41dd4725 --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'tsup'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse( + readFileSync(resolve(__dirname, 'package.json'), 'utf-8'), +); + +const external = Object.keys({ + ...(pkg.dependencies ?? {}), + ...(pkg.peerDependencies ?? {}), +}); + +export default defineConfig({ + entry: ['src/index.ts', 'bin/gridplus.ts'], + outDir: './dist', + format: ['esm'], + target: 'node20', + sourcemap: true, + clean: true, + bundle: true, + dts: true, + silent: true, + external, + tsconfig: './tsconfig.json', +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3442ba99..2c918f21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,46 @@ importers: specifier: 3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(jiti@1.21.7)(msw@2.12.7(@types/node@24.10.4)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0) + packages/cli: + dependencies: + '@gridplus/btc': + specifier: workspace:* + version: link:../btc + chalk: + specifier: ^5.0.0 + version: 5.6.2 + commander: + specifier: ^12.0.0 + version: 12.1.0 + dotenv: + specifier: ^16.0.0 + version: 16.6.1 + gridplus-sdk: + specifier: workspace:* + version: link:../sdk + inquirer: + specifier: ^12.0.0 + version: 12.11.1(@types/node@24.10.4) + lattice-eth2-utils: + specifier: ^0.5.1 + version: 0.5.1 + ora: + specifier: ^8.0.0 + version: 8.2.0 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.0 + version: 1.9.4 + '@types/node': + specifier: ^24.10.4 + version: 24.10.4 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@24.10.4))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + packages/docs: dependencies: '@docusaurus/core': @@ -1046,9 +1086,18 @@ packages: '@braintree/sanitize-url@6.0.4': resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + '@chainsafe/as-sha256@0.3.1': + resolution: {integrity: sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==} + '@chainsafe/bls-keystore@3.1.0': resolution: {integrity: sha512-OR9hV9N53woNc6R2d08O3Oi3Ykx7uZARMx51W+WFP1fDaFSA/QaMraPGGA8tSx7livkb/Scwe3Basj9Pib67HA==} + '@chainsafe/persistent-merkle-tree@0.4.2': + resolution: {integrity: sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==} + + '@chainsafe/ssz@0.9.4': + resolution: {integrity: sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1857,6 +1906,9 @@ packages: '@ethereumjs/common@3.2.0': resolution: {integrity: sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==} + '@ethereumjs/common@4.3.0': + resolution: {integrity: sha512-shBNJ0ewcPNTUfZduHiczPmqkfJDn0Dh/9BR5fq7xUFTuIq7Fu1Vx00XDwQVIrpVL70oycZocOhBM6nDO+4FEQ==} + '@ethereumjs/rlp@10.1.0': resolution: {integrity: sha512-r67BJbwilammAqYI4B5okA66cNdTlFzeWxPNJOolKV52ZS/flo0tUBf4x4gxWXBgh48OgsdFV1Qp5pRoSe8IhQ==} engines: {node: '>=18'} @@ -1867,6 +1919,11 @@ packages: engines: {node: '>=14'} hasBin: true + '@ethereumjs/rlp@5.0.2': + resolution: {integrity: sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==} + engines: {node: '>=18'} + hasBin: true + '@ethereumjs/tx@10.1.0': resolution: {integrity: sha512-svG6pyzUZDpunafszf2BaolA6Izuvo8ZTIETIegpKxAXYudV1hmzPQDdSI+d8nHCFyQfEFbQ6tfUq95lNArmmg==} engines: {node: '>=18'} @@ -1875,6 +1932,10 @@ packages: resolution: {integrity: sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==} engines: {node: '>=14'} + '@ethereumjs/tx@5.3.0': + resolution: {integrity: sha512-uv++XYuIfuqYbvymL3/o14hHuC6zX0nRQ1nI2FHsbkkorLZ2ChEIDqVeeVk7Xc9/jQNU/22sk9qZZkRlsveXxw==} + engines: {node: '>=18'} + '@ethereumjs/util@10.1.0': resolution: {integrity: sha512-GGTCkRu1kWXbz2JoUnIYtJBOoA9T5akzsYa91Bh+DZQ3Cj4qXj3hkNU0Rx6wZlbcmkmhQfrjZfVt52eJO/y2nA==} engines: {node: '>=18'} @@ -1883,6 +1944,64 @@ packages: resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} engines: {node: '>=14'} + '@ethereumjs/util@9.1.0': + resolution: {integrity: sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==} + engines: {node: '>=18'} + + '@ethersproject/abi@5.8.0': + resolution: {integrity: sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==} + + '@ethersproject/abstract-provider@5.8.0': + resolution: {integrity: sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==} + + '@ethersproject/abstract-signer@5.8.0': + resolution: {integrity: sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==} + + '@ethersproject/address@5.8.0': + resolution: {integrity: sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==} + + '@ethersproject/base64@5.8.0': + resolution: {integrity: sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==} + + '@ethersproject/bignumber@5.8.0': + resolution: {integrity: sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==} + + '@ethersproject/bytes@5.8.0': + resolution: {integrity: sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==} + + '@ethersproject/constants@5.8.0': + resolution: {integrity: sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==} + + '@ethersproject/hash@5.8.0': + resolution: {integrity: sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==} + + '@ethersproject/keccak256@5.8.0': + resolution: {integrity: sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==} + + '@ethersproject/logger@5.8.0': + resolution: {integrity: sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==} + + '@ethersproject/networks@5.8.0': + resolution: {integrity: sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==} + + '@ethersproject/properties@5.8.0': + resolution: {integrity: sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==} + + '@ethersproject/rlp@5.8.0': + resolution: {integrity: sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==} + + '@ethersproject/signing-key@5.8.0': + resolution: {integrity: sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==} + + '@ethersproject/strings@5.8.0': + resolution: {integrity: sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==} + + '@ethersproject/transactions@5.8.0': + resolution: {integrity: sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==} + + '@ethersproject/web@5.8.0': + resolution: {integrity: sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==} + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -1893,6 +2012,15 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@5.1.21': resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} @@ -1911,10 +2039,100 @@ packages: '@types/node': optional: true + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@3.0.10': resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} @@ -2039,10 +2257,18 @@ packages: '@types/react': '>=16' react: '>=16' + '@metamask/abi-utils@2.0.4': + resolution: {integrity: sha512-StnIgUB75x7a7AgUhiaUZDpCsqGp7VkNnZh2XivXkJ6mPkE83U8ARGQj5MbRis7VJY8BC5V1AbB1fjdh0hupPQ==} + engines: {node: '>=16.0.0'} + '@metamask/abi-utils@3.0.0': resolution: {integrity: sha512-a/l0DiSIr7+CBYVpHygUa3ztSlYLFCQMsklLna+t6qmNY9+eIO5TedNxhyIyvaJ+4cN7TLy0NQFbp9FV3X2ktg==} engines: {node: ^18.18 || ^20.14 || >=22} + '@metamask/eth-sig-util@7.0.3': + resolution: {integrity: sha512-PAtGnOkYvh90k2lEZldq/FK7GTLF6WxE+2bV85PoA3pqlJnmJCAY62tuvxHSwnVngSKlc4mcNvjnUg2eYO6JGg==} + engines: {node: ^16.20 || ^18.16 || >=20} + '@metamask/eth-sig-util@8.2.0': resolution: {integrity: sha512-LZDglIh4gYGw9Myp+2aIwKrj6lIJpMC4e0m7wKJU+BxLLBFcrTgKrjdjstXGVWvuYG3kutlh9J+uNBRPJqffWQ==} engines: {node: ^18.18 || ^20.14 || >=22} @@ -2055,6 +2281,10 @@ packages: resolution: {integrity: sha512-wRnoSDD9jTWOge/+reFviJQANhS+uy8Y+OEwRanp5mQeGTjBFmK1r2cTOnei2UCZRV1crXHzeJVSFEoDDcgRbA==} engines: {node: ^18.18 || ^20.14 || >=22} + '@metamask/utils@9.3.0': + resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} + engines: {node: '>=16.0.0'} + '@microsoft/api-extractor-model@7.32.2': resolution: {integrity: sha512-Ussc25rAalc+4JJs9HNQE7TuO9y6jpYQX9nWD1DhqUzYPBr3Lr7O9intf+ZY8kD5HnIqeIRJX7ccCT0QyBy2Ww==} @@ -2392,6 +2622,10 @@ packages: '@solana/web3.js@1.98.4': resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + '@sovpro/delimited-stream@1.1.0': + resolution: {integrity: sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw==} + engines: {node: '>= 8'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2699,6 +2933,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/uuid@11.0.0': resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed. @@ -3201,6 +3438,11 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + borc@3.0.0: + resolution: {integrity: sha512-ec4JmVC46kE0+layfnwM3l15O70MlFiEbmQHY/vpqIKiUtPVntv4BY4NVnz3N4vb21edV3mY97XVckFvYHWF9g==} + engines: {node: '>=4'} + hasBin: true + borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -3333,6 +3575,10 @@ packages: caniuse-lite@1.0.30001762: resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + case@1.6.3: + resolution: {integrity: sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==} + engines: {node: '>= 0.8.0'} + cbor-bigdecimal@10.0.11: resolution: {integrity: sha512-xoKFNdC1I+gbQtIyY30O+X0XDILcklFkvLE2GVY64tRFaSFZA3rj1Pk5fQZ7B/+I+dJn9GViBjdZedqtJL/BXg==} engines: {node: '>=20'} @@ -3374,6 +3620,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -3426,6 +3675,14 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -3473,6 +3730,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -4062,6 +4323,10 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} @@ -4092,9 +4357,15 @@ packages: elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + elliptic@6.5.6: + resolution: {integrity: sha512-mpzdtpeCLuS3BmE3pO3Cpp5bbjlOPY2Q0PgoF+Od1XZrHLYI28Xe3ossCmYCQt11FQKEYd9+PF8jymTvtWJSHQ==} + elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4498,6 +4769,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4582,6 +4857,9 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + gridplus-sdk@2.7.1: + resolution: {integrity: sha512-KJgNN0dnLL5/NMUJnQ2sy3vAs3AJnpYDeC5jQSo2rZVyauV6eAS9SJMTYikENbgB/BDnn5gOCbvob/CwbMMKbg==} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -4779,6 +5057,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -4836,6 +5118,15 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@12.11.1: + resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -4928,6 +5219,10 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-network-error@1.3.0: resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} engines: {node: '>=16'} @@ -4982,6 +5277,14 @@ packages: is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -5006,6 +5309,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iso-url@1.2.1: + resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} + engines: {node: '>=12'} + isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} @@ -5098,6 +5405,12 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + + js-sha3@0.9.3: + resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5138,6 +5451,10 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json-text-sequence@0.3.0: + resolution: {integrity: sha512-7khKIYPKwXQem4lWXfpIN/FEnhztCeRPSxH4qm3fVlqulwujrRDD54xAwDDn/qVKpFtV550+QAkcWJcufzqQuA==} + engines: {node: '>=10.18.0'} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -5186,6 +5503,9 @@ packages: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} + lattice-eth2-utils@0.5.1: + resolution: {integrity: sha512-S1VeykS8K9bBeQ0S+OXEdmq7myR21hy0CidO83Uaydo7g1a9LTjK2ScVK3nuY11/3nCzS5i9DrLGW36vE38Zmg==} + launch-editor@2.12.0: resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} @@ -5238,6 +5558,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -5663,6 +5987,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -5867,6 +6195,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -5879,6 +6211,10 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -6775,6 +7111,10 @@ packages: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -6819,12 +7159,19 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -6865,6 +7212,10 @@ packages: resolution: {integrity: sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==} engines: {node: '>=18.0.0'} + secp256k1@5.0.0: + resolution: {integrity: sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==} + engines: {node: '>=14.0.0'} + secp256k1@5.0.1: resolution: {integrity: sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==} engines: {node: '>=18.0.0'} @@ -7081,6 +7432,10 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + stream-chain@2.2.5: resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} @@ -7105,6 +7460,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -7620,6 +7979,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@13.0.0: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true @@ -8953,11 +9316,23 @@ snapshots: '@braintree/sanitize-url@6.0.4': {} + '@chainsafe/as-sha256@0.3.1': {} + '@chainsafe/bls-keystore@3.1.0': dependencies: ethereum-cryptography: 2.2.1 uuid: 9.0.1 + '@chainsafe/persistent-merkle-tree@0.4.2': + dependencies: + '@chainsafe/as-sha256': 0.3.1 + + '@chainsafe/ssz@0.9.4': + dependencies: + '@chainsafe/as-sha256': 0.3.1 + '@chainsafe/persistent-merkle-tree': 0.4.2 + case: 1.6.3 + '@colors/colors@1.5.0': optional: true @@ -10205,10 +10580,16 @@ snapshots: '@ethereumjs/util': 8.1.0 crc-32: 1.2.2 + '@ethereumjs/common@4.3.0': + dependencies: + '@ethereumjs/util': 9.1.0 + '@ethereumjs/rlp@10.1.0': {} '@ethereumjs/rlp@4.0.1': {} + '@ethereumjs/rlp@5.0.2': {} + '@ethereumjs/tx@10.1.0': dependencies: '@ethereumjs/common': 10.1.0 @@ -10223,6 +10604,13 @@ snapshots: '@ethereumjs/util': 8.1.0 ethereum-cryptography: 2.2.1 + '@ethereumjs/tx@5.3.0': + dependencies: + '@ethereumjs/common': 4.3.0 + '@ethereumjs/rlp': 5.0.2 + '@ethereumjs/util': 9.1.0 + ethereum-cryptography: 2.2.1 + '@ethereumjs/util@10.1.0': dependencies: '@ethereumjs/rlp': 10.1.0 @@ -10234,6 +10622,134 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 + '@ethereumjs/util@9.1.0': + dependencies: + '@ethereumjs/rlp': 5.0.2 + ethereum-cryptography: 2.2.1 + + '@ethersproject/abi@5.8.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/abstract-provider@5.8.0': + 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 + + '@ethersproject/abstract-signer@5.8.0': + 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 + + '@ethersproject/address@5.8.0': + 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 + + '@ethersproject/base64@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + + '@ethersproject/bignumber@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + bn.js: 5.2.2 + + '@ethersproject/bytes@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/constants@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + + '@ethersproject/hash@5.8.0': + 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 + + '@ethersproject/keccak256@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + js-sha3: 0.8.0 + + '@ethersproject/logger@5.8.0': {} + + '@ethersproject/networks@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/properties@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/rlp@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/signing-key@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + bn.js: 5.2.2 + elliptic: 6.6.1 + hash.js: 1.1.7 + + '@ethersproject/strings@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/transactions@5.8.0': + 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 + + '@ethersproject/web@5.8.0': + 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 + '@hapi/hoek@9.3.0': {} '@hapi/topo@5.1.0': @@ -10242,6 +10758,16 @@ snapshots: '@inquirer/ansi@1.0.2': {} + '@inquirer/checkbox@4.3.2(@types/node@24.10.4)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.4 + '@inquirer/confirm@5.1.21(@types/node@24.10.4)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.4) @@ -10262,8 +10788,95 @@ snapshots: optionalDependencies: '@types/node': 24.10.4 + '@inquirer/editor@4.2.23(@types/node@24.10.4)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/expand@4.0.23(@types/node@24.10.4)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/external-editor@1.0.3(@types/node@24.10.4)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.10.4 + '@inquirer/figures@1.0.15': {} + '@inquirer/input@4.3.1(@types/node@24.10.4)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/number@3.0.23(@types/node@24.10.4)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/password@4.0.23(@types/node@24.10.4)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/prompts@7.10.1(@types/node@24.10.4)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.10.4) + '@inquirer/confirm': 5.1.21(@types/node@24.10.4) + '@inquirer/editor': 4.2.23(@types/node@24.10.4) + '@inquirer/expand': 4.0.23(@types/node@24.10.4) + '@inquirer/input': 4.3.1(@types/node@24.10.4) + '@inquirer/number': 3.0.23(@types/node@24.10.4) + '@inquirer/password': 4.0.23(@types/node@24.10.4) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.4) + '@inquirer/search': 3.2.2(@types/node@24.10.4) + '@inquirer/select': 4.4.2(@types/node@24.10.4) + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/rawlist@4.1.11(@types/node@24.10.4)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/search@3.2.2(@types/node@24.10.4)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.4 + + '@inquirer/select@4.4.2(@types/node@24.10.4)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.4 + '@inquirer/type@3.0.10(@types/node@24.10.4)': optionalDependencies: '@types/node': 24.10.4 @@ -10423,6 +11036,13 @@ snapshots: '@types/react': 18.3.27 react: 18.3.1 + '@metamask/abi-utils@2.0.4': + dependencies: + '@metamask/superstruct': 3.2.1 + '@metamask/utils': 9.3.0 + transitivePeerDependencies: + - supports-color + '@metamask/abi-utils@3.0.0': dependencies: '@metamask/superstruct': 3.2.1 @@ -10430,6 +11050,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@metamask/eth-sig-util@7.0.3': + dependencies: + '@ethereumjs/util': 8.1.0 + '@metamask/abi-utils': 2.0.4 + '@metamask/utils': 9.3.0 + '@scure/base': 1.1.9 + ethereum-cryptography: 2.2.1 + tweetnacl: 1.0.3 + transitivePeerDependencies: + - supports-color + '@metamask/eth-sig-util@8.2.0': dependencies: '@ethereumjs/rlp': 4.0.1 @@ -10460,6 +11091,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@metamask/utils@9.3.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.4.3 + pony-cause: 2.1.11 + semver: 7.7.3 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + '@microsoft/api-extractor-model@7.32.2(@types/node@24.10.4)': dependencies: '@microsoft/tsdoc': 0.16.0 @@ -10810,6 +11455,8 @@ snapshots: - typescript - utf-8-validate + '@sovpro/delimited-stream@1.1.0': {} + '@standard-schema/spec@1.1.0': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': @@ -11160,6 +11807,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} + '@types/uuid@11.0.0': dependencies: uuid: 13.0.0 @@ -11733,6 +12382,16 @@ snapshots: boolbase@1.0.0: {} + borc@3.0.0: + dependencies: + bignumber.js: 9.3.1 + buffer: 6.0.3 + commander: 2.20.3 + ieee754: 1.2.1 + iso-url: 1.2.1 + json-text-sequence: 0.3.0 + readable-stream: 3.6.2 + borsh@0.7.0: dependencies: bn.js: 5.2.2 @@ -11908,6 +12567,8 @@ snapshots: caniuse-lite@1.0.30001762: {} + case@1.6.3: {} + cbor-bigdecimal@10.0.11(bignumber.js@9.3.1): dependencies: bignumber.js: 9.3.1 @@ -11943,6 +12604,8 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} + check-error@2.1.3: {} cheerio-select@2.1.0: @@ -12008,6 +12671,12 @@ snapshots: cli-boxes@3.0.0: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -12048,6 +12717,8 @@ snapshots: commander@10.0.1: {} + commander@12.1.0: {} + commander@14.0.2: {} commander@2.20.3: {} @@ -12668,6 +13339,8 @@ snapshots: dependencies: is-obj: 2.0.0 + dotenv@16.6.1: {} + dotenv@17.2.3: {} dunder-proto@1.0.1: @@ -12699,6 +13372,16 @@ snapshots: elkjs@0.9.3: {} + elliptic@6.5.6: + dependencies: + bn.js: 4.12.2 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + elliptic@6.6.1: dependencies: bn.js: 4.12.2 @@ -12709,6 +13392,8 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -13208,6 +13893,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -13323,6 +14010,30 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + gridplus-sdk@2.7.1: + dependencies: + '@ethereumjs/common': 4.3.0 + '@ethereumjs/rlp': 5.0.2 + '@ethereumjs/tx': 5.3.0 + '@ethersproject/abi': 5.8.0 + '@metamask/eth-sig-util': 7.0.3 + '@types/uuid': 10.0.0 + aes-js: 3.1.2 + bech32: 2.0.0 + bignumber.js: 9.3.1 + bitwise: 2.2.1 + borc: 3.0.0 + bs58check: 4.0.0 + buffer: 6.0.3 + crc-32: 1.2.2 + elliptic: 6.5.6 + hash.js: 1.1.7 + js-sha3: 0.9.3 + secp256k1: 5.0.0 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -13669,6 +14380,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -13705,6 +14420,18 @@ snapshots: inline-style-parser@0.2.7: {} + inquirer@12.11.1(@types/node@24.10.4): + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/prompts': 7.10.1(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.4) + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 24.10.4 + internmap@1.0.1: {} internmap@2.0.3: {} @@ -13773,6 +14500,8 @@ snapshots: global-dirs: 3.0.1 is-path-inside: 3.0.3 + is-interactive@2.0.0: {} + is-network-error@1.3.0: {} is-node-process@1.2.0: {} @@ -13805,6 +14534,10 @@ snapshots: is-typedarray@1.0.0: {} + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -13823,6 +14556,8 @@ snapshots: isexe@2.0.0: {} + iso-url@1.2.1: {} + isobject@3.0.1: {} isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)): @@ -13967,6 +14702,10 @@ snapshots: joycon@3.1.1: {} + js-sha3@0.8.0: {} + + js-sha3@0.9.3: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -13996,6 +14735,10 @@ snapshots: json-stringify-safe@5.0.1: {} + json-text-sequence@0.3.0: + dependencies: + '@sovpro/delimited-stream': 1.1.0 + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -14043,6 +14786,16 @@ snapshots: dependencies: package-json: 8.1.1 + lattice-eth2-utils@0.5.1: + dependencies: + '@chainsafe/ssz': 0.9.4 + '@ethersproject/abi': 5.8.0 + '@noble/hashes': 1.8.0 + bn.js: 5.2.2 + gridplus-sdk: 2.7.1 + transitivePeerDependencies: + - supports-color + launch-editor@2.12.0: dependencies: picocolors: 1.1.1 @@ -14086,6 +14839,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -15025,6 +15783,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + mimic-response@3.1.0: {} mimic-response@4.0.0: {} @@ -15210,6 +15970,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + open@10.2.0: dependencies: default-browser: 5.4.0 @@ -15225,6 +15989,18 @@ snapshots: opener@1.5.2: {} + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + outvariant@1.4.3: {} ox@0.11.1(typescript@5.9.3)(zod@4.3.5): @@ -16293,6 +17069,11 @@ snapshots: dependencies: lowercase-keys: 3.0.0 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + retry@0.13.1: {} rettime@0.7.0: {} @@ -16367,12 +17148,18 @@ snapshots: run-applescript@7.1.0: {} + run-async@4.0.6: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 rw@1.3.3: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + sade@1.8.1: dependencies: mri: 1.2.0 @@ -16414,6 +17201,12 @@ snapshots: node-addon-api: 5.1.0 node-gyp-build: 4.8.4 + secp256k1@5.0.0: + dependencies: + elliptic: 6.6.1 + node-addon-api: 5.1.0 + node-gyp-build: 4.8.4 + secp256k1@5.0.1: dependencies: elliptic: 6.6.1 @@ -16679,6 +17472,8 @@ snapshots: std-env@3.10.0: {} + stdin-discarder@0.2.2: {} + stream-chain@2.2.5: {} stream-json@1.9.1: @@ -16711,6 +17506,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -17235,6 +18036,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + uuid@13.0.0: {} uuid@8.3.2: {} From 350dca39aff589b54d503126ace72b5fc3c1211b Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:48:48 -0500 Subject: [PATCH 2/7] feat(cli): implement full command suite Session infrastructure: - session.ts - ~/.gridplus/ persistence with SDK callbacks - config.ts - CLI configuration management - output.ts - JSON/human formatting with chalk colors - prompts.ts - Inquirer prompts with ora spinners Commands implemented: - setup - Interactive device setup - connect - Connect to saved device - pair - Pair with pairing code - address [path] - ETH/BTC/Solana addresses with --type - pubkey [path] - secp256k1/ed25519/BLS keys - sign - Ethereum transaction signing - sign-message - Personal sign and EIP-712 - eth2 deposit-data - Validator deposit data export - eth2 bls-change - BLS withdrawal credential migration All commands support -j/--json output flag. --- packages/cli/package.json | 2 +- packages/cli/src/commands/address.ts | 164 ++++++++++++ packages/cli/src/commands/connect.ts | 86 ++++++ packages/cli/src/commands/eth2/bls-change.ts | 128 +++++++++ .../cli/src/commands/eth2/deposit-data.ts | 171 ++++++++++++ packages/cli/src/commands/eth2/index.ts | 2 + packages/cli/src/commands/index.ts | 8 + packages/cli/src/commands/pair.ts | 64 +++++ packages/cli/src/commands/pubkey.ts | 124 +++++++++ packages/cli/src/commands/setup.ts | 80 ++++++ packages/cli/src/commands/sign-message.ts | 167 ++++++++++++ packages/cli/src/commands/sign.ts | 127 +++++++++ packages/cli/src/index.ts | 58 ++-- packages/cli/src/lib/config.ts | 93 +++++++ packages/cli/src/lib/index.ts | 4 + packages/cli/src/lib/output.ts | 158 +++++++++++ packages/cli/src/lib/prompts.ts | 253 ++++++++++++++++++ packages/cli/src/lib/session.ts | 113 ++++++++ pnpm-lock.yaml | 40 +-- 19 files changed, 1772 insertions(+), 70 deletions(-) create mode 100644 packages/cli/src/commands/address.ts create mode 100644 packages/cli/src/commands/connect.ts create mode 100644 packages/cli/src/commands/eth2/bls-change.ts create mode 100644 packages/cli/src/commands/eth2/deposit-data.ts create mode 100644 packages/cli/src/commands/eth2/index.ts create mode 100644 packages/cli/src/commands/index.ts create mode 100644 packages/cli/src/commands/pair.ts create mode 100644 packages/cli/src/commands/pubkey.ts create mode 100644 packages/cli/src/commands/setup.ts create mode 100644 packages/cli/src/commands/sign-message.ts create mode 100644 packages/cli/src/commands/sign.ts create mode 100644 packages/cli/src/lib/config.ts create mode 100644 packages/cli/src/lib/index.ts create mode 100644 packages/cli/src/lib/output.ts create mode 100644 packages/cli/src/lib/prompts.ts create mode 100644 packages/cli/src/lib/session.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 97c4d849..61501088 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,9 +17,9 @@ "dependencies": { "gridplus-sdk": "workspace:*", "@gridplus/btc": "workspace:*", + "@inquirer/prompts": "^7.0.0", "lattice-eth2-utils": "^0.5.1", "commander": "^12.0.0", - "inquirer": "^12.0.0", "chalk": "^5.0.0", "ora": "^8.0.0", "dotenv": "^16.0.0" diff --git a/packages/cli/src/commands/address.ts b/packages/cli/src/commands/address.ts new file mode 100644 index 00000000..daf02b58 --- /dev/null +++ b/packages/cli/src/commands/address.ts @@ -0,0 +1,164 @@ +import { Command } from 'commander'; +import { + fetchAddress, + fetchAddressesByDerivationPath, + fetchBtcLegacyAddresses, + fetchBtcSegwitAddresses, + fetchBtcWrappedSegwitAddresses, + fetchSolanaAddresses, + setup, +} from 'gridplus-sdk'; +import { + address as outputAddress, + error, + getStoredClient, + hasSession, + info, + output, + setStoredClient, + withSpinner, +} from '../lib/index.js'; + +export type AddressType = + | 'eth' + | 'btc-legacy' + | 'btc-segwit' + | 'btc-wrapped-segwit' + | 'solana'; + +export const addressCommand = new Command('address') + .description('Get addresses from your Lattice device') + .argument('[path]', "Derivation path (e.g., \"m/44'/60'/0'/0/0\" or index)") + .option( + '-t, --type ', + 'Address type: eth|btc-legacy|btc-segwit|btc-wrapped-segwit|solana', + 'eth', + ) + .option('-n, --count ', 'Number of addresses to fetch', '1') + .option('-i, --index ', 'Starting index', '0') + .option('-j, --json', 'Output in JSON format') + .action(async (path, options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + // Initialize the SDK + await withSpinner('Connecting to device...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + const addressType = options.type as AddressType; + const count = Number.parseInt(options.count, 10); + const startIndex = Number.parseInt(options.index, 10); + + let addresses: string[]; + + // If a specific derivation path is provided, use it + if (path && typeof path === 'string' && !Number.isFinite(Number(path))) { + info(`Fetching address at path: ${path}`); + addresses = await withSpinner('Fetching addresses...', async () => { + return fetchAddressesByDerivationPath(path, { + n: count, + startPathIndex: startIndex, + }); + }); + } else { + // Use address type to determine which fetch function to use + const index = path ? Number.parseInt(path, 10) : startIndex; + + switch (addressType) { + case 'btc-legacy': + info('Fetching Bitcoin Legacy (P2PKH) addresses...'); + addresses = await withSpinner('Fetching addresses...', async () => { + return fetchBtcLegacyAddresses({ + n: count, + startPathIndex: index, + }); + }); + break; + + case 'btc-segwit': + info('Fetching Bitcoin Native SegWit (P2WPKH) addresses...'); + addresses = await withSpinner('Fetching addresses...', async () => { + return fetchBtcSegwitAddresses({ + n: count, + startPathIndex: index, + }); + }); + break; + + case 'btc-wrapped-segwit': + info('Fetching Bitcoin Wrapped SegWit (P2SH-P2WPKH) addresses...'); + addresses = await withSpinner('Fetching addresses...', async () => { + return fetchBtcWrappedSegwitAddresses({ + n: count, + startPathIndex: index, + }); + }); + break; + + case 'solana': + info('Fetching Solana addresses...'); + addresses = await withSpinner('Fetching addresses...', async () => { + return fetchSolanaAddresses({ + n: count, + startPathIndex: index, + }); + }); + break; + default: + info('Fetching Ethereum addresses...'); + if (count === 1) { + const addr = await withSpinner( + 'Fetching address...', + async () => { + return fetchAddress(index); + }, + ); + addresses = [addr]; + } else { + addresses = await withSpinner( + 'Fetching addresses...', + async () => { + return fetchAddressesByDerivationPath("m/44'/60'/0'/0/x", { + n: count, + startPathIndex: index, + }); + }, + ); + } + break; + } + } + + // Output results + if (options.json) { + output( + { + type: addressType, + count: addresses.length, + addresses, + }, + 'json', + ); + } else { + if (addresses.length === 1) { + outputAddress(addresses[0], addressType.toUpperCase()); + } else { + addresses.forEach((addr, i) => { + outputAddress(addr, `[${startIndex + i}]`); + }); + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Failed to fetch addresses: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/connect.ts b/packages/cli/src/commands/connect.ts new file mode 100644 index 00000000..858c6453 --- /dev/null +++ b/packages/cli/src/commands/connect.ts @@ -0,0 +1,86 @@ +import { Command } from 'commander'; +import { connect, setup } from 'gridplus-sdk'; +import { + error, + getStoredClient, + hasSession, + info, + loadSession, + output, + setStoredClient, + success, + withSpinner, +} from '../lib/index.js'; + +export const connectCommand = new Command('connect') + .description('Connect to a previously configured Lattice device') + .option('-j, --json', 'Output in JSON format') + .action(async (options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + const session = loadSession(); + if (!session) { + error('Failed to load session data.'); + process.exit(1); + } + + info(`Connecting to device: ${session.deviceId}`); + + // Initialize the SDK with stored credentials + const isPaired = await withSpinner('Connecting...', async () => { + // First setup the SDK state handlers + await setup({ + getStoredClient, + setStoredClient, + }); + + // Then connect to the device + return connect(session.deviceId); + }); + + if (isPaired) { + success('Connected and paired!'); + if (options.json) { + output( + { + status: 'connected', + paired: true, + deviceId: session.deviceId, + baseUrl: session.baseUrl, + appName: session.name, + }, + 'json', + ); + } else { + output({ + deviceId: session.deviceId, + baseUrl: session.baseUrl, + appName: session.name, + status: 'paired', + }); + } + } else { + success('Connected but not paired.'); + info('Run "gp pair" to pair with the device.'); + if (options.json) { + output( + { + status: 'connected', + paired: false, + deviceId: session.deviceId, + }, + 'json', + ); + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Connection failed: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/eth2/bls-change.ts b/packages/cli/src/commands/eth2/bls-change.ts new file mode 100644 index 00000000..d5b3e208 --- /dev/null +++ b/packages/cli/src/commands/eth2/bls-change.ts @@ -0,0 +1,128 @@ +import { writeFileSync } from 'node:fs'; +import { Command } from 'commander'; +import { getClient, parseDerivationPath, setup } from 'gridplus-sdk'; +import * as eth2 from 'lattice-eth2-utils'; +import { + error, + getStoredClient, + hasSession, + info, + output, + promptValidatorIndex, + promptWithdrawalAddress, + setStoredClient, + success, + withSpinner, +} from '../../lib/index.js'; + +// Type assertion helper to work around lattice-eth2-utils expecting npm gridplus-sdk Client +type Eth2Client = Parameters< + typeof eth2.BLSToExecutionChange.generateObject +>[0]; + +// Default ETH2 validator derivation path (EIP-2334) +// m/12381/3600//0/0 +const DEFAULT_VALIDATOR_PATH = [12381, 3600, 0, 0, 0]; + +export const blsChangeCommand = new Command('bls-change') + .description( + 'Generate a signed BLS-to-execution-change message to update withdrawal credentials', + ) + .option('-i, --index ', 'Validator index') + .option('-p, --path ', 'Custom derivation path for validator key') + .option('-w, --withdrawal
', 'New ETH1 withdrawal address') + .option('-v, --validator-index ', 'On-chain validator index (required)') + .option('-o, --output ', 'Output file for signed message JSON') + .option('-j, --json', 'Output in JSON format') + .action(async (options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + // Initialize the SDK + await withSpinner('Connecting to device...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + // Get the SDK client instance + const client = await getClient(); + + // Determine the derivation path + let validatorPath: number[]; + if (options.path) { + validatorPath = parseDerivationPath(options.path); + } else { + const keyIndex = options.index ? Number.parseInt(options.index, 10) : 0; + validatorPath = [...DEFAULT_VALIDATOR_PATH]; + validatorPath[2] = keyIndex; // Set the validator index in derivation path + } + + info(`Validator derivation path: m/${validatorPath.join('/')}`); + + // Get the on-chain validator index + let validatorIndex: number; + if (options.validatorIndex) { + validatorIndex = Number.parseInt(options.validatorIndex, 10); + } else { + validatorIndex = await promptValidatorIndex(); + } + + info(`On-chain validator index: ${validatorIndex}`); + + // Get new withdrawal address + const withdrawalAddress = + options.withdrawal || (await promptWithdrawalAddress()); + info(`New withdrawal address: ${withdrawalAddress}`); + + // Build BLS change options + const blsChangeOpts = { + toExecutionAddress: withdrawalAddress, + validatorIndex, + network: eth2.Constants.NETWORKS.MAINNET_GENESIS, + }; + + info('Generating BLS-to-execution-change message...'); + info('Please confirm the signing request on your Lattice device.'); + + const signedMessage = await withSpinner( + 'Generating signed message...', + async () => { + // Cast to work around type incompatibility between workspace and npm gridplus-sdk + return eth2.BLSToExecutionChange.generateObject( + client as unknown as Eth2Client, + validatorPath, + blsChangeOpts, + ); + }, + ); + + success('BLS-to-execution-change message generated!'); + + // Output the signed message + if (options.output) { + const outputData = JSON.stringify(signedMessage, null, 2); + writeFileSync(options.output, outputData); + success(`Signed message written to: ${options.output}`); + } + + if (options.json || !options.output) { + output(signedMessage, options.json ? 'json' : 'human'); + } + + info(''); + info('To submit this message:'); + info('1. Broadcast via a beacon node API endpoint'); + info(' POST /eth/v1/beacon/pool/bls_to_execution_changes'); + info('2. Or use a tool like https://beaconcha.in/tools/broadcast'); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Failed to generate BLS change message: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/eth2/deposit-data.ts b/packages/cli/src/commands/eth2/deposit-data.ts new file mode 100644 index 00000000..3facbcb6 --- /dev/null +++ b/packages/cli/src/commands/eth2/deposit-data.ts @@ -0,0 +1,171 @@ +import { writeFileSync } from 'node:fs'; +import { Command } from 'commander'; +import { getClient, parseDerivationPath, setup } from 'gridplus-sdk'; +import * as eth2 from 'lattice-eth2-utils'; +import { + error, + getStoredClient, + hasSession, + info, + output, + promptConfirm, + promptWithdrawalAddress, + setStoredClient, + success, + withSpinner, +} from '../../lib/index.js'; + +// Type assertion helper to work around lattice-eth2-utils expecting npm gridplus-sdk Client +type Eth2Client = Parameters[0]; + +// Default ETH2 validator derivation path (EIP-2334) +// m/12381/3600//0/0 +const DEFAULT_VALIDATOR_PATH = [12381, 3600, 0, 0, 0]; + +export const depositDataCommand = new Command('deposit-data') + .description('Generate ETH2 validator deposit data and optional keystores') + .option('-i, --index ', 'Validator index (default: 0)', '0') + .option('-p, --path ', 'Custom derivation path for validator key') + .option('-w, --withdrawal
', 'ETH1 withdrawal address') + .option( + '--withdrawal-path ', + 'Derivation path for withdrawal credentials', + ) + .option('-o, --output ', 'Output file for deposit data JSON') + .option('--keystore', 'Also export encrypted keystore') + .option('--keystore-output ', 'Output file for keystore') + .option( + '-n, --network ', + 'Network: mainnet|goerli|holesky', + 'mainnet', + ) + .option('-j, --json', 'Output in JSON format') + .action(async (options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + // Initialize the SDK + await withSpinner('Connecting to device...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + // Get the SDK client instance + const client = await getClient(); + + // Determine the derivation path + let validatorPath: number[]; + if (options.path) { + validatorPath = parseDerivationPath(options.path); + } else { + const validatorIndex = Number.parseInt(options.index, 10); + validatorPath = [...DEFAULT_VALIDATOR_PATH]; + validatorPath[2] = validatorIndex; // Set the validator index + } + + info(`Validator derivation path: m/${validatorPath.join('/')}`); + + // Get withdrawal address + let withdrawalCredentials: string | undefined; + if (options.withdrawal) { + withdrawalCredentials = options.withdrawal; + } else if (!options.withdrawalPath) { + const useAddress = await promptConfirm( + 'Use an ETH1 address for withdrawal credentials?', + true, + ); + if (useAddress) { + withdrawalCredentials = await promptWithdrawalAddress(); + } + } + + // Build deposit data options + const depositOpts: { + withdrawalCredentials?: string; + withdrawalPath?: number[]; + network?: { + networkName: string; + forkVersion: Buffer; + validatorsRoot: Buffer; + }; + } = {}; + + if (withdrawalCredentials) { + depositOpts.withdrawalCredentials = withdrawalCredentials; + info(`Using withdrawal address: ${withdrawalCredentials}`); + } else if (options.withdrawalPath) { + depositOpts.withdrawalPath = parseDerivationPath( + options.withdrawalPath, + ); + info(`Using withdrawal path: ${options.withdrawalPath}`); + } + + // Set network configuration + depositOpts.network = eth2.Constants.NETWORKS.MAINNET_GENESIS; + + info('Generating deposit data...'); + info('Please confirm the signing request on your Lattice device.'); + + const depositData = await withSpinner( + 'Generating deposit data...', + async () => { + // Cast to work around type incompatibility between workspace and npm gridplus-sdk + return eth2.DepositData.generateObject( + client as unknown as Eth2Client, + validatorPath, + depositOpts, + ); + }, + ); + + success('Deposit data generated!'); + + // Output deposit data + if (options.output) { + const outputData = JSON.stringify([depositData], null, 2); + writeFileSync(options.output, outputData); + success(`Deposit data written to: ${options.output}`); + } + + if (options.json || !options.output) { + output(depositData, options.json ? 'json' : 'human'); + } + + // Export keystore if requested + if (options.keystore) { + info('Exporting encrypted keystore...'); + info('Please confirm the export request on your Lattice device.'); + + const keystore = await withSpinner( + 'Exporting keystore...', + async () => { + // Cast to work around type incompatibility between workspace and npm gridplus-sdk + return eth2.DepositData.exportKeystore( + client as unknown as Eth2Client, + validatorPath, + ); + }, + ); + + success('Keystore exported!'); + + if (options.keystoreOutput) { + writeFileSync(options.keystoreOutput, keystore); + success(`Keystore written to: ${options.keystoreOutput}`); + } else { + info('Keystore (save this securely):'); + console.log(keystore); + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Failed to generate deposit data: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/eth2/index.ts b/packages/cli/src/commands/eth2/index.ts new file mode 100644 index 00000000..64c5dc0a --- /dev/null +++ b/packages/cli/src/commands/eth2/index.ts @@ -0,0 +1,2 @@ +export { blsChangeCommand } from './bls-change.js'; +export { depositDataCommand } from './deposit-data.js'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts new file mode 100644 index 00000000..ab50bd58 --- /dev/null +++ b/packages/cli/src/commands/index.ts @@ -0,0 +1,8 @@ +export { addressCommand } from './address.js'; +export { connectCommand } from './connect.js'; +export { pairCommand } from './pair.js'; +export { pubkeyCommand } from './pubkey.js'; +export { setupCommand } from './setup.js'; +export { signCommand } from './sign.js'; +export { signMessageCommand } from './sign-message.js'; +export { blsChangeCommand, depositDataCommand } from './eth2/index.js'; diff --git a/packages/cli/src/commands/pair.ts b/packages/cli/src/commands/pair.ts new file mode 100644 index 00000000..61b4d148 --- /dev/null +++ b/packages/cli/src/commands/pair.ts @@ -0,0 +1,64 @@ +import { Command } from 'commander'; +import { pair, setup } from 'gridplus-sdk'; +import { + error, + getStoredClient, + hasSession, + info, + loadSession, + promptPairingCode, + setStoredClient, + success, + withSpinner, +} from '../lib/index.js'; + +export const pairCommand = new Command('pair') + .description('Pair with a Lattice device using a pairing code') + .argument('[code]', 'Pairing code from device (or will prompt)') + .action(async (code) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + const session = loadSession(); + if (!session) { + error('Failed to load session data.'); + process.exit(1); + } + + // Get pairing code + const pairingCode = code || (await promptPairingCode()); + + info('Initiating pairing with your Lattice device...'); + info('Please confirm the pairing request on your device screen.'); + + // Initialize the SDK with stored credentials + await withSpinner('Setting up connection...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + // Perform pairing + const isPaired = await withSpinner('Pairing with device...', async () => { + return pair(pairingCode); + }); + + if (isPaired) { + success('Pairing successful!'); + info('Your CLI is now paired with your Lattice device.'); + info('You can now use commands like "gp address" and "gp sign".'); + } else { + error('Pairing failed. Please try again.'); + process.exit(1); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Pairing failed: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/pubkey.ts b/packages/cli/src/commands/pubkey.ts new file mode 100644 index 00000000..e21cf8c8 --- /dev/null +++ b/packages/cli/src/commands/pubkey.ts @@ -0,0 +1,124 @@ +import { Command } from 'commander'; +import { Constants, fetchAddressesByDerivationPath, setup } from 'gridplus-sdk'; +import { + error, + getStoredClient, + hasSession, + hex, + info, + output, + setStoredClient, + withSpinner, +} from '../lib/index.js'; + +export type PubkeyType = 'secp256k1' | 'ed25519' | 'bls12_381_g1'; + +// Map pubkey types to SDK flags +const PUBKEY_FLAGS: Record = { + secp256k1: Constants.GET_ADDR_FLAGS.SECP256K1_PUB, + ed25519: Constants.GET_ADDR_FLAGS.ED25519_PUB, + bls12_381_g1: Constants.GET_ADDR_FLAGS.BLS12_381_G1_PUB, +}; + +// Default derivation paths for different key types +const DEFAULT_PATHS: Record = { + secp256k1: "m/44'/60'/0'/0/0", + ed25519: "m/44'/501'/0'/0'", + bls12_381_g1: 'm/12381/3600/0/0/0', +}; + +export const pubkeyCommand = new Command('pubkey') + .description('Get public keys from your Lattice device') + .argument('[path]', 'Derivation path') + .option( + '-t, --type ', + 'Key type: secp256k1|ed25519|bls12_381_g1', + 'secp256k1', + ) + .option('-n, --count ', 'Number of keys to fetch', '1') + .option('-i, --index ', 'Starting index', '0') + .option('-j, --json', 'Output in JSON format') + .action(async (path, options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + // Initialize the SDK + await withSpinner('Connecting to device...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + const keyType = options.type as PubkeyType; + const count = Number.parseInt(options.count, 10); + const startIndex = Number.parseInt(options.index, 10); + + // Validate key type + if (!PUBKEY_FLAGS[keyType]) { + error( + `Invalid key type: ${keyType}. Use secp256k1, ed25519, or bls12_381_g1.`, + ); + process.exit(1); + } + + // Use provided path or default for key type + const derivationPath = path || DEFAULT_PATHS[keyType]; + const flag = PUBKEY_FLAGS[keyType]; + + info(`Fetching ${keyType} public key(s) at path: ${derivationPath}`); + + const pubkeys = await withSpinner('Fetching public keys...', async () => { + // For multiple keys, we need to handle the path with an index + if (count > 1) { + // Replace the last component with a wildcard if fetching multiple + const pathParts = derivationPath.split('/'); + const lastPart = pathParts[pathParts.length - 1]; + if (!lastPart?.toLowerCase().includes('x')) { + pathParts[pathParts.length - 1] = 'x'; + } + const wildcardPath = pathParts.join('/'); + return fetchAddressesByDerivationPath(wildcardPath, { + n: count, + startPathIndex: startIndex, + flag, + }); + } + + // Single key + return fetchAddressesByDerivationPath(derivationPath, { + n: 1, + flag, + }); + }); + + // Output results + if (options.json) { + output( + { + type: keyType, + path: derivationPath, + count: pubkeys.length, + publicKeys: pubkeys, + }, + 'json', + ); + } else { + if (pubkeys.length === 1) { + hex(pubkeys[0], `${keyType.toUpperCase()} Public Key`); + } else { + pubkeys.forEach((pk, i) => { + hex(pk, `[${startIndex + i}]`); + }); + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Failed to fetch public keys: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts new file mode 100644 index 00000000..39b0c8bf --- /dev/null +++ b/packages/cli/src/commands/setup.ts @@ -0,0 +1,80 @@ +import { Command } from 'commander'; +import { Utils, setup } from 'gridplus-sdk'; +import { + error, + getStoredClient, + info, + promptAppName, + promptDeviceId, + promptPassword, + saveSession, + setStoredClient, + success, + withSpinner, +} from '../lib/index.js'; + +export const setupCommand = new Command('setup') + .description( + 'Interactive device setup - configure connection to your Lattice', + ) + .option('-d, --device-id ', 'Device ID (skip prompt)') + .option('-p, --password ', 'Device password (skip prompt)') + .option('-n, --name ', 'App name shown on device') + .option('--base-url ', 'Custom base URL for Lattice relay') + .action(async (options) => { + try { + info('Setting up GridPlus Lattice connection...'); + + // Get device ID + const deviceId = options.deviceId || (await promptDeviceId()); + + // Get password + const password = options.password || (await promptPassword()); + + // Get app name + const name = options.name || (await promptAppName()); + + // Generate app secret from credentials + const appSecret = Utils.generateAppSecret(deviceId, password, name); + const appSecretHex = appSecret.toString('hex'); + + // Determine base URL + const baseUrl = options.baseUrl || 'https://signing.gridpl.us'; + + // Save session data first so the SDK callbacks can use it + saveSession({ + deviceId, + baseUrl, + name, + appSecret: appSecretHex, + }); + + // Try to connect using the SDK + const isPaired = await withSpinner( + 'Connecting to device...', + async () => { + return setup({ + deviceId, + password, + name, + appSecret: appSecretHex, + baseUrl, + getStoredClient, + setStoredClient, + }); + }, + ); + + if (isPaired) { + success('Device setup complete! Already paired.'); + info('You can now use other commands to interact with your Lattice.'); + } else { + success('Device connected but not yet paired.'); + info('Run "gp pair" to complete pairing with your device.'); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Setup failed: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/sign-message.ts b/packages/cli/src/commands/sign-message.ts new file mode 100644 index 00000000..dcb4779c --- /dev/null +++ b/packages/cli/src/commands/sign-message.ts @@ -0,0 +1,167 @@ +import { readFileSync } from 'node:fs'; +import { Command } from 'commander'; +import { setup, signMessage } from 'gridplus-sdk'; +import { + error, + getStoredClient, + hasSession, + hex, + info, + output, + setStoredClient, + success, + withSpinner, +} from '../lib/index.js'; + +export const signMessageCommand = new Command('sign-message') + .description('Sign a message (personal sign or EIP-712 typed data)') + .argument( + '', + 'Message to sign (string, hex, or path to JSON file for EIP-712)', + ) + .option('--typed', 'Sign EIP-712 typed data (expects JSON)') + .option('--file', 'Read message from file') + .option('-j, --json', 'Output signature in JSON format') + .action(async (message, options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + // Initialize the SDK + await withSpinner('Connecting to device...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + let payload: string | Record; + + // Read from file if specified + if (options.file) { + try { + const fileContent = readFileSync(message, 'utf-8'); + if (options.typed) { + payload = JSON.parse(fileContent); + } else { + payload = fileContent; + } + } catch { + error(`Failed to read file: ${message}`); + process.exit(1); + } + } else if (options.typed) { + // Parse EIP-712 typed data + try { + payload = JSON.parse(message); + } catch { + error( + 'Invalid EIP-712 JSON. Expected format: { types, domain, primaryType, message }', + ); + process.exit(1); + } + + // Validate EIP-712 structure + if ( + typeof payload !== 'object' || + !payload || + !('types' in payload) || + !('domain' in payload) || + !('primaryType' in payload) || + !('message' in payload) + ) { + error( + 'Invalid EIP-712 structure. Required: types, domain, primaryType, message', + ); + process.exit(1); + } + + info('Signing EIP-712 typed data...'); + } else { + payload = message; + info('Signing personal message...'); + } + + info('Please confirm the message on your Lattice device.'); + + const signatureData = await withSpinner( + 'Waiting for signature...', + async () => { + return signMessage(payload as Parameters[0]); + }, + ); + + success('Message signed!'); + + // Format the output + if (options.json) { + const result: Record = { + sig: {}, + }; + + if (signatureData.sig) { + result.sig = { + r: Buffer.isBuffer(signatureData.sig.r) + ? `0x${signatureData.sig.r.toString('hex')}` + : signatureData.sig.r, + s: Buffer.isBuffer(signatureData.sig.s) + ? `0x${signatureData.sig.s.toString('hex')}` + : signatureData.sig.s, + v: signatureData.sig.v, + }; + } + + if (signatureData.pubkey) { + result.pubkey = Buffer.isBuffer(signatureData.pubkey) + ? `0x${signatureData.pubkey.toString('hex')}` + : signatureData.pubkey; + } + + output(result, 'json'); + } else { + if (signatureData.sig) { + const r = Buffer.isBuffer(signatureData.sig.r) + ? `0x${signatureData.sig.r.toString('hex')}` + : signatureData.sig.r; + const s = Buffer.isBuffer(signatureData.sig.s) + ? `0x${signatureData.sig.s.toString('hex')}` + : signatureData.sig.s; + + hex(String(r), 'r'); + hex(String(s), 's'); + info(`v: ${signatureData.sig.v}`); + + // Also output concatenated signature + if (signatureData.sig.v !== undefined) { + // Convert v to a number for calculation + let vNum: number; + if (typeof signatureData.sig.v === 'number') { + vNum = signatureData.sig.v; + } else if (typeof signatureData.sig.v === 'bigint') { + vNum = Number(signatureData.sig.v); + } else if (Buffer.isBuffer(signatureData.sig.v)) { + vNum = signatureData.sig.v.readUInt8(0); + } else { + vNum = Number.parseInt(String(signatureData.sig.v), 10); + } + + const vHex = + vNum < 27 + ? vNum.toString(16).padStart(2, '0') + : (vNum - 27).toString(16).padStart(2, '0'); + + const rHex = String(r).slice(2); + const sHex = String(s).slice(2); + hex(`0x${rHex}${sHex}${vHex}`, 'Signature'); + } + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Message signing failed: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/sign.ts b/packages/cli/src/commands/sign.ts new file mode 100644 index 00000000..042ce557 --- /dev/null +++ b/packages/cli/src/commands/sign.ts @@ -0,0 +1,127 @@ +import { Command } from 'commander'; +import { setup, sign } from 'gridplus-sdk'; +import type { Hex } from 'viem'; +import { + error, + getStoredClient, + hasSession, + hex, + info, + output, + setStoredClient, + success, + withSpinner, +} from '../lib/index.js'; + +export const signCommand = new Command('sign') + .description('Sign an Ethereum transaction') + .argument('', 'Transaction data (hex-encoded RLP or JSON)') + .option('-j, --json', 'Output signature in JSON format') + .option('--raw', 'Treat input as raw hex-encoded transaction') + .action(async (tx, options) => { + try { + // Check if we have a saved session + if (!hasSession()) { + error('No device configured. Run "gp setup" first.'); + process.exit(1); + } + + // Initialize the SDK + await withSpinner('Connecting to device...', async () => { + return setup({ + getStoredClient, + setStoredClient, + }); + }); + + let transaction: Hex | Record; + + // Parse the transaction + if (options.raw || tx.startsWith('0x')) { + // Raw hex transaction + transaction = (tx.startsWith('0x') ? tx : `0x${tx}`) as Hex; + info('Signing raw transaction...'); + } else { + // Try to parse as JSON + try { + const parsed = JSON.parse(tx); + transaction = parsed as Record; + info('Signing transaction...'); + } catch { + error( + 'Invalid transaction format. Provide hex-encoded transaction or valid JSON.', + ); + process.exit(1); + } + } + + info('Please confirm the transaction on your Lattice device.'); + + const signatureData = await withSpinner( + 'Waiting for signature...', + async () => { + // Cast to expected type - the SDK handles the actual type checking + return sign(transaction as Parameters[0]); + }, + ); + + success('Transaction signed!'); + + // Format the output + if (options.json) { + const result: Record = { + sig: {}, + }; + + if (signatureData.sig) { + result.sig = { + r: Buffer.isBuffer(signatureData.sig.r) + ? `0x${signatureData.sig.r.toString('hex')}` + : signatureData.sig.r, + s: Buffer.isBuffer(signatureData.sig.s) + ? `0x${signatureData.sig.s.toString('hex')}` + : signatureData.sig.s, + v: signatureData.sig.v, + }; + } + + if (signatureData.pubkey) { + result.pubkey = Buffer.isBuffer(signatureData.pubkey) + ? `0x${signatureData.pubkey.toString('hex')}` + : signatureData.pubkey; + } + + if (signatureData.tx) { + result.signedTx = Buffer.isBuffer(signatureData.tx) + ? `0x${signatureData.tx.toString('hex')}` + : signatureData.tx; + } + + output(result, 'json'); + } else { + if (signatureData.tx) { + const signedTx = Buffer.isBuffer(signatureData.tx) + ? `0x${signatureData.tx.toString('hex')}` + : signatureData.tx; + hex(String(signedTx), 'Signed Transaction'); + } + + if (signatureData.sig) { + const r = Buffer.isBuffer(signatureData.sig.r) + ? `0x${signatureData.sig.r.toString('hex')}` + : signatureData.sig.r; + const s = Buffer.isBuffer(signatureData.sig.s) + ? `0x${signatureData.sig.s.toString('hex')}` + : signatureData.sig.s; + + hex(String(r), 'r'); + hex(String(s), 's'); + info(`v: ${signatureData.sig.v}`); + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Signing failed: ${message}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c66d5bad..bb185823 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,42 +1,36 @@ import { Command } from 'commander'; +import { + addressCommand, + blsChangeCommand, + connectCommand, + depositDataCommand, + pairCommand, + pubkeyCommand, + setupCommand, + signCommand, + signMessageCommand, +} from './commands/index.js'; export const program = new Command() .name('gridplus') .description('CLI for GridPlus SDK - interact with Lattice hardware wallets') .version('0.1.0'); -// Placeholder commands - implementations in future phases -program - .command('setup') - .description('Interactive device setup and pairing') - .action(() => console.log('Not yet implemented')); +// Register device management commands +program.addCommand(setupCommand); +program.addCommand(connectCommand); +program.addCommand(pairCommand); -program - .command('address [path]') - .description('Get ETH address at derivation path') - .option('-t, --type ', 'Address type', 'eth') - .action(() => console.log('Not yet implemented')); +// Register address/key commands +program.addCommand(addressCommand); +program.addCommand(pubkeyCommand); -program - .command('pubkey [path]') - .description('Get public key at derivation path') - .option( - '-t, --type ', - 'Key type: secp256k1|ed25519|bls12_381', - 'secp256k1', - ) - .action(() => console.log('Not yet implemented')); +// Register signing commands +program.addCommand(signCommand); +program.addCommand(signMessageCommand); -const eth2 = program - .command('eth2') - .description('Ethereum 2.0 staking commands'); - -eth2 - .command('deposit-data') - .description('Export validator deposit data and keystores') - .action(() => console.log('Not yet implemented')); - -eth2 - .command('bls-change') - .description('Change BLS withdrawal credentials to ETH1 address') - .action(() => console.log('Not yet implemented')); +// Register ETH2 commands as a subcommand group +const eth2 = new Command('eth2').description('Ethereum 2.0 staking commands'); +eth2.addCommand(depositDataCommand); +eth2.addCommand(blsChangeCommand); +program.addCommand(eth2); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts new file mode 100644 index 00000000..d50f7dec --- /dev/null +++ b/packages/cli/src/lib/config.ts @@ -0,0 +1,93 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { getSessionDirectory } from './session.js'; + +/** + * CLI configuration options + */ +export interface CliConfig { + /** Default output format: 'json' or 'human' */ + outputFormat: 'json' | 'human'; + /** Default network for addresses */ + defaultNetwork: 'mainnet' | 'testnet'; + /** Default base URL for Lattice device */ + defaultBaseUrl: string; + /** Default app name for pairing */ + defaultAppName: string; +} + +const DEFAULT_CONFIG: CliConfig = { + outputFormat: 'human', + defaultNetwork: 'mainnet', + defaultBaseUrl: 'https://signing.gridpl.us', + defaultAppName: 'GridPlus CLI', +}; + +/** + * Gets the config file path + */ +function getConfigFilePath(): string { + return join(getSessionDirectory(), 'config.json'); +} + +/** + * Loads CLI configuration from disk + */ +export function loadConfig(): CliConfig { + try { + const configPath = getConfigFilePath(); + if (!existsSync(configPath)) { + return { ...DEFAULT_CONFIG }; + } + const data = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(data) as Partial; + return { ...DEFAULT_CONFIG, ...parsed }; + } catch { + return { ...DEFAULT_CONFIG }; + } +} + +/** + * Saves CLI configuration to disk + */ +export function saveConfig(config: Partial): void { + const configPath = getConfigFilePath(); + const currentConfig = loadConfig(); + const newConfig = { ...currentConfig, ...config }; + writeFileSync(configPath, JSON.stringify(newConfig, null, 2)); +} + +/** + * Gets a specific config value + */ +export function getConfigValue( + key: K, +): CliConfig[K] { + const config = loadConfig(); + return config[key]; +} + +/** + * Sets a specific config value + */ +export function setConfigValue( + key: K, + value: CliConfig[K], +): void { + saveConfig({ [key]: value }); +} + +/** + * Resets configuration to defaults + */ +export function resetConfig(): void { + const configPath = getConfigFilePath(); + writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2)); +} + +/** + * Gets the default config + */ +export function getDefaultConfig(): CliConfig { + return { ...DEFAULT_CONFIG }; +} diff --git a/packages/cli/src/lib/index.ts b/packages/cli/src/lib/index.ts new file mode 100644 index 00000000..517157d5 --- /dev/null +++ b/packages/cli/src/lib/index.ts @@ -0,0 +1,4 @@ +export * from './session.js'; +export * from './config.js'; +export * from './output.js'; +export * from './prompts.js'; diff --git a/packages/cli/src/lib/output.ts b/packages/cli/src/lib/output.ts new file mode 100644 index 00000000..1dea706e --- /dev/null +++ b/packages/cli/src/lib/output.ts @@ -0,0 +1,158 @@ +import chalk from 'chalk'; +import { loadConfig } from './config.js'; + +export type OutputFormat = 'json' | 'human'; + +/** + * Outputs data in the specified format + */ +export function output(data: unknown, format?: OutputFormat): void { + const outputFormat = format ?? loadConfig().outputFormat; + + if (outputFormat === 'json') { + console.log(JSON.stringify(data, null, 2)); + } else { + if (typeof data === 'string') { + console.log(data); + } else if (Array.isArray(data)) { + data.forEach((item, index) => { + if (typeof item === 'object' && item !== null) { + console.log(chalk.bold(`[${index}]`)); + formatObject(item as Record, ' '); + } else { + console.log(`[${index}] ${String(item)}`); + } + }); + } else if (typeof data === 'object' && data !== null) { + formatObject(data as Record); + } else { + console.log(String(data)); + } + } +} + +/** + * Formats an object for human-readable output + */ +function formatObject(obj: Record, indent = ''): void { + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + console.log(`${indent}${chalk.cyan(key)}:`); + formatObject(value as Record, `${indent} `); + } else if (Array.isArray(value)) { + console.log(`${indent}${chalk.cyan(key)}:`); + value.forEach((item, index) => { + if (typeof item === 'object' && item !== null) { + console.log(`${indent} [${index}]`); + formatObject(item as Record, `${indent} `); + } else { + console.log(`${indent} [${index}] ${String(item)}`); + } + }); + } else { + console.log(`${indent}${chalk.cyan(key)}: ${chalk.white(String(value))}`); + } + } +} + +/** + * Outputs a success message + */ +export function success(message: string): void { + console.log(`${chalk.green('✓')} ${message}`); +} + +/** + * Outputs an error message + */ +export function error(message: string): void { + console.error(`${chalk.red('✗')} ${message}`); +} + +/** + * Outputs a warning message + */ +export function warn(message: string): void { + console.log(`${chalk.yellow('⚠')} ${message}`); +} + +/** + * Outputs an info message + */ +export function info(message: string): void { + console.log(`${chalk.blue('ℹ')} ${message}`); +} + +/** + * Outputs a debug message (only in verbose mode) + */ +export function debug(message: string): void { + if (process.env.DEBUG || process.env.VERBOSE) { + console.log(`${chalk.gray('[debug]')} ${message}`); + } +} + +/** + * Outputs a table of data + */ +export function table( + headers: string[], + rows: string[][], + format?: OutputFormat, +): void { + const outputFormat = format ?? loadConfig().outputFormat; + + if (outputFormat === 'json') { + const data = rows.map((row) => { + const obj: Record = {}; + headers.forEach((header, index) => { + obj[header] = row[index] ?? ''; + }); + return obj; + }); + console.log(JSON.stringify(data, null, 2)); + } else { + // Calculate column widths + const widths = headers.map((h, i) => + Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)), + ); + + // Print header + const headerRow = headers + .map((h, i) => chalk.bold(h.padEnd(widths[i] ?? 0))) + .join(' '); + console.log(headerRow); + console.log(widths.map((w) => '-'.repeat(w)).join(' ')); + + // Print rows + for (const row of rows) { + const rowStr = row + .map((cell, i) => (cell ?? '').padEnd(widths[i] ?? 0)) + .join(' '); + console.log(rowStr); + } + } +} + +/** + * Outputs an address with optional label + */ +export function address(addr: string, label?: string): void { + if (label) { + console.log(`${chalk.cyan(label)}: ${chalk.white(addr)}`); + } else { + console.log(chalk.white(addr)); + } +} + +/** + * Outputs a hex value + */ +export function hex(value: string, label?: string): void { + const formattedValue = value.startsWith('0x') ? value : `0x${value}`; + if (label) { + console.log(`${chalk.cyan(label)}: ${chalk.yellow(formattedValue)}`); + } else { + console.log(chalk.yellow(formattedValue)); + } +} diff --git a/packages/cli/src/lib/prompts.ts b/packages/cli/src/lib/prompts.ts new file mode 100644 index 00000000..c9df0bdf --- /dev/null +++ b/packages/cli/src/lib/prompts.ts @@ -0,0 +1,253 @@ +import { confirm, input, password, select } from '@inquirer/prompts'; +import ora, { type Ora } from 'ora'; +import { loadConfig } from './config.js'; + +/** + * Prompts for device URL/IP + */ +export async function promptDeviceUrl(): Promise { + const config = loadConfig(); + const deviceUrl = await input({ + message: 'Enter your Lattice device URL or IP:', + default: config.defaultBaseUrl, + validate: (value) => { + if (!value.trim()) { + return 'Device URL is required'; + } + return true; + }, + }); + return deviceUrl.trim(); +} + +/** + * Prompts for device ID + */ +export async function promptDeviceId(): Promise { + const deviceId = await input({ + message: 'Enter your Lattice device ID:', + validate: (value) => { + if (!value.trim()) { + return 'Device ID is required'; + } + return true; + }, + }); + return deviceId.trim(); +} + +/** + * Prompts for app name + */ +export async function promptAppName(): Promise { + const config = loadConfig(); + const appName = await input({ + message: 'Enter an app name (shown on device):', + default: config.defaultAppName, + }); + return appName.trim() || config.defaultAppName; +} + +/** + * Prompts for password + */ +export async function promptPassword(): Promise { + const pwd = await password({ + message: 'Enter your Lattice password:', + validate: (value) => { + if (!value.trim()) { + return 'Password is required'; + } + return true; + }, + }); + return pwd; +} + +/** + * Prompts for pairing code + */ +export async function promptPairingCode(): Promise { + const code = await input({ + message: 'Enter the pairing code from your Lattice device:', + validate: (value) => { + if (!value.trim()) { + return 'Pairing code is required'; + } + return true; + }, + }); + return code.trim(); +} + +/** + * Prompts for confirmation + */ +export async function promptConfirm( + message: string, + defaultValue = false, +): Promise { + return confirm({ + message, + default: defaultValue, + }); +} + +/** + * Address type options for prompts + */ +export type AddressTypeOption = + | 'eth' + | 'btc-legacy' + | 'btc-segwit' + | 'btc-wrapped-segwit' + | 'solana'; + +/** + * Prompts for address type + */ +export async function promptAddressType(): Promise { + const choice = await select({ + message: 'Select address type:', + choices: [ + { value: 'eth', name: 'Ethereum (ETH)' }, + { value: 'btc-legacy', name: 'Bitcoin Legacy (P2PKH)' }, + { value: 'btc-segwit', name: 'Bitcoin Native SegWit (P2WPKH)' }, + { + value: 'btc-wrapped-segwit', + name: 'Bitcoin Wrapped SegWit (P2SH-P2WPKH)', + }, + { value: 'solana', name: 'Solana (SOL)' }, + ], + }); + return choice as AddressTypeOption; +} + +/** + * Public key type options for prompts + */ +export type PubkeyTypeOption = 'secp256k1' | 'ed25519' | 'bls12_381_g1'; + +/** + * Prompts for public key type + */ +export async function promptPubkeyType(): Promise { + const choice = await select({ + message: 'Select public key type:', + choices: [ + { value: 'secp256k1', name: 'secp256k1 (Ethereum, Bitcoin)' }, + { value: 'ed25519', name: 'ed25519 (Solana)' }, + { value: 'bls12_381_g1', name: 'BLS12-381-G1 (ETH2 Validators)' }, + ], + }); + return choice as PubkeyTypeOption; +} + +/** + * Creates a spinner with a message + */ +export function spinner(message: string): Ora { + return ora(message).start(); +} + +/** + * Runs an async operation with a spinner + */ +export async function withSpinner( + message: string, + operation: () => Promise, +): Promise { + const spin = ora(message).start(); + try { + const result = await operation(); + spin.succeed(); + return result; + } catch (err) { + spin.fail(); + throw err; + } +} + +/** + * Prompts for derivation path + */ +export async function promptDerivationPath( + defaultPath: string, +): Promise { + const path = await input({ + message: 'Enter derivation path:', + default: defaultPath, + validate: (value) => { + if (!value.trim()) { + return 'Derivation path is required'; + } + // Basic validation - should start with m/ or just be a path + if ( + !value.startsWith('m/') && + !value.startsWith("44'") && + !/^\d/.test(value) + ) { + return 'Invalid derivation path format'; + } + return true; + }, + }); + return path.trim(); +} + +/** + * Prompts for number of addresses + */ +export async function promptAddressCount(defaultCount = 1): Promise { + const count = await input({ + message: 'How many addresses to fetch?', + default: String(defaultCount), + validate: (value) => { + const num = Number.parseInt(value, 10); + if (Number.isNaN(num) || num < 1) { + return 'Must be a positive number'; + } + if (num > 10) { + return 'Maximum 10 addresses at a time'; + } + return true; + }, + }); + return Number.parseInt(count, 10); +} + +/** + * Prompts for ETH1 withdrawal address + */ +export async function promptWithdrawalAddress(): Promise { + const addr = await input({ + message: 'Enter ETH1 withdrawal address (0x...):', + validate: (value) => { + if (!value.trim()) { + return 'Withdrawal address is required'; + } + if (!/^0x[a-fA-F0-9]{40}$/.test(value)) { + return 'Invalid Ethereum address format'; + } + return true; + }, + }); + return addr.trim(); +} + +/** + * Prompts for validator index + */ +export async function promptValidatorIndex(): Promise { + const index = await input({ + message: 'Enter validator index:', + validate: (value) => { + const num = Number.parseInt(value, 10); + if (Number.isNaN(num) || num < 0) { + return 'Must be a non-negative number'; + } + return true; + }, + }); + return Number.parseInt(index, 10); +} diff --git a/packages/cli/src/lib/session.ts b/packages/cli/src/lib/session.ts new file mode 100644 index 00000000..7cdb244f --- /dev/null +++ b/packages/cli/src/lib/session.ts @@ -0,0 +1,113 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +/** + * Session data stored in ~/.gridplus/session.json + */ +export interface SessionData { + deviceId: string; + baseUrl: string; + name: string; + appSecret: string; + clientData?: string; +} + +const SESSION_DIR = join(homedir(), '.gridplus'); +const SESSION_FILE = join(SESSION_DIR, 'session.json'); + +/** + * Ensures the ~/.gridplus directory exists + */ +function ensureSessionDirectory(): void { + if (!existsSync(SESSION_DIR)) { + mkdirSync(SESSION_DIR, { recursive: true }); + } +} + +/** + * Loads session data from disk + */ +export function loadSession(): SessionData | null { + try { + if (!existsSync(SESSION_FILE)) { + return null; + } + const data = readFileSync(SESSION_FILE, 'utf-8'); + return JSON.parse(data) as SessionData; + } catch { + return null; + } +} + +/** + * Saves session data to disk + */ +export function saveSession(session: SessionData): void { + ensureSessionDirectory(); + writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2)); +} + +/** + * Clears session data from disk + */ +export function clearSession(): void { + if (existsSync(SESSION_FILE)) { + writeFileSync(SESSION_FILE, '{}'); + } +} + +/** + * Checks if a session exists + */ +export function hasSession(): boolean { + const session = loadSession(); + return session !== null && Boolean(session.deviceId); +} + +/** + * Gets the stored client data callback for SDK setup + * This is used with the SDK's getStoredClient callback pattern + */ +export function getStoredClient(): Promise { + const session = loadSession(); + return Promise.resolve(session?.clientData ?? ''); +} + +/** + * Sets the stored client data callback for SDK setup + * This is used with the SDK's setStoredClient callback pattern + */ +export function setStoredClient(clientData: string | null): Promise { + const session = loadSession(); + if (session && clientData) { + session.clientData = clientData; + saveSession(session); + } + return Promise.resolve(); +} + +/** + * Updates the client data in the current session + */ +export function updateSessionClientData(clientData: string): void { + const session = loadSession(); + if (session) { + session.clientData = clientData; + saveSession(session); + } +} + +/** + * Gets the session directory path + */ +export function getSessionDirectory(): string { + return SESSION_DIR; +} + +/** + * Gets the session file path + */ +export function getSessionFilePath(): string { + return SESSION_FILE; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c918f21..a380a4e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@gridplus/btc': specifier: workspace:* version: link:../btc + '@inquirer/prompts': + specifier: ^7.0.0 + version: 7.10.1(@types/node@24.10.4) chalk: specifier: ^5.0.0 version: 5.6.2 @@ -54,9 +57,6 @@ importers: gridplus-sdk: specifier: workspace:* version: link:../sdk - inquirer: - specifier: ^12.0.0 - version: 12.11.1(@types/node@24.10.4) lattice-eth2-utils: specifier: ^0.5.1 version: 0.5.1 @@ -5118,15 +5118,6 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - inquirer@12.11.1: - resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -7159,19 +7150,12 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} - run-async@4.0.6: - resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -14420,18 +14404,6 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@12.11.1(@types/node@24.10.4): - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/prompts': 7.10.1(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) - mute-stream: 2.0.0 - run-async: 4.0.6 - rxjs: 7.8.2 - optionalDependencies: - '@types/node': 24.10.4 - internmap@1.0.1: {} internmap@2.0.3: {} @@ -17148,18 +17120,12 @@ snapshots: run-applescript@7.1.0: {} - run-async@4.0.6: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 rw@1.3.3: {} - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - sade@1.8.1: dependencies: mri: 1.2.0 From fd504b45953a5badab5a4b7d8a90495fef8eae9e Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:11:57 -0500 Subject: [PATCH 3/7] fix(cli): address type and pubkey output bugs - Fix ETH multi-address fetch (use fetchAddress loop) - Strip "m/" prefix from derivation paths (SDK doesn't handle it) - Convert pubkey Buffer responses to hex strings - Convert Solana address Buffer responses to base58 - Add bs58 dependency for Solana encoding --- packages/cli/package.json | 20 +++++++---- packages/cli/src/commands/address.ts | 53 +++++++++++++++++----------- packages/cli/src/commands/pubkey.ts | 39 ++++++++++++++++---- pnpm-lock.yaml | 14 ++++++++ 4 files changed, 91 insertions(+), 35 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 61501088..935ffe7b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,23 +13,29 @@ "lint:fix": "biome check --write src bin", "typecheck": "tsc --noEmit" }, - "files": ["dist"], + "files": [ + "dist" + ], "dependencies": { - "gridplus-sdk": "workspace:*", "@gridplus/btc": "workspace:*", "@inquirer/prompts": "^7.0.0", - "lattice-eth2-utils": "^0.5.1", - "commander": "^12.0.0", + "bs58": "^6.0.0", "chalk": "^5.0.0", - "ora": "^8.0.0", - "dotenv": "^16.0.0" + "commander": "^12.0.0", + "dotenv": "^16.0.0", + "gridplus-sdk": "workspace:*", + "lattice-eth2-utils": "^0.5.1", + "ora": "^8.0.0" }, "devDependencies": { "@biomejs/biome": "^1.9.0", + "@types/bs58": "^5.0.0", "@types/node": "^24.10.4", "tsup": "^8.5.0", "typescript": "^5.9.2" }, "license": "MIT", - "engines": { "node": ">=20" } + "engines": { + "node": ">=20" + } } diff --git a/packages/cli/src/commands/address.ts b/packages/cli/src/commands/address.ts index daf02b58..5d881586 100644 --- a/packages/cli/src/commands/address.ts +++ b/packages/cli/src/commands/address.ts @@ -1,4 +1,5 @@ import { Command } from 'commander'; +import bs58 from 'bs58'; import { fetchAddress, fetchAddressesByDerivationPath, @@ -59,11 +60,15 @@ export const addressCommand = new Command('address') let addresses: string[]; + // Strip "m/" prefix if present - SDK doesn't handle it + const cleanPath = (p: string) => + p.startsWith('m/') ? p.slice(2) : p.startsWith('m') ? p.slice(1) : p; + // If a specific derivation path is provided, use it if (path && typeof path === 'string' && !Number.isFinite(Number(path))) { info(`Fetching address at path: ${path}`); addresses = await withSpinner('Fetching addresses...', async () => { - return fetchAddressesByDerivationPath(path, { + return fetchAddressesByDerivationPath(cleanPath(path), { n: count, startPathIndex: startIndex, }); @@ -106,33 +111,39 @@ export const addressCommand = new Command('address') case 'solana': info('Fetching Solana addresses...'); addresses = await withSpinner('Fetching addresses...', async () => { - return fetchSolanaAddresses({ + const results = await fetchSolanaAddresses({ n: count, startPathIndex: index, }); + // Convert Buffer responses to base58 Solana addresses + return results.map((addr: unknown) => { + if (typeof addr === 'string') return addr; + if (Buffer.isBuffer(addr)) return bs58.encode(addr); + if ( + addr && + typeof addr === 'object' && + 'type' in addr && + (addr as { type: string }).type === 'Buffer' && + 'data' in addr + ) { + return bs58.encode( + Buffer.from((addr as { data: number[] }).data), + ); + } + return String(addr); + }); }); break; default: info('Fetching Ethereum addresses...'); - if (count === 1) { - const addr = await withSpinner( - 'Fetching address...', - async () => { - return fetchAddress(index); - }, - ); - addresses = [addr]; - } else { - addresses = await withSpinner( - 'Fetching addresses...', - async () => { - return fetchAddressesByDerivationPath("m/44'/60'/0'/0/x", { - n: count, - startPathIndex: index, - }); - }, - ); - } + addresses = await withSpinner('Fetching addresses...', async () => { + const results: string[] = []; + for (let i = 0; i < count; i++) { + const addr = await fetchAddress(index + i); + results.push(addr); + } + return results; + }); break; } } diff --git a/packages/cli/src/commands/pubkey.ts b/packages/cli/src/commands/pubkey.ts index e21cf8c8..cbe07708 100644 --- a/packages/cli/src/commands/pubkey.ts +++ b/packages/cli/src/commands/pubkey.ts @@ -70,30 +70,55 @@ export const pubkeyCommand = new Command('pubkey') const derivationPath = path || DEFAULT_PATHS[keyType]; const flag = PUBKEY_FLAGS[keyType]; + // Strip "m/" prefix if present - SDK doesn't handle it + const cleanPath = (p: string) => + p.startsWith('m/') ? p.slice(2) : p.startsWith('m') ? p.slice(1) : p; + info(`Fetching ${keyType} public key(s) at path: ${derivationPath}`); + // Convert Buffer to hex string + const toHex = (val: unknown): string => { + if (typeof val === 'string') return val; + if (Buffer.isBuffer(val)) return `0x${val.toString('hex')}`; + if ( + val && + typeof val === 'object' && + 'type' in val && + (val as { type: string }).type === 'Buffer' && + 'data' in val + ) { + return `0x${Buffer.from((val as { data: number[] }).data).toString('hex')}`; + } + return String(val); + }; + const pubkeys = await withSpinner('Fetching public keys...', async () => { + let results: unknown[]; + // For multiple keys, we need to handle the path with an index if (count > 1) { // Replace the last component with a wildcard if fetching multiple - const pathParts = derivationPath.split('/'); + const pathParts = cleanPath(derivationPath).split('/'); const lastPart = pathParts[pathParts.length - 1]; if (!lastPart?.toLowerCase().includes('x')) { pathParts[pathParts.length - 1] = 'x'; } const wildcardPath = pathParts.join('/'); - return fetchAddressesByDerivationPath(wildcardPath, { + results = await fetchAddressesByDerivationPath(wildcardPath, { n: count, startPathIndex: startIndex, flag, }); + } else { + // Single key + results = await fetchAddressesByDerivationPath(cleanPath(derivationPath), { + n: 1, + flag, + }); } - // Single key - return fetchAddressesByDerivationPath(derivationPath, { - n: 1, - flag, - }); + // Convert Buffer responses to hex strings + return results.map(toHex); }); // Output results diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a380a4e6..1ad88830 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: '@inquirer/prompts': specifier: ^7.0.0 version: 7.10.1(@types/node@24.10.4) + bs58: + specifier: ^6.0.0 + version: 6.0.0 chalk: specifier: ^5.0.0 version: 5.6.2 @@ -67,6 +70,9 @@ importers: '@biomejs/biome': specifier: ^1.9.0 version: 1.9.4 + '@types/bs58': + specifier: ^5.0.0 + version: 5.0.0 '@types/node': specifier: ^24.10.4 version: 24.10.4 @@ -2739,6 +2745,10 @@ packages: '@types/bonjour@3.5.13': resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + '@types/bs58@5.0.0': + resolution: {integrity: sha512-cAw/jKBzo98m6Xz1X5ETqymWfIMbXbu6nK15W4LQYjeHJkVqSmM5PO8Bd9KVHQJ/F4rHcSso9LcjtgCW6TGu2w==} + deprecated: This is a stub types definition. bs58 provides its own type definitions, so you do not need this installed. + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -11571,6 +11581,10 @@ snapshots: dependencies: '@types/node': 24.10.4 + '@types/bs58@5.0.0': + dependencies: + bs58: 6.0.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 From 3ce4b54458b217e39604e209c9eb997ffec6d87a Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:25:30 -0500 Subject: [PATCH 4/7] feat(cli): add lattice-simulator support - Add `gridplus simulator setup` - one-shot setup + pair - Add `gridplus simulator info` - show simulator defaults - Add `--simulator` flag to setup and pair commands - Add SIMULATOR_DEFAULTS config (localhost:3000, SD0001, etc.) - Track `isSimulator` in session for auto-detection Simulator defaults match CI workflow: - URL: http://127.0.0.1:3000 - Device ID: SD0001 - Password/Pairing: 12345678 --- packages/cli/src/commands/index.ts | 1 + packages/cli/src/commands/pair.ts | 30 +++++-- packages/cli/src/commands/pubkey.ts | 11 ++- packages/cli/src/commands/setup.ts | 41 +++++++-- packages/cli/src/commands/simulator.ts | 115 +++++++++++++++++++++++++ packages/cli/src/index.ts | 2 + packages/cli/src/lib/config.ts | 14 +++ packages/cli/src/lib/session.ts | 2 + 8 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 packages/cli/src/commands/simulator.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ab50bd58..14d2aeae 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -5,4 +5,5 @@ export { pubkeyCommand } from './pubkey.js'; export { setupCommand } from './setup.js'; export { signCommand } from './sign.js'; export { signMessageCommand } from './sign-message.js'; +export { simulatorCommand } from './simulator.js'; export { blsChangeCommand, depositDataCommand } from './eth2/index.js'; diff --git a/packages/cli/src/commands/pair.ts b/packages/cli/src/commands/pair.ts index 61b4d148..049785db 100644 --- a/packages/cli/src/commands/pair.ts +++ b/packages/cli/src/commands/pair.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { pair, setup } from 'gridplus-sdk'; import { + SIMULATOR_DEFAULTS, error, getStoredClient, hasSession, @@ -15,7 +16,11 @@ import { export const pairCommand = new Command('pair') .description('Pair with a Lattice device using a pairing code') .argument('[code]', 'Pairing code from device (or will prompt)') - .action(async (code) => { + .option( + '-s, --simulator', + 'Use simulator pairing secret (auto-pair with simulator)', + ) + .action(async (code, options) => { try { // Check if we have a saved session if (!hasSession()) { @@ -29,11 +34,20 @@ export const pairCommand = new Command('pair') process.exit(1); } - // Get pairing code - const pairingCode = code || (await promptPairingCode()); + const isSimulator = + options.simulator === true || session.isSimulator === true; - info('Initiating pairing with your Lattice device...'); - info('Please confirm the pairing request on your device screen.'); + // Get pairing code (use simulator default if --simulator flag or session is simulator) + const pairingCode = isSimulator + ? SIMULATOR_DEFAULTS.pairingSecret + : code || (await promptPairingCode()); + + if (isSimulator) { + info('Initiating pairing with lattice-simulator...'); + } else { + info('Initiating pairing with your Lattice device...'); + info('Please confirm the pairing request on your device screen.'); + } // Initialize the SDK with stored credentials await withSpinner('Setting up connection...', async () => { @@ -50,7 +64,11 @@ export const pairCommand = new Command('pair') if (isPaired) { success('Pairing successful!'); - info('Your CLI is now paired with your Lattice device.'); + if (isSimulator) { + info('Your CLI is now paired with the simulator.'); + } else { + info('Your CLI is now paired with your Lattice device.'); + } info('You can now use commands like "gp address" and "gp sign".'); } else { error('Pairing failed. Please try again.'); diff --git a/packages/cli/src/commands/pubkey.ts b/packages/cli/src/commands/pubkey.ts index cbe07708..bbc7905c 100644 --- a/packages/cli/src/commands/pubkey.ts +++ b/packages/cli/src/commands/pubkey.ts @@ -111,10 +111,13 @@ export const pubkeyCommand = new Command('pubkey') }); } else { // Single key - results = await fetchAddressesByDerivationPath(cleanPath(derivationPath), { - n: 1, - flag, - }); + results = await fetchAddressesByDerivationPath( + cleanPath(derivationPath), + { + n: 1, + flag, + }, + ); } // Convert Buffer responses to hex strings diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 39b0c8bf..37648954 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { Utils, setup } from 'gridplus-sdk'; import { + SIMULATOR_DEFAULTS, error, getStoredClient, info, @@ -21,25 +22,42 @@ export const setupCommand = new Command('setup') .option('-p, --password ', 'Device password (skip prompt)') .option('-n, --name ', 'App name shown on device') .option('--base-url ', 'Custom base URL for Lattice relay') + .option( + '-s, --simulator', + 'Use simulator defaults (localhost:3000, SD0001, etc.)', + ) .action(async (options) => { try { - info('Setting up GridPlus Lattice connection...'); + const isSimulator = options.simulator === true; + + if (isSimulator) { + info('Setting up connection to lattice-simulator...'); + } else { + info('Setting up GridPlus Lattice connection...'); + } - // Get device ID - const deviceId = options.deviceId || (await promptDeviceId()); + // Get device ID (use simulator default if --simulator flag) + const deviceId = isSimulator + ? SIMULATOR_DEFAULTS.deviceId + : options.deviceId || (await promptDeviceId()); - // Get password - const password = options.password || (await promptPassword()); + // Get password (use simulator default if --simulator flag) + const password = isSimulator + ? SIMULATOR_DEFAULTS.password + : options.password || (await promptPassword()); // Get app name - const name = options.name || (await promptAppName()); + const name = + options.name || (isSimulator ? 'GridPlus CLI' : await promptAppName()); // Generate app secret from credentials const appSecret = Utils.generateAppSecret(deviceId, password, name); const appSecretHex = appSecret.toString('hex'); - // Determine base URL - const baseUrl = options.baseUrl || 'https://signing.gridpl.us'; + // Determine base URL (use simulator default if --simulator flag) + const baseUrl = isSimulator + ? SIMULATOR_DEFAULTS.baseUrl + : options.baseUrl || 'https://signing.gridpl.us'; // Save session data first so the SDK callbacks can use it saveSession({ @@ -47,6 +65,7 @@ export const setupCommand = new Command('setup') baseUrl, name, appSecret: appSecretHex, + isSimulator, }); // Try to connect using the SDK @@ -70,7 +89,11 @@ export const setupCommand = new Command('setup') info('You can now use other commands to interact with your Lattice.'); } else { success('Device connected but not yet paired.'); - info('Run "gp pair" to complete pairing with your device.'); + if (isSimulator) { + info('Run "gp pair --simulator" to auto-pair with the simulator.'); + } else { + info('Run "gp pair" to complete pairing with your device.'); + } } } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/packages/cli/src/commands/simulator.ts b/packages/cli/src/commands/simulator.ts new file mode 100644 index 00000000..ca68452f --- /dev/null +++ b/packages/cli/src/commands/simulator.ts @@ -0,0 +1,115 @@ +import { Command } from 'commander'; +import { Utils, pair, setup } from 'gridplus-sdk'; +import { + SIMULATOR_DEFAULTS, + error, + getStoredClient, + info, + saveSession, + setStoredClient, + success, + withSpinner, +} from '../lib/index.js'; + +/** + * One-shot setup command for simulator - combines setup + pair + */ +const simulatorSetupCommand = new Command('setup') + .description('One-shot setup and pair with lattice-simulator') + .option('-n, --name ', 'App name shown on device', 'GridPlus CLI') + .action(async (options) => { + try { + info('Setting up connection to lattice-simulator...'); + info(` URL: ${SIMULATOR_DEFAULTS.baseUrl}`); + info(` Device ID: ${SIMULATOR_DEFAULTS.deviceId}`); + + const deviceId = SIMULATOR_DEFAULTS.deviceId; + const password = SIMULATOR_DEFAULTS.password; + const name = options.name; + const baseUrl = SIMULATOR_DEFAULTS.baseUrl; + + // Generate app secret from credentials + const appSecret = Utils.generateAppSecret(deviceId, password, name); + const appSecretHex = appSecret.toString('hex'); + + // Save session data first so the SDK callbacks can use it + saveSession({ + deviceId, + baseUrl, + name, + appSecret: appSecretHex, + isSimulator: true, + }); + + // Try to connect using the SDK + const isAlreadyPaired = await withSpinner( + 'Connecting to simulator...', + async () => { + return setup({ + deviceId, + password, + name, + appSecret: appSecretHex, + baseUrl, + getStoredClient, + setStoredClient, + }); + }, + ); + + if (isAlreadyPaired) { + success('Simulator setup complete! Already paired.'); + info('You can now use other commands to interact with the simulator.'); + return; + } + + // Perform pairing with simulator default secret + const isPaired = await withSpinner( + 'Pairing with simulator...', + async () => { + return pair(SIMULATOR_DEFAULTS.pairingSecret); + }, + ); + + if (isPaired) { + success('Simulator setup and pairing complete!'); + info('You can now use commands like "gp address" and "gp sign".'); + } else { + error('Pairing failed. Is the simulator running?'); + process.exit(1); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + error(`Simulator setup failed: ${message}`); + info(''); + info('Make sure the lattice-simulator is running:'); + info(' docker run -p 3000:3000 gridplus/lattice-simulator'); + process.exit(1); + } + }); + +/** + * Info command showing simulator configuration + */ +const simulatorInfoCommand = new Command('info') + .description('Show simulator default configuration') + .action(() => { + info('Lattice Simulator Defaults:'); + info(` URL: ${SIMULATOR_DEFAULTS.baseUrl}`); + info(` Device ID: ${SIMULATOR_DEFAULTS.deviceId}`); + info(` Password: ${SIMULATOR_DEFAULTS.password}`); + info(` Pairing Secret: ${SIMULATOR_DEFAULTS.pairingSecret}`); + info(''); + info('To start the simulator:'); + info(' docker run -p 3000:3000 gridplus/lattice-simulator'); + }); + +/** + * Parent simulator command group + */ +export const simulatorCommand = new Command('simulator') + .alias('sim') + .description('Commands for working with lattice-simulator'); + +simulatorCommand.addCommand(simulatorSetupCommand); +simulatorCommand.addCommand(simulatorInfoCommand); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index bb185823..05ddcfbb 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -9,6 +9,7 @@ import { setupCommand, signCommand, signMessageCommand, + simulatorCommand, } from './commands/index.js'; export const program = new Command() @@ -20,6 +21,7 @@ export const program = new Command() program.addCommand(setupCommand); program.addCommand(connectCommand); program.addCommand(pairCommand); +program.addCommand(simulatorCommand); // Register address/key commands program.addCommand(addressCommand); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index d50f7dec..d74f56ac 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -16,6 +16,20 @@ export interface CliConfig { defaultAppName: string; } +/** + * Simulator configuration defaults + */ +export const SIMULATOR_DEFAULTS = { + /** Default URL for lattice-simulator */ + baseUrl: 'http://127.0.0.1:3000', + /** Default device ID for simulator */ + deviceId: 'SD0001', + /** Default password for simulator */ + password: '12345678', + /** Default pairing secret for simulator */ + pairingSecret: '12345678', +} as const; + const DEFAULT_CONFIG: CliConfig = { outputFormat: 'human', defaultNetwork: 'mainnet', diff --git a/packages/cli/src/lib/session.ts b/packages/cli/src/lib/session.ts index 7cdb244f..55b5e5e4 100644 --- a/packages/cli/src/lib/session.ts +++ b/packages/cli/src/lib/session.ts @@ -11,6 +11,8 @@ export interface SessionData { name: string; appSecret: string; clientData?: string; + /** Flag indicating if this session is connected to a simulator */ + isSimulator?: boolean; } const SESSION_DIR = join(homedir(), '.gridplus'); From 9d00ab9c054e7e1c681f25722a41b58c070b7750 Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:49:56 -0500 Subject: [PATCH 5/7] ci: add CLI smoke tests with simulator Run CLI tests in parallel with SDK e2e tests: - gridplus simulator setup - gridplus address (ETH, BTC, Solana) - gridplus pubkey - JSON output verification CLI tests run concurrently while e2e tests execute. --- .github/workflows/build-test.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 247cfe2e..015220c0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -99,7 +99,7 @@ jobs: sleep 1 done - - name: Run SDK e2e tests with simulator + - name: Run SDK e2e tests and CLI tests with simulator if: env.INTERNAL_EVENT == 'true' working-directory: ${{ github.workspace }} env: @@ -111,7 +111,26 @@ jobs: PAIRING_SECRET: '12345678' ENC_PW: '12345678' APP_NAME: 'lattice-manager' - run: pnpm run e2e -- --reporter=basic + run: | + # Run SDK e2e tests and CLI tests in parallel + pnpm run e2e -- --reporter=basic & + E2E_PID=$! + + # Run CLI smoke tests + ./packages/cli/dist/bin/gridplus.js simulator setup + ./packages/cli/dist/bin/gridplus.js address + ./packages/cli/dist/bin/gridplus.js address --count 3 + ./packages/cli/dist/bin/gridplus.js address --type btc-segwit + ./packages/cli/dist/bin/gridplus.js address --type solana + ./packages/cli/dist/bin/gridplus.js pubkey + ./packages/cli/dist/bin/gridplus.js address --json + echo "CLI tests passed!" + + # Wait for e2e tests to complete + wait $E2E_PID + E2E_EXIT=$? + + exit $E2E_EXIT - name: Show simulator logs on failure if: failure() && env.INTERNAL_EVENT == 'true' From 0098f1fc39cc48a68fba08b4f3074b7f81d1d090 Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:05:29 -0500 Subject: [PATCH 6/7] docs: add comprehensive CLI reference guide - Add full CLI documentation with all commands and options - Cover device setup, addresses, pubkeys, signing, and ETH2 commands - Include simulator quick start and CI integration examples - Add troubleshooting section and JSON output examples --- packages/docs/docs/cli.md | 431 +++++++++++++++++++++++++++ packages/docs/docs/reference/util.md | 16 +- packages/docs/sidebars.js | 4 + 3 files changed, 443 insertions(+), 8 deletions(-) create mode 100644 packages/docs/docs/cli.md diff --git a/packages/docs/docs/cli.md b/packages/docs/docs/cli.md new file mode 100644 index 00000000..7817a9e5 --- /dev/null +++ b/packages/docs/docs/cli.md @@ -0,0 +1,431 @@ +--- +id: 'cli' +title: '🖥️ CLI Reference' +sidebar_position: 3 +--- + +# GridPlus CLI + +The GridPlus CLI (`@gridplus/cli`) is a command-line tool for interacting with Lattice hardware wallets. It supports both real Lattice1 devices and the lattice-simulator for local development and CI testing. + +## Installation + +```bash +# Using npm +npm install -g @gridplus/cli + +# Using yarn +yarn global add @gridplus/cli + +# Using pnpm +pnpm add -g @gridplus/cli +``` + +After installation, the `gridplus` command is available globally. + +## Quick Start + +### Using with Lattice Simulator (Recommended for Development) + +The fastest way to get started is with the [lattice-simulator](https://github.com/GridPlus/lattice-simulator), which provides a local mock device: + +```bash +# 1. Start the simulator +docker run -p 3000:3000 -e LATTICE_AUTO_APPROVE=true gridplus/lattice-simulator + +# 2. One-shot setup and pair +gridplus simulator setup + +# 3. Get addresses +gridplus address +``` + +:::tip +Set the `LATTICE_AUTO_APPROVE=true` environment variable when running the simulator to automatically approve requests without manual intervention. +::: + +### Using with a Real Lattice Device + +```bash +# 1. Configure your device connection +gridplus setup + +# 2. When prompted, enter: +# - Device ID (6-character ID from Settings on your Lattice) +# - Password (for local encryption, not sent to device) +# - App name (shown on device during pairing) + +# 3. Pair with your device +gridplus pair +# Enter the 6-digit code displayed on your Lattice + +# 4. Ready to use! +gridplus address +``` + +## Commands + +### Device Management + +#### `gridplus setup` + +Interactive device setup - configure connection to your Lattice. + +```bash +# Interactive setup (prompts for all values) +gridplus setup + +# With options +gridplus setup --device-id ABC123 --password mypass --name "My App" + +# For simulator (uses localhost:3000, SD0001, etc.) +gridplus setup --simulator +``` + +| Option | Description | +|--------|-------------| +| `-d, --device-id ` | Device ID (skip prompt) | +| `-p, --password ` | Device password (skip prompt) | +| `-n, --name ` | App name shown on device | +| `--base-url ` | Custom base URL for Lattice relay | +| `-s, --simulator` | Use simulator defaults | + +#### `gridplus connect` + +Reconnect to a previously configured device. + +```bash +gridplus connect +``` + +#### `gridplus pair [code]` + +Pair with a Lattice device using a pairing code. + +```bash +# Interactive (prompts for code) +gridplus pair + +# With code directly +gridplus pair 123456 + +# Auto-pair with simulator +gridplus pair --simulator +``` + +| Option | Description | +|--------|-------------| +| `-s, --simulator` | Use simulator pairing secret (auto-pair) | + +--- + +### Simulator Commands + +#### `gridplus simulator setup` + +One-shot setup and pair with lattice-simulator. Combines `setup --simulator` and `pair --simulator` into a single command. + +```bash +gridplus simulator setup +``` + +#### `gridplus simulator info` + +Display simulator default configuration. + +```bash +gridplus simulator info +``` + +Output: +``` +Lattice Simulator Defaults: + URL: http://127.0.0.1:3000 + Device ID: SD0001 + Password: 12345678 + Pairing Secret: 12345678 +``` + +:::info Simulator Alias +You can use `gridplus sim` as a shorthand for `gridplus simulator`. +::: + +--- + +### Address Commands + +#### `gridplus address` + +Get addresses from your Lattice device. + +```bash +# Default: single Ethereum address +gridplus address + +# Multiple addresses +gridplus address --count 5 + +# Different address types +gridplus address --type btc-legacy +gridplus address --type btc-segwit +gridplus address --type btc-wrapped-segwit +gridplus address --type solana + +# Specific starting index +gridplus address --index 10 --count 3 + +# Custom derivation path +gridplus address "m/44'/60'/0'/0/5" + +# JSON output for scripting +gridplus address --json +``` + +| Option | Description | +|--------|-------------| +| `-t, --type ` | Address type: `eth`, `btc-legacy`, `btc-segwit`, `btc-wrapped-segwit`, `solana` | +| `-n, --count ` | Number of addresses to fetch (default: 1) | +| `-i, --index ` | Starting index (default: 0) | +| `-j, --json` | Output in JSON format | + +**Address Types:** + +| Type | Description | Example | +|------|-------------|---------| +| `eth` (default) | Ethereum address (m/44'/60'/0'/0/x) | `0x1234...` | +| `btc-legacy` | Bitcoin P2PKH (m/44'/0'/0'/0/x) | `1A1zP1...` | +| `btc-segwit` | Bitcoin Native SegWit P2WPKH (m/84'/0'/0'/0/x) | `bc1q...` | +| `btc-wrapped-segwit` | Bitcoin Wrapped SegWit P2SH-P2WPKH (m/49'/0'/0'/0/x) | `3J98t1...` | +| `solana` | Solana address (m/44'/501'/0'/0') | `4fYNw3...` | + +--- + +### Public Key Commands + +#### `gridplus pubkey` + +Get public keys from your Lattice device. + +```bash +# Default: secp256k1 public key +gridplus pubkey + +# Ed25519 (for Solana) +gridplus pubkey --type ed25519 + +# BLS (for Ethereum staking) +gridplus pubkey --type bls12_381_g1 + +# Custom derivation path +gridplus pubkey "m/44'/60'/0'/0/0" --type secp256k1 + +# Multiple keys +gridplus pubkey --count 3 + +# JSON output +gridplus pubkey --json +``` + +| Option | Description | +|--------|-------------| +| `-t, --type ` | Key type: `secp256k1`, `ed25519`, `bls12_381_g1` | +| `-n, --count ` | Number of keys to fetch (default: 1) | +| `-i, --index ` | Starting index (default: 0) | +| `-j, --json` | Output in JSON format | + +**Key Types and Default Paths:** + +| Type | Curve | Default Path | +|------|-------|--------------| +| `secp256k1` | ECDSA | m/44'/60'/0'/0/0 | +| `ed25519` | Ed25519 | m/44'/501'/0'/0' | +| `bls12_381_g1` | BLS | m/12381/3600/0/0/0 | + +--- + +### Signing Commands + +#### `gridplus sign ` + +Sign a transaction. + +```bash +# Sign a serialized transaction +gridplus sign "0x02f87001..." + +# JSON output +gridplus sign "0x02f87001..." --json +``` + +:::warning +Transaction signing requires the transaction to be pre-serialized. For a better developer experience, consider using the SDK directly for transaction construction. +::: + +#### `gridplus sign-message ` + +Sign a message using personal_sign or EIP-712 typed data. + +```bash +# Personal sign (plain text message) +gridplus sign-message "Hello, Lattice!" + +# Sign hex data +gridplus sign-message "0xdeadbeef" + +# EIP-712 typed data (JSON string) +gridplus sign-message '{"types":...,"domain":...,"primaryType":"...","message":...}' --typed + +# Read message from file +gridplus sign-message ./message.txt --file + +# Read EIP-712 from file +gridplus sign-message ./typed-data.json --file --typed + +# JSON output +gridplus sign-message "Hello" --json +``` + +| Option | Description | +|--------|-------------| +| `--typed` | Sign EIP-712 typed data (expects JSON) | +| `--file` | Read message from file | +| `-j, --json` | Output signature in JSON format | + +--- + +### Ethereum 2.0 Commands + +#### `gridplus eth2 deposit-data` + +Generate ETH2 validator deposit data. + +```bash +gridplus eth2 deposit-data +``` + +#### `gridplus eth2 bls-change` + +Generate BLS credentials change message for validator withdrawal address updates. + +```bash +gridplus eth2 bls-change +``` + +--- + +## JSON Output + +All commands support `-j, --json` for machine-readable output, useful for scripting and CI pipelines. + +```bash +# Example: Get address as JSON +gridplus address --json + +# Output: +# {"type":"eth","count":1,"addresses":["0x..."]} + +# Parse with jq +gridplus address --json | jq -r '.addresses[0]' +``` + +--- + +## Session Management + +The CLI stores session data in `~/.gridplus/session.json`. This includes: + +- Device ID +- Base URL +- App name +- Encrypted app secret +- Simulator flag + +To clear your session and start fresh: + +```bash +rm -rf ~/.gridplus +``` + +--- + +## CI Integration + +The CLI is designed to work well in CI environments with the lattice-simulator. + +### GitHub Actions Example + +```yaml +jobs: + test: + runs-on: ubuntu-latest + services: + lattice-simulator: + image: gridplus/lattice-simulator + env: + LATTICE_AUTO_APPROVE: "true" + ports: + - 3000:3000 + + steps: + - uses: actions/checkout@v4 + + - name: Setup CLI + run: npm install -g @gridplus/cli + + - name: Configure simulator + run: gridplus simulator setup + + - name: Verify connection + run: | + # Get ETH address + gridplus address --json + + # Get BTC address + gridplus address --type btc-segwit --json + + # Get public key + gridplus pubkey --json +``` + +### Environment Variables + +The CLI respects the following environment variables: + +| Variable | Description | +|----------|-------------| +| `LATTICE_AUTO_APPROVE` | Set to `true` on the simulator to auto-approve requests | +| `LATTICE_MNEMONIC` | Set custom mnemonic on simulator for deterministic testing | + +--- + +## Troubleshooting + +### "No device configured" + +Run `gridplus setup` first to configure your device connection. + +### Connection Timeout + +- **Real device**: Ensure your Lattice is connected to the internet and reachable +- **Simulator**: Ensure the simulator is running: `docker run -p 3000:3000 gridplus/lattice-simulator` + +### Pairing Failed + +- **Real device**: Check that you entered the correct 6-digit code from the device screen +- **Simulator**: Use `gridplus pair --simulator` to auto-pair + +### "Setup failed: Request failed" + +The device may not be reachable. Check: +- The Device ID is correct +- The base URL is correct (default: `https://signing.gridpl.us` for real devices) +- Network connectivity + +--- + +## See Also + +- [Getting Started](/) - SDK overview and concepts +- [Addresses](./addresses) - Address derivation in depth +- [Signing](./signing) - Transaction and message signing +- [Testing](./testing) - Testing with the SDK diff --git a/packages/docs/docs/reference/util.md b/packages/docs/docs/reference/util.md index f4f8beed..6c5c4996 100644 --- a/packages/docs/docs/reference/util.md +++ b/packages/docs/docs/reference/util.md @@ -22,7 +22,7 @@ The properly formatted v value as Buffer or BN ### Source -packages/sdk/src/util.ts:878 +packages/sdk/src/util.ts:873 *** @@ -52,7 +52,7 @@ Fetches calldata from a remote scanner based on the transaction's `chainId` ### Source -packages/sdk/src/util.ts:628 +packages/sdk/src/util.ts:623 *** @@ -78,7 +78,7 @@ an application secret as a Buffer ### Source -packages/sdk/src/util.ts:705 +packages/sdk/src/util.ts:700 *** @@ -103,7 +103,7 @@ BN object containing the `v` param ### Source -packages/sdk/src/util.ts:732 +packages/sdk/src/util.ts:727 *** @@ -134,13 +134,13 @@ Usage: ### Source -packages/sdk/src/util.ts:921 +packages/sdk/src/util.ts:916 *** ## selectDefFrom4byteABI() -> **selectDefFrom4byteABI**(`abiData`: `any`[], `selector`: `string`): `any` +> **selectDefFrom4byteABI**(`abiData`: `any`[], `selector`: `string`): `unknown`[] Takes a list of ABI data objects and a selector, and returns the earliest ABI data object that matches the selector. @@ -154,8 +154,8 @@ matches the selector. ### Returns -`any` +`unknown`[] ### Source -packages/sdk/src/util.ts:392 +packages/sdk/src/util.ts:387 diff --git a/packages/docs/sidebars.js b/packages/docs/sidebars.js index bc7e4a54..7542929c 100644 --- a/packages/docs/sidebars.js +++ b/packages/docs/sidebars.js @@ -24,6 +24,10 @@ const sidebars = { type: 'doc', id: 'migration-v3-to-v4', }, + { + type: 'doc', + id: 'cli', + }, { type: 'category', label: 'Basic Functionality', From df6623f62be741b569fe394f7b366affc969e2f9 Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:05:36 -0500 Subject: [PATCH 7/7] fix(ci): run CLI tests sequentially after SDK e2e tests The simulator only supports one pairing at a time. Running SDK e2e tests (as 'lattice-manager') and CLI tests (as 'GridPlus CLI') in parallel caused pairing conflicts where one would fail with "Pairing failed". --- .github/workflows/build-test.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 015220c0..fd4cd3a6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -99,7 +99,7 @@ jobs: sleep 1 done - - name: Run SDK e2e tests and CLI tests with simulator + - name: Run SDK e2e tests with simulator if: env.INTERNAL_EVENT == 'true' working-directory: ${{ github.workspace }} env: @@ -111,12 +111,14 @@ jobs: PAIRING_SECRET: '12345678' ENC_PW: '12345678' APP_NAME: 'lattice-manager' - run: | - # Run SDK e2e tests and CLI tests in parallel - pnpm run e2e -- --reporter=basic & - E2E_PID=$! + run: pnpm run e2e -- --reporter=basic - # Run CLI smoke tests + - name: Run CLI smoke tests with simulator + if: env.INTERNAL_EVENT == 'true' + working-directory: ${{ github.workspace }} + run: | + # CLI tests run after e2e tests to avoid pairing conflicts + # (simulator only supports one pairing at a time) ./packages/cli/dist/bin/gridplus.js simulator setup ./packages/cli/dist/bin/gridplus.js address ./packages/cli/dist/bin/gridplus.js address --count 3 @@ -126,12 +128,6 @@ jobs: ./packages/cli/dist/bin/gridplus.js address --json echo "CLI tests passed!" - # Wait for e2e tests to complete - wait $E2E_PID - E2E_EXIT=$? - - exit $E2E_EXIT - - name: Show simulator logs on failure if: failure() && env.INTERNAL_EVENT == 'true' run: |