diff --git a/.gitignore b/.gitignore index 648c86304..83de291e0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,9 @@ FileTest bin # Wallet keystore files created at runtime -Wallet/ -Mnemonic/ -wallet_data/ +/Wallet/ +/Mnemonic/ +/wallet_data/ # QA runtime output qa/results/ @@ -38,3 +38,6 @@ qa/.verify.lock/ graphify-out/ .vscode/ docs/superpowers + + +/ts-deprecated/ \ No newline at end of file diff --git a/ts/.dependency-cruiser.cjs b/ts/.dependency-cruiser.cjs new file mode 100644 index 000000000..1a1bcea08 --- /dev/null +++ b/ts/.dependency-cruiser.cjs @@ -0,0 +1,49 @@ +/** + * Enforced dependency direction: + * + * bootstrap (composition) -> inbound/outbound adapters -> application -> domain + * inbound adapters -> application -> domain + * + * Inbound and outbound adapters are peers. They may meet only in bootstrap/composition. + */ +module.exports = { + forbidden: [ + { + name: "no-circular", + severity: "error", + from: {}, + to: { circular: true }, + }, + { + name: "domain-is-independent", + severity: "error", + from: { path: "^src/domain/" }, + to: { path: "^src/(application|adapters|bootstrap)/" }, + }, + { + name: "application-owns-ports", + severity: "error", + comment: "production application code depends on domain and its own ports, never adapters", + from: { path: "^src/application/", pathNot: "\\.test\\.ts$" }, + to: { path: "^src/(adapters|bootstrap)/" }, + }, + { + name: "inbound-does-not-know-outbound", + severity: "error", + comment: "CLI adapters call application ports/use-cases; bootstrap/composition supplies outbound implementations", + from: { path: "^src/adapters/inbound/", pathNot: "\\.test\\.ts$" }, + to: { path: "^src/(adapters/outbound|bootstrap)/" }, + }, + { + name: "outbound-does-not-know-inbound", + severity: "error", + from: { path: "^src/adapters/outbound/", pathNot: "\\.test\\.ts$" }, + to: { path: "^src/(adapters/inbound|bootstrap)/" }, + }, + ], + options: { + doNotFollow: { path: "node_modules" }, + tsConfig: { fileName: "tsconfig.json" }, + enhancedResolveOptions: { extensions: [".ts", ".js"] }, + }, +}; diff --git a/ts/.gitignore b/ts/.gitignore new file mode 100644 index 000000000..1754c3ace --- /dev/null +++ b/ts/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +.wallet-cli/ +.env +.private/ diff --git a/ts/README.md b/ts/README.md new file mode 100644 index 000000000..89c123878 --- /dev/null +++ b/ts/README.md @@ -0,0 +1,52 @@ +# wallet-cli + +TypeScript implementation of the TRON wallet CLI. This project owns its source, dependencies, +tests and live-test artifacts independently. + +The CLI contract is: + +- stable `wallet-cli.result.v1` JSON envelopes; +- deterministic `0 / 1 / 2` exit codes; +- exactly one terminal stdout frame in JSON mode; +- encrypted local wallet storage; +- stdin/TTY secret handling without secret argv/env values; +- TRON mainnet, Nile and Shasta targets; +- software and Ledger signing; +- extensible family plugins without a universal-chain abstraction. + +## Commands + +```bash +npm ci +npm run typecheck +npm run depcruise +npm test +npm run build +``` + +The live Nile suite reads the test identity from `../ts/.private/.env.test`, uses an isolated +`WALLET_CLI_HOME`, and never copies or logs the private material: + +```bash +npm run test:live:nile +``` + +Its raw output is written to +`docs/nile-full-command-test-2026-06-29-run2-rawlogs.md`. + +## Architecture + +```mermaid +flowchart LR + ENTRY[index.ts] --> BOOTSTRAP[bootstrap
runner + composition + family plugins] + BOOTSTRAP --> IN[inbound CLI adapter] + BOOTSTRAP --> OUT[outbound adapters] + IN --> APPLICATION[application
contracts + use cases + ports] + OUT --> APPLICATION + APPLICATION --> DOMAIN[domain
wallet + chain facts + value types] +``` + +Dependency direction is enforced by dependency-cruiser. Inbound and outbound adapters are peers +and may meet only in `bootstrap/composition.ts`. See the +[architecture source of truth](./docs/typescript-wallet-cli-architecture-source-of-truth.zh-TW.md) +for the complete contract; `docs/architecture.md` is its compact English overview. diff --git a/ts/docs/architecture.md b/ts/docs/architecture.md new file mode 100644 index 000000000..fe3b89850 --- /dev/null +++ b/ts/docs/architecture.md @@ -0,0 +1,230 @@ +# wallet-cli architecture — English summary + +This document is a non-normative English overview. The single, authoritative architecture and +behavior contract is +[typescript-wallet-cli-architecture-source-of-truth.zh-TW.md](./typescript-wallet-cli-architecture-source-of-truth.zh-TW.md). + +## 1. Dependency model + +```mermaid +flowchart LR + BOOTSTRAP[bootstrap
process lifecycle + composition root] --> INBOUND[adapters/inbound
CLI] + BOOTSTRAP --> OUTBOUND[adapters/outbound
filesystem / TRON / Ledger / price] + INBOUND --> APPLICATION[application
contracts / use cases / ports] + OUTBOUND --> APPLICATION + APPLICATION --> DOMAIN[domain
pure rules and values] +``` + +Rules: + +1. `domain` imports neither application, adapters nor bootstrap. +2. Production `application` imports neither adapters nor bootstrap. +3. Inbound and outbound adapters never import each other or bootstrap. +4. `bootstrap/composition.ts` is the only general composition root. +5. Chain-specific adapter and use-case construction belongs to a family plugin. +6. Circular dependencies are forbidden. +7. Type-only dependencies must follow the same conceptual direction even when the dependency + checker would ignore their runtime edge. + +## 2. Package tree + +```text +src/ +├── index.ts +├── bootstrap/ +│ ├── argv.ts +│ ├── runner.ts +│ ├── composition.ts +│ ├── family-registry.ts +│ └── families/ +│ ├── types.ts +│ └── tron.ts +├── domain/ +│ ├── address/ +│ ├── amounts/ +│ ├── derivation/ +│ ├── errors/ +│ ├── family/ +│ ├── resources/ +│ ├── sources/ +│ ├── types/ +│ └── wallet/ +├── application/ +│ ├── contracts/ +│ │ ├── execution-policy.ts +│ │ ├── execution-scope.ts +│ │ └── progress.ts +│ ├── ports/ +│ │ ├── chain/ +│ │ ├── network-registry.ts +│ │ └── prompt.ts +│ ├── services/ +│ │ ├── capability/ +│ │ ├── pipeline/ +│ │ ├── signer/ +│ │ └── target/ +│ └── use-cases/ +│ └── tron/ +└── adapters/ + ├── inbound/cli/ + │ ├── commands/ + │ ├── contracts/ + │ ├── context/ + │ ├── help/ + │ ├── input/ + │ ├── output/ + │ ├── registry/ + │ ├── render/ + │ └── shell/ + └── outbound/ + ├── chain/tron/ + ├── config/ + ├── keystore/ + ├── ledger/ + ├── persistence/ + ├── price/ + └── tokenbook/ +``` + +## 3. Responsibilities + +### Domain + +Contains pure wallet, account, address, amount, derivation, source, family and transaction value +rules. Domain code performs no filesystem, network, device, terminal or process I/O. + +### Application + +Defines what the product does and which external capabilities it requires. + +- `contracts`: adapter-neutral execution policy, scopes and progress events. +- `ports`: capabilities required by application workflows, implemented by inbound or outbound adapters. +- `services`: reusable orchestration such as signer resolution and transaction pipeline. +- `use-cases`: wallet/config and family-specific workflows. + +Application services consume ports such as `WalletRepository`, `ChainGatewayProvider`, +`LedgerDevice`, `TokenRepository`, `PriceProvider`, `NetworkRegistry`, `PromptPort` and +`BackupWriter`. They never construct or import filesystem, TronWeb, Ledger transport, CoinGecko, +Zod, yargs, CLI rendering or output-envelope implementations. + +### Inbound CLI adapter + +Owns yargs, zod command schemas, help/catalog generation, TTY input, stream discipline, output +envelopes and human rendering. A command definition translates CLI input into a use-case call; it +does not implement persistence or provider transport. CLI-only contracts (`CommandDefinition`, +`ExecutionContext`, globals, session and output envelopes) live under `inbound/cli/contracts`. + +### Outbound adapters + +Implement application ports: + +- encrypted file keystore and atomic persistence; +- YAML configuration document; +- secure backup writer; +- TRON gateway and TronGrid history reader; +- Ledger device transport; +- token repository and price provider. + +### Bootstrap + +- `argv.ts`: pre-yargs global scan required to select output and secret channels. +- `composition.ts`: constructs process-scoped adapters and shared services. +- `runner.ts`: executes one invocation and owns the terminal error boundary. +- `family-registry.ts`: enabled plugin list and family-keyed projections. +- `families/*.ts`: family-specific gateway/signing/use-case/command composition. + +## 4. Command flow + +```mermaid +flowchart LR + ARGV[argv] --> SHELL[yargs shell] + SHELL --> DEF[CommandDefinition] + DEF --> TARGET[target + capability gates] + TARGET --> USECASE[application use case] + USECASE --> PORT[application port] + PORT --> ADAPTER[outbound adapter] + USECASE --> RESULT[typed result] + RESULT --> FORMAT[text or JSON formatter] + FORMAT --> STREAM[one terminal frame] +``` + +Zod remains the single source for command input shape, defaults, validation, help fields and JSON +Schema. Global flags remain table-driven. Business execution modes and confirmation behavior have +one implementation in application services, not duplicate command helpers. + +## 5. Transaction flow + +```mermaid +flowchart LR + RESOLVE[resolve signer] --> BUILD[build] + BUILD --> ESTIMATE[estimate] + ESTIMATE --> MODE{mode} + MODE -->|dry-run| PLAN[plan] + MODE -->|sign| SIGN[software or Ledger sign] + SIGN --> BROADCAST{broadcast?} + BROADCAST -->|no| SIGNED[signed transaction] + BROADCAST -->|yes| SUBMIT[submitted receipt] + SUBMIT --> CONFIRM{wait?} + CONFIRM --> FINAL[submitted / confirmed / failed] +``` + +Chain-specific builders and confirmation readers are provided by the family gateway. The shared +pipeline knows only signer and broadcaster ports. + +## 6. Chain-family extension + +A family plugin owns the concrete composition for one chain family: + +The complete EVM implementation order, public discovery requirements, network contract and +acceptance checklist are defined in [evm-development-plan.zh-TW.md](./evm-development-plan.zh-TW.md). + +```ts +interface FamilyPlugin { + meta: FamilyMeta & { family: F } + signStrategy: SignStrategy + createGateway(network: NetworkDescriptor): ChainGatewayMap[F] + createModule(dependencies: FamilyApplicationDependencies): ChainModule +} +``` + +Adding EVM requires: + +1. Add `evm` to `ChainFamily` and `FamilyMeta` facts. +2. Extend the discriminated `NetworkDescriptor` union. +3. Extend `ChainGatewayMap` with an `EvmGateway` port. +4. Implement EVM address codec, gateway and signing strategy. +5. Implement EVM use cases and CLI command module without changing TRON use cases. +6. Add `bootstrap/families/evm.ts` and one registry entry. +7. Add routing, signer, capability, output and contract tests. + +Only genuinely shared capabilities receive shared ports. TRON staking/resource methods and EVM +gas/nonce methods stay in their family gateways; no universal gateway may accumulate unrelated +chain operations. + +## 7. Behavioral invariants + +- JSON success and error use `wallet-cli.result.v1`. +- Usage errors exit 2; execution errors exit 1; success/meta requests exit 0. +- JSON stdout contains exactly one terminal frame. +- Progress and diagnostics use stderr. +- Unknown exceptions are redacted. +- A run consumes stdin through at most one secret channel. +- Secrets never enter logs, envelopes, argv or environment variables. +- Dry-run does not decrypt, sign or broadcast. +- Watch-only accounts never sign. +- Every persistent mutation is locked and atomically replaced. +- Already-broadcast transactions do not become command failures solely because confirmation times + out. + +## 8. Verification gates + +```bash +npm run typecheck +npm run depcruise +npm test +npm run build +npm run test:live:nile +``` + +`test:live:nile` exercises the live surface in an isolated wallet home and emits a raw log without +exposing the test secret. diff --git a/ts/docs/evm-development-plan.md b/ts/docs/evm-development-plan.md new file mode 100644 index 000000000..637f0802f --- /dev/null +++ b/ts/docs/evm-development-plan.md @@ -0,0 +1,593 @@ +# wallet-cli EVM Support Development Plan + +> Status: to be implemented +> Applies to: `ts-refactor` +> Purpose: list the full scope of everything that must be modified, added, and accepted when extending the current TRON-only CLI to TRON + EVM. This document deliberately does not expand into the implementation details of functions and RPC payloads. + +## 1. Definition of Done + +EVM support cannot merely mean "can connect to an Ethereum RPC". When complete, all of the following must hold simultaneously: + +1. `evm` is a formal `ChainFamily`, no longer simulated by a type cast in tests. +2. It can resolve Ethereum mainnet `evm:1`, as well as any `evm:` added in the config file, e.g. `evm:11155111`, `evm:8453`, `evm:31337`. +3. An EVM network can be selected by canonical id or a unique alias, and can be set as `defaultNetwork`. +4. seed, private key, watch-only, and the Ledger Ethereum app all have explicit EVM behavior. +5. EVM has its own gateway, signing strategy, use cases, and CLI command module, and does not stuff EVM methods into the TRON gateway. +6. `wallet-cli --help`, family help, command help, `--json-schema`, and `wallet-cli networks` all let a user/agent discover EVM. +7. Text and JSON output correctly present EVM address, chain id, native currency, gas, fee, nonce, transaction hash, and receipt. +8. The existing TRON commands, output, secret handling, and exit-code contract remain unchanged. + +Development dependency order: + +```text +public contract → Domain → Persistence/Config → Application ports → EVM adapters + → EVM use cases → CLI commands → Bootstrap → Help/Output → Tests/Docs +``` + +### 1.1 The Real Code-Change Classification + +The "locations involved" that appear later in this document include modifications, references, and verification — it does not mean every file must change. Based on the current code, the actual classification is as follows. + +#### Definitely added + +- `application/ports/chain/evm-gateway.ts` +- `adapters/outbound/chain/evm/*` +- `application/use-cases/evm/*` +- `adapters/inbound/cli/commands/evm/*` +- `bootstrap/families/evm.ts` +- EVM confirmation, fixtures, unit/integration/live tests +- Explorer/history adapter (added only if EVM history/ABI metadata is to be provided) + +#### Definitely modified + +- Domain family/address/network/wallet/transaction types +- Config builtins, custom-network validation, and network display +- Keystore schema migration and the EVM-address backfill for old wallets +- `ChainGatewayMap` and the family plugin registry +- The file location of the generic gateway registry (currently misplaced in `chain/tron/provider.ts`) +- The outbound Ledger dispatcher (if Ledger EVM is exposed) +- Token builtin/normalization and the CoinGecko network mapping +- Root/family/merged command help, the global `--network` description, and the wallet help examples +- The family renderer, EVM transaction/fee text output +- Dependencies, architecture docs, help/golden baselines + +#### In principle not modified, only adding EVM tests to verify reusability + +- `application/services/transaction-mode.ts` +- `application/services/pipeline/index.ts` +- `application/services/signer/index.ts` +- `application/services/signer/software.ts` +- `application/services/signer/ledger.ts` +- `application/services/target/index.ts` +- `application/contracts/execution-policy.ts` +- `application/contracts/execution-scope.ts` +- `application/ports/chain/broadcaster.ts` +- `application/ports/ledger-device.ts` +- `application/ports/network-registry.ts` +- `application/ports/token-repository.ts` +- `application/ports/price-provider.ts` +- `adapters/inbound/cli/registry/index.ts` +- `adapters/inbound/cli/shell/index.ts` +- `adapters/inbound/cli/command-id.ts` +- `adapters/inbound/cli/context/index.ts` +- `adapters/inbound/cli/arity/index.ts` +- `adapters/inbound/cli/output/envelope.ts` +- stream, secret, prompt, and output formatter infrastructure + +#### Modified only if a public decision requires it + +- `CapabilityRegistry` and bootstrap capability composition: needed only if history/indexer, legacy/EIP-1559, etc. require per-network gating. +- Help catalog: EVM commands enter the catalog automatically via the registry; modification is needed only to add a families/networks summary at the top level of the catalog. +- `WalletService`: the existing family detection and repository delegation are reusable; usually only tests are needed, and migration should stay in the persistence adapter. +- Shared transaction types/pipeline: types must be extended with display fields, but the pipeline control flow is in principle unchanged. + +If the implementation must modify one of the above "in principle not modified" modules, you should first point out which family-neutral capability the current abstraction lacks; you must not add `if (family === "evm")` just because the EVM adapter is awkward to write. + +## 2. Public Decisions to Lock Before Development + +The following decisions must be written into the architecture contract first, otherwise help, the network schema, the command schema, and the tests will be revised repeatedly. + +### 2.1 Network identity + +Network aliases are deferred while selector input accepts canonical ids only. Alias-related requirements below remain future work for when that feature is restored. + +- The EVM canonical network id is fixed as `evm:`. +- The `xxx` in `evm:xxx` is the EIP-155 chain id, not a name; the actual value must be a positive-integer string. +- Minimum builtin networks: + - `evm:1`, with recommended aliases `eth`, `ethereum`. + - One public testnet, recommended `evm:11155111`, alias `sepolia`. +- Other networks are added via `config.yaml`; whether to additionally build in Base, Polygon, BSC, etc. is a product decision. +- An alias must be globally unique; a duplicate alias must keep the `ambiguous_network_alias` error. +- The chain id reported by RPC must match the configured value; the mismatch must not be ignored before signing or broadcast. +- `defaultNetwork` is still recommended to remain `tron:mainnet`, unless there is a separate product migration decision. + +The recommended public shape of a custom network: + +```yaml +defaultNetwork: evm:1 +networks: + evm:1: + family: evm + chainId: "1" + aliases: [eth, ethereum] + rpcUrl: https://example.invalid + feeModel: eip1559 + nativeCurrency: + name: Ether + symbol: ETH + decimals: 18 + + evm:31337: + family: evm + chainId: "31337" + aliases: [local] + rpcUrl: http://127.0.0.1:8545 + feeModel: eip1559 + nativeCurrency: + name: Ether + symbol: ETH + decimals: 18 +``` + +### 2.2 The First-Version Command Surface + +The same logical path selects the TRON or EVM implementation via `--network`. A separate top-level `ethereum ...` execution grammar must not be created for EVM. + +| Command group | First-version EVM requirement | Notes | +| --- | --- | --- | +| wallet lifecycle | supported | create/import/derive show the EVM address afterward; watch can recognize `0x...`. | +| `account balance` | supported | uses the network native currency. | +| `account info` | supported | EVM semantics are defined by the EVM use case, not by imitating the TRON resource fields. | +| `account history` | explicit decision | standard JSON-RPC has no address history; an explorer/indexer adapter is required, otherwise it must not claim availability in the help/capability of an unsupported network. | +| `account portfolio` | supported | native coin + ERC-20 token book + price provider. | +| `token add/list/remove/balance/info` | supported | the EVM token kind is `erc20`. | +| `tx send/broadcast/status/info` | supported | native/ERC-20, legacy/EIP-1559, raw signed tx. | +| `contract call/send/deploy/info` | supported or explicitly degraded | if `contract info` needs ABI metadata, there must be an explorer source; with only bytecode, the help must describe it truthfully. | +| `message sign` | supported | software and Ledger behavior are consistent. | +| `block` | supported | latest or a specified block. | +| `stake ...` | no EVM implementation | stays TRON-only; root/family help must mark it. | + +### 2.3 SDK and Hardware Wallet + +- Select a single EVM SDK; the current architecture recommends `viem`, added to production dependencies. +- If full Ledger EVM support is claimed, add an Ethereum Ledger app adapter and the corresponding package (e.g. `@ledgerhq/hw-app-eth`). +- If the first version does not do Ledger EVM, `FAMILIES.evm.ledger`, `import ledger --help`, and the README must not show the Ethereum app. + +## 3. Phased Development Checklist + +### Phase 0: Contract and Test Baseline + +- [ ] Confirm the network id, builtin networks, command matrix, Ledger, and history/indexer decisions in Section 2. +- [ ] Build EVM address, transaction, receipt, legacy fee, EIP-1559 fee, ERC-20, block, and RPC error fixtures. +- [ ] Establish a new multichain help/golden baseline; the existing TRON-only help parity must no longer serve as the sole truth for the entire root help. +- [ ] Define the upgrade strategy and rollback behavior for the old `wallets.json` version. + +### Phase 1: Domain + +Locations involved: + +- `src/domain/family/index.ts` +- `src/domain/address/index.ts` +- `src/domain/types/network.ts` +- `src/domain/types/tx.ts` +- `src/domain/types/token.ts` +- `src/domain/types/wallet.ts` +- `src/domain/sources/index.ts` +- `src/domain/derivation/index.ts` +- `src/domain/wallet/index.ts` +- `src/domain/errors/index.ts` + +Work list: + +- [ ] Add `evm` to `ChainFamily` and `FAMILIES`. +- [ ] Register the EVM BIP44 coin type `60`, smallest unit `wei`, address codec, and Ledger metadata. +- [ ] Add EVM address derive, validate, normalize/checksum rules and tests. +- [ ] Restore `NetworkDescriptor` to a discriminated union keyed by `family`. +- [ ] Add `EvmNetworkDescriptor`: `rpcUrl`, decimal chain id, fee model, native currency, and optional explorer/history config. +- [ ] Verify that the canonical id's family and chain id are consistent with the descriptor. +- [ ] Extend the transaction view: gas, gas price, max fee, priority fee, effective gas price, nonce, EVM receipt/status, and other fields. +- [ ] Remove TRON-only naming assumptions from the shared view; when keeping backward-compatible TRON JSON fields, document them explicitly. +- [ ] Confirm the family constraint of the `erc20` token kind and contract-address normalization. +- [ ] Update seed/private-key address derivation, dedup, account projection, and family-detection tests. +- [ ] Complete typed errors for network/chain mismatch, invalid chain id, invalid EVM address, etc. + +### Phase 2: Wallet Persistence and Migration + +Locations involved: + +- `src/adapters/outbound/keystore/index.ts` +- `src/domain/types/wallet.ts` +- `src/domain/wallet/index.ts` +- `src/application/ports/account-store.ts` +- `src/application/ports/wallet-repository.ts` +- `src/application/use-cases/wallet-service.ts` +- `src/adapters/outbound/persistence/backup-writer.ts` +- wallet/keystore tests and migration fixtures + +Work list: + +- [ ] Raise the `wallets.json` schema version. +- [ ] Handle existing seed/private-key wallets that have only a TRON cached address; adding `evm` to `ChainAddresses` must not invalidate old files. +- [ ] Define the timing for the EVM address's lazy backfill / explicit migration, and the UX when a master password is needed. +- [ ] Ensure migration uses an atomic write and lock, and a failure does not corrupt the original file. +- [ ] Newly created and newly imported seed/private-key wallets generate both a TRON and an EVM address. +- [ ] `derive` for a new account generates addresses for both families. +- [ ] watch-only automatically recognizes TRON/EVM; an EVM address is normalized before storage. +- [ ] Ledger/watch remain single-family sources. +- [ ] list/current/use/rename/delete/backup and address lookup support EVM addresses. +- [ ] Backup metadata includes both known TRON/EVM addresses; for an old wallet not yet backfilled, the field must not be fabricated. +- [ ] Verify behavior for the same private key, a different BIP44 seed path, old-data migration, and dedup. + +### Phase 3: Config and NetworkRegistry + +Locations involved: + +- `src/adapters/outbound/config/builtins.ts` +- `src/adapters/outbound/config/index.ts` +- `src/adapters/outbound/config/yaml-config-document.ts` +- `src/application/ports/network-registry.ts` +- `src/application/use-cases/config-service.ts` +- `src/adapters/inbound/cli/commands/config.ts` +- `src/adapters/inbound/cli/commands/network.ts` + +Work list: + +- [ ] Add the `evm:1` and the chosen-testnet builtin descriptors. +- [ ] Apply runtime schema validation to a user-defined network, no longer casting directly to `NetworkDescriptor`. +- [ ] Support any valid `evm:`, and reject inconsistencies among `family`, id, and chain id. +- [ ] Validate `rpcUrl`, native currency, fee model, aliases, and the optional indexer/explorer fields. +- [ ] When alias support is restored, `NetworkRegistry.resolve()` supports the EVM canonical id, case-insensitive aliases, and the ambiguity check. +- [ ] `config defaultNetwork evm:1`, alias setting, and persistence work. +- [ ] `wallet-cli networks` displays builtin and custom EVM networks, the native symbol, and a safe summary of RPC/fee model/capabilities. +- [ ] Do not leak any API key that the RPC URL may contain in ordinary output; a redaction rule is needed. +- [ ] The network RPC client verifies the remote chain id on first use. + +### Phase 4: Application Ports and Shared Services + +Locations involved: + +- `src/application/ports/chain/evm-gateway.ts` (added) +- `src/application/ports/chain/gateway-provider.ts` +- `src/application/services/evm-confirmation.ts` (added) +- `src/application/services/capability/index.ts` (modified only if per-network capability is needed) + +The following shared modules are expected to only gain EVM reuse tests, with no production-code change: `Broadcaster`, `LedgerDevice`, +`TxPipeline`, `SignerResolver`, software/device signer, `TargetResolver`, `transactionMode`. + +Work list: + +- [ ] Define `EvmGateway`, holding only the read/build/estimate/broadcast capabilities EVM needs. +- [ ] Add `evm` to `ChainGatewayMap`, preserving the typed family lookup. +- [ ] Use EVM fixtures to verify that the shared `Broadcaster`, `TxPipeline`, `Signer`, and `SignStrategy` can carry an EVM transaction directly; the control flow is expected to be unchanged. +- [ ] Implement EVM confirmation normalization, preserving the existing contract of returning a submitted receipt after a `--wait` timeout. +- [ ] Support the legacy and EIP-1559 fee models; a network-specific trait must not be misjudged by a family-wide command capability as supported by all EVM networks. +- [ ] Adjust the capability registration to distinguish "the family has this command" from "this network has an indexer / EIP-1559 / etc. capability". +- [ ] Preserve the invariants: watch-only cannot sign, a wrong-family account is blocked, and dry-run does not decrypt the private key. + +### Phase 5: EVM Outbound Adapters + +Recommended new locations: + +```text +src/adapters/outbound/chain/evm/ +├── index.ts +├── provider.ts +├── evm.ts +├── signing-strategy.ts +├── evm-responses.ts +└── history-reader.ts # added only if history/indexer support is decided +``` + +Work list: + +- [ ] Implement a per-network EVM JSON-RPC client/gateway. +- [ ] Implement native balance, nonce/code, block, transaction, receipt, and fee/gas reads. +- [ ] Implement native/ERC-20 transfer, contract call/send/deploy, estimate, and raw transaction broadcast. +- [ ] Implement software transaction signing and personal-message signing. +- [ ] Validate every RPC response before normalizing, to avoid passing a provider-specific shape into a use case. +- [ ] Uniformly handle errors such as revert reason, replacement/nonce, insufficient funds, underpriced fee, chain mismatch, and timeout. +- [ ] If supporting account history / ABI metadata, add a separate explorer/indexer adapter; do not pretend standard JSON-RPC can provide it. +- [ ] Add EVM adapter unit tests with a mocked transport, not relying on a public RPC. + +### Phase 6: Ledger EVM Adapter + +Locations involved: + +- `src/adapters/outbound/ledger/index.ts` or split into family-specific device adapters +- `src/application/ports/ledger-device.ts` +- `src/application/services/signer/ledger.ts` +- `src/application/services/ledger-account.ts` +- `src/adapters/inbound/cli/commands/wallet.ts` +- `src/bootstrap/composition.ts` +- package dependencies and tsup bundling config + +Work list: + +- [ ] Add Ethereum Ledger app transport, address, transaction signing, and message signing. +- [ ] The EVM derivation path uses coin type 60, and supports the existing flow for index/path/address scan. +- [ ] `import ledger --app ethereum` appears in the schema, help, interactive choices, and tests. +- [ ] The precheck compares the device address with the cached address. +- [ ] Classify user rejection, wrong app, locked device, wrong seed, and transport error. +- [ ] Update the tsup `noExternal` / native addon config and the Ledger emulator/real-device verification. + +### Phase 7: EVM Application Use Cases + +Recommended new locations: + +```text +src/application/use-cases/evm/ +├── account-service.ts +├── token-service.ts +├── transaction-service.ts +├── contract-service.ts +└── block-service.ts +``` + +Work list: + +- [ ] Implement EVM account balance/info/portfolio; handle history per the Section 2 decision. +- [ ] Implement ERC-20 metadata, balance, and token book workflows. +- [ ] Implement native/ERC-20 send, signed raw tx broadcast, status/info. +- [ ] Implement the agreed first-version semantics of contract call/send/deploy/info. +- [ ] Implement EVM block query. +- [ ] Reuse `MessageService`, `TxPipeline`, `TransactionMode`, the token repository, and the price port; do not reuse a use case carrying TRON semantics. +- [ ] All returned shapes use a family-aware, stably-emittable normalized view. + +### Phase 8: EVM CLI Command Module + +Recommended new locations: + +```text +src/adapters/inbound/cli/commands/evm/ +├── index.ts +├── account.ts +├── token.ts +├── tx.ts +├── contract.ts +├── message.ts +└── block.ts +``` + +Shared locations involved: + +- `src/adapters/inbound/cli/commands/shared.ts` +- `src/adapters/inbound/cli/schemas/index.ts` +- `src/adapters/inbound/cli/arity/index.ts` +- `src/adapters/inbound/cli/registry/index.ts` +- `src/adapters/inbound/cli/shell/index.ts` +- `src/adapters/inbound/cli/context/index.ts` +- `src/adapters/inbound/cli/command-id.ts` + +Work list: + +- [ ] Each EVM command registers `family: "evm"`, the logical path, capability, requirements, Zod fields, examples, and formatter. +- [ ] EVM address, hash, hex data, ABI, quantity, gas, fee, nonce, and block identifier use an EVM-specific schema. +- [ ] `tx send` handles both native and ERC-20, but does not expose TRC10/TRC20 flags. +- [ ] The legacy/EIP-1559 command flags, mutual-exclusion conditions, and defaults are driven by a single schema that drives both help and JSON Schema. +- [ ] EVM does not register `stake` commands. +- [ ] Logical routing selects the EVM implementation via `--network evm:`; the same path for TRON/EVM does not pollute each other's fields or examples. +- [ ] The command id is stable as `evm.`, e.g. `evm.tx.send`. + +### Phase 9: Bootstrap and Family Composition + +Locations involved: + +- `src/bootstrap/families/evm.ts` (added) +- `src/bootstrap/families/types.ts` +- `src/bootstrap/family-registry.ts` +- `src/bootstrap/composition.ts` +- `src/bootstrap/runner.test.ts` +- `src/adapters/outbound/chain/tron/provider.ts` (may be renamed to a family-neutral gateway-registry location) + +Work list: + +- [ ] Build the `evmFamily` plugin: meta, gateway factory, sign strategy, use cases, command module. +- [ ] Add `evmFamily` to `FAMILY_REGISTRY`. +- [ ] `familyMap()` is complete for both TRON/EVM factories and signing strategies. +- [ ] The gateway cache is still isolated by canonical network id, not sharing a client across different chains. +- [ ] Capability composition is produced correctly from family commands + per-network traits. +- [ ] Bootstrap tests expect the enabled families to be `tron`, `evm`, and verify the command registration of both. + +### Phase 10: Help, Discovery, and Machine Catalog + +This phase is a necessary condition for publicly supporting EVM; it must not be treated as a documentation wrap-up. + +Locations involved: + +- `src/adapters/inbound/cli/help/index.ts` +- `src/adapters/inbound/cli/help/catalog.ts` +- `src/adapters/inbound/cli/globals/index.ts` +- `src/adapters/inbound/cli/registry/index.ts` +- `src/adapters/inbound/cli/commands/network.ts` +- help/golden tests and baselines + +Must support and test: + +- [ ] `wallet-cli --help` + - Shows the supported families: TRON, EVM. + - Explains that the command implementation is selected by `--network`. + - Has at least one `--network evm:1` example. + - `stake` is clearly marked TRON-only. +- [ ] `wallet-cli evm --help` + - Shows the EVM-available command tree, without `stake`. + - If the `evm` prefix is used only for help/catalog discovery and cannot be used for ordinary execution, it must say so explicitly in the output. +- [ ] `wallet-cli evm tx send --help` + - Shows only EVM fields, EVM examples, the fee-model explanation, and the EVM address format. +- [ ] `wallet-cli tx send --help` + - The merged logical help must clearly mark family-specific flags/examples; it must not just take the metadata of the registry's first family. +- [ ] `wallet-cli tx send --network evm:1 --help` + - Meta parsing must correctly consume the `--network` value and parse it into EVM help; it must not treat `evm:1` as a command positional. +- [ ] `wallet-cli networks --help` + - Explains the canonical id `evm:`, aliases, and the source of custom networks. +- [ ] `wallet-cli --json-schema` + - The full catalog includes the `evm.*` commands. + - The top level of the catalog should add an enabled-families and available-networks summary. +- [ ] `wallet-cli evm --json-schema` + - Emits only EVM chain commands; the schema and examples contain no TRON-only flags. +- [ ] `--json-schema` for each EVM leaf + - The input schema, requires, capability, examples, and stdin flags are correct. +- [ ] global `--network` description + - The examples include at least `tron:nile`, `evm:1`, an alias, and the config fallback. +- [ ] unknown/disabled family, unknown EVM network, and family/network mismatch all emit a clear usage error and exit 2. + +### Phase 11: Text and JSON Output + +Locations involved: + +- `src/adapters/inbound/cli/render/index.ts` +- `src/adapters/inbound/cli/render/scalars.ts` +- `src/adapters/inbound/cli/output/envelope.ts` +- `src/adapters/inbound/cli/contracts/envelope.ts` +- formatter/envelope/golden tests + +Work list: + +- [ ] Add EVM hooks to `FAMILY_RENDER`. +- [ ] Display the native amount per the network `nativeCurrency`, not assuming every EVM network is ETH. +- [ ] EVM transaction info/receipt shows hash, from/to/value, nonce, gas, fee, status, block, and contract address. +- [ ] Both legacy and EIP-1559 fee text render correctly. +- [ ] wallet/list/current/import/derive show both TRON and EVM addresses, and the address is not wrongly abbreviated or mislabeled by family. +- [ ] The `networks` text table shows the EVM chain id, native symbol, and fee model. +- [ ] The JSON envelope keeps `wallet-cli.result.v1` and emits: + - `command: "evm...."` + - `chain.family: "evm"` + - `chain.networkId: "evm:"` + - `chain.chainId: ""` +- [ ] All wei, gas, fee, nonce, and block quantities avoid JavaScript number precision loss; JSON uses a stable string rule. +- [ ] error, warning, and progress still obey the stdout/stderr and single-terminal-frame contract. + +### Phase 12: Token Book and Price Provider + +Locations involved: + +- `src/adapters/outbound/tokenbook/builtins.ts` +- `src/adapters/outbound/tokenbook/index.ts` +- `src/application/ports/token-repository.ts` +- `src/adapters/outbound/price/coingecko.ts` +- `src/application/ports/price-provider.ts` + +Work list: + +- [ ] Add official ERC-20 token entries for the chosen builtin EVM networks; a testnet may keep an empty list. +- [ ] The ERC-20 contract id uses a consistent normalized/checksummed comparison to avoid case-based duplicates. +- [ ] Confirm the token book scope is still `(networkId, accountRef)`, so different EVM chains do not share a list. +- [ ] The CoinGecko native-coin id and asset platform must not be derived from the `evm:` prefix alone; they must be decided by the actual network mapping/config. +- [ ] When a custom EVM network has no price mapping, return null/warning, and do not fail the portfolio command. +- [ ] token price lookup, official/user merge, remove protection, and portfolio tests cover EVM. + +### Phase 13: Tests and Quality Gates + +#### Unit tests + +- [ ] EVM address derive/validate/checksum. +- [ ] BIP44 coin type 60 and seed/private-key address derivation. +- [ ] network descriptor validation, canonical id, arbitrary chain id, aliases, RPC chain mismatch. +- [ ] wallet migration, backfill, dedup, watch/Ledger family pinning. +- [ ] EVM gateway RPC normalization and typed errors. +- [ ] software/Ledger transaction and message signing. +- [ ] legacy/EIP-1559 transaction build, estimate, broadcast, confirmation. +- [ ] ERC-20, contract, block, account, and portfolio use cases. +- [ ] EVM commands, registry routing, target/capability gates, renderers, envelopes. + +#### CLI/golden tests + +- [ ] root/family/group/leaf `--help`. +- [ ] root/family/leaf `--json-schema`. +- [ ] `networks` text + JSON include `evm:1` and custom `evm:31337`. +- [ ] `config defaultNetwork evm:1` and alias round trip. +- [ ] `--network evm:1` routes to an `evm.*` command id. +- [ ] The same logical command yields a different schema, client, and output under TRON/EVM. +- [ ] wrong-family account, unknown chain, alias collision, unsupported network trait. +- [ ] JSON one-frame, exit `0/1/2`, stderr progress, secret redaction. +- [ ] All old TRON golden tests still pass; the expected output of the root help switches to the new multichain baseline. + +#### Integration/live tests + +- [ ] Add a local EVM suite, recommended Anvil, covering account, native/ERC-20 send, contract, block, sign-only, broadcast, `--wait`. +- [ ] Add a public EVM testnet smoke suite, using an isolated wallet home and secret source, not logging the private key. +- [ ] Keep the Nile live suite, confirming the EVM changes cause no TRON regression. +- [ ] If supporting Ledger EVM, add Speculos or real-device smoke tests. + +#### Required commands + +```bash +npm run typecheck +npm run depcruise +npm test +npm run build +npm run test:parity:help +npm run test:live:nile +# new: EVM local integration suite +# new: EVM public-testnet smoke suite +``` + +### Phase 14: User Documentation and Release + +Locations involved: + +- `README.md` +- `docs/architecture.md` +- network/config example docs +- command/help baselines +- release notes and migration notes + +Work list: + +- [ ] Change the README to TRON + EVM, adding `evm:1`, custom chain, wallet, and send examples. +- [ ] Mark the architecture diagram and the family-extension section as EVM-implemented, no longer writing it as a future item. +- [ ] The docs list the builtin networks, canonical id, aliases, custom-network schema, and how to set `defaultNetwork`. +- [ ] Explain that the same wallet has different TRON/EVM derivation paths, and that watch/Ledger are single-family. +- [ ] Explain optional capabilities such as EVM history, contract metadata, price, and Ledger, and their network requirements. +- [ ] Provide the behavior of upgrading from the old wallets schema, the backup recommendation, and the failure-recovery method. +- [ ] Before release, run end-to-end acceptance once each with a brand-new home and an old-version home. + +## 4. Master Table of File Changes + +| Layer | Modified | Added | +| --- | --- | --- | +| Domain | family, address, network, wallet, tx, token, errors | EVM codec/types (may cohere within existing modules) | +| Application contracts/ports | gateway map, ledger/transaction contracts, capabilities | `evm-gateway.ts` | +| Application services | signer, pipeline, target, capability | `evm-confirmation.ts` | +| Application use cases | shared message/wallet integration | `use-cases/evm/*` | +| Outbound adapters | config, keystore, ledger, tokenbook, price, gateway registry | `chain/evm/*` | +| Inbound CLI | schemas, shell, registry, help, render, output, wallet/network commands | `commands/evm/*` | +| Bootstrap | family types, registry, composition, tests | `families/evm.ts` | +| Tooling | dependencies, tsup, test scripts, baselines | EVM local/live scripts and fixtures | +| Docs | README, architecture, network/config docs | migration/release notes | + +## 5. Unacceptable Shortcuts + +- Do not merely add `evm` to the union without handling the old-wallet address-cache migration. +- Do not type-cast a custom network directly in `ConfigLoader` without validation. +- Do not add EVM RPC methods into `TronGateway` or create a universal gateway that contains all chains' methods. +- Do not let all EVM networks automatically gain explorer, history, or EIP-1559 capability just because the family has the command. +- Do not let root help, leaf help, and JSON Schema still show only TRON examples. +- Do not hardcode all EVM native currencies as ETH. +- Do not convert a bigint fee/value into an unsafe JavaScript number. +- Do not leak an API key / private key in the RPC URL, an error, a verbose log, the JSON envelope, or a test artifact. +- Do not break TRON command ids, the JSON envelope, exit codes, or the stdout/stderr discipline by adding EVM. + +## 6. Final Acceptance Examples + +EVM may be declared publicly supported only when all of the following behaviors hold: + +```bash +wallet-cli --help +wallet-cli evm --help +wallet-cli evm tx send --help +wallet-cli tx send --network evm:1 --help +wallet-cli --json-schema +wallet-cli evm --json-schema + +wallet-cli networks +wallet-cli config defaultNetwork evm:1 +wallet-cli account balance --network evm:1 +wallet-cli account balance --network evm:31337 +wallet-cli tx send --network evm:1 --to 0x... --amount 0.01 +wallet-cli token balance --network evm:1 --contract 0x... +wallet-cli contract call --network evm:1 --contract 0x... ... +wallet-cli block --network evm:1 +wallet-cli message sign --network evm:1 --message hello +``` + +Here `evm:31337` must be providable by the user's config; it is not required that every chain id become a builtin network. diff --git a/ts/docs/typescript-wallet-cli-architecture-source-of-truth.md b/ts/docs/typescript-wallet-cli-architecture-source-of-truth.md new file mode 100644 index 000000000..812046bbb --- /dev/null +++ b/ts/docs/typescript-wallet-cli-architecture-source-of-truth.md @@ -0,0 +1,721 @@ +# TypeScript Wallet CLI Architecture Specification (Source of Truth) + +```mermaid +flowchart LR + USER([User / Agent]):::ext + + subgraph INB["📥 inbound · CLI"] + IN["parse argv ▶
◀ render output"] + end + + subgraph CORE["Core (independently testable)"] + direction TB + APP["🎯 application
use cases · ports"] + DOM["💎 domain
pure rules"] + APP --> DOM + end + + subgraph OUTB["📤 outbound"] + OUT["Keystore · TronWeb
Ledger · CoinGecko"] + end + + USER ==>|drives| IN + IN ==>|calls| APP + APP ==>|"calls port"| OUT + OUT -.->|"implements port (dependency points inward)"| APP + IN ==>|renders result| USER + + classDef ext fill:#e8e8e8,stroke:#888,color:#333 + classDef inb fill:#d4edff,stroke:#1f78b4,color:#0b3d66 + classDef core fill:#d5f5e3,stroke:#27ae60,color:#145a32 + classDef outb fill:#fadbd8,stroke:#c0392b,color:#641e16 + class USER ext + class IN inb + class APP,DOM core + class OUT outb +``` + +> Status: the single current architecture contract +> Applies to version: `wallet-cli 0.1.x` +> Runtime: Node.js 20+, ESM, TypeScript +> Current chain support: TRON (mainnet, Nile, Shasta) + +This document fully defines the system boundaries, dependency direction, composition, command routing, application ports, wallet and transaction flows, persistence, output, and extension rules of the TypeScript Wallet CLI. The document itself is the single specification for architecture and behavior; it does not require any other design document to be understood. + +If the implementation and this document disagree, the change must fix one side or the other — the document must not describe an abstraction that does not exist for any length of time. + +--- + +## 1. System Goals and Boundaries + +### 1.1 Goals + +1. Provide humans and agents with the same stable CLI, JSON envelope, command id, and exit codes. +2. Keep Domain and Application as the core; isolate external I/O behind ports and adapters. +3. inbound CLI and outbound infrastructure are peers, assembled only in Bootstrap. +4. Keep chain-family differences inside the family plugin, family use cases, gateway, and signing strategy. +5. Encrypt private keys, mnemonics, and BIP39 passphrases at rest; Ledger/watch-only hold no secrets. +6. Each execution produces exactly one terminal result on stdout; progress and diagnostics go to stderr. +7. A single Zod schema drives validation, yargs arity, help, and JSON Schema. +8. Use dependency-cruiser, typecheck, contract tests, unit tests, and build to prevent architectural and behavioral regression. + +### 1.2 Current Boundaries + +- The only formal `ChainFamily` is currently `tron`; EVM is a planned but not-yet-public family. +- Ledger currently implements only the TRON app. +- Network transport is TRON FullNode HTTP / TronWeb; `httpEndpoint` is not an Ethereum JSON-RPC or gRPC endpoint. +- `create`, the various `import` commands, `delete`, and `backup` may be interactive in a controlled way; other commands fail fast when arguments are missing. +- Secrets are not accepted from argv plaintext or ordinary files; only a dedicated stdin channel or hidden TTY prompt is allowed. + +--- + +## 2. Architecture and Dependency Rules + +### 2.1 The Four Architectural Areas + +```mermaid +flowchart LR + BOOTSTRAP[bootstrap
process lifecycle and assembly] --> INBOUND[adapters/inbound
drives Application] + BOOTSTRAP --> OUTBOUND[adapters/outbound
implements Application ports] + INBOUND --> APPLICATION[application
use cases, orchestration, ports] + OUTBOUND --> APPLICATION + APPLICATION --> DOMAIN[domain
pure rules and values] +``` + +| Area | May depend on | Must not depend on | +| --- | --- | --- | +| `domain` | Node / third-party pure libraries, same area | `application`, `adapters`, `bootstrap` | +| `application` | `domain`, application-internal contracts/ports | `adapters`, `bootstrap` | +| `adapters/inbound` | `application`, `domain`, inbound-internal | `adapters/outbound`, `bootstrap` | +| `adapters/outbound` | `application` ports, `domain`, outbound-internal | `adapters/inbound`, `bootstrap` | +| `bootstrap` | all areas | none; but it only does assembly and process lifecycle | + +These are conceptual dependency rules. Even when a type-only import produces no runtime edge, it must still follow the same direction. Circular dependencies are always forbidden. + +The diagram below is a detailed view (dependency view) of the same rules. **Solid lines are the runtime call direction (left to right); dashed lines are the compile-time dependency/implements direction (always pointing inward).** Their opposite directions are exactly what dependency inversion looks like in concrete form: application calls outbound (rightward), but outbound depends on application's port (leftward). This diagram depicts responsibilities and dependencies, not the order of process execution; the real runtime entry/exit is wrapped by `bootstrap/runner.ts` (see §3.1). + +```mermaid +flowchart LR + USER([User / Agent]):::ext + + subgraph INB["inbound · CLI (driving side)"] + direction TB + IN_PARSE["Controller
shell · arity · Zod schemas"] + IN_CMD["commands
argv to use case"] + IN_OUT["Presenter
envelope · render · stream"] + IN_PARSE --> IN_CMD --> IN_OUT + end + + subgraph CORE["Core (independently testable)"] + direction TB + subgraph APP["application"] + direction TB + UC["use-cases
TronTransactionService · WalletService"] + SVC["services
TxPipeline · SignerResolver · Target"] + CON["contracts
ExecutionPolicy · TransactionScope"] + PORT{{"ports (owned by application)
WalletRepository · TronGateway · LedgerDevice"}} + UC --> SVC + UC -.in/out.-> CON + UC --> PORT + SVC --> PORT + end + subgraph DOM["domain (pure rules · zero I/O)"] + DM["address · amounts · derivation
wallet · family · errors"] + end + APP --> DOM + end + + subgraph OUTB["outbound (implements ports)"] + direction TB + O_KS["keystore to WalletRepository"] + O_TRON["chain/tron to TronGateway"] + O_PRICE["price to PriceProvider"] + O_LED["ledger to LedgerDevice"] + O_CFG["config · persistence · tokenbook"] + end + + subgraph EXT["Frameworks & Drivers"] + E["TRON nodes · filesystem
Ledger · CoinGecko"] + end + + %% call direction (runtime, solid) — L→R spine + USER ==>|drives| IN_PARSE + IN_CMD ==>|calls| UC + PORT ==>|resolved to adapter| OUTB + OUTB ==>|I/O| EXT + IN_OUT -. result .-> USER + + %% dependency / implements (compile-time, dashed, points inward) + O_KS -.->|implements| PORT + O_TRON -.->|implements| PORT + O_PRICE -.->|implements| PORT + O_LED -.->|implements| PORT + O_CFG -.->|implements| PORT + + %% bootstrap: bottom rail, injects upward + subgraph BOOT["bootstrap (single composition root)"] + direction LR + COMP["composition.ts
new + inject"] + PLUG["family-registry
FamilyPlugin (TRON · EVM later)"] + end + PLUG -.-> COMP + COMP -.->|inject| UC + COMP -.->|inject| O_KS + COMP -.->|inject| O_TRON + COMP -.->|wire| IN_CMD + + classDef ext fill:#e8e8e8,stroke:#888,color:#333 + classDef boot fill:#fff3cd,stroke:#d4a017,color:#5c4500 + classDef inb fill:#d4edff,stroke:#1f78b4,color:#0b3d66 + classDef app fill:#d5f5e3,stroke:#27ae60,color:#145a32 + classDef dom fill:#fdebd0,stroke:#e67e22,color:#7e3f0b + classDef outb fill:#fadbd8,stroke:#c0392b,color:#641e16 + + class USER,E ext + class COMP,PLUG boot + class IN_PARSE,IN_CMD,IN_OUT inb + class UC,SVC,CON,PORT app + class DM dom + class O_KS,O_TRON,O_PRICE,O_LED,O_CFG outb +``` + +### 2.2 Why inbound and outbound do not depend on each other + +A CLI command should not know about Keystore, TronWeb, CoinGecko, or the Ledger transport; it only calls a use case. An outbound adapter likewise should not know about Zod, yargs, the CLI envelope, or the renderer; it only implements an application port. The two are injected into the same object graph only in `bootstrap/composition.ts`. + +### 2.3 Actual Directory Responsibilities + +```text +src/ +├── index.ts # process entry +├── bootstrap/ +│ ├── argv.ts # global/secret flags scan before yargs +│ ├── runner.ts # invocation lifecycle + terminal error funnel +│ ├── composition.ts # the single general composition root +│ ├── family-registry.ts # enabled family plugins and familyMap +│ └── families/ +│ ├── types.ts # FamilyPlugin contract +│ └── tron.ts # TRON gateway/use cases/commands assembly +├── domain/ +│ ├── address/ amounts/ derivation/# pure value rules +│ ├── errors/ # typed errors + exit semantics +│ ├── family/ resources/ sources/ # exhaustive facts registries +│ ├── types/ # domain data shapes +│ └── wallet/ # account refs, address projections, vault codec +├── application/ +│ ├── contracts/ # execution policy/scope/progress +│ ├── ports/ # required external capabilities +│ ├── services/ # target/capability/signer/pipeline/confirmation +│ └── use-cases/ # wallet/config/message/TRON workflows +└── adapters/ + ├── inbound/cli/ + │ ├── commands/ # schema + use-case translation + │ ├── contracts/ context/ # CLI-only command/runtime contracts + │ ├── globals/ arity/ schemas/ # flag single source + Zod projections + │ ├── shell/ registry/ help/ # routing and discovery + │ ├── input/ # secret + prompt + │ └── output/ render/ stream/ # terminal presentation + └── outbound/ + ├── chain/tron/ # gateway, history, signing strategy + ├── config/ keystore/ # config and wallet persistence + ├── ledger/ # device adapter + ├── persistence/ # crypto, atomic FS, backup writer + ├── tokenbook/ # TokenRepository + └── price/ # PriceProvider +``` + +--- + +## 3. Startup, Composition, and Process Lifecycle + +### 3.1 Startup Flow + +```mermaid +flowchart LR + ARGV[process.argv] --> PRE[parseGlobals] + PRE --> COMPOSE[composeCliRuntime] + COMPOSE --> META{help/version/schema
or bare invocation?} + META -->|yes| HELP[HelpService] + META -->|no| SHELL[buildCli + parseAsync] + HELP --> FUNNEL[Runner terminal boundary] + SHELL --> FUNNEL + FUNNEL --> CLOSE[close Prompter] +``` + +1. `src/index.ts` only calls `main(process.argv)` and sets `process.exitCode`; it does not call `process.exit()`. +2. `bootstrap/argv.ts` scans globals before yargs, because the output mode and secret source must be decided first. +3. `composeCliRuntime()` loads config and builds streams, formatter, outbound adapters, application services/use cases, the command registry, and the target/capability gates. +4. `FAMILY_REGISTRY` assembles each family's metadata, sign strategy, gateway factory, and command module into a plugin. +5. The family plugin builds family-specific use cases and then injects them into the inbound `ChainModule`. +6. Command-backed capabilities are derived from the registry's `capability` field and merged with network traits. +7. A meta request short-circuits before building the yargs execution, but uses the same streams and error-output rules. +8. The Runner catches all typed/unknown errors, normalizes them, emits output, decides the exit code, and finally closes the `/dev/tty` handle. + +### 3.2 The `FamilyPlugin` Contract + +```ts +interface FamilyPlugin { + readonly meta: FamilyMeta & { family: F } + readonly signStrategy: SignStrategy + createGateway(network: NetworkDescriptor): ChainGatewayMap[F] + createModule(deps: FamilyApplicationDependencies): ChainModule +} +``` + +`bootstrap/families/tron.ts` is TRON's concrete composition: it builds the `TronRpcClient`, the TronGrid history reader, the TRON use cases, and the `TronModule`. Application and adapters must not import the family registry in reverse. + +--- + +## 4. Command Contract and Dispatch + +### 4.1 `CommandDefinition` + +`CommandDefinition` is the contract of the inbound CLI adapter, not a Domain/Application model. + +| Field | Contract | +| --- | --- | +| `path` | Neutral commands use the full path; chain commands use a cross-family logical path. | +| `family` | Omitted for neutral commands; when present, the resolved network selects the family implementation. | +| `stdin` | A dedicated stdin channel for `privateKey`, `mnemonic`, `tx`, `message`. | +| `network` | `none`, `optional`, `required`; today both optional/required can fall back to the default network. | +| `wallet` | `none` or `optional`; optional can override the active account with `--account`. | +| `auth` | An unlock declaration for help/catalog; actual software signing uses lazy decrypt. | +| `broadcasts` | Controls whether help reveals `--wait`. | +| `passwordMode` | `establish` or `verify`, controls interactive master-password priming. | +| `interactive` | Only commands that explicitly opt in may open a TTY prompt. | +| `capability` | A per-network capability that must pass before execution. | +| `fields` / `input` | Zod field metadata and the complete validation schema. | +| `run` | Translates CLI input/context into a use-case call and returns structured data. | +| `formatText` | Optional text renderer; JSON does not use it. | + +The stable command id is derived from metadata: a neutral command is `path.join(".")`, e.g. `import.mnemonic`; a chain command is `family.path`, e.g. `tron.tx.send`. + +### 4.2 The Two Command Classes and Routing + +```mermaid +flowchart LR + PATH[Parsed path] --> KIND{neutral exact match?} + KIND -->|yes| NEUTRAL[resolveNeutral] + KIND -->|no| CAND[resolveCandidates] + CAND --> NET[resolve explicit/default network] + NET --> FAMILY[choose candidate by network.family] + NEUTRAL --> EXEC[common executeCommand] + FAMILY --> EXEC +``` + +- `tron` is not a public prefix for ordinary execution commands; `--network` decides the family. +- Help/JSON Schema may use the family prefix to address a concrete implementation precisely. +- An unknown top-level/subcommand/flag must return `unknown_command` or `invalid_option`; yargs must not silently succeed. + +### 4.3 The Fixed Dispatch Order + +```mermaid +flowchart LR + ROUTE[Route] --> FLAGS[Reject unknown flags] + FLAGS --> TARGET[Resolve target] + TARGET --> CAP[Capability gate] + CAP --> PASSWORD[Optional password prime] + PASSWORD --> GAP[Optional TTY gap-fill] + GAP --> ZOD[Zod parse] + ZOD --> CTX[Build ExecutionContext] + CTX --> ACCOUNT[Resolve account if wallet-bound] + ACCOUNT --> RUN[Command → use case] + RUN --> FORMAT[Text or JSON] + FORMAT --> RESULT[Stream result exactly once] +``` + +`ExecutionContext` is the CLI context; an Application workflow receives only the narrower `ExecutionPolicy`, `ExecutionSelection`, `AccountScope`, or `TransactionScope`, and does not depend on the full picture of CLI streams/config/envelope. + +--- + +## 5. The Public Command Surface + +```text +wallet-cli +├── create +├── import mnemonic | private-key | ledger | watch +├── list | use | current | rename | derive | backup | delete +├── config | networks +├── account balance | info | history | portfolio +├── token balance | info | add | list | remove +├── tx send | broadcast | status | info +├── contract call | send | deploy | info +├── stake freeze | unfreeze | withdraw | cancel-unfreeze | delegate | undelegate +├── message sign +└── block [number] +``` + +Neutral commands do not touch a chain. Chain commands are currently all provided by the TRON plugin. All transaction-creating commands jointly support: + +- `--dry-run`: build + estimate, no decrypt, no sign, no broadcast. +- `--sign-only`: build + estimate + sign, returns a signed transaction. +- No mode flag: sign + broadcast. +- `--wait`: wait for confirmation only after broadcast. + +### 5.1 Global Flags + +| Flag | Runtime semantics | +| --- | --- | +| `--output` / `-o` | `text` or `json`; defaults from config. | +| `--network` | Canonical network id; a chain command falls back to `defaultNetwork` when omitted. | +| `--account` | Account ref/label/address; overrides only for this execution. | +| `--timeout` | Timeout for a single RPC/device operation. | +| `--verbose` / `-v` | Additional diagnostics. | +| `--wait` | Poll for confirmation after broadcast. | +| `--wait-timeout` | Upper bound for confirmation polling, default 60000 ms. | +| `--password-stdin` | Read the master password from fd 0. | +| `--help` / `--version` / `--json-schema` | Meta requests. | + +The single registration point for global flags is `adapters/inbound/cli/globals/GLOBAL_FLAG_SPECS`; the argv scan, yargs options, and help/catalog are all projected from it. + +--- + +## 6. Domain Model + +### 6.1 Wallet, Account, and Source + +```mermaid +flowchart LR + WALLET[Wallet wlt_x] --> SOURCE[One Source] + SOURCE -->|seed| HD[wlt_x.0 / .1 / ...] + SOURCE -->|privateKey| PK[one account wlt_x] + SOURCE -->|ledger| LEDGER[one single-family account] + SOURCE -->|watch| WATCH[one single-family account] +``` + +```ts +type Source = + | { type: "seed"; vaultId: string; addresses: Record } + | { type: "privateKey"; keyId: string; addresses: ChainAddresses } + | { type: "ledger"; family: ChainFamily; path: string; address: string } + | { type: "watch"; family: ChainFamily; address: string } +``` + +| Source | HD | Local secret | Family scope | Signing | +| --- | --- | --- | --- | --- | +| seed | yes | encrypted entropy/passphrase | all enabled families | software | +| privateKey | no | encrypted raw key | all enabled families | software | +| ledger | no | none | single family/path | device | +| watch | no | none | single family | forbidden | + +The account is the unit of selection and operation. `--account` accepts a canonical ref, a unique label, or a unique address; for a multi-account seed, when only a wallet ref is given, the index must not be guessed. + +### 6.2 Derivation and Addresses + +- BIP39 English wordlist; `create` generates 128-bit entropy (12 words). +- HD path: `m/44'/{coinType}'/{account}'/0/0`; the TRON coin type is 195. +- secp256k1 derives the address from an uncompressed 65-byte public key. +- The seed vault stores encrypted entropy and an optional BIP39 passphrase, not the mnemonic string directly. +- The public address cache lives in wallet metadata; read/build/estimate do not require decrypting secrets. +- The Domain `family`, `sources`, and `resources` registries must be exhaustively keyed; adding a union member forces the type system to fill in the related facts. + +### 6.3 Active Account + +- The first registered account automatically becomes active. +- `use` persistently changes `activeAccount`; `--account` does not persist. +- When the active account is deleted, the first remaining account is chosen; if none, it is set to `null`. +- `current` returns only the persistent active account. + +--- + +## 7. Application: Use Cases, Services, and Ports + +### 7.1 Ports + +Application defines capabilities, not concrete technologies: + +| Port | Purpose | Current adapter | +| --- | --- | --- | +| `WalletRepository` / `AccountStore` | wallet/account query, mutation, decrypt | `Keystore` | +| `BackupWriter` | safely write a plaintext backup | `SecureBackupWriter` | +| `ConfigDocumentRepository` | atomic config document update | `YamlConfigDocument` | +| `NetworkRegistry` | canonical network id/default resolution | outbound config registry | +| `LedgerDevice` | address, tx/message signing, app config | `Ledger` | +| `ChainGatewayProvider` | obtain a gateway by network/family | `ChainGatewayRegistry` | +| `TronGateway` | TRON reads/build/estimate/broadcast | `TronRpcClient` | +| `TronHistoryReader` | TronGrid transaction history | `TronGridHistoryReader` | +| `TokenRepository` | official/user token book | `TokenBook` | +| `PriceProvider` | best-effort USD price | CoinGecko/Null provider | +| `PromptPort` | the minimal interaction capability Application needs | inbound Prompter | + +`PromptPort` is one of the few ports implemented by an inbound adapter and consumed by Application; this does not change the dependency direction, because Application owns only the interface. + +### 7.2 Use Cases + +- `WalletService`: create/import/list/use/current/rename/derive/delete/backup, with no knowledge of JSON/Zod/yargs. +- `ConfigService`: effective config view, key validation, canonical network normalization, and document update. +- `MessageService`: sign a message via the signer port. +- TRON use cases: account, token, transaction, contract, stake, block; they use only the TRON gateway and the necessary shared ports. + +An inbound command's responsibility is to turn argv/Zod input and `ExecutionContext` into use-case input and then choose a stable output view; it must not do persistence or provider transport itself. + +### 7.3 Reusable Services + +- `TargetResolver`: network selection and single-family account compatibility. +- `CapabilityRegistry`: per-network feature gate. +- `SignerResolver`: source → software/device signer. +- `TxPipeline`: shared build/estimate/sign/broadcast lifecycle. +- `transactionMode`: decides `dryRun`/`signOnly`/broadcast mode. +- `tronConfirmation`: TRON-specific polling/receipt normalization, not pushed into the generic pipeline. + +--- + +## 8. Network, Gateway, and Capability + +The current descriptor: + +```ts +interface TronNetworkDescriptor { + id: string + family: "tron" + chainId: string + aliases: string[] + httpEndpoint?: string + feeModel?: "tron-resource" + capabilities: string[] +} +``` + +| ID | Alias | Endpoint | +| --- | --- | --- | +| `tron:mainnet` | `tron` | `https://api.trongrid.io` | +| `tron:nile` | `nile` | `https://nile.trongrid.io` | +| `tron:shasta` | `shasta` | `https://api.shasta.trongrid.io` | + +Canonical-id resolution is case-insensitive. Aliases remain descriptor metadata but are not accepted as network selectors. Both `network: optional/required` adopt `config.defaultNetwork` when not specified, and that value must be a canonical id. Ledger/watch pin a single family, and a family mismatch must fail before any RPC. + +`ChainGatewayRegistry` is injected with the family factory by Bootstrap and caches the client by network id. Its generic `client()` may only use the truly common minimal capabilities; a family use case obtains the `TronGateway` via the guarded `get(net, "tron")`. TRON staking and the future EVM gas/nonce must not be forced into a universal gateway. + +Capabilities consist of two parts: the command-backed keys declared by registered commands, plus the network traits in `NetworkDescriptor.capabilities`. The gate must happen before the use case. + +--- + +## 9. Signer and Transaction Flow + +### 9.1 Signer Resolution + +```mermaid +flowchart LR + REF[account ref] --> SOURCE{source.type} + SOURCE -->|seed| SEED[lazy decrypt vault + derive] + SOURCE -->|privateKey| KEY[lazy decrypt key] + SOURCE -->|ledger| DEVICE[Ledger signer + path] + SOURCE -->|watch| REJECT[watch_only_no_signer] + SEED --> SOFT[SoftwareSigner + family strategy] + KEY --> SOFT +``` + +The software signer obtains the key only at the moment of an actual `sign()`; a dry-run does not trigger decryption. The Ledger signer verifies the app/address before signing, and if the cached address does not match the device it returns `wrong_device_seed`. + +### 9.2 Pipeline + +```mermaid +flowchart LR + RESOLVE[resolve signer] --> BUILD[build + timeout] + BUILD --> EST[estimate + timeout] + EST --> MODE{mode} + MODE -->|dry-run| PLAN[plan] + MODE -->|sign| SIGN[software / Ledger sign] + SIGN --> CAST{broadcast?} + CAST -->|no| SIGNED[signed] + CAST -->|yes| SEND[broadcast] + SEND --> WAIT{--wait?} + WAIT -->|no| SUB[submitted] + WAIT -->|yes| CONFIRM[family confirmation] + CONFIRM --> FINAL[confirmed / failed
or timeout → submitted] +``` + +The pipeline knows only the signer and the `Broadcaster` port; the family use case provides build, estimate, and confirm callbacks. `timeoutMs` limits a single operation; `waitTimeoutMs` limits confirmation polling. Once a transaction has been broadcast, a polling error/timeout must not reclassify the command as not-broadcast — it returns `submitted`. + +### 9.3 Ledger + +- When `SPECULOS_PORT` is present, use the Speculos HTTP transport; otherwise USB/HID. +- Transports and `hw-app-trx` are lazily imported and closed after each operation. +- The `m/` is stripped from the Ledger path before it is passed to the app. +- APDU `0x6985` → `signing_rejected`; an unavailable device/app/transport → `auth_required`. + +--- + +## 10. Persistence and Cryptography + +### 10.1 Root and Files + +The root uses a non-empty `WALLET_CLI_HOME` in order of preference, otherwise `$HOME/.wallet-cli`. + +```text +/ +├── config.yaml +├── wallets.json +├── tokens.json +├── verifier.json +├── vaults/vlt_.json +├── keys/key_.json +└── backups/-.json +``` + +`AtomicFileStore` writes use a unique temp file in the same directory, mode `0600`, and an atomic rename. Mutations are serialized with `.lock` + `O_EXCL`; a dead PID/stale lock can be reclaimed. + +### 10.2 `wallets.json` + +```json +{ + "version": 1, + "activeAccount": "wlt_abcd1234.0", + "wallets": [{ + "id": "wlt_abcd1234", + "source": { + "type": "seed", + "vaultId": "vlt_efgh5678", + "addresses": { "0": { "tron": "T..." } } + } + }], + "labels": { "wlt_abcd1234.0": "main" } +} +``` + +IDs are random 5-byte Crockford base32 lowercase strings. Labels are case-insensitively unique and must not begin with `wlt_`. The seed's known indices equal the `addresses` keys; Ledger/watch are deduplicated by source identity and are not merged with a software wallet that has the same address. + +### 10.3 Token and Config + +The user entries in `tokens.json` are partitioned by `|`; the effective list is official first, then user-only, deduplicated by `(kind,id)`. Official entries cannot be deleted/overwritten. + +`config.yaml` is shallow-merged with the builtin config. The only writable keys are `defaultNetwork`, `defaultOutput`, `timeoutMs`; `networks` is a CLI read-only view. Runtime globals are not written back to config. + +### 10.4 Encrypted Blobs + +`verifier.json`, vaults, and keys use scrypt (N=262144, r=8, p=1, dkLen=32), AES-128-CTR, and a `keccak256(derivedKey[16..31] + ciphertext)` MAC. Each blob has its own 32-byte salt and 16-byte IV but shares the keystore master password. A MAC mismatch returns `auth_failed`; the password is never written to disk. + +Backup is allowed only for seed/private-key; the plaintext secret file must be `0600` and must not overwrite an existing file; the terminal/envelope returns only metadata, not the secret. + +--- + +## 11. Secret and Interaction Policy + +```mermaid +flowchart LR + NEED[Secret needed] --> SOURCE{source} + SOURCE -->|--kind-stdin| STDIN[fd 0 once] + SOURCE -->|interactive TTY| HIDDEN[hidden prompt] + SOURCE -->|none| ERROR[missing_option / auth_required] +``` + +- A handler must not read `process.stdin` directly; `StreamManager.readStdinOnce()` reads at most once per execution. +- A single invocation may use fd 0 through only one `--*-stdin` channel. +- Secret argv, `MASTER_PASSWORD`, `--*-file`, and ordinary env secrets are not supported. +- A secret must not enter logs, diagnostics, error details, or the result envelope. +- Interactive allowlist: create, the four imports, delete, backup; the order is password → field gap-fill/account selection → command confirm. + +--- + +## 12. Output, Stream, and Error + +| Data | Text mode | JSON mode | +| --- | --- | --- | +| Successful terminal result | stdout once | one result envelope on stdout | +| Failed terminal result | stderr once | one error envelope on stdout | +| Progress | stderr | stderr JSON event | +| Warning | stderr/collected | `meta.warnings` | +| Debug | verbose stderr | verbose stderr | + +The JSON schema is fixed as `wallet-cli.result.v1`. A chain command envelope includes family, network id/name, and chain id; a neutral command omits chain. `bigint` is converted to a decimal string and `Uint8Array` to hex. A second terminal result must throw `internal_error`. + +Exit codes: success/meta = 0; execution error = 1; usage error = 2. An unknown exception is normalized into a redacted `internal_error`; the raw text of a third-party error must not enter the public envelope. + +--- + +## 13. Help and Machine-Readable Introspection + +Supports root/group/leaf help, version, the full catalog JSON Schema, and a single-command JSON Schema. The data flow: + +```mermaid +flowchart LR + ZOD[fields + input] --> ARITY[yargs arity] + ZOD --> HELP[help flags] + ZOD --> SCHEMA[JSON Schema] + DEF[CommandDefinition] --> HELP + DEF --> CATALOG[machine catalog] + GLOBAL[GLOBAL_FLAG_SPECS] --> ARITY & HELP & CATALOG +``` + +A hand-maintained command flag table must not be created separately. The public help/output is a stable contract; when it changes, automated tests must verify root, group, leaf, JSON Schema, and functional scenarios. + +--- + +## 14. Rules for Adding Features + +### 14.1 Adding a Command + +1. Decide whether it is a neutral or a family logical command. +2. Application first creates/extends the use case and the required ports. +3. Outbound capabilities implement the port with an adapter; the use case must not import the adapter. +4. The inbound command defines the Zod fields/input, policy metadata, use-case translation, and renderer. +5. Register it with the neutral registrar or the family `ChainModule`. +6. Add use-case, adapter, registry/dispatch, and output/help tests, and update the inventory in this document. + +A command is forbidden to build TronWeb/Keystore directly, write to process stdout, perform a filesystem mutation, or treat a provider wire response as the renderer's business model. + +### 14.2 Adding a Chain Family + +```mermaid +flowchart LR + DOMAIN[1 Domain family/address/network] --> PORT[2 family gateway port] + PORT --> ADAPTER[3 gateway + signing adapters] + ADAPTER --> USECASE[4 family use cases] + USECASE --> CLI[5 family CLI module] + CLI --> PLUGIN[6 bootstrap plugin + registry] + PLUGIN --> TEST[7 routing/output/contract tests] +``` + +Adding a family must extend `ChainFamily`/`FAMILIES`, the discriminated network/address types, `ChainGatewayMap`, the sign strategy, the gateway, use cases, commands, the family plugin, and networks/render/tests. Only a genuinely identical intent and I/O shape may be factored into a shared port; the TRON resource model and the EVM gas/nonce must remain separate. + +### 14.3 Adding a Wallet Source + +Synchronously update the `Source` union, `SOURCE_KINDS`, the import workflow, repository persistence/migration, dedup, signer resolution, cleanup, descriptor rendering, and tests. An unknown source must not fall into a silent default. + +--- + +## 15. Invariants That Must Be Maintained + +### 15.1 Architecture + +- Domain has no external I/O and does not depend on upper layers. +- Production Application does not import adapters/bootstrap. +- Inbound/Outbound adapters do not import each other. +- `bootstrap/composition.ts` is the single general composition root; family-specific composition lives in plugins. +- Application owns the ports; adapters implement the ports. +- No circular dependencies and no use of type-only imports to bypass a conceptual boundary. + +### 15.2 Behavior and Security + +- JSON stdout is exactly one terminal frame, schema `wallet-cli.result.v1`. +- The usage/execution/success exit codes are fixed at 2/1/0. +- Secrets do not enter argv/env/log/envelope; stdin uses at most one channel per execution, read once. +- Watch-only never signs; dry-run never decrypts, signs, or broadcasts. +- All persistent mutations are locked, and all replacement writes use an atomic rename. +- A broadcast transaction does not become a command failure because of a confirmation timeout. +- An unknown exception is redacted from the user. + +### 15.3 Verification Gates + +```bash +npm run typecheck +npm run depcruise +npm test +npm run build +``` + +When real TRON behavior is involved, additionally run `npm run test:live:nile` with an isolated wallet home; test secrets must not be logged or copied. An architectural change must at minimum pass typecheck, dependency-cruiser, unit tests, and build. + +--- + +## 16. Architectural Judgment Criteria + +When ownership is disputed, decide in order: + +1. No I/O, describes business values and invariants: Domain. +2. Describes what the product does or what external capability it needs: Application use case/service/port. +3. Turns terminal/argv/Zod into application input: Inbound CLI adapter. +4. Implements filesystem, HTTP, device, price, etc. as a port: Outbound adapter. +5. Chooses a concrete implementation and wires the object graph: Bootstrap. + +If a single module is parsing argv, calling a provider, writing a file, and rendering output all at once, the responsibilities have not yet been separated. The core standard is not the directory name, but whether dependencies point from the outside in, whether external details are replaceable, and whether the use case can be tested with only ports. diff --git a/ts/package-lock.json b/ts/package-lock.json new file mode 100644 index 000000000..d01fc0be0 --- /dev/null +++ b/ts/package-lock.json @@ -0,0 +1,5143 @@ +{ + "name": "wallet-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wallet-cli", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@ledgerhq/hw-app-trx": "^6.36.3", + "@ledgerhq/hw-transport-node-hid": "^6.33.4", + "@ledgerhq/hw-transport-node-speculos-http": "^6.36.4", + "@noble/ciphers": "^2.2.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0", + "tronweb": "^6.3.0", + "yaml": "^2.9.0", + "yargs": "^18.0.0", + "zod": "^4.4.3" + }, + "bin": { + "wallet-cli": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/yargs": "^17.0.35", + "dependency-cruiser": "^17.4.3", + "tsup": "^8.5.1", + "tsx": "^4.22.4", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ledgerhq/devices": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.15.1.tgz", + "integrity": "sha512-o4XEHiwPytnZxYUnOC5JoHX5TPDJpeMXPfXjwhsXVJ9LAbahxQWzC37Iuzk8ZY/oC2yDptzqjs/qNP2y9D9vCA==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/logs": "^6.17.0", + "rxjs": "7.8.2", + "semver": "7.7.3" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.36.0.tgz", + "integrity": "sha512-o2Q5hNvf2TzAzlH8ORAozppRbzixRPYDfmSQrP7FOcM997OEH7qDleXgp/uMpvRdxR/t3CJCG+n0i+bU/oYMKA==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/hw-app-trx": { + "version": "6.36.3", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-trx/-/hw-app-trx-6.36.3.tgz", + "integrity": "sha512-MOVRjIqX/kwjFcwy7trk99OcKDCyB70sjmqynmGV5rgZKoG+J9pBsn3q8g2FS4DSsxO1o7piCw6KXjBi3fiyDQ==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/hw-transport": "6.35.4" + } + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.35.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.35.4.tgz", + "integrity": "sha512-FMVxiniQp0pgSIEBX9CjXYBuIAH9BTm3OcrN+/2LqkB8QBeFZdiwmO4BeN9nc2aWspe9z6kxN3SuUyouedNokA==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.15.1", + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/logs": "^6.17.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-node-hid": { + "version": "6.33.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-6.33.4.tgz", + "integrity": "sha512-/k0rH+wTpTmUifdxWuOER/dM3vPxW7RQIF0tByGC6noszVC3SGOZVcskBqJZ6xwIs1TvAHvZlEFvZWbnUJMBiw==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.15.1", + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/hw-transport": "6.35.4", + "@ledgerhq/hw-transport-node-hid-noevents": "^6.35.4", + "@ledgerhq/logs": "^6.17.0", + "lodash": "^4.17.21", + "node-hid": "2.1.2", + "usb": "2.9.0" + } + }, + "node_modules/@ledgerhq/hw-transport-node-hid-noevents": { + "version": "6.35.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-6.35.4.tgz", + "integrity": "sha512-187670Uuuek9ECcT92NQm1PnDfFHT6gBmaJinatnueMLV9lk3q4IrpFZqTotr+LhPNub+YdGQFjYJImcXvYWtw==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.15.1", + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/hw-transport": "6.35.4", + "@ledgerhq/logs": "^6.17.0", + "node-hid": "2.1.2" + } + }, + "node_modules/@ledgerhq/hw-transport-node-speculos-http": { + "version": "6.36.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-node-speculos-http/-/hw-transport-node-speculos-http-6.36.4.tgz", + "integrity": "sha512-Ae3Z5mCZcZ3hhOhSRB2us69pjlI0ghLuIydfGR6PdxxxscPqcGXqB7RbcnABAC5cKC/0Wj4NXllssH3hcI9GCA==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/hw-transport": "6.35.4", + "@ledgerhq/logs": "^6.17.0", + "axios": "1.13.5", + "rxjs": "7.8.2" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", + "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", + "license": "Apache-2.0" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.2.0.tgz", + "integrity": "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.14.tgz", + "integrity": "sha512-Qu3Nn6JFuF4+sHKYl+IcX9vYiI40ogleXzFFSxoE1W94rG98o/kXs8uJ0QSfFzuwBCZWlGfUGpPkgwuuX4PchA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dependency-cruiser": { + "version": "17.4.3", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.4.3.tgz", + "integrity": "sha512-L4GLuAvmXevWnPCIaFfOz6eD92c+yY+pDgVqgufrLDnW3xYA799CSZQlly2r2N13nhAlnZY6VzY7Rx5pHNvk2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "8.16.0", + "acorn-jsx": "5.3.2", + "acorn-jsx-walk": "2.0.0", + "acorn-loose": "8.5.2", + "acorn-walk": "8.3.5", + "commander": "14.0.3", + "enhanced-resolve": "5.22.1", + "ignore": "7.0.5", + "interpret": "3.1.1", + "is-installed-globally": "1.0.0", + "json5": "2.2.3", + "picomatch": "4.0.4", + "prompts": "2.4.2", + "rechoir": "0.8.0", + "safe-regex": "2.1.1", + "semver": "7.8.1", + "tsconfig-paths-webpack-plugin": "4.2.0", + "watskeburt": "5.0.3" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^20.12||^22||>=24" + } + }, + "node_modules/dependency-cruiser/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers": { + "version": "6.13.5", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz", + "integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-hid": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-2.1.2.tgz", + "integrity": "sha512-qhCyQqrPpP93F/6Wc/xUR7L8mAJW0Z6R7HMQV8jCHHksAxNDe/4z4Un/H9CpLOT+5K39OPyt9tIQlavxWES3lg==", + "hasInstallScript": true, + "license": "(MIT OR X11)", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^3.0.2", + "prebuild-install": "^7.1.1" + }, + "bin": { + "hid-showdevices": "src/show-devices.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tronweb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tronweb/-/tronweb-6.3.0.tgz", + "integrity": "sha512-5CAjDO4/KfymgjKFgnXgfKKQp0xgOn8otCBYjgYAEIpLZDKNAk14Z0dDeg0UqYuceCiyMHjW7a19Rsz8EmhAOw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "7.26.10", + "axios": "1.15.0", + "bignumber.js": "9.1.2", + "ethereum-cryptography": "2.2.1", + "ethers": "6.13.5", + "eventemitter3": "5.0.1", + "google-protobuf": "3.21.4", + "semver": "7.7.1", + "validator": "13.15.23" + } + }, + "node_modules/tronweb/node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/tronweb/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/tronweb/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/usb": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.9.0.tgz", + "integrity": "sha512-G0I/fPgfHUzWH8xo2KkDxTTFruUWfppgSFJ+bQxz/kVY2x15EQ/XDB7dqD1G432G4gBG4jYQuF3U7j/orSs5nw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^6.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=10.20.0 <11.x || >=12.17.0 <13.0 || >=14.0.0" + } + }, + "node_modules/usb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/watskeburt": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-5.0.3.tgz", + "integrity": "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA==", + "dev": true, + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^20.12||^22.13||>=24.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/ts/package.json b/ts/package.json new file mode 100644 index 000000000..c186a4462 --- /dev/null +++ b/ts/package.json @@ -0,0 +1,48 @@ +{ + "name": "wallet-cli", + "version": "0.1.0", + "description": "Multichain standard CLI wallet (TypeScript)", + "type": "module", + "bin": { + "wallet-cli": "./dist/index.js" + }, + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsup", + "dev": "tsx src/index.ts", + "typecheck": "tsc --noEmit", + "depcruise": "depcruise src", + "test": "vitest run", + "test:parity:help": "node scripts/compare-help-parity.mjs", + "test:live:nile": "npm run build && node scripts/nile-live-suite.mjs", + "test:parity:live-report": "node scripts/compare-live-report.mjs", + "test:watch": "vitest" + }, + "license": "MIT", + "dependencies": { + "@ledgerhq/hw-app-trx": "^6.36.3", + "@ledgerhq/hw-transport-node-hid": "^6.33.4", + "@ledgerhq/hw-transport-node-speculos-http": "^6.36.4", + "@noble/ciphers": "^2.2.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0", + "tronweb": "^6.3.0", + "yaml": "^2.9.0", + "yargs": "^18.0.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/yargs": "^17.0.35", + "dependency-cruiser": "^17.4.3", + "tsup": "^8.5.1", + "tsx": "^4.22.4", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + } +} diff --git a/ts/scripts/compare-help-parity.mjs b/ts/scripts/compare-help-parity.mjs new file mode 100644 index 000000000..9e66b29b6 --- /dev/null +++ b/ts/scripts/compare-help-parity.mjs @@ -0,0 +1,63 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const baseline = join(root, "docs/baselines/nile-full-command-test-2026-06-29-run2-rawlogs.md"); +const originalEntry = resolve(root, "../ts/src/index.ts"); +const refactorEntry = join(root, "src/index.ts"); +const tsx = join(root, "node_modules/.bin/tsx"); + +const helpSection = readFileSync(baseline, "utf8").split("## 1. Wallet & account management")[0]; +const cases = [...helpSection.matchAll( + /```\n\$ wallet-cli (.*--help)\n([\s\S]*?)\n# exit=(\d+)\n```/g, +)].map((match) => ({ + invocation: match[1].trim(), + expectedStdout: `${match[2]}\n`, + expectedStatus: Number(match[3]), +})); + +if (cases.length === 0) throw new Error("baseline contains no help invocations"); + +function execute(entry, args) { + const home = mkdtempSync(join(tmpdir(), "wallet-cli-parity-")); + const result = spawnSync(tsx, [entry, ...args], { + encoding: "utf8", + env: { ...process.env, NO_COLOR: "1", WALLET_CLI_HOME: home }, + timeout: 20_000, + }); + return { status: result.status, stdout: result.stdout, stderr: result.stderr }; +} + +const failures = []; +for (const { invocation, expectedStdout, expectedStatus } of cases) { + const args = invocation.split(/\s+/); + const original = execute(originalEntry, args); + const refactor = execute(refactorEntry, args); + if ( + original.status !== refactor.status || + original.stdout !== refactor.stdout || + original.stderr !== refactor.stderr || + refactor.status !== expectedStatus || + refactor.stdout !== expectedStdout + ) { + failures.push({ invocation, original, refactor, expectedStdout, expectedStatus }); + } +} + +if (failures.length > 0) { + for (const failure of failures) { + process.stderr.write(`parity mismatch: wallet-cli ${failure.invocation}\n`); + process.stderr.write(` original exit=${failure.original.status}\n`); + process.stderr.write(` refactor exit=${failure.refactor.status}\n`); + if (failure.original.stdout !== failure.refactor.stdout) process.stderr.write(" stdout differs\n"); + if (failure.original.stderr !== failure.refactor.stderr) process.stderr.write(" stderr differs\n"); + if (failure.refactor.status !== failure.expectedStatus) process.stderr.write(" baseline exit differs\n"); + if (failure.refactor.stdout !== failure.expectedStdout) process.stderr.write(" baseline stdout differs\n"); + } + process.exitCode = 1; +} else { + process.stdout.write(`help parity passed: ${cases.length} invocations (original + raw-log baseline)\n`); +} diff --git a/ts/scripts/compare-live-report.mjs b/ts/scripts/compare-live-report.mjs new file mode 100644 index 000000000..f04d6e27a --- /dev/null +++ b/ts/scripts/compare-live-report.mjs @@ -0,0 +1,116 @@ +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const baselinePath = join(root, "docs/baselines/nile-full-command-test-2026-06-29-run2-rawlogs.md"); +const reportPath = join(root, "docs/nile-full-command-test-2026-06-29-run2-rawlogs.md"); +const privateEnvPath = resolve(root, "../ts/.private/.env.test"); + +function blocks(path) { + const markdown = readFileSync(path, "utf8"); + return [...markdown.matchAll(/```\n\$ ([^\n]+)\n([\s\S]*?)\n# exit=(\d+)\n```/g)] + .map((match) => ({ command: match[1], output: match[2], exit: Number(match[3]) })); +} + +function envelope(block) { + for (const line of block.output.split(/\r?\n/)) { + if (!line.startsWith("{")) continue; + try { + const value = JSON.parse(line); + if (value?.schema === "wallet-cli.result.v1") return value; + } catch { + // The historical baseline contains one intentionally truncated sign-only JSON line. + } + } +} + +function byCommand(items) { + const result = new Map(); + for (const block of items) { + const value = envelope(block); + if (!value) continue; + const list = result.get(value.command) ?? []; + list.push(value); + result.set(value.command, list); + } + return result; +} + +function dataKeys(values) { + return new Set(values.flatMap((value) => { + const data = value.data; + if (Array.isArray(data)) return data.length > 0 && typeof data[0] === "object" + ? Object.keys(data[0]) + : []; + return data && typeof data === "object" ? Object.keys(data) : []; + })); +} + +const baseline = blocks(baselinePath); +const actual = blocks(reportPath); +const baselineEnvelopes = byCommand(baseline); +const actualEnvelopes = byCommand(actual); +const failures = []; + +for (const [command, expected] of baselineEnvelopes) { + const observed = actualEnvelopes.get(command); + if (!observed) { + failures.push(`missing JSON command envelope: ${command}`); + continue; + } + const expectedSuccess = expected.some((value) => value.success === true); + if (expectedSuccess && !observed.some((value) => value.success === true)) { + failures.push(`no successful JSON envelope for ${command}`); + } + const expectedKeys = dataKeys(expected.filter((value) => value.success)); + const observedKeys = dataKeys(observed.filter((value) => value.success)); + for (const key of expectedKeys) { + if (!observedKeys.has(key)) failures.push(`${command} is missing baseline data key: ${key}`); + } + for (const value of observed) { + if (value.schema !== "wallet-cli.result.v1") failures.push(`${command} schema changed`); + if (!value.meta || typeof value.meta.durationMs !== "number" || !Array.isArray(value.meta.warnings)) { + failures.push(`${command} meta contract changed`); + } + if (command.startsWith("tron.") && !value.chain) failures.push(`${command} omitted chain`); + if (!command.startsWith("tron.") && value.chain) failures.push(`${command} unexpectedly has chain`); + } +} + +const expectedErrors = new Set(baseline.flatMap((block) => { + const value = envelope(block); + return value?.success === false ? [value.error?.code] : []; +}).filter(Boolean)); +const observedErrors = new Set(actual.flatMap((block) => { + const value = envelope(block); + if (value?.success === false) return [value.error?.code]; + const match = /^error \[([^\]]+)\]:/m.exec(block.output); + return match ? [match[1]] : []; +}).filter(Boolean)); +for (const code of expectedErrors) { + if (!observedErrors.has(code)) failures.push(`missing baseline error code: ${code}`); +} + +const privateValues = readFileSync(privateEnvPath, "utf8").split(/\r?\n/).flatMap((line) => { + const at = line.indexOf("="); + if (at < 1) return []; + return [line.slice(at + 1).trim().replace(/^(['"])(.*)\1$/, "$2")]; +}).filter(Boolean); +const report = readFileSync(reportPath, "utf8"); +if (privateValues.some((value) => report.includes(value))) failures.push("private test material leaked into report"); + +if (actual.length < baseline.length) { + failures.push(`live coverage regressed: baseline=${baseline.length}, actual=${actual.length}`); +} + +if (failures.length > 0) { + for (const failure of failures) process.stderr.write(`- ${failure}\n`); + process.exitCode = 1; +} else { + process.stdout.write( + `live report parity passed: ${actual.length} blocks, ${actualEnvelopes.size} JSON command contracts, ` + + `${observedErrors.size} error codes\n`, + ); +} + diff --git a/ts/scripts/import-wallet.exp b/ts/scripts/import-wallet.exp new file mode 100644 index 000000000..54e9bca6e --- /dev/null +++ b/ts/scripts/import-wallet.exp @@ -0,0 +1,29 @@ +#!/usr/bin/expect -f + +set timeout 30 +log_user 1 + +spawn node $env(CLI_ENTRY) import $env(IMPORT_KIND) --label $env(IMPORT_LABEL) + +expect { + -re {Set master password} { + send -- "$env(TEST_MASTER_PASSWORD)\r" + expect -re {Confirm master password} + send -- "$env(TEST_MASTER_PASSWORD)\r" + } + -re {Master password} { + send -- "$env(TEST_MASTER_PASSWORD)\r" + } + timeout { exit 124 } +} + +expect { + -re {Paste private key} { send -- "$env(TEST_IMPORT_SECRET)\r" } + -re {Paste recovery phrase} { send -- "$env(TEST_IMPORT_SECRET)\r" } + timeout { exit 124 } +} + +expect eof +set result [wait] +exit [lindex $result 3] + diff --git a/ts/scripts/nile-live-suite.mjs b/ts/scripts/nile-live-suite.mjs new file mode 100644 index 000000000..6df5846f6 --- /dev/null +++ b/ts/scripts/nile-live-suite.mjs @@ -0,0 +1,309 @@ +import { spawnSync } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const entry = join(root, "dist/index.js"); +const expectScript = join(root, "scripts/import-wallet.exp"); +const privateEnvPath = resolve(root, "../ts/.private/.env.test"); +const baselinePath = join(root, "docs/baselines/nile-full-command-test-2026-06-29-run2-rawlogs.md"); +const reportPath = join(root, "docs/nile-full-command-test-2026-06-29-run2-rawlogs.md"); +const walletHome = mkdtempSync(join(tmpdir(), "wallet-cli-refactor-nile-")); +const password = `Aa1!${randomBytes(12).toString("hex")}`; + +const fixtures = parseEnv(readFileSync(privateEnvPath, "utf8")); +const privateKey = required(fixtures, "TEST_TRON_PRIVATE_KEY"); +const mnemonic = required(fixtures, "TEST_TRON_MNEMONIC"); +const USDT = process.env.TEST_TRC20_CONTRACT ?? "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; +const TRC10 = process.env.TEST_TRC10_ASSET_ID ?? "1005416"; +const WATCH_ADDRESS = process.env.TEST_WATCH_ADDRESS ?? "TEWBqkD8YMn8FbZjzQifQhQLHiYjcT2zvG"; +const FIXED_BLOCK = process.env.TEST_TRON_BLOCK ?? "68723818"; + +const baseEnv = { + ...process.env, + NO_COLOR: "1", + WALLET_CLI_HOME: walletHome, +}; + +const sections = []; +const stats = { total: 0, exit0: 0, exit1: 0, exit2: 0, other: 0 }; + +function parseEnv(raw) { + return Object.fromEntries(raw.split(/\r?\n/).flatMap((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) return []; + const at = trimmed.indexOf("="); + if (at < 1) return []; + const value = trimmed.slice(at + 1).trim().replace(/^(['"])(.*)\1$/, "$2"); + return [[trimmed.slice(0, at).trim(), value]]; + })); +} + +function required(values, key) { + const value = values[key]; + if (!value) throw new Error(`missing ${key} in ${privateEnvPath}`); + return value; +} + +function shellDisplay(args, inputLabel) { + const quoted = args.map((value) => /[\s'"(){}[\]]/.test(value) + ? `'${value.replaceAll("'", "'\\''")}'` + : value); + return `${inputLabel ? `${inputLabel} | ` : ""}wallet-cli ${quoted.join(" ")}`; +} + +function record(display, result) { + const status = result.status ?? 1; + stats.total++; + if (status === 0) stats.exit0++; + else if (status === 1) stats.exit1++; + else if (status === 2) stats.exit2++; + else stats.other++; + const output = `${result.stderr ?? ""}${result.stdout ?? ""}`.replace(/\n$/, ""); + sections.push(`\n\`\`\`\n$ ${display}\n${output}${output ? "\n" : ""}# exit=${status}\n\`\`\`\n`); +} + +function run(args, options = {}) { + const result = spawnSync("node", [entry, ...args], { + encoding: "utf8", + env: baseEnv, + input: options.input, + timeout: options.timeout ?? 90_000, + }); + if (result.error) throw result.error; + if (options.record !== false) { + record(options.display ?? shellDisplay(args, options.inputLabel), result); + } + return result; +} + +function runJson(args, options = {}) { + const result = run(["-o", "json", ...args], options); + try { + return { result, envelope: JSON.parse(result.stdout) }; + } catch { + throw new Error(`expected JSON from: ${args.join(" ")}\n${result.stdout}\n${result.stderr}`); + } +} + +function runPassword(args, options = {}) { + return run([...args, "--password-stdin"], { + ...options, + input: `${options.password ?? password}\n`, + inputLabel: options.inputLabel ?? "printf ", + }); +} + +function runInteractive(kind, label, secret) { + const result = spawnSync("expect", [expectScript], { + encoding: "utf8", + env: { + ...baseEnv, + CLI_ENTRY: entry, + IMPORT_KIND: kind, + IMPORT_LABEL: label, + TEST_MASTER_PASSWORD: password, + TEST_IMPORT_SECRET: secret, + }, + timeout: 90_000, + }); + if (result.error) throw result.error; + record(`pty: wallet-cli import ${kind} --label ${label}`, result); + return result; +} + +function heading(title) { + sections.push(`\n## ${title}\n`); +} + +function txId(envelope) { + return envelope?.data?.txId ?? envelope?.data?.hash; +} + +async function waitForConfirmation(id) { + if (!id) return; + const deadline = Date.now() + 90_000; + while (Date.now() < deadline) { + const { envelope } = runJson(["tx", "status", "--txid", id], { record: false }); + if (envelope?.data?.confirmed) return; + await new Promise((resolve) => setTimeout(resolve, 3000)); + } +} + +function assertSuccess(result, label) { + if (result.status !== 0) { + throw new Error(`${label} failed with ${result.status}\n${result.stdout}\n${result.stderr}`); + } +} + +mkdirSync(dirname(reportPath), { recursive: true }); +sections.push( + "# wallet-cli refactor — full live command test on Nile — RAW LOGS\n", + `\nGenerated: ${new Date().toISOString()} \n`, + "Network: `tron:nile` \n", + "Wallet home: isolated temporary directory \n", + "Secrets: loaded from `../ts/.private/.env.test`, never printed or copied.\n", +); + +heading("0. Help surface (raw-log-derived groups and leaves)"); +const baselineHelp = readFileSync(baselinePath, "utf8").split("## 1. Wallet & account management")[0]; +const helpCommands = [...baselineHelp.matchAll(/^\$ wallet-cli (.*--help)$/gm)] + .map((match) => match[1].trim()); +for (const command of helpCommands) run(command.split(/\s+/)); + +heading("1. Wallet and account management"); +assertSuccess(runInteractive("private-key", "funded", privateKey), "private-key import"); +assertSuccess(run(["config", "defaultNetwork", "tron:nile"]), "default network"); +assertSuccess(runPassword(["create", "--label", "recipient"]), "recipient create"); +const recipient2 = runJson(["create", "--label", "recipient2", "--password-stdin"], { + input: `${password}\n`, + inputLabel: "printf ", +}); +assertSuccess(recipient2.result, "recipient2 create"); +assertSuccess(runInteractive("mnemonic", "funded-again", mnemonic), "mnemonic import"); +assertSuccess(run(["import", "watch", "--address", WATCH_ADDRESS, "--label", "watched"]), "watch import"); +run(["list"]); +const listed = runJson(["list"]); +const funded = listed.envelope.data.find((account) => account.label === "funded"); +const recipient = listed.envelope.data.find((account) => account.label === "recipient"); +if (!funded?.addresses?.tron || !recipient?.addresses?.tron) throw new Error("missing funded/recipient account"); +const fundedAddress = funded.addresses.tron; +const recipientAddress = recipient.addresses.tron; +run(["current"]); +run(["use", "recipient"]); +run(["current"]); +run(["use", "funded"]); +run(["rename", "recipient", "--label", "recipient"]); +runPassword(["derive", "--account", "recipient"]); +runPassword(["-o", "json", "derive", "--account", "recipient", "--index", "5"]); +const backupPath = join(walletHome, "backup.json"); +runPassword(["backup", "recipient2", "--out", backupPath]); +run(["delete", "recipient2", "--yes"]); +run(["networks"]); +runJson(["networks"]); +run(["config"]); +run(["config", "defaultNetwork"]); +runJson(["config"]); + +heading("2. Account queries"); +run(["account", "balance"]); +run(["account", "balance", "--network", "tron:nile"]); +run(["account", "balance", "--network", "tron:shasta"]); +runJson(["account", "balance"]); +run(["account", "balance", "--account", "recipient"]); +run(["account", "balance", "--account", fundedAddress]); +run(["account", "info"]); +runJson(["account", "info"]); +run(["account", "portfolio"]); +runJson(["account", "portfolio"]); +run(["account", "history", "--limit", "5"]); +runJson(["account", "history", "--limit", "3"]); + +heading("3. Token commands"); +run(["token", "list"]); +runJson(["token", "list"]); +run(["token", "info", "--contract", USDT]); +runJson(["token", "info", "--contract", USDT]); +run(["token", "info", "--asset-id", TRC10]); +run(["token", "balance", "--contract", USDT]); +runJson(["token", "balance", "--contract", USDT]); +run(["token", "balance", "--asset-id", TRC10]); +run(["token", "add", "--contract", USDT]); +run(["token", "list"]); +runJson(["token", "list"]); + +heading("4. Transactions"); +runPassword(["tx", "send", "--to", recipientAddress, "--amount", "1", "--dry-run"]); +runPassword(["-o", "json", "tx", "send", "--to", recipientAddress, "--amount", "1", "--dry-run"]); +const signed = runJson(["tx", "send", "--to", recipientAddress, "--amount", "1", "--sign-only", "--password-stdin"], { + input: `${password}\n`, + inputLabel: "printf ", +}); +assertSuccess(signed.result, "sign-only transfer"); +const broadcast = runJson(["tx", "broadcast", "--tx-stdin"], { + input: `${JSON.stringify(signed.envelope.data.signed)}\n`, + inputLabel: "printf ", +}); +const liveTrx = runJson(["tx", "send", "--to", recipientAddress, "--amount", "1", "--password-stdin"], { + input: `${password}\n`, + inputLabel: "printf ", +}); +runPassword(["tx", "send", "--to", recipientAddress, "--contract", USDT, "--amount", "0.001", "--dry-run"]); +const liveUsdt = runJson(["tx", "send", "--to", recipientAddress, "--contract", USDT, "--amount", "0.001", "--password-stdin"], { + input: `${password}\n`, + inputLabel: "printf ", +}); +runPassword(["tx", "send", "--to", recipientAddress, "--asset-id", TRC10, "--amount", "1", "--dry-run"]); +const liveTrc10 = runJson(["tx", "send", "--to", recipientAddress, "--asset-id", TRC10, "--amount", "1", "--password-stdin"], { + input: `${password}\n`, + inputLabel: "printf ", +}); +const trxId = txId(liveTrx.envelope); +const usdtId = txId(liveUsdt.envelope); +await waitForConfirmation(trxId); +await waitForConfirmation(usdtId); +await waitForConfirmation(txId(liveTrc10.envelope)); +await waitForConfirmation(txId(broadcast.envelope)); +run(["tx", "status", "--txid", trxId]); +runJson(["tx", "status", "--txid", trxId]); +run(["tx", "info", "--txid", trxId]); +runJson(["tx", "info", "--txid", usdtId]); + +heading("5. Contracts"); +run(["contract", "info", "--contract", USDT]); +runJson(["contract", "info", "--contract", USDT]); +const balanceParams = JSON.stringify([{ type: "address", value: fundedAddress }]); +run(["contract", "call", "--contract", USDT, "--method", "balanceOf(address)", "--params", balanceParams]); +runJson(["contract", "call", "--contract", USDT, "--method", "balanceOf(address)", "--params", balanceParams]); +runJson(["contract", "call", "--contract", USDT, "--method", "decimals()"]); + +heading("6. Stake and message signing"); +runPassword(["stake", "freeze", "--amount-sun", "1000000", "--resource", "energy"]); +runPassword(["stake", "freeze", "--amount-sun", "1000000", "--resource", "bandwidth", "--dry-run"]); +runPassword(["stake", "delegate", "--amount-sun", "1000000", "--receiver", recipientAddress, "--resource", "energy", "--dry-run"]); +runPassword(["stake", "undelegate", "--amount-sun", "1000000", "--receiver", recipientAddress, "--resource", "energy", "--dry-run"]); +runPassword(["stake", "unfreeze", "--amount-sun", "1000000", "--resource", "energy", "--dry-run"]); +runPassword(["stake", "withdraw"]); +runPassword(["stake", "cancel-unfreeze", "--dry-run"]); +runPassword(["message", "sign", "--message", "hello tron"]); +runPassword(["-o", "json", "message", "sign", "--message", "hello tron"]); + +heading("7. Blocks"); +run(["block"]); +runJson(["block"]); +run(["block", "--number", FIXED_BLOCK]); +run(["block", FIXED_BLOCK]); + +heading("8. Error and exit-code surface"); +run(["--version"]); +run(["frobnicate"]); +run(["account", "resources"]); +run(["stake", "prices"]); +run(["account", "balance", "--network", "mainnet-eth"]); +run(["account", "balance", "--network", "ethereum"]); +runJson(["account", "balance", "--network", "bogus"]); +run(["import", "ledger", "--app", "ethereum", "--label", "l"]); +runPassword(["tx", "send", "--amount", "1"]); +runPassword(["tx", "send", "--to", "GARBAGE", "--amount", "1"]); +runPassword(["tx", "send", "--to", recipientAddress, "--amount", "1", "--raw-amount", "1000000"]); +runPassword(["tx", "send", "--to", recipientAddress, "--amount", "1", "--dry-run"], { + password: "Wrong1!Password", +}); +const unknownTx = "00".repeat(32); +run(["tx", "status", "--txid", unknownTx]); +runJson(["tx", "info", "--txid", unknownTx]); +run(["account", "balance", "--account", "no-such-wallet"]); +runPassword(["tx", "send", "--account", "watched", "--to", recipientAddress, "--amount", "1"]); +runPassword(["tx", "send", "--to", recipientAddress, "--amount", "1", "--sign-only"], { + password: "Wrong1!Password", +}); + +sections.push( + `\n## Summary\n\nInvocations: ${stats.total}; exit 0: ${stats.exit0}; exit 1: ${stats.exit1}; exit 2: ${stats.exit2}; other: ${stats.other}.\n`, +); +writeFileSync(reportPath, sections.join("")); +process.stdout.write(`Nile live suite complete: ${reportPath}\n`); +process.stdout.write(`invocations=${stats.total} exit0=${stats.exit0} exit1=${stats.exit1} exit2=${stats.exit2} other=${stats.other}\n`); diff --git a/ts/src/adapters/inbound/cli/arity/arity.test.ts b/ts/src/adapters/inbound/cli/arity/arity.test.ts new file mode 100644 index 000000000..a7c6ee69a --- /dev/null +++ b/ts/src/adapters/inbound/cli/arity/arity.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; +import { z } from "zod"; +import { ciEnum, enumOptions, introspectFields } from "./index.js"; + +describe("enumOptions", () => { + it("returns the literals of an enum field (through optional/default wrappers)", () => { + expect(enumOptions(z.enum(["tron", "ethereum"]))).toEqual(["tron", "ethereum"]); + expect(enumOptions(z.enum(["a", "b"]).optional())).toEqual(["a", "b"]); + }); + it("returns undefined for non-enum fields", () => { + expect(enumOptions(z.string())).toBeUndefined(); + }); + it("descends ciEnum's preprocess pipe to find the literals (through default/optional)", () => { + expect(enumOptions(ciEnum(["energy", "bandwidth"]))).toEqual(["energy", "bandwidth"]); + expect(enumOptions(ciEnum(["energy", "bandwidth"]).default("bandwidth"))).toEqual(["energy", "bandwidth"]); + expect(enumOptions(ciEnum(["native", "token"]).optional())).toEqual(["native", "token"]); + }); +}); + +describe("ciEnum", () => { + const schema = ciEnum(["energy", "bandwidth"]); + it("accepts the canonical lowercase literal", () => { + expect(schema.parse("energy")).toBe("energy"); + }); + it("accepts upper/mixed case and normalizes to the lowercase literal", () => { + expect(schema.parse("ENERGY")).toBe("energy"); + expect(schema.parse("BandWidth")).toBe("bandwidth"); + }); + it("still rejects values outside the literal set", () => { + expect(schema.safeParse("cpu").success).toBe(false); + }); +}); + +describe("introspectFields — defaults & choices", () => { + const fields = introspectFields( + z.object({ + to: z.string().describe("recipient"), + feeLimit: z.coerce.number().int().positive().default(100_000_000).describe("fee cap"), + resource: z.enum(["energy", "bandwidth"]).default("bandwidth").describe("resource type"), + only: z.enum(["native", "token"]).optional().describe("filter"), + flag: z.boolean().default(false).describe("a switch"), + }), + ); + const by = (name: string) => fields.find((f) => f.name === name)!; + + it("captures the default value of a defaulted field", () => { + expect(by("feeLimit").defaultValue).toBe(100_000_000); + expect(by("resource").defaultValue).toBe("bandwidth"); + expect(by("flag").defaultValue).toBe(false); + }); + + it("leaves defaultValue undefined for non-defaulted fields", () => { + expect(by("to").defaultValue).toBeUndefined(); + expect(by("only").defaultValue).toBeUndefined(); + }); + + it("captures enum choices (through default/optional wrappers)", () => { + expect(by("resource").choices).toEqual(["energy", "bandwidth"]); + expect(by("only").choices).toEqual(["native", "token"]); + expect(by("to").choices).toBeUndefined(); + }); +}); diff --git a/ts/src/adapters/inbound/cli/arity/index.ts b/ts/src/adapters/inbound/cli/arity/index.ts new file mode 100644 index 000000000..c9291e4b8 --- /dev/null +++ b/ts/src/adapters/inbound/cli/arity/index.ts @@ -0,0 +1,112 @@ +/** + * Arity adapter — derive the minimal arity hints yargs needs from a command's zod `fields` + * (boolean→switch, else takes a value). Validation/types/defaults/cross-field checks stay in + * zod only — single source of truth. [replaces FlagSpecRegistry] + */ +import type { Argv } from "yargs"; +import { z, type ZodObject, type ZodRawShape, type ZodType } from "zod"; + +// ── account-ref brand ───────────────────────────────────────────────────────── +// A `string` field that names an existing account (accountId/label/address). Branded so the +// TTY gap-fill can offer an arrow-select of existing accounts instead of free text. The brand +// lives on the FINAL schema instance (zod methods clone), so accountRef applies min+describe +// itself and must be the terminal call — no further chaining. +const ACCOUNT_REF = new WeakSet(); +export function accountRef(describe: string): ZodType { + const s = z.string().min(1).describe(describe); + ACCOUNT_REF.add(s); + return s; +} +export function isAccountRef(schema: unknown): boolean { + return typeof schema === "object" && schema !== null && ACCOUNT_REF.has(schema); +} + +// ── case-insensitive enum ────────────────────────────────────────────────────── +// TRON brands its resources/networks in canonical case (ENERGY/BANDWIDTH, Nile, TRON), so an +// exact-match z.enum rejects the casing users most naturally type. ciEnum lower-cases the input +// before matching; `values` must therefore be the lowercase literals. --help still shows those +// literals — enumOptions() descends the preprocess pipe to find them. +export function ciEnum(values: T) { + return z.preprocess((v) => (typeof v === "string" ? v.toLowerCase() : v), z.enum(values)); +} + +export interface FieldInfo { + name: string; + kebab: string; + baseType: string; + optional: boolean; + hasDefault: boolean; + /** the default value (when hasDefault) — surfaced verbatim in --help. */ + defaultValue?: unknown; + /** literal options when the field is an enum — surfaced as `` in --help. */ + choices?: string[]; + description?: string; +} + +export function camelToKebab(s: string): string { + return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); +} + +function unwrap(schema: ZodType): { base: ZodType; optional: boolean; hasDefault: boolean; defaultValue?: unknown; description?: string } { + let s: any = schema; + let optional = false; + let hasDefault = false; + let defaultValue: unknown; + let description: string | undefined = s?.description; + while (s?.def && (s.def.type === "optional" || s.def.type === "default" || s.def.type === "nullable")) { + if (s.def.type === "optional" || s.def.type === "nullable") optional = true; + if (s.def.type === "default") { + hasDefault = true; + defaultValue = s.def.defaultValue; // zod v4: plain value + } + description ??= s.description; + s = s.def.innerType; + } + description ??= s?.description; + return { base: s, optional, hasDefault, defaultValue, description }; +} + +export function introspectFields(fields: ZodObject): FieldInfo[] { + const shape = fields.shape; + return Object.entries(shape).map(([name, schema]) => { + const { base, optional, hasDefault, defaultValue, description } = unwrap(schema as ZodType); + return { + name, + kebab: camelToKebab(name), + baseType: (base as any)?.def?.type ?? "unknown", + optional, + hasDefault, + defaultValue, + choices: enumOptions(schema as ZodType), + description, + }; + }); +} + +/** literal options of an enum field (after unwrapping optional/default), else undefined. */ +export function enumOptions(schema: ZodType): string[] | undefined { + const { base } = unwrap(schema as ZodType); + let def = (base as unknown as { def?: { type?: string; entries?: Record; out?: { def?: any } } }).def; + // ciEnum() wraps the enum in a preprocess pipe; the literals live on the pipe's output side. + if (def?.type === "pipe") def = def.out?.def; + if (def?.type !== "enum" || !def.entries) return undefined; + return Object.values(def.entries); +} + +/** apply one command's zod fields as yargs options (arity only; requiredness stays in zod). */ +function applyArity(y: Argv, fields: ZodObject): Argv { + for (const f of introspectFields(fields)) { + y.option(f.kebab, { + type: f.baseType === "boolean" ? "boolean" : "string", + describe: f.description, + demandOption: false, // requiredness is enforced by zod, not yargs + }); + } + return y; +} + +/** union the arity hints of every command in a namespace group (single source = zod). */ +export function applyCommands(y: Argv, fields: ZodObject[]): Argv { + for (const f of fields) applyArity(y, f); + return y; +} diff --git a/ts/src/adapters/inbound/cli/command-id.ts b/ts/src/adapters/inbound/cli/command-id.ts new file mode 100644 index 000000000..22d1a9aa6 --- /dev/null +++ b/ts/src/adapters/inbound/cli/command-id.ts @@ -0,0 +1,12 @@ +/** + * Canonical command identifier — derived purely from `family` + `path`, never stored. + * This is the value surfaced as the `command` field in every result/error envelope, and the + * stable handle agents key on. Chain commands are family-qualified so the same logical path + * (e.g. tx send) yields a per-chain id (tron.tx.send); neutral commands + * are just their path (create, import.mnemonic, config.get, networks). + */ +import type { ChainFamily } from "../../../domain/types/index.js"; + +export function commandId(cmd: { family?: ChainFamily; path: string[] }): string { + return cmd.family ? [cmd.family, ...cmd.path].join(".") : cmd.path.join("."); +} diff --git a/ts/src/adapters/inbound/cli/commands/config.ts b/ts/src/adapters/inbound/cli/commands/config.ts new file mode 100644 index 000000000..4b3e65bf2 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/config.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../contracts/index.js"; +import { + CONFIG_KEYS, + type ConfigService, +} from "../../../../application/use-cases/config-service.js"; +import { CommandRegistry } from "../registry/index.js"; +import { TextFormatters } from "../render/index.js"; + +export function registerConfigCommands(registry: CommandRegistry, service: ConfigService): void { + const fields = z.object({ + key: z.enum(CONFIG_KEYS).optional() + .describe("config key to read or set; omit to show the whole effective config"), + value: z.string().min(1).optional().describe("new value; omit to read the key"), + }); + + registry.add({ + path: ["config"], + network: "none", + wallet: "none", + auth: "none", + summary: "Show / get / set configuration values", + fields, + input: fields, + examples: [ + { cmd: "wallet-cli config" }, + { cmd: "wallet-cli config defaultNetwork" }, + { cmd: "wallet-cli config defaultNetwork tron:nile" }, + ], + formatText: TextFormatters.config, + run: async (ctx, _network, input) => service.execute(input, ctx.config, ctx.networkRegistry), + } satisfies CommandDefinition); +} diff --git a/ts/src/adapters/inbound/cli/commands/network.ts b/ts/src/adapters/inbound/cli/commands/network.ts new file mode 100644 index 000000000..14f7f2a59 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/network.ts @@ -0,0 +1,23 @@ +/** + * Network command — list known networks. Neutral and not bound to one family. + */ +import { z } from "zod"; +import type { CommandDefinition } from "../contracts/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { TextFormatters } from "../render/index.js"; + +export function registerNetworkCommands(reg: CommandRegistry): void { + const empty = z.object({}); + + // ── networks ──────────────────────────────────────────────────────────────── + reg.add({ + path: ["networks"], network: "none", wallet: "none", auth: "none", + summary: "List known networks", fields: empty, input: empty, + examples: [{ cmd: "wallet-cli networks" }], + formatText: TextFormatters.networks, + run: async (ctx) => + ctx.networkRegistry.all().map((n) => ({ + id: n.id, family: n.family, chainId: n.chainId, feeModel: n.feeModel, + })), + } satisfies CommandDefinition); +} diff --git a/ts/src/adapters/inbound/cli/commands/shared.ts b/ts/src/adapters/inbound/cli/commands/shared.ts new file mode 100644 index 000000000..a85e8d14e --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/shared.ts @@ -0,0 +1,60 @@ +/** + * Shared chain-command factories — only for commands whose intent and input shape are + * identical across families. Divergent commands (for example send-native, + * with chain-specific amount units + build/estimate) live explicitly in each chain module. + */ +import { z } from "zod"; +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { Schemas } from "../schemas/index.js"; +import { TextFormatters } from "../render/index.js"; +import type { MessageService } from "../../../../application/use-cases/message-service.js"; + +// ── execution-mode flags shared by every signing command ───────────────────────── +/** dry-run / sign-only fields; default (no flag) = sign AND broadcast on-chain. */ +export const txModeFields = { + dryRun: z.boolean().default(false).describe("build and estimate only, with no signature and no broadcast; mutually exclusive with --sign-only"), + signOnly: z.boolean().default(false).describe("sign and output the transaction without broadcasting; mutually exclusive with --dry-run; broadcast later with tx broadcast"), +}; +// ── unified --amount / --raw-amount selector (shared by every chain's `tx send`) ──── +const decimalAmount = z.string().regex(/^\d+(\.\d+)?$/, "must be a non-negative decimal string"); + +/** the `--amount`/`--raw-amount` field pair; descriptions vary per chain (units differ). */ +export function unifiedAmountFields(amountDesc: string, rawDesc: string) { + return { + amount: decimalAmount.optional().describe(amountDesc), + rawAmount: Schemas.uintString().optional().describe(rawDesc), + }; +} + +/** superRefine: exactly one of --amount or --raw-amount must be present. */ +export function amountSelector(v: { amount?: string; rawAmount?: string }, ctx: z.RefinementCtx): void { + const n = [v.amount !== undefined, v.rawAmount !== undefined].filter(Boolean).length; + if (n !== 1) ctx.addIssue({ code: "custom", path: ["amount"], message: "provide exactly one of --amount or --raw-amount" }); +} + +/** message sign — direct SignerResolver path (no node, no TxPipeline). */ +export function messageSignCommand(family: ChainFamily, service: MessageService): CommandDefinition { + // --message OR --message-stdin (the latter is a global data channel via SecretResolver). + const fields = z.object({ + message: z.string().min(1).optional().describe("message text to sign; provide this OR --message-stdin; exactly one is required"), + }); + return { + path: ["message", "sign"], + stdin: "message", + family, + network: "optional", + wallet: "optional", + auth: "required", + capability: "message.sign", + summary: "Sign an arbitrary message (TIP-191/V2 · EIP-191)", + fields, + input: fields, + examples: [{ cmd: `wallet-cli message sign --network ${family === "tron" ? "tron:nile" : "evm:1"} --message "hello"` }], + formatText: TextFormatters.messageSign, + run: async (ctx, _net, input) => { + const message = ctx.secrets.pick(input.message, "message", "message"); + return service.sign(family, ctx.activeAccount, message); + }, + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts b/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts new file mode 100644 index 000000000..8f2c8b630 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from "vitest"; +import type { TronUseCases } from "./tron/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { registerWalletCommands } from "./wallet.js"; +import { registerConfigCommands } from "./config.js"; +import { registerNetworkCommands } from "./network.js"; +import { TronModule } from "./tron/index.js"; +import { commandId } from "../command-id.js"; +import { TextFormatters } from "../render/index.js"; +import type { TextRenderContext } from "../contracts/index.js"; +import type { ConfigService } from "../../../../application/use-cases/config-service.js"; + +const ctx = (over: Partial = {}): TextRenderContext => ({ command: "x", ...over }); + +describe("text formatters", () => { + it("every registered command has a command-owned text formatter", () => { + const services = {} as TronUseCases; + const registry = new CommandRegistry(); + registerWalletCommands(registry, {} as Parameters[1]); + registerConfigCommands(registry, {} as ConfigService); + registerNetworkCommands(registry); + new TronModule(services).registerCommands(registry); + + const missing = registry.all() + .filter((cmd) => typeof cmd.formatText !== "function") + .map(commandId) + .sort(); + + expect(missing).toEqual([]); + }); +}); + +describe("tokenBalance formatter", () => { + it("formats balance with decimals and symbol when metadata is present", () => { + const out = TextFormatters.tokenBalance({ address: "TXaddress", token: "TR7token", balance: "1204560000", symbol: "USDT", decimals: 6 }, ctx()); + expect(out).toContain("1204.56"); + expect(out).toContain("USDT"); + }); + it("falls back to raw scalar balance when metadata is missing", () => { + const out = TextFormatters.tokenBalance({ address: "TXaddress", token: "TR7token", balance: "1204560000" }, ctx()); + expect(out).toContain("1204560000"); + }); + it("prefers the account label over the address when present", () => { + const out = TextFormatters.tokenBalance({ address: "TXaddress", token: "t", balance: "1" }, ctx({ accountLabel: "main" })); + expect(out).toContain("main"); + expect(out).not.toContain("TXaddress"); + }); +}); + +describe("txReceipt formatter (typed kind, narrowed — no command-id matching)", () => { + it("tx send submitted (default): pending receipt with txid + track hint, no fee/energy", () => { + const out = TextFormatters.txReceipt({ kind: "send", family: "tron", stage: "submitted", txId: "abc123", rawAmount: "5000000", token: "USDT", decimals: 6, to: "TrecipientAddress" }); + expect(out).toContain("⏳"); + expect(out).toContain("Sent 5 USDT"); + expect(out).toContain("TrecipientAddress"); + expect(out).toContain("abc123"); + expect(out).toContain("pending — not yet on-chain"); + expect(out).toContain("Track it: wallet-cli tx info --txid abc123"); + expect(out).not.toContain("Fee"); + }); + it("tx send TRC20 via --contract --raw-amount (no symbol): never mislabels as TRX", () => { + const out = TextFormatters.txReceipt({ kind: "send", family: "tron", stage: "submitted", txId: "t20", rawAmount: "10000", contract: "TXYZtokenContract", to: "Tdest" }); + expect(out).toContain("Sent 10000 TXYZtokenContract"); + expect(out).not.toContain("TRX"); + }); + it("tx send TRC10 via --asset-id --raw-amount (no symbol): labels by asset id, not TRX", () => { + const out = TextFormatters.txReceipt({ kind: "send", family: "tron", stage: "submitted", txId: "t10", rawAmount: "500000", assetId: "1005416", to: "Tdest" }); + expect(out).toContain("Sent 500000 asset 1005416"); + expect(out).not.toContain("TRX"); + }); + it("tx send confirmed (--wait): success receipt with real block + fee", () => { + const out = TextFormatters.txReceipt({ kind: "send", family: "tron", stage: "confirmed", txId: "abc", rawAmount: "1000000", to: "Tdest", blockNumber: 66000000, feeSun: "268000" }); + expect(out).toContain("✅"); + expect(out).toContain("Sent 1 TRX"); + expect(out).toContain("#66,000,000"); + expect(out).toContain("0.268 TRX"); + expect(out).toContain("success"); + }); + it("contract send failed (--wait): failure receipt with reason", () => { + const out = TextFormatters.txReceipt({ kind: "contract-send", family: "tron", stage: "failed", txId: "abc", method: "transfer(address,uint256)", contract: "TR7contract", result: "OUT_OF_ENERGY", blockNumber: 1, failed: true }); + expect(out).toContain("❌"); + expect(out).toContain("Called transfer"); + expect(out).toContain("TR7contract"); + expect(out).toContain("OUT_OF_ENERGY"); + }); + it("dry-run with an energy estimate (TRC20/contract): renders energy, never [object Object]", () => { + const out = TextFormatters.txReceipt({ + kind: "send", family: "tron", mode: "dry-run", + fee: { feeModel: "tron-resource", energy: 29650, availableEnergy: 133440569 } as any, + tx: { txID: "deadbeef" } as any, rawAmount: "10000", contract: "TXYZtoken", to: "Tdest", + } as any); + expect(out).toContain("Dry run"); + expect(out).not.toContain("[object Object]"); + expect(out).toContain("29,650 energy"); + expect(out).toContain("covered by staked energy"); // availableEnergy >= energy + }); + it("dry-run energy estimate with insufficient available energy: no 'covered' note", () => { + const out = TextFormatters.txReceipt({ + kind: "send", family: "tron", mode: "dry-run", + fee: { feeModel: "tron-resource", energy: 29650, availableEnergy: 100 } as any, + tx: { txID: "deadbeef" } as any, rawAmount: "10000", contract: "TXYZtoken", to: "Tdest", + } as any); + expect(out).toContain("29,650 energy"); + expect(out).not.toContain("covered by staked energy"); + }); + it("stake freeze submitted: renders staked amount and resource", () => { + const out = TextFormatters.txReceipt({ kind: "stake-freeze", family: "tron", stage: "submitted", txId: "abc", amountSun: "2000000", resource: "energy" }); + expect(out).toContain("Staked"); + expect(out).toContain("2 TRX"); + expect(out).toContain("energy"); + }); +}); + +describe("txStatus formatter (family-agnostic; command supplies `failed`)", () => { + it("tron: confirmed when not failed", () => { + const out = TextFormatters.txStatus({ family: "tron", txid: "abc", confirmed: true, failed: false, blockNumber: 123 }); + expect(out).toContain("confirmed"); + expect(out).toContain("#123"); + }); + it("tron: failed when command flags it", () => { + const out = TextFormatters.txStatus({ family: "tron", txid: "abc", confirmed: true, failed: true, blockNumber: 1 }); + expect(out).toContain("failed"); + }); + it("pending when not yet confirmed", () => { + const out = TextFormatters.txStatus({ family: "tron", txid: "abc", confirmed: false, failed: false }); + expect(out).toContain("pending"); + }); +}); + +describe("txInfo formatter (per-family, narrowed on family)", () => { + it("tron: shows TRX amount, energy and fee in TRX", () => { + const out = TextFormatters.txInfo({ + family: "tron", txid: "abc", from: "Tfrom", to: "Tto", amount: "1.5", symbol: "TRX", + status: "SUCCESS", blockNumber: 66000000, energyUsed: 28000, feeSun: 268000, transaction: {}, info: {}, + }); + expect(out).toContain("1.5 TRX"); + expect(out).toContain("#66,000,000"); + expect(out).toContain("28,000"); + expect(out).toContain("0.268 TRX"); + expect(out).toContain("SUCCESS"); + }); +}); + +describe("contractInfo formatter", () => { + it("uses normalized methods + count", () => { + const out = TextFormatters.contractInfo({ address: "TR7c", name: "Foo", methods: ["a", "b"], functionCount: 2 }); + expect(out).toContain("Foo"); + expect(out).toContain("Methods"); + expect(out).toContain("2 (a / b)"); + }); + it("falls back to raw contract/info ABI shape", () => { + const out = TextFormatters.contractInfo({ address: "TR7c", contract: { name: "Bar", abi: { entrys: [{ type: "Function", name: "x" }] } } }); + expect(out).toContain("Bar"); + expect(out).toContain("1 (x)"); + }); +}); + +describe("accountHistory formatter", () => { + it("renders normalized rows", () => { + const out = TextFormatters.accountHistory({ + address: "TXaddr", + records: [{ time: 1700000000000, type: "Transfer", amount: "1000000", symbol: "TRX", counterparty: "Tother", status: "ok" }], + }, ctx()); + expect(out).toContain("Transfer"); + expect(out).toContain("Tother"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/commands/tron/account.ts b/ts/src/adapters/inbound/cli/commands/tron/account.ts new file mode 100644 index 000000000..285992b1e --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/account.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import type { TronAccountService } from "../../../../../application/use-cases/tron/account-service.js"; +import { ciEnum } from "../../arity/index.js"; +import { TextFormatters } from "../../render/index.js"; + +export function accountCommands(service: TronAccountService): CommandDefinition[] { + return [balance(service), info(service), history(service), portfolio(service)]; +} + +function balance(service: TronAccountService): CommandDefinition { + const fields = z.object({}); + return { + path: ["account", "balance"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "account.balance.native", + summary: "Show native balance (TRX/SUN)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account balance --network tron:nile" }], + formatText: TextFormatters.accountBalance, + run: async (ctx, network) => service.balance(ctx, network!, "tron"), + }; +} + +function info(service: TronAccountService): CommandDefinition { + const fields = z.object({}); + return { + path: ["account", "info"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + summary: "Show raw account data (getAccount; TRON includes resources)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account info --network tron:nile" }], + formatText: TextFormatters.accountInfo, + run: async (ctx, network) => service.info(ctx, network!), + }; +} + +function history(service: TronAccountService): CommandDefinition { + const fields = z.object({ + limit: z.coerce.number().int().positive().max(200).default(20) + .describe("maximum records to return, in records; range: 1-200"), + only: ciEnum(["native", "token"]).optional() + .describe("filter history by transfer type; omit to show all transfer types"), + }); + return { + path: ["account", "history"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + summary: "Show transaction history (requires TronGrid)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account history --network tron:nile --limit 10" }], + formatText: TextFormatters.accountHistory, + run: async (ctx, network, input) => service.historyFor(ctx, network!, input), + }; +} + +function portfolio(service: TronAccountService): CommandDefinition { + const fields = z.object({}); + return { + path: ["account", "portfolio"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "account.portfolio", + summary: "Show native + token balances with best-effort USD value", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account portfolio --network tron:nile" }], + formatText: TextFormatters.accountPortfolio, + run: async (ctx, network) => service.portfolio(ctx, network!), + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/block.ts b/ts/src/adapters/inbound/cli/commands/tron/block.ts new file mode 100644 index 000000000..ffb9312f0 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/block.ts @@ -0,0 +1,26 @@ +/** + * TRON block group — block lookup. + */ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import type { TronBlockService } from "../../../../../application/use-cases/tron/block-service.js"; +import { TextFormatters } from "../../render/index.js"; + +function blockGet(service: TronBlockService): CommandDefinition { + const fields = z.object({ number: z.coerce.number().int().nonnegative().optional().describe("block number to fetch, in block height; omit to fetch the latest block") }); + return { + path: ["block"], family: "tron", + network: "optional", wallet: "none", auth: "none", + summary: "Get a block (latest if omitted)", fields, input: fields, + examples: [ + { cmd: "wallet-cli block --network tron:nile" }, + { cmd: "wallet-cli block 12345 --network tron:nile" }, + ], + formatText: TextFormatters.block, + run: async (_ctx, net, input) => service.get(net!, input.number), + }; +} + +export function blockCommands(service: TronBlockService): CommandDefinition[] { + return [blockGet(service)]; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/contract.ts b/ts/src/adapters/inbound/cli/commands/tron/contract.ts new file mode 100644 index 000000000..c567595f3 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/contract.ts @@ -0,0 +1,134 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import { UsageError } from "../../../../../domain/errors/index.js"; +import type { TronContractService } from "../../../../../application/use-cases/tron/contract-service.js"; +import type { TronContractParameter } from "../../../../../application/ports/chain/tron-gateway.js"; +import { Schemas } from "../../schemas/index.js"; +import { txModeFields } from "../shared.js"; +import { TextFormatters } from "../../render/index.js"; + +function jsonArray(raw: string | undefined, flag = "--params"): unknown[] { + if (!raw) return []; + try { + const value = JSON.parse(raw); + if (Array.isArray(value)) return value; + } catch { + // Fall through to the stable usage error. + } + throw new UsageError("invalid_value", `${flag} must be a JSON array`); +} + +export function contractCommands(service: TronContractService): CommandDefinition[] { + return [call(service), send(service), deploy(service), info(service)]; +} + +function call(service: TronContractService): CommandDefinition { + const fields = z.object({ + contract: Schemas.addressFor("tron").describe("TRON contract address"), + method: z.string().min(1).describe("function signature, e.g. balanceOf(address)"), + params: z.string().optional() + .describe("JSON array of ABI parameters as {type,value}; omit to pass no parameters"), + }); + return { + path: ["contract", "call"], family: "tron", + network: "optional", wallet: "none", auth: "none", + capability: "contract.call", + summary: "Read-only call (triggerConstantContract)", + fields, + input: fields, + examples: [{ + cmd: `wallet-cli contract call --network tron:nile --contract TR7... --method "balanceOf(address)" --params '[{"type":"address","value":"T..."}]'`, + }], + formatText: TextFormatters.contractCall, + run: async (_ctx, network, input) => service.call( + network!, input.contract, input.method, jsonArray(input.params) as TronContractParameter[], + ), + }; +} + +function send(service: TronContractService): CommandDefinition { + const fields = z.object({ + contract: Schemas.addressFor("tron").describe("TRON contract address"), + method: z.string().min(1).describe("function signature, e.g. transfer(address,uint256)"), + params: z.string().optional() + .describe("JSON array of ABI parameters as {type,value}; omit to pass no parameters"), + callValueSun: z.coerce.number().int().nonnegative().default(0) + .describe("native TRX attached to the call, in SUN"), + feeLimit: z.coerce.number().int().positive().default(100_000_000) + .describe("maximum energy fee to burn, in SUN"), + ...txModeFields, + }); + return { + path: ["contract", "send"], family: "tron", + network: "required", wallet: "optional", auth: "required", + broadcasts: true, + capability: "contract.call", + summary: "State-changing call (triggerSmartContract)", + fields, + input: fields, + examples: [{ + cmd: `wallet-cli contract send --network tron:nile --contract TR7... --method "transfer(address,uint256)" --params '[...]'`, + }], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => service.send(ctx, network!, { + ...input, + parameters: jsonArray(input.params) as TronContractParameter[], + }), + }; +} + +function deploy(service: TronContractService): CommandDefinition { + const fields = z.object({ + abi: z.string().min(1).describe("contract ABI as a JSON array string"), + bytecode: z.string().min(1).describe("compiled contract bytecode as hex, 0x-prefixed or bare"), + feeLimit: z.coerce.number().int().positive().describe("maximum energy fee to burn, in SUN"), + constructorSig: z.string().optional() + .describe("constructor signature, e.g. constructor(uint256); omit when the contract has no constructor args"), + params: z.string().optional() + .describe("constructor args as a JSON array of {type,value}; omit to pass no constructor args"), + ...txModeFields, + }); + return { + path: ["contract", "deploy"], family: "tron", + network: "required", wallet: "optional", auth: "required", + broadcasts: true, + capability: "contract.deploy", + summary: "Deploy a smart contract", + fields, + input: fields, + examples: [{ + cmd: "wallet-cli contract deploy --network tron:nile --abi '[...]' --bytecode 60... --fee-limit 1000000000", + }], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => { + let abi: unknown; + try { + abi = JSON.parse(input.abi); + } catch { + throw new UsageError("invalid_value", "--abi must be valid JSON"); + } + return service.deploy(ctx, network!, { + ...input, + abi, + parameters: jsonArray(input.params), + }); + }, + }; +} + +function info(service: TronContractService): CommandDefinition { + const fields = z.object({ + contract: Schemas.addressFor("tron").describe("TRON contract address"), + }); + return { + path: ["contract", "info"], family: "tron", + network: "optional", wallet: "none", auth: "none", + capability: "contract.call", + summary: "Show contract ABI + metadata", + fields, + input: fields, + examples: [{ cmd: "wallet-cli contract info --network tron:nile --contract TR7..." }], + formatText: TextFormatters.contractInfo, + run: async (_ctx, network, input) => service.info(network!, input.contract), + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/index.ts b/ts/src/adapters/inbound/cli/commands/tron/index.ts new file mode 100644 index 000000000..6d7244b9b --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/index.ts @@ -0,0 +1,48 @@ +/** + * TronModule — TRON's own command surface. No universal + * provider: TRON-specific build/estimate/codecs live in the per-group files; only infra + * (TxPipeline, SignerResolver, RpcProvider) is shared. Implements the ChainModule contract. + */ +import type { ChainModule, CommandRegistryLike } from "../../contracts/index.js"; +import type { TronAccountService } from "../../../../../application/use-cases/tron/account-service.js"; +import type { TronTokenService } from "../../../../../application/use-cases/tron/token-service.js"; +import type { TronTransactionService } from "../../../../../application/use-cases/tron/transaction-service.js"; +import type { TronStakeService } from "../../../../../application/use-cases/tron/stake-service.js"; +import type { TronBlockService } from "../../../../../application/use-cases/tron/block-service.js"; +import type { TronContractService } from "../../../../../application/use-cases/tron/contract-service.js"; +import type { MessageService } from "../../../../../application/use-cases/message-service.js"; +import { accountCommands } from "./account.js"; +import { tokenCommands } from "./token.js"; +import { txCommands } from "./tx.js"; +import { stakeCommands } from "./stake.js"; +import { blockCommands } from "./block.js"; +import { contractCommands } from "./contract.js"; +import { messageCommands } from "./message.js"; + +export interface TronUseCases { + tronAccount: TronAccountService; + tronToken: TronTokenService; + tronTransaction: TronTransactionService; + tronStake: TronStakeService; + tronBlock: TronBlockService; + tronContract: TronContractService; + message: MessageService; +} + +export class TronModule implements ChainModule { + readonly family = "tron" as const; + constructor(private readonly services: TronUseCases) {} + + registerCommands(reg: CommandRegistryLike): void { + const groups = [ + accountCommands(this.services.tronAccount), + tokenCommands(this.services.tronToken), + txCommands(this.services.tronTransaction), + stakeCommands(this.services.tronStake), + blockCommands(this.services.tronBlock), + contractCommands(this.services.tronContract), + messageCommands(this.services.message), + ]; + for (const cmds of groups) for (const cmd of cmds) reg.add(cmd); + } +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/message.ts b/ts/src/adapters/inbound/cli/commands/tron/message.ts new file mode 100644 index 000000000..afffe2e6c --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/message.ts @@ -0,0 +1,11 @@ +/** + * TRON message group — message signing (TIP-191/V2). The command itself comes from the + * shared (family-agnostic) factory; this file just scopes it to TRON. + */ +import type { CommandDefinition } from "../../contracts/index.js"; +import type { MessageService } from "../../../../../application/use-cases/message-service.js"; +import { messageSignCommand } from "../shared.js"; + +export function messageCommands(service: MessageService): CommandDefinition[] { + return [messageSignCommand("tron", service)]; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/shared.ts b/ts/src/adapters/inbound/cli/commands/tron/shared.ts new file mode 100644 index 000000000..a95b44f7e --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/shared.ts @@ -0,0 +1,18 @@ +import type { z } from "zod"; + +/** TRC20 contract XOR TRC10 asset id; exactly one selector is required. */ +export function tokenSelector( + value: { contract?: string; assetId?: string }, + context: z.RefinementCtx, +): void { + const count = [value.contract, value.assetId] + .filter((candidate) => candidate !== undefined).length; + if (count !== 1) { + context.addIssue({ + code: "custom", + path: ["contract"], + message: "exactly one of --contract (TRC20) or --asset-id (TRC10) is required", + }); + } +} + diff --git a/ts/src/adapters/inbound/cli/commands/tron/stake.ts b/ts/src/adapters/inbound/cli/commands/tron/stake.ts new file mode 100644 index 000000000..a1ff05d78 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/stake.ts @@ -0,0 +1,122 @@ +import { z } from "zod"; +import type { NetworkDescriptor } from "../../../../../domain/types/index.js"; +import type { + CommandDefinition, + ExecutionContext, +} from "../../contracts/index.js"; +import type { TronStakeService } from "../../../../../application/use-cases/tron/stake-service.js"; +import { RESOURCES } from "../../../../../domain/resources/index.js"; +import { Schemas } from "../../schemas/index.js"; +import { ciEnum } from "../../arity/index.js"; +import { txModeFields } from "../shared.js"; +import { TextFormatters } from "../../render/index.js"; + +const resourceField = (description: string) => + ciEnum(RESOURCES).default("bandwidth").describe(description); + +interface StakeCommandOptions { + capability?: string; + refine?: (value: any, context: z.RefinementCtx) => void; +} + +type StakeExecutor = ( + context: ExecutionContext, + network: NetworkDescriptor, + input: any, +) => Promise; + +function command( + action: string, + summary: string, + execute: StakeExecutor, + extra: z.ZodRawShape = {}, + options: StakeCommandOptions = {}, +): CommandDefinition { + const fields = z.object({ ...extra, ...txModeFields }); + return { + path: ["stake", action], family: "tron", + network: "required", wallet: "optional", auth: "required", + broadcasts: true, + capability: options.capability ?? "staking.freeze", + summary, + fields, + input: options.refine ? fields.superRefine(options.refine) : fields, + examples: [{ cmd: `wallet-cli stake ${action} --network tron:nile` }], + formatText: TextFormatters.txReceipt, + run: async (context, network, input) => execute(context, network!, input), + }; +} + +export function stakeCommands(service: TronStakeService): CommandDefinition[] { + return [ + command( + "freeze", + "Stake TRX for energy/bandwidth (FreezeBalanceV2)", + (context, network, input) => service.freeze(context, network, input), + { + amountSun: Schemas.uintString().describe("amount to freeze as staked TRX, in SUN"), + resource: resourceField("resource type to obtain"), + }, + ), + command( + "unfreeze", + "Unstake TRX (UnfreezeBalanceV2)", + (context, network, input) => service.unfreeze(context, network, input), + { + amountSun: Schemas.uintString().describe("amount to unfreeze as staked TRX, in SUN"), + resource: resourceField("resource type to release"), + }, + ), + command( + "withdraw", + "Withdraw expired unfrozen TRX (WithdrawExpireUnfreeze)", + (context, network, input) => service.withdraw(context, network, input), + ), + command( + "cancel-unfreeze", + "Cancel all pending unstakes (roll back to frozen)", + (context, network, input) => service.cancelUnfreeze(context, network, input), + ), + command( + "delegate", + "Delegate resource to another address (DelegateResourceV2)", + (context, network, input) => service.delegate(context, network, input), + { + amountSun: Schemas.uintString() + .describe("staked-TRX amount backing the delegated resource, in SUN"), + receiver: Schemas.addressFor("tron") + .describe("TRON address receiving the delegated resource"), + resource: resourceField("resource type to delegate or reclaim"), + lock: z.boolean().default(false) + .describe("lock the delegation and prevent early undelegation"), + lockPeriod: z.coerce.number().int().positive().optional() + .describe("lock duration in blocks, approximately 3 seconds per block; requires --lock"), + }, + { + capability: "staking.delegate", + refine: (value, context) => { + if (value.lockPeriod !== undefined && !value.lock) { + context.addIssue({ + code: "custom", + message: "--lock-period requires --lock", + path: ["lockPeriod"], + }); + } + }, + }, + ), + command( + "undelegate", + "Reclaim delegated resource (UnDelegateResourceV2)", + (context, network, input) => service.undelegate(context, network, input), + { + amountSun: Schemas.uintString() + .describe("staked-TRX amount backing the resource to reclaim, in SUN"), + receiver: Schemas.addressFor("tron") + .describe("TRON address that previously received the delegated resource"), + resource: resourceField("resource type to delegate or reclaim"), + }, + { capability: "staking.delegate" }, + ), + ]; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/token.ts b/ts/src/adapters/inbound/cli/commands/tron/token.ts new file mode 100644 index 000000000..ff18b0e06 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/token.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import type { TronTokenService } from "../../../../../application/use-cases/tron/token-service.js"; +import { Schemas } from "../../schemas/index.js"; +import { TextFormatters } from "../../render/index.js"; +import { tokenSelector } from "./shared.js"; + +const selectorFields = z.object({ + contract: Schemas.addressFor("tron").optional() + .describe("TRC20 contract address; provide exactly one of --contract or --asset-id"), + assetId: z.string().regex(/^\d+$/).optional() + .describe("TRC10 numeric asset id; provide exactly one of --asset-id or --contract"), +}); + +export function tokenCommands(service: TronTokenService): CommandDefinition[] { + return [balance(service), info(service), add(service), list(service), remove(service)]; +} + +function balance(service: TronTokenService): CommandDefinition { + return { + path: ["token", "balance"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "account.balance.token", + summary: "Show a single token balance (--contract / --asset-id)", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token balance --network tron:nile --contract TR7..." }], + formatText: TextFormatters.tokenBalance, + run: async (ctx, network, input) => service.balance(ctx, network!, input), + }; +} + +function info(service: TronTokenService): CommandDefinition { + return { + path: ["token", "info"], family: "tron", + network: "optional", wallet: "none", auth: "none", + capability: "account.balance.token", + summary: "Show token metadata (name/symbol/decimals/totalSupply)", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token info --network tron:nile --contract TR7..." }], + formatText: TextFormatters.tokenInfo, + run: async (_ctx, network, input) => service.info(network!, input), + }; +} + +function add(service: TronTokenService): CommandDefinition { + return { + path: ["token", "add"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "token.tokenbook", + summary: "Add a token to the address book (fetches symbol/decimals)", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token add --network tron:nile --contract TR7..." }], + formatText: TextFormatters.tokenBookAdd, + run: async (ctx, network, input) => service.add(ctx, network!, input), + }; +} + +function list(service: TronTokenService): CommandDefinition { + const fields = z.object({}); + return { + path: ["token", "list"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "token.tokenbook", + summary: "List the address book (official + user)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli token list --network tron:nile" }], + formatText: TextFormatters.tokenBookList, + run: async (ctx, network) => service.list(ctx, network!), + }; +} + +function remove(service: TronTokenService): CommandDefinition { + return { + path: ["token", "remove"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "token.tokenbook", + summary: "Remove a user-added token from the address book", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token remove --network tron:nile --contract TR7..." }], + formatText: TextFormatters.tokenBookRemove, + run: async (ctx, network, input) => service.remove(ctx, network!, input), + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/tx.ts b/ts/src/adapters/inbound/cli/commands/tron/tx.ts new file mode 100644 index 000000000..59860410d --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/tx.ts @@ -0,0 +1,123 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import { UsageError } from "../../../../../domain/errors/index.js"; +import type { TronTransactionService } from "../../../../../application/use-cases/tron/transaction-service.js"; +import { Schemas } from "../../schemas/index.js"; +import { + amountSelector, + txModeFields, + unifiedAmountFields, +} from "../shared.js"; +import { TextFormatters } from "../../render/index.js"; + +export function txCommands(service: TronTransactionService): CommandDefinition[] { + return [send(service), broadcast(service), status(service), info(service)]; +} + +function send(service: TronTransactionService): CommandDefinition { + const fields = z.object({ + to: Schemas.addressFor("tron").describe("recipient TRON base58 address"), + token: z.string().min(1).optional() + .describe("token symbol from the address book; mutually exclusive with --contract and --asset-id"), + contract: Schemas.addressFor("tron").optional() + .describe("TRC20 contract address; omit with --asset-id for native TRX"), + assetId: z.string().regex(/^\d+$/).optional() + .describe("TRC10 numeric asset id; omit with --contract for native TRX"), + feeLimit: z.coerce.number().int().positive().default(100_000_000) + .describe("maximum TRX energy fee to burn for TRC20 transfers, in SUN"), + ...unifiedAmountFields( + "human amount: TRX for native, token units for TRC20/TRC10; mutually exclusive with --raw-amount", + "raw integer amount in SUN or token base units; mutually exclusive with --amount", + ), + ...txModeFields, + }); + return { + path: ["tx", "send"], family: "tron", + network: "optional", wallet: "optional", auth: "required", + broadcasts: true, + capability: "tx.send", + summary: "Transfer native TRX (default) or a token", + fields, + input: fields.superRefine(amountSelector).superRefine(tokenOptional), + examples: [ + { cmd: "wallet-cli tx send --network tron:nile --to T... --amount 1" }, + { cmd: "wallet-cli tx send --network tron:mainnet --to T... --token USDT --amount 5" }, + { cmd: "wallet-cli tx send --network tron:nile --to T... --contract TR7... --amount 5" }, + { cmd: "wallet-cli tx send --network tron:nile --to T... --asset-id 1002000 --raw-amount 1000000" }, + ], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => service.send(ctx, network!, input), + }; +} + +function broadcast(service: TronTransactionService): CommandDefinition { + const fields = z.object({ + transaction: z.string().optional() + .describe("signed TRON transaction JSON; provide this OR --tx-stdin; exactly one is required"), + }); + return { + path: ["tx", "broadcast"], stdin: "tx", family: "tron", + network: "required", wallet: "none", auth: "none", + broadcasts: true, + capability: "tx.broadcast", + summary: "Broadcast a presigned transaction", + fields, + input: fields, + examples: [{ cmd: "wallet-cli tx broadcast --network tron:nile --tx-stdin < signed.json" }], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => { + const raw = ctx.secrets.pick(input.transaction, "tx", "transaction"); + try { + return service.broadcast(ctx, network!, JSON.parse(raw)); + } catch (error) { + if (error instanceof SyntaxError) { + throw new UsageError("invalid_value", "TRON presigned tx must be JSON"); + } + throw error; + } + }, + }; +} + +function status(service: TronTransactionService): CommandDefinition { + const fields = z.object({ txid: z.string().min(1).describe("TRON transaction id/hash") }); + return { + path: ["tx", "status"], family: "tron", + network: "optional", wallet: "none", auth: "none", + summary: "Show confirmation status of a transaction", + fields, + input: fields, + examples: [{ cmd: "wallet-cli tx status --network tron:nile --txid abc123" }], + formatText: TextFormatters.txStatus, + run: async (_ctx, network, input) => service.status(network!, input.txid), + }; +} + +function info(service: TronTransactionService): CommandDefinition { + const fields = z.object({ txid: z.string().min(1).describe("TRON transaction id/hash") }); + return { + path: ["tx", "info"], family: "tron", + network: "optional", wallet: "none", auth: "none", + summary: "Show full transaction detail + receipt", + fields, + input: fields, + examples: [{ cmd: "wallet-cli tx info --network tron:nile --txid abc123" }], + formatText: TextFormatters.txInfo, + run: async (_ctx, network, input) => service.info(network!, input.txid), + }; +} + +function tokenOptional( + value: { token?: string; contract?: string; assetId?: string }, + context: z.RefinementCtx, +): void { + const count = [value.token, value.contract, value.assetId] + .filter((candidate) => candidate !== undefined).length; + if (count > 1) { + context.addIssue({ + code: "custom", + path: ["token"], + message: "choose at most one of --token, --contract or --asset-id", + }); + } +} diff --git a/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts new file mode 100644 index 000000000..1ee465995 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { walletImportLedgerInput } from "./wallet.js"; + +const ok = (v: unknown) => walletImportLedgerInput.safeParse(v).success; + +describe("wallet import-ledger contract", () => { + it("requires --app", () => { + expect(ok({ index: 0 })).toBe(false); + }); + + it("accepts exactly one locator", () => { + expect(ok({ app: "tron", index: 0 })).toBe(true); + expect(ok({ app: "tron", path: "m/44'/195'/0'/0/0" })).toBe(true); + expect(ok({ app: "tron", address: "Tabc", scanLimit: 30 })).toBe(true); + }); + + it("accepts --app with no locator (defaults to index 0 downstream)", () => { + expect(ok({ app: "tron" })).toBe(true); + }); + + it("rejects more than one locator (mutually exclusive)", () => { + expect(ok({ app: "tron", index: 0, path: "m/44'/195'/0'/0/0" })).toBe(false); + expect(ok({ app: "tron", index: 0, address: "Tabc" })).toBe(false); + }); + + it("coerces --index and --scan-limit from strings", () => { + const r = walletImportLedgerInput.safeParse({ app: "tron", index: "2", scanLimit: "30" }); + expect(r.success && (r.data as { index?: number }).index).toBe(2); + }); + + it("rejects a hidden-family app (EVM is not currently exposed)", () => { + expect(ok({ app: "ethereum", index: 0 })).toBe(false); + }); +}); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.test.ts new file mode 100644 index 000000000..9dcaa960c --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/wallet.test.ts @@ -0,0 +1,355 @@ +import { describe, it, expect } from "vitest"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { z } from "zod"; +import { Keystore } from "../../../outbound/keystore/index.js"; +import { AtomicFileStore } from "../../../outbound/persistence/fs/index.js"; +import { SecretResolver } from "../input/secret/index.js"; +import { StreamManager } from "../stream/index.js"; +import { Prompter } from "../input/prompt/index.js"; +import { ConfigLoader, NetworkRegistry } from "../../../outbound/config/index.js"; +import { buildExecutionContext, RuntimeDeps } from "../context/index.js"; +import { createOutputFormatter } from "../output/index.js"; +import { registerWalletCommands, walletImportLedgerFields, walletImportLedgerInput } from "./wallet.js"; +import { CommandRegistry } from "../registry/index.js"; +import { commandId } from "../command-id.js"; +import type { Globals } from "../contracts/index.js"; +import { Derivation } from "../../../../domain/derivation/index.js"; +import { WalletService } from "../../../../application/use-cases/wallet-service.js"; + +// ── test constants ───────────────────────────────────────────────────────────── +const VALID_MNEMONIC = "test test test test test test test test test test test junk"; +const VALID_PRIVATE_KEY = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; +const VALID_PASSWORD = "Abcdef1!"; + +// ── helpers ──────────────────────────────────────────────────────────────────── + +interface FakePromptOpts { + tty?: boolean; + hiddenAnswers?: string[]; + confirmResult?: boolean; + confirmAnswer?: string; +} + +function makeFakeBackend(opts: FakePromptOpts = {}): ConstructorParameters[0] { + const { tty = true, hiddenAnswers = [], confirmResult = true, confirmAnswer } = opts; + let hiddenIdx = 0; + + return { + isTTY: () => tty, + async question(_prompt: string, hidden: boolean) { + if (hidden) { + return hiddenAnswers[hiddenIdx++] ?? VALID_PASSWORD; + } + // confirm prompts are not hidden + return confirmAnswer ?? ""; + }, + async readKey() { return { name: "return" }; }, + write(_s: string) {}, + beginRaw() {}, + endRaw() {}, + }; +} + +function buildTestDeps(opts: FakePromptOpts & { root?: string } = {}): { + deps: RuntimeDeps; + ks: Keystore; + prompter: Prompter; + streams: StreamManager; + secrets: SecretResolver; +} { + const root = opts.root ?? mkdtempSync(join(tmpdir(), "wallet-test-")); + const store = new AtomicFileStore(); + const streams = new StreamManager("text", false); + const prompter = new Prompter(makeFakeBackend(opts)); + // password: "-" means "read from stdin"; we prime the stdin via streams + // Actually: use no paths for password, rely on primePassword via prompter + const secrets = new SecretResolver(streams, {}, prompter); + const ks = new Keystore(root, store, () => secrets.masterPassword()); + const config = ConfigLoader.load(); + const networkRegistry = new NetworkRegistry(config); + const formatter = createOutputFormatter("text", streams, Date.now()); + const deps: RuntimeDeps = { config, networkRegistry, streams, secrets, keystore: ks, prompter, formatter }; + return { deps, ks, prompter, streams, secrets }; +} + +function buildGlobals(): Globals { + return { output: "text", verbose: false }; +} + +function buildServices(ks: Keystore) { + const ledger = {} as any; + return { + walletService: new WalletService(ks, ledger, { + write: () => ({ out: "unused", fileMode: "0600", bytes: 0 }), + }), + ledger, + tokenBook: {} as any, + priceProvider: {} as any, + rpc: {} as any, + signerResolver: {} as any, + txPipeline: {} as any, + capabilityRegistry: {} as any, + }; +} + +/** Resolve a command by its derived canonical id (e.g. "create", "import.mnemonic"). */ +function getCmd(registry: CommandRegistry, id: string): ReturnType { + const cmd = registry.all().find((c) => commandId(c) === id) ?? null; + if (!cmd) throw new Error(`command not found: ${id}`); + return cmd; +} + +async function runCmd( + cmdId: string, + input: Record, + opts: FakePromptOpts & { root?: string } = {}, +) { + const { deps, ks } = buildTestDeps(opts); + // Prime the password before command runs (simulating what dispatch does for passwordMode) + await deps.secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + const cmd = getCmd(registry, cmdId)!; + const result = await cmd.run(ctx, undefined as any, input as any); + return { result, ks }; +} + +// ── wallet create ────────────────────────────────────────────────────────────── + +describe("wallet create", () => { + it("creates an account with a tron address", async () => { + const { result, ks } = await runCmd("create", {}, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD], + }); + expect(result).toBeDefined(); + expect(result.accountId).toMatch(/^wlt_/); + expect(result.addresses?.tron?.startsWith("T")).toBe(true); + const accounts = ks.list(); + expect(accounts).toHaveLength(1); + }); + + it("createFields schema does NOT have a words key", async () => { + // We verify the schema at runtime by checking the registered command's fields + const { deps, ks } = buildTestDeps({ tty: true, hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD] }); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + const cmd = getCmd(registry, "create")!; + const shape = (cmd.fields as any).shape as Record; + expect(Object.keys(shape)).not.toContain("words"); + expect(Object.keys(shape)).toContain("label"); + }); + + it("generates a 12-word mnemonic", async () => { + // create, then backup to read mnemonic word count + const root = mkdtempSync(join(tmpdir(), "wallet-create-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD], + }); + // prime password for create + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + const createCmd = getCmd(registry, "create")!; + const createResult = await createCmd.run(ctx, undefined as any, {} as any); + + // verify the created account has a seed source + const accountList = ks.list(); + expect(accountList).toHaveLength(1); + const wallet = ks.resolveAccount(createResult.accountId).wallet; + expect(wallet.source.type).toBe("seed"); + + // reveal the mnemonic to check word count + const vaultId = (wallet.source as any).vaultId as string; + const revealed = ks.revealMnemonic(vaultId); + const words = revealed.mnemonic.split(" ").filter(Boolean); + expect(words).toHaveLength(12); + }); +}); + +// ── wallet import-mnemonic ───────────────────────────────────────────────────── + +describe("wallet import-mnemonic", () => { + it("creates an account from a mnemonic provided via interactive prompt", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-import-mnemonic-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: true, + // order: set password, confirm password, then mnemonic + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + }); + + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + const cmd = getCmd(registry, "import.mnemonic")!; + const result = await cmd.run(ctx, undefined as any, {} as any); + + expect(result.accountId).toMatch(/^wlt_/); + expect(result.addresses?.tron?.startsWith("T")).toBe(true); + expect(ks.list()).toHaveLength(1); + }); +}); + +// ── wallet import-private-key ────────────────────────────────────────────────── + +describe("wallet import-private-key", () => { + it("creates an account from a private key provided via interactive prompt", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-import-pk-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: true, + // order: set password, confirm password, then private key + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_PRIVATE_KEY], + }); + + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + const cmd = getCmd(registry, "import.private-key")!; + const result = await cmd.run(ctx, undefined as any, {} as any); + + expect(result.accountId).toMatch(/^wlt_/); + expect(result.addresses?.tron?.startsWith("T")).toBe(true); + expect(ks.list()).toHaveLength(1); + }); +}); + +// ── wallet delete ───────────────────────────────────────────────────────────── + +describe("wallet delete", () => { + async function setupAccountForDelete(root: string, opts: FakePromptOpts) { + const { deps, ks, secrets } = buildTestDeps({ root, ...opts }); + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + // import an account first + const importCmd = getCmd(registry, "import.mnemonic")!; + const importResult = await importCmd.run(ctx, undefined as any, {} as any); + return { ctx, registry, ks, accountId: importResult.accountId }; + } + + it("deletes an account when --yes is true", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-test-")); + // hiddenAnswers for set+confirm password, then mnemonic for import + const { ctx, registry, ks, accountId } = await setupAccountForDelete(root, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + }); + + expect(ks.list()).toHaveLength(1); + + const deleteCmd = getCmd(registry, "delete")!; + await deleteCmd.run(ctx, undefined as any, { account: accountId, yes: true } as any); + + expect(ks.list()).toHaveLength(0); + }); + + it("confirms deletion by exact label when a label is available", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-label-test-")); + const { ctx, registry, ks, accountId } = await setupAccountForDelete(root, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + confirmAnswer: "wallet-1", + }); + + expect(ks.describe(accountId).label).toBe("wallet-1"); + + const deleteCmd = getCmd(registry, "delete")!; + await deleteCmd.run(ctx, undefined as any, { account: accountId } as any); + + expect(ks.list()).toHaveLength(0); + }); + + it("throws aborted when --yes is false and confirm returns wrong string", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-abort-test-")); + const { ctx, registry, ks, accountId } = await setupAccountForDelete(root, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + confirmAnswer: "wrong-ref", + }); + + expect(ks.list()).toHaveLength(1); + + const deleteCmd = getCmd(registry, "delete")!; + await expect( + deleteCmd.run(ctx, undefined as any, { account: accountId } as any), + ).rejects.toMatchObject({ code: "aborted" }); + + // account should still exist + expect(ks.list()).toHaveLength(1); + }); + + it("throws tty_required when --yes is omitted and not a TTY", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-notty-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: false, + }); + // for non-TTY, secrets need password via stdin path + const storeNonTty = new AtomicFileStore(); + // Use a separate TTY keystore just to create the account + const root2 = mkdtempSync(join(tmpdir(), "wallet-delete-notty2-test-")); + const streams2 = new StreamManager("text", false); + const fakeBackend2 = { + isTTY: () => true, + async question(_p: string, hidden: boolean) { return VALID_PASSWORD; }, + async readKey() { return { name: "return" }; }, + write(_s: string) {}, + beginRaw() {}, + endRaw() {}, + }; + const prompter2 = new Prompter(fakeBackend2); + const secrets2 = new SecretResolver(streams2, {}, prompter2); + const ks2 = new Keystore(root2, storeNonTty, () => secrets2.masterPassword()); + await secrets2.primePassword({ mode: "set" }); + const { accountId } = ks2.import({ secret: VALID_MNEMONIC, type: "seed" }); + + // Now set up a non-TTY context pointing to the same root + const storeNonTty2 = new AtomicFileStore(); + const streamsNT = new StreamManager("text", false); + const fakeBackendNT = { + isTTY: () => false, + async question(_p: string, _hidden: boolean) { return ""; }, + async readKey() { return { name: "return" }; }, + write(_s: string) {}, + beginRaw() {}, + endRaw() {}, + }; + const prompterNT = new Prompter(fakeBackendNT); + // prime password via stdin path by priming it directly + const secretsNT = new SecretResolver(streamsNT, {}, prompterNT); + // manually prime the password so ks.delete can proceed if needed + // Actually we just need the delete to fail at the TTY check, before touching ks + const ksNT = new Keystore(root2, storeNonTty2, () => secrets2.masterPassword()); + + const config = ConfigLoader.load(); + const networkRegistry = new NetworkRegistry(config); + const formatter = createOutputFormatter("text", streamsNT, Date.now()); + const depsNT: RuntimeDeps = { + config, networkRegistry, streams: streamsNT, secrets: secretsNT, + keystore: ksNT, prompter: prompterNT, formatter, + }; + const ctxNT = buildExecutionContext(buildGlobals(), depsNT); + const registryNT = new CommandRegistry(); + registerWalletCommands(registryNT, buildServices(ksNT)); + + const deleteCmd = getCmd(registryNT, "delete")!; + await expect( + deleteCmd.run(ctxNT, undefined as any, { account: accountId } as any), + ).rejects.toMatchObject({ code: "tty_required" }); + }); +}); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.ts b/ts/src/adapters/inbound/cli/commands/wallet.ts new file mode 100644 index 000000000..0091ca5b1 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/wallet.ts @@ -0,0 +1,239 @@ +/** + * Wallet root commands — create/import/list/current/use/backup. Not chain-bound; no --network. + * Calls WalletService rather than the transaction pipeline. + */ +import { z } from "zod"; +import type { CommandDefinition } from "../contracts/index.js"; +import { Schemas } from "../schemas/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { accountRef, ciEnum } from "../arity/index.js"; +import type { LedgerDevice } from "../../../../application/ports/ledger-device.js"; +import type { WalletService } from "../../../../application/use-cases/wallet-service.js"; +import { resolveLedgerPath, selectLedgerPath } from "../../../../application/services/ledger-account.js"; +import { ChainFamily, CHAIN_FAMILIES, FAMILIES } from "../../../../domain/family/index.js"; +import { UsageError } from "../../../../domain/errors/index.js"; +import { TextFormatters } from "../render/index.js"; + +// ── wallet import-ledger contract (module scope so it can be unit-tested) ─────── +// The selectable Ledger apps are the families with a wired Ledger app (FAMILIES[f].ledger); +// the enum drives both --help and the interactive prompt. +const LEDGER_APP_BY_FAMILY: Partial> = Object.fromEntries( + CHAIN_FAMILIES.flatMap((f) => (FAMILIES[f].ledger ? [[f, FAMILIES[f].ledger!.app]] : [])), +); +const FAMILY_BY_LEDGER_APP: Record = Object.fromEntries( + (Object.entries(LEDGER_APP_BY_FAMILY) as [ChainFamily, string][]).map(([f, app]) => [app, f]), +); +const LEDGER_APPS = CHAIN_FAMILIES + .map((f) => LEDGER_APP_BY_FAMILY[f]) + .filter((a): a is string => a !== undefined) as [string, ...string[]]; +export const walletImportLedgerFields = z.object({ + app: ciEnum(LEDGER_APPS).describe("Ledger app to open on the device, selecting the address-derivation scheme"), + index: z.coerce.number().int().nonnegative().optional().describe("HD account index to import; omit with no --path/--address to use index 0; mutually exclusive with --path and --address"), + path: z.string().optional().describe("explicit BIP32 derivation path, e.g. m/44'/195'/0'/0/0 for TRON; mutually exclusive with --index and --address"), + address: z.string().optional().describe("known address to locate by bounded scan; mutually exclusive with --index and --path"), + scanLimit: z.coerce.number().int().positive().optional().describe("number of account indexes to scan when using --address, in indexes; omit to scan 20 indexes"), + label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate"), +}); +/** --index / --path / --address are mutually exclusive (at most one locator). */ +export const walletImportLedgerInput = walletImportLedgerFields.superRefine((v, c) => { + const locators = [v.index !== undefined, v.path !== undefined, v.address !== undefined].filter(Boolean).length; + if (locators > 1) c.addIssue({ code: "custom", path: ["index"], message: "--index, --path and --address are mutually exclusive" }); +}); + +export function registerWalletCommands( + reg: CommandRegistry, + services: { walletService: WalletService; ledger: LedgerDevice }, +): void { + const wallets = services.walletService; + const empty = z.object({}); + + // ── create ─────────────────────────────────────────────────────────────── + const createFields = z.object({ + label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate"), + }); + reg.add({ + path: ["create"], network: "none", wallet: "none", auth: "required", passwordMode: "establish", + interactive: true, promptHints: { label: "default-label" }, + summary: "Create a new HD wallet (BIP39 seed)", fields: createFields, input: createFields, + examples: [{ cmd: "wallet-cli create --label main" }], + formatText: TextFormatters.walletCreated("Created", [ + "Recovery phrase is encrypted locally and was not printed.", + "Run `backup` soon and store the file offline.", + ]), + run: async (_ctx, _net, input) => { + return wallets.create(input.label); + }, + } satisfies CommandDefinition); + + // ── import mnemonic ─────────────────────────────────────────────────────── + // BIP39 passphrase intentionally NOT exposed in phase 1 ; plumbing stays. + const importMnemonicFields = z.object({ label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate; mnemonic comes from --mnemonic-stdin or interactive prompt") }); + reg.add({ + path: ["import", "mnemonic"], stdin: "mnemonic", network: "none", wallet: "none", auth: "required", passwordMode: "establish", + interactive: true, promptHints: { label: "default-label" }, + summary: "Import a BIP39 mnemonic phrase", fields: importMnemonicFields, input: importMnemonicFields, + examples: [{ cmd: "wallet-cli import mnemonic --label main" }], + formatText: TextFormatters.walletCreated("Imported", [ + "Recovery phrase was read from hidden input and was not printed.", + ]), + run: async (ctx, _net, input) => { + const secret = await ctx.secrets.resolveSecret("mnemonic"); + return wallets.importMnemonic(secret, input.label); + }, + } satisfies CommandDefinition); + + // ── import private-key ──────────────────────────────────────────────────── + const importPrivateKeyFields = z.object({ label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate; private key comes from --private-key-stdin or interactive prompt") }); + reg.add({ + path: ["import", "private-key"], stdin: "privateKey", network: "none", wallet: "none", auth: "required", passwordMode: "establish", + interactive: true, promptHints: { label: "default-label" }, + summary: "Import a raw private key", fields: importPrivateKeyFields, input: importPrivateKeyFields, + examples: [{ cmd: "wallet-cli import private-key --label hot" }], + formatText: TextFormatters.walletCreated("Imported", [ + "Private key was read from hidden input and was not printed.", + ]), + run: async (ctx, _net, input) => { + const secret = await ctx.secrets.resolveSecret("privateKey"); + return wallets.importPrivateKey(secret, input.label); + }, + } satisfies CommandDefinition); + + // ── import ledger ───────────────────────────────────────────────────────── + reg.add({ + path: ["import", "ledger"], network: "none", wallet: "none", auth: "none", + interactive: true, promptHints: { label: "default-label", index: "skip", path: "skip", address: "skip", scanLimit: "skip" }, + summary: "Register a Ledger account (watch-only; signs on device)", fields: walletImportLedgerFields, input: walletImportLedgerInput, + examples: [{ cmd: "wallet-cli import ledger --app tron --index 0 --label cold" }], + formatText: TextFormatters.walletLedger, + run: async (ctx, _net, input) => { + const family: ChainFamily = FAMILY_BY_LEDGER_APP[input.app]!; + const hasLocator = input.index !== undefined || input.path !== undefined || input.address !== undefined; + const path = hasLocator || !ctx.prompt.isTTY() + ? await resolveLedgerPath(services.ledger, family, input) + : await selectLedgerPath(services.ledger, family, ctx.prompt); + ctx.emit({ type: "awaiting_device", reason: "verify_address" }); + return wallets.importLedger(family, path, input.label); + }, + } satisfies CommandDefinition); + + // ── import watch ────────────────────────────────────────────────────────── + const importWatchFields = z.object({ + address: z.string().min(1).describe("watch-only address to track; format: TRON base58 T...; family is auto-detected"), + label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate"), + }); + reg.add({ + path: ["import", "watch"], network: "none", wallet: "none", auth: "none", + interactive: true, + summary: "Register a watch-only address (no secret)", fields: importWatchFields, input: importWatchFields, + examples: [{ cmd: "wallet-cli import watch --address T... --label team-vault" }], + formatText: TextFormatters.walletWatch, + run: async (_ctx, _net, input) => { + return wallets.importWatch(input.address, input.label); + }, + } satisfies CommandDefinition); + + // ── list ───────────────────────────────────────────────────────────────── + reg.add({ + path: ["list"], network: "none", wallet: "none", auth: "none", + summary: "List wallets/accounts (no unlock needed)", fields: empty, input: empty, + examples: [{ cmd: "wallet-cli list --output json" }], + formatText: TextFormatters.walletList, + run: async () => wallets.list(), + } satisfies CommandDefinition); + + // ── use ────────────────────────────────────────────────────────────────── + const setActiveFields = z.object({ account: z.string().min(1).describe("accountId, label, or address to make active for future commands") }); + reg.add({ + path: ["use"], network: "none", wallet: "none", auth: "none", positionalAccount: true, + summary: "Set the active account", fields: setActiveFields, input: setActiveFields, + examples: [{ cmd: "wallet-cli use main" }], + formatText: TextFormatters.walletUse, + run: async (_ctx, _net, input) => { + return wallets.use(input.account); + }, + } satisfies CommandDefinition); + + // ── current ─────────────────────────────────────────────────────────────── + // Read-only: always reports the persisted active account; ignores --account + // (use `use` to change it). wallet:"none" keeps help from + // advertising an --account override here. + reg.add({ + path: ["current"], network: "none", wallet: "none", auth: "none", + summary: "Show the current active account", fields: empty, input: empty, + examples: [{ cmd: "wallet-cli current" }], + formatText: TextFormatters.walletCurrent, + run: async () => wallets.current(), + } satisfies CommandDefinition); + + // ── rename ──────────────────────────────────────────────────────────────── + const renameFields = z.object({ + account: z.string().min(1).describe("accountId, current label, or address to rename"), + label: Schemas.label().describe("new unique label, 1-64 chars"), + }); + reg.add({ + path: ["rename"], network: "none", wallet: "none", auth: "none", positionalAccount: true, + summary: "Rename an account label", fields: renameFields, input: renameFields, + examples: [{ cmd: "wallet-cli rename main --label primary" }], + formatText: TextFormatters.walletRename, + run: async (_ctx, _net, input) => { + return wallets.rename(input.account, input.label); + }, + } satisfies CommandDefinition); + + // ── derive ──────────────────────────────────────────────────────────────── + const addAccountFields = z.object({ + account: z.string().min(1).describe("seed wallet accountId, label, or address to derive from"), + index: z.coerce.number().int().nonnegative().optional().describe("explicit HD account index, in account index; omit to use the next free index"), + label: Schemas.label().optional().describe("label for the new derived account, 1-64 chars; omit to auto-generate"), + }); + reg.add({ + path: ["derive"], network: "none", wallet: "none", auth: "required", + summary: "Derive an HD account in a seed wallet (next free, or --index)", fields: addAccountFields, input: addAccountFields, + examples: [{ cmd: "wallet-cli derive --account main --index 3" }], + formatText: TextFormatters.walletDerive, + run: async (_ctx, _net, input) => { + return wallets.derive(input.account, input.index, input.label); + }, + } satisfies CommandDefinition); + + // ── delete ──────────────────────────────────────────────────────────────── + const deleteFields = z.object({ + account: accountRef("account or wallet to delete, addressed by accountId, label, or address"), + yes: z.boolean().default(false).describe("skip the interactive confirmation; required for non-TTY deletion"), + }); + reg.add({ + path: ["delete"], network: "none", wallet: "none", auth: "none", interactive: true, positionalAccount: true, + summary: "Delete a wallet/account and clean orphan labels", fields: deleteFields, input: deleteFields, + examples: [{ cmd: "wallet-cli delete old --yes" }], + formatText: TextFormatters.walletDelete, + run: async (ctx, _net, input) => { + if (!input.yes) { + if (!ctx.prompt.isTTY()) { + throw new UsageError("tty_required", "deletion needs confirmation: pass --yes or run in a terminal"); + } + const d = wallets.describe(input.account); + const expect = d.label ?? d.accountId; + const kind = d.label ? "label" : "ref"; + const ok = await ctx.prompt.confirm({ label: `Delete ${expect}? Type the exact ${kind} to confirm`, expect }); + if (!ok) throw new UsageError("aborted", "deletion not confirmed"); + } + return wallets.delete(input.account); + }, + } satisfies CommandDefinition); + + // ── backup ──────────────────────────────────────────────────────────────── + // Writes the secret + metadata to a 0600 FILE (never stdout/envelope): the secret stays off + // screen, logs and AI context. stdout returns only metadata + the written path. + // master password via dispatch prime (passwordMode: "verify"); --password-stdin is the non-interactive source. + const backupFields = z.object({ + account: accountRef("account or wallet to export, addressed by accountId, label, or address"), + out: z.string().optional().describe("output file path; omit to write /backups/-.json; file is created with mode 0600 and never overwritten"), + }); + reg.add({ + path: ["backup"], network: "none", wallet: "none", auth: "required", passwordMode: "verify", interactive: true, positionalAccount: true, + summary: "Export an account's secret + metadata to a 0600 file", fields: backupFields, input: backupFields, + examples: [{ cmd: "wallet-cli backup main --out ~/main-backup.json --password-stdin" }], + formatText: TextFormatters.walletBackup, + run: async (_ctx, _net, input) => wallets.backup(input.account, input.out), + } satisfies CommandDefinition); +} diff --git a/ts/src/adapters/inbound/cli/context/context.test.ts b/ts/src/adapters/inbound/cli/context/context.test.ts new file mode 100644 index 000000000..7e97044cb --- /dev/null +++ b/ts/src/adapters/inbound/cli/context/context.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { buildExecutionContext, RuntimeDeps } from "./index.js"; +import { StreamManager } from "../stream/index.js"; +import { createOutputFormatter } from "../output/index.js"; +import type { Globals } from "../contracts/index.js"; + +function ctxWith(output: "text" | "json") { + const out: string[] = []; + const err: string[] = []; + const sm = new StreamManager(output, false, (s) => out.push(s), (s) => err.push(s)); + const formatter = createOutputFormatter(output, sm, 0); + const globals = { output, verbose: false } as Globals; + // only streams + formatter are exercised by emit(); the rest is lazily used elsewhere. + const deps = { config: { timeoutMs: 1 }, streams: sm, formatter } as unknown as RuntimeDeps; + return { ctx: buildExecutionContext(globals, deps), out, err }; +} + +describe("ExecutionContext.emit (progress events)", () => { + it("routes a json event through formatter+streams to stderr, never stdout", () => { + const { ctx, out, err } = ctxWith("json"); + ctx.emit({ type: "awaiting_device", reason: "sign" }); + expect(out).toEqual([]); + expect(JSON.parse(err[0]!)).toEqual({ type: "awaiting_device", reason: "sign" }); + }); + + it("renders a human line in text mode", () => { + const { ctx, err } = ctxWith("text"); + ctx.emit({ type: "broadcasting" }); + expect(err[0]).toContain("broadcasting"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/context/index.ts b/ts/src/adapters/inbound/cli/context/index.ts new file mode 100644 index 000000000..87fe6726e --- /dev/null +++ b/ts/src/adapters/inbound/cli/context/index.ts @@ -0,0 +1,94 @@ +/** + * ExecutionContext — assemble runtime context from config, environment, and flags. Selection is + * account-level: activeAccount is resolved lazily from --account/--wallet or wallets.json. + * Build is side-effect-free; secrets never enter the serializable surface. + */ +import type { AccountRef, ChainFamily, Config, OutputMode } from "../../../../domain/types/index.js"; +import type { ProgressEvent } from "../../../../application/contracts/index.js"; +import type { NetworkRegistry } from "../../../../application/ports/network-registry.js"; +import type { ExecutionContext, Globals, SecretResolver, StreamManager } from "../contracts/index.js"; +import type { OutputFormatter } from "../output/index.js"; +import type { Prompter } from "../input/prompt/index.js"; +import type { AccountStore } from "../../../../application/ports/account-store.js"; +import { accountRef, walletAddress } from "../../../../domain/wallet/index.js"; +import { WalletError } from "../../../../domain/errors/index.js"; +import { SOURCE_KINDS } from "../../../../domain/sources/index.js"; + +export interface RuntimeDeps { + config: Config; + networkRegistry: NetworkRegistry; + streams: StreamManager; + secrets: SecretResolver; + keystore: AccountStore; + prompter: Prompter; + formatter: OutputFormatter; +} + +class ExecutionContextImpl implements ExecutionContext { + output: OutputMode; + timeoutMs: number; + wait: boolean; + waitTimeoutMs: number; + #activeRef?: AccountRef; + + constructor( + private readonly globals: Globals, + private readonly deps: RuntimeDeps, + ) { + this.output = globals.output ?? deps.config.defaultOutput; + this.timeoutMs = globals.timeoutMs ?? deps.config.timeoutMs; + this.wait = globals.wait ?? false; + this.waitTimeoutMs = globals.waitTimeoutMs ?? 60_000; + } + + get config(): Config { + return this.deps.config; + } + get networkRegistry(): NetworkRegistry { + return this.deps.networkRegistry; + } + get streams(): StreamManager { + return this.deps.streams; + } + get secrets(): SecretResolver { + return this.deps.secrets; + } + get prompt(): Prompter { + return this.deps.prompter; + } + get network(): string | undefined { + return this.globals.network; + } + + get activeAccount(): AccountRef { + if (this.#activeRef) return this.#activeRef; + const ks = this.deps.keystore; + let ref: AccountRef | null; + if (this.globals.account) { + const { wallet, index } = ks.resolveAccount(this.globals.account); + ref = accountRef(wallet.id, SOURCE_KINDS[wallet.source.type].isHD ? index : null); + } else { + ref = ks.activeAccount(); + } + if (!ref) { + throw new WalletError("missing_wallet_address", "no active account; import one or pass --account"); + } + this.#activeRef = ref; + return ref; + } + + resolveAddress(family: ChainFamily): string { + const { wallet, index } = this.deps.keystore.resolveAccount(this.activeAccount); + const address = walletAddress(wallet, family, index); + if (!address) throw new WalletError("missing_wallet_address", `active account has no ${family} address`); + return address; + } + + emit(e: ProgressEvent): void { + this.deps.streams.event(this.deps.formatter.event(e)); + } +} + +export function buildExecutionContext(globals: Globals, deps: RuntimeDeps): ExecutionContext { + return new ExecutionContextImpl(globals, deps); +} diff --git a/ts/src/adapters/inbound/cli/contracts/command.ts b/ts/src/adapters/inbound/cli/contracts/command.ts new file mode 100644 index 000000000..8f92102d9 --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/command.ts @@ -0,0 +1,74 @@ +/** CLI command metadata, validation, rendering and registration contracts. */ +import type { ZodObject, ZodRawShape, ZodType } from "zod"; +import type { ChainFamily } from "../../../../domain/family/index.js"; +import type { NetworkDescriptor } from "../../../../domain/types/network.js"; +import type { NetworkRequirement, WalletRequirement } from "../../../../application/contracts/index.js"; +import type { ExecutionContext } from "./execution-context.js"; + +export interface Example { + cmd: string; + note?: string; +} + +// "optional" = the command operates on an account; --account is optional and falls back to the +// active account (errors only if no account exists at all). "none" = never touches an account. +// (No "required": no command forces --account — active is always a valid default. cf. network.) +// "required" = unlocks the master password (sign / read secrets / encrypt); +// "none" = never unlocks. (No middle state — a command either needs the password or it doesn't.) +export type AuthRequirement = "none" | "required"; + +/** secret/payload channel a command reads from stdin; documents the matching --*-stdin flag. */ +export type StdinChannel = "privateKey" | "mnemonic" | "tx" | "message"; + +export interface TextRenderContext { + command: string; + net?: NetworkDescriptor; + /** label of the resolved active account, injected centrally; absent for wallet:"none" commands. */ + accountLabel?: string; +} + +export type TextFormatter = (data: O, ctx: TextRenderContext) => string | null; + +export interface CommandDefinition { + /** full typed path. Neutral commands carry their complete path (e.g. ["import","mnemonic"], + * ["config","get"], ["create"]); chain commands carry the logical path (e.g. ["tx","send"]) + * shared across families. The only routing discriminator is `family` present/absent. + * The stable identity (envelope `command` field) is derived from command metadata, not stored. */ + path: string[]; + family?: ChainFamily; + /** declares the command reads from a *-stdin channel; drives help/catalog input-flag docs. */ + stdin?: StdinChannel; + network: NetworkRequirement; + wallet: WalletRequirement; + auth: AuthRequirement; + /** broadcasts a transaction on-chain (✍️); enables the --wait global flag in help projection. */ + broadcasts?: boolean; + /** opt-in interactive master-password handling: "establish" = set on first wallet else verify; "verify" = require existing. Commands without this keep the lazy hasMasterPassword guard. */ + passwordMode?: "establish" | "verify"; + /** bind an `[account]` positional on the CLI surface (account-targeting neutral leaves: use/rename/backup/delete). */ + positionalAccount?: boolean; + /** allow interactive TTY prompts (master password, secret, gap-fill, confirm). Absent ⇒ fail fast — safer for scripts/agents. */ + interactive?: boolean; + /** gap-fill prompt hints, by field name: "skip" = never prompt this optional field; "default-label" = offer a generated default. */ + promptHints?: Record; + capability?: string; + summary?: string; + /** per-field zod object; feeds the arity adapter + HelpService. */ + fields: ZodObject; + /** full validation schema (often fields.superRefine), used in dispatch. */ + input: ZodType; + examples: Example[]; + run(ctx: ExecutionContext, net: NetworkDescriptor | undefined, input: I): Promise; + /** Optional command-specific renderer for text mode. JSON mode always uses the envelope. */ + formatText?: TextFormatter; +} + +export interface ChainModule { + family: ChainFamily; + registerCommands(reg: CommandRegistryLike): void; +} + +/** structural view of CommandRegistry needed by ChainModule.registerCommands. */ +export interface CommandRegistryLike { + add(cmd: CommandDefinition): void; +} diff --git a/ts/src/adapters/inbound/cli/contracts/envelope.ts b/ts/src/adapters/inbound/cli/contracts/envelope.ts new file mode 100644 index 000000000..721c30b20 --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/envelope.ts @@ -0,0 +1,46 @@ +/** + * SharedTypes — output contract (result/error envelopes + progress events) + * and the global runtime flags parsed off argv. + */ +import type { ChainFamily } from "../../../../domain/family/index.js"; +import type { NetworkId } from "../../../../domain/types/network.js"; +import type { OutputMode } from "../../../../domain/types/primitives.js"; + +export interface ChainView { + family: ChainFamily; + networkId: NetworkId; + network: string; + chainId: string; +} +export interface Meta { + durationMs: number; + warnings: string[]; +} +export interface ResultEnvelope { + schema: "wallet-cli.result.v1"; + success: true; + command: string; + chain?: ChainView; + data: unknown; + meta: Meta; +} +export interface ErrorEnvelope { + schema: "wallet-cli.result.v1"; + success: false; + command: string; + chain?: ChainView; + error: { code: string; message: string; details?: object }; + meta: Meta; +} + +// ═══════════════ global runtime flags parsed off argv ═════════════════════ +export interface Globals { + /** absent until the config default is resolved (runner bootstrap / buildExecutionContext). */ + output?: OutputMode; + network?: string; + account?: string; + timeoutMs?: number; + verbose: boolean; + wait?: boolean; + waitTimeoutMs?: number; +} diff --git a/ts/src/adapters/inbound/cli/contracts/execution-context.ts b/ts/src/adapters/inbound/cli/contracts/execution-context.ts new file mode 100644 index 000000000..86c33c94f --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/execution-context.ts @@ -0,0 +1,16 @@ +import type { Config, OutputMode } from "../../../../domain/types/index.js"; +import type { TransactionScope } from "../../../../application/contracts/index.js"; +import type { NetworkRegistry } from "../../../../application/ports/network-registry.js"; +import type { PromptPort } from "../../../../application/ports/prompt.js"; +import type { SecretResolver, StreamManager } from "./runtime.js"; + +/** CLI command context; application workflows receive only narrower execution scopes. */ +export interface ExecutionContext extends TransactionScope { + readonly config: Config; + readonly networkRegistry: NetworkRegistry; + readonly streams: StreamManager; + readonly secrets: SecretResolver; + readonly prompt: PromptPort; + readonly output: OutputMode; + readonly network?: string; +} diff --git a/ts/src/adapters/inbound/cli/contracts/index.ts b/ts/src/adapters/inbound/cli/contracts/index.ts new file mode 100644 index 000000000..5b7ce27aa --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/index.ts @@ -0,0 +1,4 @@ +export * from "./command.js"; +export * from "./envelope.js"; +export * from "./execution-context.js"; +export * from "./runtime.js"; diff --git a/ts/src/adapters/inbound/cli/contracts/runtime.ts b/ts/src/adapters/inbound/cli/contracts/runtime.ts new file mode 100644 index 000000000..06c6788de --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/runtime.ts @@ -0,0 +1,40 @@ +/** CLI runtime seams implemented by stream and secret input adapters. */ +import type { NetworkDescriptor } from "../../../../domain/types/network.js"; + +export type DiagnosticLevel = "info" | "debug" | "warn"; + +export interface StreamManager { + result(text: string): void; + diagnostic(level: DiagnosticLevel, msg: string): void; + /** always-on stderr line. */ + errorLine(msg: string): void; + /** intermediate progress frame → stderr plain line; null is skipped (StreamManager). */ + event(frame: string | null): void; + readStdinOnce(): string; + /** warnings accumulated for the JSON envelope's meta.warnings. */ + warnings(): string[]; +} + +export type SecretKind = "password" | "privateKey" | "mnemonic" | "tx" | "message"; +export interface SecretResolver { + masterPassword(): string; + /** whether a master-password source exists, WITHOUT consuming stdin. */ + hasMasterPassword(): boolean; + /** whether a source for `kind` is configured, WITHOUT consuming it. */ + has(kind: SecretKind): boolean; + read(kind: SecretKind): string; + /** read a required source; missing → missing_option (usage), not secret_source_error. */ + require(kind: SecretKind): string; + /** exactly-one selector: inline value XOR the file/stdin source for `kind`. */ + pick(inline: string | undefined, kind: SecretKind, inlineFlag: string): string; + /** resolve a non-password secret: stdin source → hidden prompt → missing_option. */ + resolveSecret(kind: "mnemonic" | "privateKey"): Promise; + /** establish/verify the master password before synchronous keystore use. */ + primePassword(plan: { mode: "set" | "verify"; verify?: (pw: string) => boolean }): Promise; +} + +/** Mutable dispatch contract: CliShell records the in-flight command (+ resolved network) here so the + * runner's single terminal catch can attach commandId/net to the error envelope across yargs. */ +export interface SessionRef { + current?: { commandId: string; net?: NetworkDescriptor }; +} diff --git a/ts/src/adapters/inbound/cli/globals/index.ts b/ts/src/adapters/inbound/cli/globals/index.ts new file mode 100644 index 000000000..0750afb58 --- /dev/null +++ b/ts/src/adapters/inbound/cli/globals/index.ts @@ -0,0 +1,132 @@ +/** + * Single source of truth for GLOBAL (kubectl-style) flags — the flag list, arity (kinds, aliases, + * yargs scalar types, choices), per-flag COERCION, and the DOCUMENTATION facts. Everything that + * otherwise drifts across a flag's touch-points derives from GLOBAL_FLAG_SPECS: + * - bootstrap/runner's pre-yargs scan (token maps via globalTokenMaps + coerceGlobalValue), + * - cli/shell's yargs `.options()` declaration (globalYargsOptions), and + * - cli/help/catalog's documentation projection (globalFlagDoc over description/defaultValue). + * Add a global flag HERE only; every layer is a projection. + * + * Array order is the documented display order (it's what cli/help renders). Other projections are + * order-independent. + */ +import type { SecretKind } from "../contracts/index.js"; + +export type GlobalFlagKind = "value" | "boolean" | "secret-stdin"; + +export interface GlobalFlagSpec { + /** kebab flag name, no leading dashes (e.g. "wait-timeout", "password-stdin"). */ + name: string; + /** single-char alias, no dash (e.g. "o", "v"). */ + alias?: string; + kind: GlobalFlagKind; + /** value flags only: yargs scalar type. */ + valueType?: "string" | "number"; + /** value flags only: restrict accepted values (yargs `choices`). */ + choices?: readonly string[]; + /** secret-stdin flags only: which secret kind this `--` binds. */ + secretKey?: SecretKind; + /** override the derived camelCase field name when the runtime Globals key differs from the flag + * (e.g. `--timeout` → `timeoutMs`); defaults to globalFlagField(name). */ + field?: string; + /** human-readable flag description (cli/help text + --json-schema catalog). */ + description: string; + /** documented default; absent → rendered as plain "[optional]". */ + defaultValue?: string | number | boolean; + /** secret-stdin only: documented under each owning command, not in the global flag list. */ + commandScoped?: boolean; +} + +export const GLOBAL_FLAG_SPECS: readonly GlobalFlagSpec[] = [ + { name: "output", alias: "o", kind: "value", valueType: "string", choices: ["text", "json"], + description: "result format", defaultValue: "config.defaultOutput (built-in: text)" }, + { name: "network", kind: "value", valueType: "string", + description: "canonical network id, e.g. tron:mainnet, tron:nile, tron:shasta; chain commands fall back to config.defaultNetwork when omitted" }, + { name: "account", kind: "value", valueType: "string", + description: "accountId, label, or address for wallet-bound commands; falls back to the active account set by use" }, + { name: "timeout", kind: "value", valueType: "number", field: "timeoutMs", + description: "per RPC/device call timeout, in milliseconds", defaultValue: "config.timeoutMs (built-in: 60000)" }, + { name: "verbose", alias: "v", kind: "boolean", + description: "show extra diagnostic output", defaultValue: false }, + { name: "wait", kind: "boolean", + description: "after broadcast, poll until the tx is confirmed/failed before returning; default returns the submitted txid without blocking", defaultValue: false }, + { name: "wait-timeout", kind: "value", valueType: "number", field: "waitTimeoutMs", + description: "--wait polling cap, in milliseconds; on timeout return the submitted receipt", defaultValue: 60000 }, + { name: "password-stdin", kind: "secret-stdin", secretKey: "password", + description: "read the master password from stdin (fd 0); only one *-stdin flag can consume stdin per run" }, + { name: "private-key-stdin", kind: "secret-stdin", secretKey: "privateKey", commandScoped: true, + description: "read the private key from stdin (fd 0)" }, + { name: "mnemonic-stdin", kind: "secret-stdin", secretKey: "mnemonic", commandScoped: true, + description: "read the BIP39 mnemonic from stdin (fd 0)" }, + { name: "tx-stdin", kind: "secret-stdin", secretKey: "tx", commandScoped: true, + description: "read the signed transaction JSON from stdin (fd 0)" }, + { name: "message-stdin", kind: "secret-stdin", secretKey: "message", commandScoped: true, + description: "read the message bytes/text from stdin (fd 0)" }, +]; + +/** kebab → camel default; the `field` override wins when the runtime Globals key differs from the flag. */ +export const globalFlagField = (name: string): string => name.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase()); +const specField = (f: GlobalFlagSpec): string => f.field ?? globalFlagField(f.name); + +/** value-flag spec keyed by its runtime Globals field, for coercion. */ +const VALUE_SPEC_BY_FIELD: Record = Object.fromEntries( + GLOBAL_FLAG_SPECS.filter((f) => f.kind === "value").map((f) => [specField(f), f]), +); + +/** + * Coerce a raw value-flag string per its spec; `undefined` = invalid (caller falls back to default). + * Derives entirely from valueType/choices: number flags accept a non-negative finite value (true of + * every number global today — add a per-flag `min` if that ever changes), choice flags must match, + * everything else passes through as a string. + */ +export function coerceGlobalValue(field: string, raw: string): string | number | undefined { + const spec = VALUE_SPEC_BY_FIELD[field]; + if (!spec) return raw; + if (spec.valueType === "number") { + const n = Number(raw); + return Number.isFinite(n) && n >= 0 ? n : undefined; + } + if (spec.choices) return spec.choices.includes(raw) ? raw : undefined; + return raw; +} + +interface YargsOption { + type: "string" | "number" | "boolean"; + choices?: readonly string[]; + alias?: string; +} + +/** yargs `.options()` shape, keyed by kebab flag name. secret-stdin + boolean flags are presence flags. */ +export function globalYargsOptions(): Record { + const out: Record = {}; + for (const f of GLOBAL_FLAG_SPECS) { + const o: YargsOption = { type: f.kind === "value" ? f.valueType! : "boolean" }; + if (f.choices) o.choices = f.choices; + if (f.alias) o.alias = f.alias; + out[f.name] = o; + } + return out; +} + +/** Token-keyed lookup maps for the pre-yargs scan (bootstrap/runner). Sibling projection to + * globalYargsOptions — the other layer that derives from GLOBAL_FLAG_SPECS. */ +export interface GlobalTokenMaps { + /** flag token (`--long` or `-alias`) → runtime Globals field. */ + valueFlags: Record; + booleanFlags: Record; + /** `---stdin` → secret kind (the only stdin source is fd 0). */ + secretStdinFlags: Record; +} + +export function globalTokenMaps(): GlobalTokenMaps { + const valueFlags: Record = {}; + const booleanFlags: Record = {}; + const secretStdinFlags: Record = {}; + for (const f of GLOBAL_FLAG_SPECS) { + const tokens = f.alias ? [`--${f.name}`, `-${f.alias}`] : [`--${f.name}`]; + if (f.kind === "value") for (const t of tokens) valueFlags[t] = specField(f); + else if (f.kind === "boolean") for (const t of tokens) booleanFlags[t] = specField(f); + else secretStdinFlags[`--${f.name}`] = f.secretKey!; + } + return { valueFlags, booleanFlags, secretStdinFlags }; +} diff --git a/ts/src/adapters/inbound/cli/help/catalog.ts b/ts/src/adapters/inbound/cli/help/catalog.ts new file mode 100644 index 000000000..b1c422f89 --- /dev/null +++ b/ts/src/adapters/inbound/cli/help/catalog.ts @@ -0,0 +1,86 @@ +/** + * Help catalog — the machine-readable half of HelpService: the structured global/stdin flag + * model and the `--json-schema` command catalog. The human text renderer lives in./index. + * Single structured source: the flag model is rendered as text in command --help AND emitted as + * `globalFlags` in the root catalog. + */ +import { z } from "zod"; +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { commandId } from "../command-id.js"; +import { GLOBAL_FLAG_SPECS, type GlobalFlagSpec } from "../globals/index.js"; + +// Flags accepted on every command (kubectl-style globals + secret channels). The flag model — arity, +// descriptions, defaults, and the global-vs-command-scoped split — is owned by domain metadata +// GLOBAL_FLAG_SPECS; this layer is the rendered shape (`--flag`/`-alias` tokens) and a pure +// projection over it. GLOBAL_FLAGS = the globally-listed flags; STDIN_FLAGS = the command-scoped ones. +export interface GlobalFlag { + flag: string; + alias?: string; + type: "string" | "number" | "boolean"; + values?: string[]; + description: string; + optional?: boolean; + defaultValue?: string | number | boolean; +} + +/** spec → rendered GlobalFlag (adds the `--`/`-` token prefixes the help/catalog surface use). */ +function globalFlagDoc(f: GlobalFlagSpec): GlobalFlag { + return { + flag: `--${f.name}`, + ...(f.alias ? { alias: `-${f.alias}` } : {}), + type: f.kind === "value" ? f.valueType! : "boolean", + ...(f.choices ? { values: [...f.choices] } : {}), + description: f.description, + ...(f.defaultValue !== undefined ? { defaultValue: f.defaultValue } : {}), + }; +} + +export const GLOBAL_FLAGS: readonly GlobalFlag[] = GLOBAL_FLAG_SPECS.filter((f) => !f.commandScoped).map(globalFlagDoc); + +// stdin channel → its documented --*-stdin flag, derived from the command-scoped specs (keyed by secretKey). +const STDIN_FLAGS = Object.fromEntries( + GLOBAL_FLAG_SPECS.filter((f) => f.commandScoped).map((f) => [f.secretKey, globalFlagDoc(f)]), +) as Record, GlobalFlag>; + +export function inputFlagsFor(cmd: CommandDefinition): readonly GlobalFlag[] { + return cmd.stdin ? [STDIN_FLAGS[cmd.stdin]] : []; +} + +/** usage line: the typed path is complete for both kinds (neutral = full, chain = logical). */ +export function commandUsage(cmd: CommandDefinition): string { + return `wallet-cli ${cmd.path.join(" ")} [flags]`; +} + +/** never let one un-convertible schema break the whole catalog. */ +function commandInputSchema(input: CommandDefinition["input"]): unknown { + try { + return z.toJSONSchema(input as z.ZodType); + } catch { + return { type: "object" }; + } +} + +/** machine-readable catalog of the whole command surface — the agent's single discovery call. */ +export function buildCatalog(registry: CommandRegistry, version: string, familyFilter?: ChainFamily): string { + const commands = registry + .all() + .filter((c) => !familyFilter || c.family === familyFilter) + .map((cmd) => ({ cmd, id: commandId(cmd) })) + .sort((a, b) => a.id.localeCompare(b.id)) + .map(({ cmd, id }) => ({ + id, + kind: cmd.family ? "chain" : "neutral", + ...(cmd.family ? { family: cmd.family } : {}), + path: cmd.path, + usage: commandUsage(cmd), + summary: cmd.summary ?? "", + requires: { network: cmd.network, auth: cmd.auth, wallet: cmd.wallet }, + ...(cmd.capability ? { capability: cmd.capability } : {}), + examples: cmd.examples.map((e) => e.cmd), + ...(inputFlagsFor(cmd).length ? { inputFlags: inputFlagsFor(cmd) } : {}), + inputSchema: commandInputSchema(cmd.input), + })); + return JSON.stringify({ tool: "wallet-cli", version, globalFlags: GLOBAL_FLAGS, commands }); +} diff --git a/ts/src/adapters/inbound/cli/help/index.ts b/ts/src/adapters/inbound/cli/help/index.ts new file mode 100644 index 000000000..f142b267a --- /dev/null +++ b/ts/src/adapters/inbound/cli/help/index.ts @@ -0,0 +1,396 @@ +/** + * HelpService — --help / --version / --json-schema. Zod-driven: every flag's help, + * required/optional/default, examples, and the agent JSON-schema come from the command's + * zod fields/input; one schema supplies validation, types, help, and agent schema. + * + * Two command kinds, discriminated by `family`: neutral (full path) and chain (logical path, + * per-family impls). A leading family token (e.g. tron) is an optional addressing prefix here. + */ +import { z } from "zod"; +import type { ChainFamily, ExitCode } from "../../../../domain/types/index.js"; +import type { CommandDefinition, StreamManager } from "../contracts/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { introspectFields, type FieldInfo } from "../arity/index.js"; +import { GLOBAL_FLAGS, type GlobalFlag, inputFlagsFor, buildCatalog } from "./catalog.js"; + +const META = new Set(["--help", "-h", "--version", "-V", "--json-schema"]); + +export function hasMeta(tokens: string[]): boolean { + return tokens.some((t) => META.has(t)); +} + +export class HelpService { + constructor( + private readonly registry: CommandRegistry, + private readonly streams: StreamManager, + private readonly version: string, + ) {} + + handleMeta(tokens: string[]): ExitCode { + if (tokens.includes("--version") || tokens.includes("-V")) { + this.streams.result(this.version); + return 0; + } + const positionals = tokens.filter((t) => !t.startsWith("-")); + const { family, path } = this.#split(positionals); + const concrete = this.#resolveConcrete(family, path); + + if (tokens.includes("--json-schema")) { + if (concrete) { + this.streams.result(JSON.stringify(z.toJSONSchema(concrete.input))); + return 0; + } + // no concrete command → machine catalog (every command + flags), optionally scoped to a + // chain family (`tron --json-schema`). Mirrors the help tree. + this.streams.result(this.#catalog(family)); + return 0; + } + + if (concrete) { + this.streams.result(this.#renderCommand(concrete)); + return 0; + } + if (!family && path.length === 1 && this.#isNeutralGroup(path[0]!)) { + this.streams.result(this.#renderNeutralGroup(path[0]!)); + return 0; + } + if (path.length > 1 && this.#isChainGroup(path[0]!)) { + let candidates = this.registry.resolveCandidates(path); + if (family) candidates = candidates.filter((c) => c.family === family); + if (candidates.length > 0) { + this.streams.result(this.#renderLogicalCommand(path, candidates)); + return 0; + } + } + this.streams.result(this.#renderTree(path[0])); + return 0; + } + + /** strip an optional leading family token (e.g. tron) — a help/catalog addressing prefix. */ + #split(positionals: string[]): { family?: ChainFamily; path: string[] } { + const head = positionals[0]; + if (head && (this.registry.families() as string[]).includes(head)) { + return { family: head as ChainFamily, path: positionals.slice(1) }; + } + return { path: positionals }; + } + + /** resolve to a single command: a neutral command by full path, or a family-pinned chain command. */ + #resolveConcrete(family: ChainFamily | undefined, path: string[]): CommandDefinition | null { + if (path.length === 0) return null; + if (family) return this.registry.resolveForFamily(path, family); + const neutral = this.registry.resolveNeutral(path); + if (neutral) return neutral; + // single-segment chain leaf (e.g. `block`): resolve by its HEAD so `block`, `block 123`, and even + // `block ` all render the leaf help instead of a phantom `block COMMAND` group. Group heads + // like `account` have no command at the bare path, so they stay groups (headLeaf is undefined). + const headLeaf = this.registry.resolveCandidates([path[0]!])[0]; + if (headLeaf && headLeaf.path.length === 1) return headLeaf; + return null; + } + + #renderTree(head?: string): string { + if (!head) return this.#renderRoot(); + if (this.#isChainGroup(head)) return this.#renderLogicalNs(head); + if (this.#isNeutralGroup(head)) return this.#renderNeutralGroup(head); + return this.#renderRoot(); + } + + /** top-level overview: first release presents TRON as the product surface. + * Docker-style three groups: Common (高频入口) / Management (链上资源名词) / Commands (本机治理). */ + #renderRoot(): string { + const common = [ + ["create", "Create a new HD wallet (BIP39 seed)", ""], + ["import", "Import a wallet", ""], + ["list", "List wallets / accounts", ""], + ] as const; + const management = [ + ["account", "Query on-chain account state", ""], + ["token", "Manage the token address book and query tokens", ""], + ["tx", "Build, send, broadcast, and inspect transactions", ""], + ["contract", "Call, send, deploy, and inspect smart contracts", ""], + ["stake", "Stake / delegate resources", "tron"], + ["message", "Sign arbitrary messages", ""], + ["block", "Get a block (latest if omitted)", ""], + ] as const; + const commands = [ + ["use", "Set the active account", ""], + ["current", "Show the current (active) account", ""], + ["rename", "Rename an account label", ""], + ["derive", "Derive the next HD account from a seed wallet", ""], + ["backup", "Export an account's secret + metadata (0600)", ""], + ["delete", "Delete a wallet / account", ""], + ["config", "Show / get / set configuration values", ""], + ["networks", "List known networks", ""], + ] as const; + const sections = [common, management, commands] as const; + const nameWidth = Math.max(...sections.flat().map(([name]) => name.length)) + 2; + // chain-only groups carry a right-hand (family) tag; align it past the widest description. + const tagCol = Math.max(...sections.flat().map(([, desc]) => desc.length)) + 2; + const commandRow = (name: string, desc: string, tag: string): string => { + const body = ` ${name.padEnd(nameWidth)}${dim(desc)}`; + return tag ? `${body}${" ".repeat(Math.max(2, tagCol - desc.length))}(${tag})` : body.trimEnd(); + }; + const row = (width: number) => (name: string, desc: string): string => ` ${name.padEnd(width)}${desc ? dim(desc) : ""}`.trimEnd(); + const optionRows = [ + ["-o, --output string", 'Output format ("text", "json") (default from config)'], + ["--network string", 'Canonical network id, e.g. "tron:mainnet", "tron:nile", "tron:shasta"'], + ["--account string", "Account label or address to act as (overrides active)"], + ["--timeout int", "Request timeout in milliseconds"], + ["-v, --verbose", "Verbose / debug logging"], + ["-h, --help", "Show help"], + ["-V, --version", "Print version information and quit"], + ] as const; + const optionRow = row(Math.max(...optionRows.map(([name]) => name.length)) + 2); + + // Usage first, description after (: 描述统一在 Usage 之后); root Usage is the inline form. + const lines = [ + `${bold("Usage:")} wallet-cli [OPTIONS] COMMAND`, + "", + `${bold("wallet-cli")} — CLI wallet for TRON.`, + "Agent-first: deterministic exit codes, JSON output, no interactive prompts.", + "", + bold("Common Commands:"), + ]; + for (const [name, desc, tag] of common) lines.push(commandRow(name, desc, tag)); + + lines.push("", bold("Management Commands:")); + for (const [name, desc, tag] of management) lines.push(commandRow(name, desc, tag)); + + lines.push("", bold("Commands:")); + for (const [name, desc, tag] of commands) lines.push(commandRow(name, desc, tag)); + + lines.push("", bold("Global Options:")); + for (const [name, desc] of optionRows) lines.push(optionRow(name, desc)); + lines.push("", "Run 'wallet-cli COMMAND --help' for more information on a command."); + return lines.join("\n"); + } + + /** neutral group (`import --help`): list the group's sub-commands. Derived from the registry. */ + #renderNeutralGroup(head: string): string { + const cmds = this.#neutralGroupCommands(head); + const rows = cmds.map((c) => [c.path[1] ?? "", c.summary ?? ""] as const); + return this.#renderGroup(head, rows, 1000); + } + + /** logical resource group (`account --help`): default surface, implementations chosen by --network/defaultNetwork. */ + #renderLogicalNs(group: string): string { + const commands = this.#chainGroupCommands(group); + const rows = commands.map((c) => [c.path[1] ?? "", c.summary ?? ""] as const); + return this.#renderGroup(group, rows, 18); + } + + /** shared group skeleton (群组层): inline Usage → description → verb list → footer. */ + #renderGroup(group: string, rows: ReadonlyArray, maxWidth: number): string { + const width = Math.min(maxWidth, Math.max(0, ...rows.map(([verb]) => verb.length)) + 2); + const lines = [`${bold("Usage:")} wallet-cli ${group} COMMAND`, ""]; + const desc = GROUP_DESCRIPTIONS[group]; + if (desc) lines.push(desc, ""); + lines.push(bold("Commands:")); + for (const [verb, summary] of rows) lines.push(` ${verb.padEnd(width)} ${summary}`.trimEnd()); + lines.push("", `Run 'wallet-cli ${group} COMMAND --help' for more information on a command.`); + return lines.join("\n"); + } + + /** logical leaf (`account balance --help`): merge per-family flags; addressing/auth taken from the first impl. */ + #renderLogicalCommand(path: string[], candidates: CommandDefinition[]): string { + const fields = new Map(); + for (const cmd of candidates) { + for (const f of introspectFields(cmd.fields)) fields.set(f.name, f); + } + const base = candidates[0]!; + return this.#renderLeaf({ + path, + summary: base.summary, + network: base.network, + auth: base.auth, + wallet: base.wallet, + broadcasts: base.broadcasts, + fields: [...fields.values()], + inputFlags: inputFlagsFor(base), + examples: base.examples, + }); + } + + #renderCommand(cmd: CommandDefinition): string { + return this.#renderLeaf({ + path: cmd.path, + summary: cmd.summary, + network: cmd.network, + auth: cmd.auth, + wallet: cmd.wallet, + broadcasts: cmd.broadcasts, + fields: introspectFields(cmd.fields), + inputFlags: inputFlagsFor(cmd), + examples: cmd.examples, + }); + } + + /** shared leaf skeleton (叶子层): Usage → description → Requires → Flags → Input flags → Global flags → Examples. */ + #renderLeaf(c: { + path: string[]; + summary?: string; + network: CommandDefinition["network"]; + auth: CommandDefinition["auth"]; + wallet: CommandDefinition["wallet"]; + broadcasts?: boolean; + fields: FieldInfo[]; + inputFlags: readonly GlobalFlag[]; + examples: CommandDefinition["examples"]; + }): string { + const lines = ["Usage:", ` wallet-cli ${c.path.join(" ")} [flags]`]; + if (c.summary) lines.push("", c.summary); + + const requires: string[] = []; + if (c.network === "required") requires.push("--network "); + if (c.auth === "required") requires.push("master password — pass --password-stdin for non-interactive use, or enter it interactively in a TTY"); + if (c.wallet !== "none") requires.push("an account — defaults to active; override with --account (or `use `)"); + if (requires.length) { + lines.push("", "Requires:"); + for (const r of requires) lines.push(` ${r}`); + } + + if (c.fields.length) { + const heads = c.fields.map(flagHead); + const width = Math.min(34, Math.max(...heads.map((h) => h.length))); + lines.push("", "Flags:"); + c.fields.forEach((f, i) => { + const desc = f.description ?? ""; + const tag = flagTag(f); + lines.push(` ${heads[i]!.padEnd(width)} ${desc}${desc && tag ? " " : ""}${tag}`.trimEnd()); + }); + } + + if (c.inputFlags.length) { + lines.push("", "Input flags:"); + for (const f of c.inputFlags) lines.push(globalFlagLine(f)); + } + + lines.push("", "Global flags:"); + // curated per command: --network only when the command selects a network; --password-stdin + // only when it requires unlock; --account is surfaced via Requires, not repeated here. + for (const g of globalFlagsForText(c.network, c.auth, c.broadcasts ?? false)) lines.push(globalFlagLine(g)); + + if (c.examples.length) { + lines.push("", "Examples:"); + for (const e of c.examples) lines.push(` ${e.cmd}${e.note ? ` # ${e.note}` : ""}`); + } + return lines.join("\n"); + } + + /** chain groups = first path segment of every family-bound command. */ + #chainGroups(): string[] { + const seen = new Set(); + const out: string[] = []; + for (const c of this.registry.all()) { + if (!c.family) continue; + const group = c.path[0]; + if (group && !seen.has(group)) (seen.add(group), out.push(group)); + } + return out; + } + + #isChainGroup(group: string): boolean { + return this.#chainGroups().includes(group); + } + + /** chain group sub-commands, deduped across families by logical path. */ + #chainGroupCommands(group: string): Array<{ path: string[]; summary?: string }> { + const seen = new Set(); + const out: Array<{ path: string[]; summary?: string }> = []; + for (const c of this.registry.all()) { + if (!c.family || c.path[0] !== group) continue; + const key = c.path.join("."); + if (!seen.has(key)) (seen.add(key), out.push({ path: c.path, summary: c.summary })); + } + return out; + } + + /** neutral groups = heads of neutral commands that have sub-verbs (e.g. import). */ + #neutralGroupCommands(head: string): CommandDefinition[] { + return this.registry.all().filter((c) => !c.family && c.path[0] === head && c.path.length > 1); + } + + #isNeutralGroup(head: string): boolean { + return this.#neutralGroupCommands(head).length > 0; + } + + /** machine-readable catalog of the whole command surface — the agent's single discovery call. */ + #catalog(familyFilter?: ChainFamily): string { + return buildCatalog(this.registry, this.version, familyFilter); + } +} + +/** "--flag " header for a command flag — enum fields list their choices instead of . */ +function flagHead(f: FieldInfo): string { + const typ = f.choices ? ` <${f.choices.join("|")}>` : f.baseType === "boolean" ? "" : ` <${f.baseType}>`; + return `--${f.kebab}${typ}`; +} + +/** "[required]" / "[optional, default: X]" / "[optional]" tag derived from the zod schema. */ +function flagTag(f: FieldInfo): string { + if (!f.optional && !f.hasDefault) return "[required]"; + if (f.hasDefault) return `[optional, default: ${formatDefault(f.defaultValue)}]`; + return "[optional]"; +} + +function formatDefault(v: unknown): string { + if (typeof v === "string") return v === "" ? '""' : v; + return String(v); +} + +// Per-command "Global flags" projection: output/timeout/verbose always; --network only when the +// command selects a network; --password-stdin only when it requires unlock; --wait/--wait-timeout +// only for ✍️ broadcast commands; --account is never repeated here (it surfaces under Requires). The full +// GLOBAL_FLAGS array still backs the --json-schema catalog. +function globalFlagsForText(network: CommandDefinition["network"], auth: CommandDefinition["auth"], broadcasts: boolean): GlobalFlag[] { + return GLOBAL_FLAGS.filter((g) => { + if (g.flag === "--account") return false; + if (g.flag === "--network") return network !== "none"; + if (g.flag === "--password-stdin") return auth === "required"; + if (g.flag === "--wait" || g.flag === "--wait-timeout") return broadcasts; + return true; + }); +} + +/** one rendered " --flag description [tag]" line, shared by Input flags and Global flags. */ +function globalFlagLine(g: GlobalFlag): string { + const tag = globalFlagTag(g); + return ` ${globalFlagHead(g).padEnd(26)} ${g.description}${g.description && tag ? " " : ""}${tag}`.trimEnd(); +} + +// Group (群组层) one-line descriptions, keyed by the registry group head. Only groups that surface a +// ` --help` page need an entry; absent → the description line is omitted. +const GROUP_DESCRIPTIONS: Record = { + import: "Import a wallet from an existing secret or device.", + account: "Query on-chain account state.", + token: "Manage the token address book and query tokens.", + tx: "Build, send, broadcast, and inspect transactions.", + contract: "Call, send, deploy, and inspect smart contracts.", + stake: "Stake / delegate resources (TRON Stake 2.0).", + message: "Sign arbitrary messages.", + block: "Get a block (latest if omitted).", +}; + +/** "--output, -o " style header for text help. */ +function globalFlagHead(g: GlobalFlag): string { + const head = g.alias ? `${g.flag}, ${g.alias}` : g.flag; + const typ = g.type === "boolean" ? "" : ` <${g.values ? g.values.join("|") : g.type}>`; + return `${head}${typ}`; +} + +function globalFlagTag(g: GlobalFlag): string { + if (g.defaultValue !== undefined) return `[optional, default: ${formatDefault(g.defaultValue)}]`; + return "[optional]"; +} + +/** color only when stdout is a TTY and NO_COLOR is unset — piped/redirected help stays plain. */ +function colorOn(): boolean { + return !!process.stdout.isTTY && !process.env.NO_COLOR; +} +function bold(s: string): string { + return colorOn() ? `\x1b[1m${s}\x1b[0m` : s; +} +function dim(s: string): string { + return colorOn() ? `\x1b[2m${s}\x1b[0m` : s; +} diff --git a/ts/src/adapters/inbound/cli/input/prompt/index.ts b/ts/src/adapters/inbound/cli/input/prompt/index.ts new file mode 100644 index 000000000..6b0a02975 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/index.ts @@ -0,0 +1,249 @@ +/** + * Prompter — the single owner of interactive TTY I/O. Logic (validation loops, + * confirm match, select navigation) is backend-agnostic; the real backend talks to + * /dev/tty. Prompts/echo never touch stdout. + */ +import { openSync, closeSync } from "node:fs"; +import { ReadStream } from "node:tty"; +import * as readline from "node:readline"; +import { ExecutionError } from "../../../../../domain/errors/index.js"; + +export type KeyEvent = { name?: string; ctrl?: boolean; sequence?: string }; +export interface Choice { value: T; label: string } + +export interface PromptBackend { + isTTY(): boolean; + /** read one line; when hidden, keystrokes are not echoed. */ + question(prompt: string, hidden: boolean): Promise; + /** resolve on the next keypress (raw mode must be active). */ + readKey(): Promise; + write(s: string): void; + beginRaw(): void; + endRaw(): void; + /** release any held resources (e.g. the /dev/tty stream) so the process can exit. */ + close?(): void; +} + +export class Prompter { + #renderedSelectLines = 0; + #interactive = true; + + constructor(private readonly be: PromptBackend) {} + + /** + * Per-dispatch interaction policy. Dispatch sets this per command so non-interactive commands + * behave as if there were no TTY — every prompt site already gates on isTTY(), so flipping this + * off makes gap-fill / password / secret prompts fail-fast on missing input instead of blocking. + */ + setInteractive(allowed: boolean): void { this.#interactive = allowed; } + + isTTY(): boolean { return this.#interactive && this.be.isTTY(); } + + /** release backend resources at end of run (no-op for in-memory backends). */ + close(): void { this.be.close?.(); } + + async text(o: { label: string; validate?: (v: string) => string | null }): Promise { + for (;;) { + const v = (await this.be.question(`${color("cyan", "?")} ${o.label}: `, false)).trim(); + const err = o.validate?.(v); + if (err) { this.be.write(`${color("red", " x")} ${err}\n`); continue; } + return v; + } + } + + async hidden(o: { label: string; confirm?: boolean; confirmLabel?: string; validate?: (v: string) => string | null }): Promise { + for (;;) { + const v = await this.be.question(`${color("cyan", "?")} ${o.label}: `, true); + const err = o.validate?.(v); + if (err) { this.be.write(`${color("red", " x")} ${err}\n`); continue; } + if (o.confirm) { + const again = await this.be.question(`${color("cyan", "?")} ${o.confirmLabel ?? "Confirm"}: `, true); + if (v !== again) { this.be.write(`${color("red", " x")} entries do not match\n`); continue; } + } + return v; + } + } + + async confirm(o: { label: string; expect?: string }): Promise { + const suffix = o.expect === undefined ? " [y/N]" : ""; + const v = await this.be.question(`${color("yellow", "?")} ${o.label}${suffix}: `, false); + if (o.expect !== undefined) return v.trim() === o.expect; + return /^y(es)?$/i.test(v.trim()); + } + + async select(o: { label: string; choices: Choice[]; loadMore?: () => Promise[]> }): Promise { + let items = [...o.choices]; + let idx = 0; + this.be.beginRaw(); + try { + this.#renderedSelectLines = 0; + this.#render(o.label, items, idx); + for (;;) { + const k = await this.be.readKey(); + if (k.ctrl && k.name === "c") throw new ExecutionError("aborted", "cancelled"); + if (k.name === "up") idx = Math.max(0, idx - 1); + else if (k.name === "down") { + if (idx === items.length - 1 && o.loadMore) { + const more = await o.loadMore(); + if (more.length > items.length) { items = more; idx++; } + } else { + idx = Math.min(items.length - 1, idx + 1); + } + } else if (k.name === "return") return items[idx]!.value; + this.#render(o.label, items, idx); + } + } finally { + this.be.endRaw(); + this.#renderedSelectLines = 0; + } + } + + #render(label: string, items: Choice[], idx: number): void { + if (this.#renderedSelectLines > 0) { + this.be.write(`\x1b[${this.#renderedSelectLines}F\x1b[J`); + } + const lines = items.map((c, i) => `${i === idx ? color("cyan", ">") : " "} ${c.label}`); + const frame = [ + `${color("cyan", "?")} ${label} ${dim("(Up/Down, Enter)")}`, + ...lines, + ]; + this.#renderedSelectLines = frame.length; + this.be.write(`${frame.join("\n")}\n`); + } +} + +function color(kind: "cyan" | "red" | "yellow" | "green", s: string): string { + if (process.env.NO_COLOR) return s; + const code = { cyan: 36, red: 31, yellow: 33, green: 32 }[kind]; + return `\x1b[${code}m${s}\x1b[0m`; +} + +function dim(s: string): string { + return process.env.NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`; +} + +/** Real backend: reads /dev/tty, writes prompts to /dev/tty (never stdout). */ +export class TtyBackend implements PromptBackend { + #tty: boolean; + #fd?: number; + #input?: ReadStream; + #keyQueue: KeyEvent[] = []; + #pendingKey?: (key: KeyEvent) => void; + #keyListener?: (s: string, key: KeyEvent) => void; + constructor() { + // Probe for a controlling terminal without holding the fd; the real stream opens on first prompt. + try { + closeSync(openSync("/dev/tty", "r")); + this.#tty = true; + } catch { + this.#tty = false; + } + } + isTTY(): boolean { return this.#tty; } + write(s: string): void { process.stderr.write(s); } + + /** + * ONE persistent tty.ReadStream for the whole run (a real TTY stream, unlike fs.createReadStream). + * Per-prompt fds caused two failures: an undestroyed fs stream hung the event loop, and opening a + * fresh fd per prompt let the previous prompt's async teardown reset the terminal AFTER the next + * prompt set raw mode → the confirm prompt echoed the secret. A single reused stream avoids both; + * the runner calls close once at the end to release it so the process exits. + */ + #stream(): ReadStream { + if (!this.#input) { + this.#fd = openSync("/dev/tty", "r"); + this.#input = new ReadStream(this.#fd); + } + return this.#input; + } + + /** + * Visible input goes through readline (echoes typed text, manages the prompt). Hidden input is read + * MANUALLY in raw mode: readline's terminal-mode redraw emits `ESC[1G ESC[0J` which erases the + * just-written prompt on a real terminal (prompt vanished → looked hung). A raw manual read writes + * the prompt once and never redraws; raw mode means the OS never echoes the secret. + */ + question(prompt: string, hidden: boolean): Promise { + const input = this.#stream(); + if (!hidden) { + const rl = readline.createInterface({ input, output: process.stderr, terminal: true }); + return new Promise((resolve) => { + rl.question(prompt, (ans) => { rl.close(); input.pause(); resolve(ans); }); + }); + } + return new Promise((resolve) => { + process.stderr.write(prompt); + input.setRawMode(true); + input.resume(); + let buf = ""; + const finish = (val: string): void => { + input.setRawMode(false); + input.off("data", onData); + input.pause(); + process.stderr.write("\n"); + resolve(val); + }; + const onData = (d: Buffer): void => { + for (const ch of d.toString("utf8")) { + const code = ch.charCodeAt(0); + if (ch === "\r" || ch === "\n") return finish(buf); // Enter + if (code === 3) { process.stderr.write("\n"); process.exit(130); } // Ctrl-C + if (code === 4) return finish(buf); // Ctrl-D + if (code === 127 || code === 8) { buf = buf.slice(0, -1); continue; } // Backspace + if (code < 32) continue; // ignore other control chars + buf += ch; + } + }; + input.on("data", onData); + }); + } + + beginRaw(): void { + const s = this.#stream(); + readline.emitKeypressEvents(s); + if (!this.#keyListener) { + this.#keyListener = (_s: string, key: KeyEvent) => { + if (this.#pendingKey) { + const resolve = this.#pendingKey; + this.#pendingKey = undefined; + resolve(key ?? {}); + return; + } + this.#keyQueue.push(key ?? {}); + }; + s.on("keypress", this.#keyListener); + } + s.setRawMode(true); + s.resume(); + } + endRaw(): void { + if (this.#keyListener) { + this.#input?.off("keypress", this.#keyListener); + this.#keyListener = undefined; + } + this.#pendingKey = undefined; + this.#keyQueue = []; + this.#input?.setRawMode(false); + this.#input?.pause(); + } + readKey(): Promise { + const queued = this.#keyQueue.shift(); + if (queued) return Promise.resolve(queued); + return new Promise((resolve) => { + this.#pendingKey = resolve; + }); + } + /** Release the persistent /dev/tty stream so the event loop drains and the process exits. */ + close(): void { + if (this.#input) { + try { this.#input.setRawMode(false); } catch { /* may already be closed */ } + this.#input.destroy(); + this.#input = undefined; + } + this.#fd = undefined; + } +} + +export function createPrompter(): Prompter { + return new Prompter(new TtyBackend()); +} diff --git a/ts/src/adapters/inbound/cli/input/prompt/prompter.test.ts b/ts/src/adapters/inbound/cli/input/prompt/prompter.test.ts new file mode 100644 index 000000000..197da86d6 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/prompter.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from "vitest"; +import { Prompter, type PromptBackend, type KeyEvent } from "./index.js"; + +class FakeBackend implements PromptBackend { + out = ""; + #answers: string[]; + #keys: KeyEvent[]; + constructor(answers: string[] = [], keys: KeyEvent[] = []) { this.#answers = answers; this.#keys = keys; } + isTTY() { return true; } + async question(prompt: string, _hidden: boolean) { this.out += prompt; return this.#answers.shift() ?? ""; } + async readKey() { return this.#keys.shift() ?? { name: "return" }; } + write(s: string) { this.out += s; } + beginRaw() {} + endRaw() {} +} + +describe("Prompter.setInteractive", () => { + it("forces isTTY false when interaction is disabled, even on a real TTY", () => { + const p = new Prompter(new FakeBackend()); // FakeBackend.isTTY() === true + expect(p.isTTY()).toBe(true); + p.setInteractive(false); + expect(p.isTTY()).toBe(false); + p.setInteractive(true); + expect(p.isTTY()).toBe(true); + }); +}); + +describe("Prompter.text", () => { + it("re-prompts until validate passes", async () => { + const be = new FakeBackend(["", " ", "ok"]); + const p = new Prompter(be); + const v = await p.text({ label: "name", validate: (s) => (s.trim() ? null : "required") }); + expect(v).toBe("ok"); + }); +}); + +describe("Prompter.hidden", () => { + it("requires the confirm entry to match", async () => { + const be = new FakeBackend(["Abcdef1!", "nope", "Abcdef1!", "Abcdef1!"]); + const p = new Prompter(be); + const v = await p.hidden({ label: "pw", confirm: true }); + expect(v).toBe("Abcdef1!"); + }); + it("re-prompts on validate failure", async () => { + const be = new FakeBackend(["weak", "Abcdef1!"]); + const p = new Prompter(be); + const v = await p.hidden({ label: "pw", validate: (s) => (s.length >= 8 ? null : "too short") }); + expect(v).toBe("Abcdef1!"); + }); +}); + +describe("Prompter.confirm", () => { + it("expect-mode returns true only when the exact ref is typed", async () => { + const ok = new Prompter(new FakeBackend(["wlt_a.0"])); + expect(await ok.confirm({ label: "type ref", expect: "wlt_a.0" })).toBe(true); + const no = new Prompter(new FakeBackend(["wrong"])); + expect(await no.confirm({ label: "type ref", expect: "wlt_a.0" })).toBe(false); + }); +}); + +describe("Prompter.select", () => { + it("arrows to an item and returns its value on enter", async () => { + const be = new FakeBackend([], [{ name: "down" }, { name: "return" }]); + const p = new Prompter(be); + const v = await p.select({ label: "pick", choices: [{ value: "a", label: "A" }, { value: "b", label: "B" }] }); + expect(v).toBe("b"); + }); + it("loads more when arrowing past the last item", async () => { + const be = new FakeBackend([], [{ name: "down" }, { name: "down" }, { name: "return" }]); + const p = new Prompter(be); + let loaded = false; + const v = await p.select({ + label: "pick", + choices: [{ value: "x0", label: "0" }], + loadMore: async () => { loaded = true; return [{ value: "x0", label: "0" }, { value: "x1", label: "1" }]; }, + }); + expect(loaded).toBe(true); + expect(v).toBe("x1"); + }); + it("advances onto the newly loaded item after a single down past the end", async () => { + const be = new FakeBackend([], [{ name: "down" }, { name: "return" }]); + const p = new Prompter(be); + const v = await p.select({ + label: "pick", + choices: [{ value: "x0", label: "0" }], + loadMore: async () => [{ value: "x0", label: "0" }, { value: "x1", label: "1" }], + }); + expect(v).toBe("x1"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/input/prompt/validators.test.ts b/ts/src/adapters/inbound/cli/input/prompt/validators.test.ts new file mode 100644 index 000000000..fdcc46ab3 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/validators.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { passwordPolicyErrors, isValidPrivateKeyHex, isValidMnemonic, PASSWORD_SPECIALS } from "./validators.js"; + +describe("passwordPolicyErrors", () => { + it("accepts a strong password", () => { + expect(passwordPolicyErrors("Abcdef1!")).toEqual([]); + }); + it("rejects too short", () => { + expect(passwordPolicyErrors("Ab1!")).toContainEqual(expect.stringContaining("8")); + }); + it("flags each missing class", () => { + const errs = passwordPolicyErrors("abcdefgh"); // no upper, digit, special + expect(errs.length).toBe(3); + }); + it("uses the documented special set", () => { + expect(PASSWORD_SPECIALS).toContain("!"); + expect(passwordPolicyErrors("Abcdefg1#")).toEqual([]); // # is in the set + }); +}); + +describe("isValidPrivateKeyHex", () => { + it("accepts 64 hex with or without 0x", () => { + const k = "a".repeat(64); + expect(isValidPrivateKeyHex(k)).toBe(true); + expect(isValidPrivateKeyHex("0x" + k)).toBe(true); + }); + it("rejects wrong length or non-hex", () => { + expect(isValidPrivateKeyHex("a".repeat(63))).toBe(false); + expect(isValidPrivateKeyHex("z".repeat(64))).toBe(false); + }); +}); + +describe("isValidMnemonic", () => { + it("accepts a valid 12-word phrase", () => { + expect(isValidMnemonic("legal winner thank year wave sausage worth useful legal winner thank yellow")).toBe(true); + }); + it("rejects garbage", () => { + expect(isValidMnemonic("not a real mnemonic phrase at all nope nope nope nope nope")).toBe(false); + }); +}); diff --git a/ts/src/adapters/inbound/cli/input/prompt/validators.ts b/ts/src/adapters/inbound/cli/input/prompt/validators.ts new file mode 100644 index 000000000..480e56d3d --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/validators.ts @@ -0,0 +1,24 @@ +/** Pure input validators for the interactive prompt layer (no I/O). */ +import { Derivation } from "../../../../../domain/derivation/index.js"; + +export const PASSWORD_SPECIALS = "!@#$%^&*()-_=+[]{};:,.?"; + +/** First-time master-password policy. Returns unmet requirements ([] = acceptable). */ +export function passwordPolicyErrors(pw: string): string[] { + const errs: string[] = []; + if (pw.length < 8) errs.push("must be at least 8 characters"); + if (!/[A-Z]/.test(pw)) errs.push("must include an uppercase letter"); + if (!/[a-z]/.test(pw)) errs.push("must include a lowercase letter"); + if (!/[0-9]/.test(pw)) errs.push("must include a digit"); + const specials = new Set(PASSWORD_SPECIALS); + if (![...pw].some((c) => specials.has(c))) errs.push(`must include a special character (${PASSWORD_SPECIALS})`); + return errs; +} + +export function isValidPrivateKeyHex(s: string): boolean { + return /^(0x)?[0-9a-fA-F]{64}$/.test(s.trim()); +} + +export function isValidMnemonic(s: string): boolean { + return Derivation.validateMnemonic(s); +} diff --git a/ts/src/adapters/inbound/cli/input/secret/index.ts b/ts/src/adapters/inbound/cli/input/secret/index.ts new file mode 100644 index 000000000..c5f8d4650 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/secret/index.ts @@ -0,0 +1,165 @@ +/** + * SecretResolver — the single place that reads secrets, memoized per source. + * Every secret kind binds to its own source `---stdin`, which reads stdin (fd 0) — at most + * one secret may use it per run. The `---file`/`/dev/fd/N` multi-fd path was removed; commands + * needing a 2nd secret (import-mnemonic/import-private-key/backup) go interactive. + * There is NO env source (no MASTER_PASSWORD): secrets never sit in env/process-table/history. + * Handlers must never touch process.stdin directly. Secrets never enter logs/envelopes. + */ +import type { SecretKind, SecretResolver as ISecretResolver, StreamManager } from "../../contracts/index.js"; +import { ExecutionError, UsageError } from "../../../../../domain/errors/index.js"; +import type { Prompter } from "../prompt/index.js"; +import { passwordPolicyErrors, isValidMnemonic, isValidPrivateKeyHex } from "../prompt/validators.js"; + +/** path per secret kind; the only source is `---stdin`, so the value is always `-` (stdin). */ +export type SecretPaths = Partial>; + +/** the flag stem for a kind (e.g. privateKey → "private-key", so `--private-key-stdin`). */ +function flagOf(kind: SecretKind): string { + return kind.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); +} + +export class SecretResolver implements ISecretResolver { + #byPath = new Map(); + #stdinUsedBy?: SecretKind; + #primed = new Map(); + + constructor( + private readonly streams: StreamManager, + private readonly paths: SecretPaths = {}, + private readonly prompter?: Prompter, + ) {} + + /** whether a master-password source is configured, WITHOUT consuming it. */ + hasMasterPassword(): boolean { + return this.paths.password !== undefined || this.#primed.has("password"); + } + + masterPassword(): string { + const primed = this.#primed.get("password"); + if (primed !== undefined) return primed; + if (this.paths.password === undefined) { + throw new ExecutionError("auth_required", "master password required: pass --password-stdin"); + } + return this.read("password"); + } + + /** whether a source for `kind` is configured, WITHOUT consuming it. */ + has(kind: SecretKind): boolean { + return this.paths[kind] !== undefined; + } + + read(kind: SecretKind): string { + const primed = this.#primed.get(kind); + if (primed !== undefined) return primed; + const path = this.paths[kind]; + if (path === undefined) { + if (kind === "password") + throw new ExecutionError("auth_required", "master password required: pass --password-stdin"); + throw new ExecutionError("secret_source_error", `missing --${flagOf(kind)}-stdin`); + } + return this.#readPath(path, kind); + } + + /** + * Read a REQUIRED source. A missing source is a usage error (forgot a required flag → + * missing_option, exit 2); secret_source_error is reserved for present-but-unreadable. + */ + require(kind: SecretKind): string { + if (!this.has(kind)) { + throw new UsageError("missing_option", `--${flagOf(kind)}-stdin is required`); + } + return this.read(kind); + } + + /** + * Exactly-one selector for commands that accept an inline value OR a stdin source + * (e.g. --transaction|--tx-stdin, --message|--message-stdin). Both → invalid_option; + * neither → missing_option (both usage/exit 2). + */ + pick(inline: string | undefined, kind: SecretKind, inlineFlag: string): string { + const hasStdin = this.has(kind); + if (inline !== undefined && hasStdin) { + throw new UsageError("invalid_option", `--${inlineFlag} and --${flagOf(kind)}-stdin are mutually exclusive`); + } + if (inline !== undefined) return inline; + if (hasStdin) return this.read(kind); + throw new UsageError("missing_option", `--${inlineFlag} or --${flagOf(kind)}-stdin is required`); + } + + async resolveSecret(kind: "mnemonic" | "privateKey"): Promise { + const validate = kind === "mnemonic" ? isValidMnemonic : isValidPrivateKeyHex; + if (this.has(kind)) { + const v = this.read(kind).trim(); + if (!validate(v)) throw new UsageError("invalid_secret", `--${flagOf(kind)}-stdin is not a valid ${kind}`); + this.streams.diagnostic("info", `${flagOf(kind)} ✓ via pipe`); + return v; + } + if (this.prompter?.isTTY()) { + const label = kind === "mnemonic" + ? "Paste recovery phrase (hidden)" + : "Paste private key (hidden)"; + const v = await this.prompter.hidden({ + label, + validate: (s) => (validate(s.trim()) ? null : `invalid ${kind}`), + }); + const trimmed = v.trim(); + this.#primed.set(kind, trimmed); + return trimmed; + } + throw new UsageError("missing_option", `--${flagOf(kind)}-stdin is required (or run in a terminal)`); + } + + async primePassword(plan: { mode: "set" | "verify"; verify?: (pw: string) => boolean }): Promise { + if (this.has("password")) { + const pw = this.read("password"); + if (plan.mode === "set") { + const errs = passwordPolicyErrors(pw); + if (errs.length) throw new UsageError("weak_password", `password too weak: ${errs.join("; ")}`); + } + if (plan.mode === "verify" && plan.verify && !plan.verify(pw)) { + throw new ExecutionError("auth_failed", "incorrect master password"); + } + this.#primed.set("password", pw); + this.streams.diagnostic("info", "password ✓ via pipe"); + return; + } + if (this.prompter?.isTTY()) { + let pw: string; + if (plan.mode === "set") { + pw = await this.prompter.hidden({ + label: "Set master password (hidden)", + confirmLabel: "Confirm master password", + confirm: true, + validate: (s) => { const e = passwordPolicyErrors(s); return e.length ? e.join("; ") : null; }, + }); + } else { + pw = ""; + for (let attempt = 0; attempt < 3; attempt++) { + pw = await this.prompter.hidden({ label: "Master password (hidden)" }); + if (plan.verify?.(pw)) { this.#primed.set("password", pw); return; } + this.streams.diagnostic("warn", "incorrect master password"); + } + throw new ExecutionError("auth_failed", "incorrect master password"); + } + this.#primed.set("password", pw); + return; + } + throw new ExecutionError("auth_required", "master password required: pass --password-stdin"); + } + + /** The only source is stdin (fd 0), so `path` is always `-`; at most one secret may use it. */ + #readPath(_path: string, kind: SecretKind): string { + if (this.#stdinUsedBy && this.#stdinUsedBy !== kind) { + throw new ExecutionError( + "secret_source_error", + `stdin already consumed by --${flagOf(this.#stdinUsedBy)}-stdin; only one secret may use stdin per run`, + ); + } + this.#stdinUsedBy = kind; + if (!this.#byPath.has("-")) { + this.#byPath.set("-", this.streams.readStdinOnce().replace(/\r?\n$/, "")); + } + return this.#byPath.get("-")!; + } +} diff --git a/ts/src/adapters/inbound/cli/input/secret/secret.test.ts b/ts/src/adapters/inbound/cli/input/secret/secret.test.ts new file mode 100644 index 000000000..af1d9f438 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/secret/secret.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi } from "vitest"; +import { SecretResolver } from "./index.js"; +import { Prompter, type PromptBackend, type KeyEvent } from "../prompt/index.js"; +import { StreamManager } from "../../stream/index.js"; + +function streams(stdin = ""): StreamManager { + // out/err captured to no-op; readStdinOnce returns the provided value + const sm = new StreamManager("text", false, () => {}, () => {}); + vi.spyOn(sm, "readStdinOnce").mockReturnValue(stdin); + return sm; +} + +class Backend implements PromptBackend { + constructor(private answers: string[], private tty = true) {} + isTTY() { return this.tty; } + async question() { return this.answers.shift() ?? ""; } + async readKey(): Promise { return { name: "return" }; } + write() {} + beginRaw() {} + endRaw() {} +} + +const PW = "Abcdef1!"; + +describe("resolveSecret", () => { + it("prompts (hidden) when no stdin source and validates mnemonic", async () => { + const valid = "legal winner thank year wave sausage worth useful legal winner thank yellow"; + const r = new SecretResolver(streams(), {}, new Prompter(new Backend(["bad phrase", valid]))); + expect(await r.resolveSecret("mnemonic")).toBe(valid); + }); + it("uses the stdin source when present", async () => { + const k = "a".repeat(64); + const r = new SecretResolver(streams(k + "\n"), { privateKey: "-" }, new Prompter(new Backend([]))); + expect(await r.resolveSecret("privateKey")).toBe(k); + }); + it("rejects a malformed stdin secret with invalid_secret", async () => { + const r = new SecretResolver(streams("zzz\n"), { privateKey: "-" }, new Prompter(new Backend([]))); + await expect(r.resolveSecret("privateKey")).rejects.toMatchObject({ code: "invalid_secret" }); + }); + it("errors when missing and no TTY", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend([], false))); + await expect(r.resolveSecret("mnemonic")).rejects.toMatchObject({ code: "missing_option" }); + }); +}); + +describe("primePassword", () => { + it("set mode prompts with confirm + policy, then masterPassword() returns it", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend(["weak", PW, PW]))); + await r.primePassword({ mode: "set" }); + expect(r.masterPassword()).toBe(PW); + }); + it("verify mode re-prompts until verify() passes", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend(["nope", PW]))); + await r.primePassword({ mode: "verify", verify: (pw) => pw === PW }); + expect(r.masterPassword()).toBe(PW); + }); + it("set mode via --password-stdin enforces policy (weak_password)", async () => { + const r = new SecretResolver(streams("weak\n"), { password: "-" }, new Prompter(new Backend([]))); + await expect(r.primePassword({ mode: "set" })).rejects.toMatchObject({ code: "weak_password" }); + }); + it("verify mode via --password-stdin just caches", async () => { + const r = new SecretResolver(streams(PW + "\n"), { password: "-" }, new Prompter(new Backend([]))); + await r.primePassword({ mode: "verify", verify: () => true }); + expect(r.masterPassword()).toBe(PW); + }); + it("verify mode via --password-stdin rejects a wrong password", async () => { + const r = new SecretResolver(streams("wrong\n"), { password: "-" }, new Prompter(new Backend([]))); + await expect(r.primePassword({ mode: "verify", verify: (pw) => pw === PW })).rejects.toMatchObject({ code: "auth_failed" }); + }); + it("no source and no TTY → auth_required", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend([], false))); + await expect(r.primePassword({ mode: "verify", verify: () => true })).rejects.toMatchObject({ code: "auth_required" }); + }); +}); + +describe("hasMasterPassword", () => { + it("hasMasterPassword is false with no source/primed even under a TTY (lazy guard must fail fast)", () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend([], /* tty */ true))); + expect(r.hasMasterPassword()).toBe(false); + }); +}); diff --git a/ts/src/adapters/inbound/cli/output/envelope.test.ts b/ts/src/adapters/inbound/cli/output/envelope.test.ts new file mode 100644 index 000000000..208d0cebc --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/envelope.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { OutputEnvelope } from "./envelope.js"; +import type { CommandDefinition } from "../contracts/index.js"; + +// Single shipping family (TRON); the envelope no longer redacts addresses — it passes the +// command's result payload through verbatim under the wallet-cli.result.v1 contract. +const cmd = { path: ["current"] } as CommandDefinition; +const m = { durationMs: 0, warnings: [] }; + +describe("OutputEnvelope.success — result payload passthrough", () => { + it("passes a single descriptor's addresses map through unchanged", () => { + const data = { accountId: "a.0", addresses: { tron: "Ttron" } }; + const env = OutputEnvelope.success(cmd, undefined, data, m); + expect((env.data as { addresses: Record }).addresses).toEqual({ tron: "Ttron" }); + }); + + it("passes a list of descriptors through unchanged", () => { + const data = [ + { accountId: "a.0", addresses: { tron: "Ttron0" } }, + { accountId: "a.1", addresses: { tron: "Ttron1" } }, + ]; + const env = OutputEnvelope.success(cmd, undefined, data, m); + expect(env.data).toEqual(data); + }); + + it("leaves data without an addresses field untouched", () => { + const data = { accountId: "a.0", scope: "wallet", secretRemoved: true }; + const env = OutputEnvelope.success(cmd, undefined, data, m); + expect(env.data).toEqual(data); + }); +}); diff --git a/ts/src/adapters/inbound/cli/output/envelope.ts b/ts/src/adapters/inbound/cli/output/envelope.ts new file mode 100644 index 000000000..b2efe326e --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/envelope.ts @@ -0,0 +1,70 @@ +/** + * OutputEnvelope — the result/error envelope builder for the OutputFormatter. Shapes + * the user-facing `wallet-cli.result.v1` contract: schema version, chain view, and meta. + * Pure (no I/O); the formatter turns the envelope into strings. + */ +import type { NetworkDescriptor } from "../../../../domain/types/index.js"; +import type { ChainView, CommandDefinition, ErrorEnvelope, Meta, ResultEnvelope } from "../contracts/index.js"; +import { commandId } from "../command-id.js"; + +type CliErrorEnvelopeShape = { code: string; message: string; details?: object }; + +const SCHEMA_VERSION = "wallet-cli.result.v1" as const; + +/** JSON serialization that keeps big numbers as strings. */ +export function toJson(value: unknown): string { + return JSON.stringify(value, (_k, v) => { + if (typeof v === "bigint") return v.toString(); + if (v instanceof Uint8Array) return Buffer.from(v).toString("hex"); + return v; + }); +} + +function chainView(net: NetworkDescriptor): ChainView { + return { + family: net.family, + networkId: net.id, + network: net.id, + chainId: net.chainId, + }; +} + +function meta(durationMs: number, warnings: string[]): Meta { + return { durationMs, warnings }; +} + +export const OutputEnvelope = { + success( + cmd: CommandDefinition, + net: NetworkDescriptor | undefined, + data: unknown, + m: { durationMs: number; warnings: string[] }, + ): ResultEnvelope { + const env: ResultEnvelope = { + schema: SCHEMA_VERSION, + success: true, + command: commandId(cmd), + data: data ?? {}, + meta: meta(m.durationMs, m.warnings), + }; + if (net) env.chain = chainView(net); // neutral commands omit chain + return env; + }, + + error( + commandId: string, + net: NetworkDescriptor | undefined, + err: CliErrorEnvelopeShape, + m: { durationMs: number; warnings: string[] }, + ): ErrorEnvelope { + const env: ErrorEnvelope = { + schema: SCHEMA_VERSION, + success: false, + command: commandId, + error: err, + meta: meta(m.durationMs, m.warnings), + }; + if (net) env.chain = chainView(net); + return env; + }, +}; diff --git a/ts/src/adapters/inbound/cli/output/index.ts b/ts/src/adapters/inbound/cli/output/index.ts new file mode 100644 index 000000000..83a137c34 --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/index.ts @@ -0,0 +1,98 @@ +/** + * OutputFormatter — turn outcomes into result/event frame strings without changing + * behavior. Instead of one class branching on `if (output === "json")`, this is an interface + * with two implementations chosen by createOutputFormatter (borrowed from ledger wallet-cli + * output.ts). The formatter only computes strings; StreamManager owns writing & stream choice. + * - success → single terminal frame (caller hands to streams.result) + * - error → terminal error (json envelope to stdout, or short stderr line) + * - event → intermediate progress frame (caller hands to streams.event); null = not shown + * JSON mode emits exactly one terminal envelope; empty data is {}; big numbers stay strings. + * Neutral commands omit `chain`. + */ +import type { NetworkDescriptor, OutputMode } from "../../../../domain/types/index.js"; +import type { ProgressEvent } from "../../../../application/contracts/index.js"; +import type { CommandDefinition, StreamManager } from "../contracts/index.js"; +import type { CliError } from "../../../../domain/errors/index.js"; +import { OutputEnvelope, toJson } from "./envelope.js"; +import { renderGenericText } from "../render/index.js"; + +export interface OutputFormatter { + /** the single result frame for the caller to hand to streams.result. */ + success(cmd: CommandDefinition, net: NetworkDescriptor | undefined, data: unknown, accountLabel?: string): string; + /** terminal error output (JSON envelope to stdout, or short line to stderr). */ + error(err: CliError, ctx?: { commandId?: string; net?: NetworkDescriptor }): void; + /** intermediate progress frame for streams.event; null = this mode does not show it. */ + event(e: ProgressEvent): string | null; +} + +abstract class BaseOutputFormatter { + constructor( + protected readonly streams: StreamManager, + protected readonly startedAt: number, + ) {} + + protected meta() { + return { durationMs: Date.now() - this.startedAt, warnings: this.streams.warnings() }; + } +} + +class JsonOutputFormatter extends BaseOutputFormatter implements OutputFormatter { + success(cmd: CommandDefinition, net: NetworkDescriptor | undefined, data: unknown): string { + // JSON mode always uses the envelope; the account label is a text-mode display nicety. + return toJson(OutputEnvelope.success(cmd, net, data, this.meta())); + } + + error(err: CliError, ctx?: { commandId?: string; net?: NetworkDescriptor }): void { + const env = OutputEnvelope.error(ctx?.commandId ?? "", ctx?.net, err.toEnvelope(), this.meta()); + this.streams.result(toJson(env)); + } + + event(e: ProgressEvent): string { + return JSON.stringify(e); + } +} + +class HumanOutputFormatter extends BaseOutputFormatter implements OutputFormatter { + success(cmd: CommandDefinition, net: NetworkDescriptor | undefined, data: unknown, accountLabel?: string): string { + const env = OutputEnvelope.success(cmd, net, data, this.meta()); + const custom = cmd.formatText?.(env.data, { command: env.command, net, accountLabel }); + if (custom) return custom; + return renderGenericText(env.command, net, env.data); + } + + error(err: CliError): void { + this.streams.errorLine(`error [${err.code}]: ${err.message}`); + } + + event(e: ProgressEvent): string { + return renderEvent(e); + } +} + +export function createOutputFormatter( + output: OutputMode, + streams: StreamManager, + startedAt: number, +): OutputFormatter { + return output === "json" + ? new JsonOutputFormatter(streams, startedAt) + : new HumanOutputFormatter(streams, startedAt); +} + +// plain human progress line (no spinner / no TTY detection — Standard CLI / agent-first). +function renderEvent(e: ProgressEvent): string { + switch (e.type) { + case "awaiting_device": + switch (e.reason) { + case "sign": return "⧖ review and approve the transaction on your device"; + case "verify_address": return "⧖ review and confirm the address on your device"; + case "open_app": return "⧖ confirm on your device to open the app"; + case "unlock": return "⧖ unlock your device with your PIN"; + } + // eslint-disable-next-line no-fallthrough + case "pre-verify-address": return `compare with your device: ${e.address}`; + case "signed": return "✓ signed; broadcasting…"; + case "broadcasting": return "broadcasting…"; + case "dry-run": return "dry run (transaction not broadcast)"; + } +} diff --git a/ts/src/adapters/inbound/cli/output/output.test.ts b/ts/src/adapters/inbound/cli/output/output.test.ts new file mode 100644 index 000000000..bf8cac187 --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/output.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect } from "vitest"; +import { createOutputFormatter } from "./index.js"; +import { StreamManager } from "../stream/index.js"; +import { UsageError } from "../../../../domain/errors/index.js"; +import { commandId } from "../command-id.js"; +import type { NetworkDescriptor } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { renderGenericText, TextFormatters } from "../render/index.js"; + +function capture(output: "text" | "json") { + const out: string[] = []; + const err: string[] = []; + const sm = new StreamManager(output, false, (s) => out.push(s), (s) => err.push(s)); + return { sm, out, err }; +} + +const cmd = { family: "tron", path: ["account", "balance"] } as unknown as CommandDefinition; +const net: NetworkDescriptor = { + id: "tron:nile", family: "tron", chainId: "nile", aliases: ["nile"], capabilities: [], +}; + +describe("createOutputFormatter (json)", () => { + it("success returns a single parseable envelope", () => { + const { sm } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + const env = JSON.parse(f.success(cmd, net, { balance: "1" })); + expect(env.success).toBe(true); + expect(env.command).toBe("tron.account.balance"); + expect(env.chain).toMatchObject({ networkId: "tron:nile", network: "tron:nile", chainId: "nile" }); + expect(env.data).toEqual({ balance: "1" }); + expect(env.meta).toMatchObject({ warnings: [] }); + }); + + it("error writes an error envelope to stdout via streams.result", () => { + const { sm, out, err } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + f.error(new UsageError("missing_option", "need --to"), { commandId: commandId(cmd), net }); + expect(err).toEqual([]); + const env = JSON.parse(out[0]!); + expect(env.success).toBe(false); + expect(env.error).toMatchObject({ code: "missing_option" }); + }); + + it("event renders an NDJSON line that parses back to the event", () => { + const { sm } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + const frame = f.event({ type: "awaiting_device", reason: "sign" }); + expect(JSON.parse(frame!)).toEqual({ type: "awaiting_device", reason: "sign" }); + }); +}); + +describe("createOutputFormatter (text)", () => { + it("generic output identifies the network by canonical id", () => { + const text = renderGenericText("tron.test", net, {}); + expect(text).toContain("network: tron:nile"); + expect(text).not.toContain("network: nile"); + }); + + it("success returns human lines naming the command", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const text = f.success(cmd, net, { balance: "1" }); + expect(text).toContain("tron.account.balance"); + expect(text).toContain("balance"); + }); + + it("error writes a short line to stderr, not stdout", () => { + const { sm, out, err } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + f.error(new UsageError("missing_option", "need --to")); + expect(out).toEqual([]); + expect(err[0]).toContain("missing_option"); + }); + + it("event renders a non-null human progress line (no spinner)", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const frame = f.event({ type: "awaiting_device", reason: "sign" }); + expect(frame).not.toBeNull(); + expect(frame).not.toContain("{"); // human text, not NDJSON + }); + + it("renders wallet create as a focused human receipt", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const walletCmd = { + path: ["create"], + formatText: TextFormatters.walletCreated("Created", [ + "Recovery phrase is encrypted locally and was not printed.", + "Run `backup` soon and store the file offline.", + ]), + } as unknown as CommandDefinition; + const text = f.success(walletCmd, undefined, { + status: "created", + accountId: "wlt_abc.0", + label: "main", + type: "seed", + active: true, + addresses: { tron: "T1234567890abcdef", evm: "0x1234567890abcdef" }, + }); + expect(text).toContain("Created wallet"); + expect(text).toContain("main"); + expect(text).toContain("Run `backup`"); + }); + + it("renders existing wallet receipts with a warning marker", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const walletCmd = { + path: ["import", "private-key"], + formatText: TextFormatters.walletCreated("Imported", [ + "Private key was read from hidden input and was not printed.", + ]), + } as unknown as CommandDefinition; + const text = f.success(walletCmd, undefined, { + status: "existing", + accountId: "wlt_abc.0", + label: "main", + type: "seed", + addresses: { tron: "T1234567890abcdef", evm: "0x1234567890abcdef" }, + }); + // icon and label live in separate ANSI spans, so assert on the pieces (not a fused substring). + expect(text).toContain("⚠"); + expect(text).toContain("Existing wallet"); + expect(text).not.toContain("✅"); // existing wallets must not show the success check + }); + + it("renders backup metadata without secret material", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const backupCmd = { path: ["backup"], formatText: TextFormatters.walletBackup } as unknown as CommandDefinition; + const text = f.success(backupCmd, undefined, { + accountId: "wlt_abc.0", + secretType: "mnemonic", + out: "/tmp/main-backup.json", + fileMode: "0600", + bytes: 512, + mnemonic: "test test test test test test test test test test test junk", + privateKey: "00".repeat(32), + }); + expect(text).toContain("/tmp/main-backup.json"); + expect(text).not.toContain("test test"); + expect(text).not.toContain("000000"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/registry/index.ts b/ts/src/adapters/inbound/cli/registry/index.ts new file mode 100644 index 000000000..e94ee912c --- /dev/null +++ b/ts/src/adapters/inbound/cli/registry/index.ts @@ -0,0 +1,76 @@ +/** + * CommandRegistry — holds all CommandDefinitions; resolves a command from a positional path + * (+ family for chain commands); exposes metadata for CliShell (yargs tree) and HelpService. + * Thin: tokenizing, flag collection, and help layout are yargs' job. + * + * Two command kinds, discriminated solely by `family`: + * - neutral (no family): addressed by its full path (create, import mnemonic, config get…). + * - chain (family set): same logical path may have per-family impls, chosen by --network. + */ +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition, CommandRegistryLike } from "../contracts/index.js"; +import { commandId } from "../command-id.js"; + +/** flat command-tree metadata for CliShell (yargs tree) + HelpService. */ +export interface CommandTreeMeta { + commands: Array<{ path: string[]; id: string; family?: ChainFamily; summary?: string }>; +} + +/** storage/lookup key: family scopes chain commands; neutral commands share the empty scope. */ +function keyOf(cmd: CommandDefinition): string { + return `${cmd.family ?? ""}:${cmd.path.join(".")}`; +} + +export class CommandRegistry implements CommandRegistryLike { + #byKey = new Map(); + + add(cmd: CommandDefinition): void { + const key = keyOf(cmd); + if (this.#byKey.has(key)) throw new Error(`duplicate command ${key}`); + this.#byKey.set(key, cmd); + } + + families(): ChainFamily[] { + const set = new Set(); + for (const cmd of this.#byKey.values()) if (cmd.family) set.add(cmd.family); + return [...set]; + } + + /** command-backed capability keys per family (deduped). Summaries are resolved by the caller + * (runner) against CAP_SUMMARIES — the registry stays free of presentation/infra concerns. */ + capabilityKeysByFamily(): Map { + const out = new Map>(); + for (const cmd of this.#byKey.values()) { + if (!cmd.family || !cmd.capability) continue; + const set = out.get(cmd.family) ?? new Set(); + set.add(cmd.capability); + out.set(cmd.family, set); + } + return new Map([...out].map(([f, s]) => [f, [...s]])); + } + + /** Resolve a neutral command (no family) by its full path. */ + resolveNeutral(path: string[]): CommandDefinition | null { + return this.#byKey.get(`:${path.join(".")}`) ?? null; + } + + /** All commands matching a logical path, regardless of family (used to pick by --network). */ + resolveCandidates(path: string[]): CommandDefinition[] { + const key = path.join("."); + return this.all().filter((c) => c.path.join(".") === key); + } + + /** Family-specific implementation of a logical path. */ + resolveForFamily(path: string[], family: ChainFamily): CommandDefinition | null { + return this.resolveCandidates(path).find((c) => c.family === family) ?? null; + } + + all(): CommandDefinition[] { + return [...this.#byKey.values()]; + } + + tree(): CommandTreeMeta { + const commands = this.all().map((c) => ({ path: c.path, id: commandId(c), family: c.family, summary: c.summary })); + return { commands }; + } +} diff --git a/ts/src/adapters/inbound/cli/registry/registry.test.ts b/ts/src/adapters/inbound/cli/registry/registry.test.ts new file mode 100644 index 000000000..f92856ba8 --- /dev/null +++ b/ts/src/adapters/inbound/cli/registry/registry.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { CommandRegistry } from "./index.js"; +import { commandId } from "../command-id.js"; + +function command(family: ChainFamily, path: string[]): CommandDefinition { + const fields = z.object({}); + return { + family, + path, + network: "optional", + wallet: "optional", + auth: "none", + fields, + input: fields, + examples: [], + run: async () => ({}), + }; +} + +describe("CommandRegistry logical resolution", () => { + it("returns every implementation for a logical path", () => { + const reg = new CommandRegistry(); + // synthetic second family via cast: only tron ships, but the registry keys on the family + // string, so this still exercises multi-implementation logical resolution. + reg.add(command("tron", ["account", "balance"])); + reg.add(command("evm" as any, ["account", "balance"])); + + expect(reg.resolveCandidates(["account", "balance"]).map((c) => commandId(c))).toEqual([ + "tron.account.balance", + "evm.account.balance", + ]); + }); + + it("selects one implementation by family", () => { + const reg = new CommandRegistry(); + reg.add(command("tron", ["account", "balance"])); + reg.add(command("evm" as any, ["account", "balance"])); + + expect(commandId(reg.resolveForFamily(["account", "balance"], "evm" as any)!)).toBe("evm.account.balance"); + expect(reg.resolveForFamily(["account", "missing"], "evm" as any)).toBeNull(); + }); +}); diff --git a/ts/src/adapters/inbound/cli/render/family-render.test.ts b/ts/src/adapters/inbound/cli/render/family-render.test.ts new file mode 100644 index 000000000..e0fb9be0f --- /dev/null +++ b/ts/src/adapters/inbound/cli/render/family-render.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { FAMILY_RENDER } from "./index.js"; + +describe("FAMILY_RENDER parity", () => { + it("nativeAmount units", () => { + expect(FAMILY_RENDER.tron.nativeAmount("1000000")).toBe("1 TRX"); + }); + it("feeFallback: tron formats sun→TRX", () => { + expect(FAMILY_RENDER.tron.feeFallback("1000000")).toBe("1 TRX"); + }); + it("addressLabel", () => { + expect(FAMILY_RENDER.tron.addressLabel).toBe("TRON address"); + }); + it("tron txInfoRows include Energy + Fee in TRX", () => { + const rows = FAMILY_RENDER.tron.txInfoRows({ txid: "t", status: "SUCCESS", feeSun: "1000000", energyUsed: 5 } as any); + expect(rows).toContainEqual(["Fee", "1 TRX"]); + expect(rows.map((r) => r[0])).toContain("Energy"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/render/index.ts b/ts/src/adapters/inbound/cli/render/index.ts new file mode 100644 index 000000000..b5c6f417e --- /dev/null +++ b/ts/src/adapters/inbound/cli/render/index.ts @@ -0,0 +1,571 @@ +import type { NetworkDescriptor, TxInfoView, TxReceiptKind, TxReceiptView, TxStatusView } from "../../../../domain/types/index.js"; +import type { TextFormatter, TextRenderContext } from "../contracts/index.js"; +import { ChainFamily } from "../../../../domain/family/index.js"; +import { RESOURCES, resourceOfRpcCode, type Resource } from "../../../../domain/resources/index.js"; +import { sourceLabel } from "../../../../domain/sources/index.js"; +import { fromBaseUnits } from "../../../../domain/amounts/index.js"; +import { formatScalar, formatInt, formatUsd, formatSun, formatTime, formatUtc, num, shorten, quote } from "./scalars.js"; +import { type Obj, type Pair, asObj, kv, query, receipt, titled, table, ok, fail, pending, warn } from "./layout.js"; + +/** + * Per-family render hooks — the one table that folds the scattered `r.family === tron ? … : …` + * branches. Adding a chain = one entry here (alongside its FAMILIES + FamilyDef entries). + */ +interface FamilyRenderHooks { + /** the full TxInfo detail rows (family-shaped: Energy/TRX vs Gas/wei). Reads the flat + * TxInfoView and picks its own family's fields — no narrowing cast (no closed union). */ + txInfoRows(r: TxInfoView): Pair[]; + /** native smallest-unit amount → display string (sun→TRX / wei). */ + nativeAmount(raw: string): string; + /** fee fallback when no structured fee object is present. */ + feeFallback(fee: unknown): string; + /** address-type label for the per-family address rows. */ + addressLabel: string; +} + +const txInfoAmount = (v: string | undefined, suffix: string): string => + v === undefined || v === "" ? "" : `${formatScalar(v)}${suffix}`; + +export const FAMILY_RENDER: Record = { + tron: { + nativeAmount: (raw) => `${formatSun(raw)} TRX`, + feeFallback: (fee) => `${formatSun(fee)} TRX`, + addressLabel: "TRON address", + txInfoRows: (r) => [ + ["TxID", r.txid], + ["From", r.from ?? ""], + ["To", r.to ?? ""], + ["Amount", txInfoAmount(r.amount, r.symbol ? ` ${r.symbol}` : "")], + ["Status", r.status ?? "unknown"], + ["Block", r.blockNumber === undefined ? "" : `#${formatInt(r.blockNumber)}`], + ["Energy", r.energyUsed === undefined ? "" : formatInt(r.energyUsed)], + ["Fee", r.feeSun === undefined ? "" : `${formatSun(r.feeSun)} TRX`], + ], + }, +}; + +export const TextFormatters = { + walletCreated: (verb: "Created" | "Imported", notes: string[]): TextFormatter => (data) => + renderWalletCreated(verb, asObj(data), notes), + walletWatch: ((data) => { + const d = asObj(data); + return receipt(ok(), `Added watch-only account ${quote(displayName(d))}`, [ + ["Address", firstAddress(d)], + ["Note", "read-only; signing operations will be rejected"], + ]); + }) satisfies TextFormatter, + walletLedger: ((data) => renderLedgerImported(asObj(data))) satisfies TextFormatter, + walletList: ((data) => renderWalletList(Array.isArray(data) ? data.map(asObj) : [])) satisfies TextFormatter, + walletUse: ((data) => { + const d = asObj(data); + return receipt(ok(), `Active account: ${displayName(d)}`, addressPairs(d)); + }) satisfies TextFormatter, + walletCurrent: ((data) => { + const d = asObj(data); + return titled(`Active account: ${displayName(d)}`, addressPairs(d)); + }) satisfies TextFormatter, + walletRename: ((data) => { + const d = asObj(data); + return receipt(ok(), "Renamed account", [ + ["Old label", String(d.previousLabel ?? "")], + ["New label", displayName(d)], + ]); + }) satisfies TextFormatter, + walletDerive: ((data) => { + const d = asObj(data); + return receipt(ok(), `Derived sub-account ${quote(displayName(d))}`, [ + ["Address", firstAddress(d)], + ["Active", d.active === true ? "yes" : ""], + ["Note", "shares master mnemonic; no separate backup needed"], + ]); + }) satisfies TextFormatter, + walletDelete: ((data) => { + const d = asObj(data); + return receipt(ok(), `Deleted wallet ${String(d.accountId ?? "")}`, [ + ["Secret removed", d.secretRemoved === false ? "no" : "yes"], + ["New active", d.newActive ? String(d.newActive) : ""], + ]); + }) satisfies TextFormatter, + walletBackup: ((data) => { + const d = asObj(data); + return [ + receipt(warn(), `Backup written ${String(d.out ?? "")}`, [ + ["Account ID", String(d.accountId ?? "")], + ["Secret", secretLabel(d.secretType)], + ["File mode", String(d.fileMode ?? "0600")], + ["Bytes", String(d.bytes ?? "?")], + ]), + "", + `${warn()} Secret material was written only to the backup file, never to stdout.`, + ].join("\n"); + }) satisfies TextFormatter, + + config: ((data) => renderConfig(asObj(data))) satisfies TextFormatter, + networks: ((data) => table( + ["Network", "Family", "Chain", "Fee model"], + (Array.isArray(data) ? data : []).map(asObj).map((n) => [ + String(n.id ?? ""), + String(n.family ?? ""), + String(n.chainId ?? ""), + String(n.feeModel ?? ""), + ]), + )) satisfies TextFormatter, + + accountBalance: ((data, ctx) => { + const d = asObj(data); + const unit = String(d.unit ?? "TRX"); + const human = unit === "TRX" ? formatSun(d.balance) : formatScalar(d.balance); + return query([identity(ctx, d.address), ["Balance", `${human} ${unit}`]]); + }) satisfies TextFormatter, + accountInfo: ((data, ctx) => renderAccountInfo(asObj(data), ctx)) satisfies TextFormatter, + accountHistory: ((data, ctx) => { + const d = asObj(data); + const rows = (Array.isArray(d.records) ? d.records : []).map(asObj).map(historyRow); + return [`${quote(acct(ctx, d.address))} recent transactions`, table(["Time", "Type", "Amount", "From / To", "Status"], rows)].join("\n"); + }) satisfies TextFormatter, + tokenBookAdd: ((data) => { + const d = asObj(data); + const token = asObj(d.token); + const verb = d.action === "updated" ? "Updated token book" : "Added to token book"; + return receipt(ok(), verb, [ + ["Name", String(token.name ?? "")], + ["Symbol", String(token.symbol ?? token.id ?? "")], + ["Decimals", token.decimals === undefined ? "" : String(token.decimals)], + ]); + }) satisfies TextFormatter, + tokenBookList: ((data) => { + const d = asObj(data); + const rows = (Array.isArray(d.tokens) ? d.tokens : []).map(asObj).map((t) => [ + String(t.symbol ?? ""), + String(t.name ?? ""), + String(t.source ?? ""), + String(t.id ?? ""), + ]); + return table(["Symbol", "Name", "Source", "Contract / ID"], rows); + }) satisfies TextFormatter, + tokenBookRemove: ((data) => { + const removed = asObj(asObj(data).removed); + return receipt(ok(), "Removed from token book", [ + ["Name", String(removed.name ?? "")], + ["Symbol", String(removed.symbol ?? "")], + ]); + }) satisfies TextFormatter, + accountPortfolio: ((data, ctx) => { + const d = asObj(data); + const rows = (Array.isArray(d.holdings) ? d.holdings : []).map(asObj).map((h) => [ + String(h.symbol ?? ""), + formatScalar(h.balance), + h.priceUsd === null || h.priceUsd === undefined ? "-" : `$${formatUsd(h.priceUsd)}`, + h.valueUsd === null || h.valueUsd === undefined ? "-" : `$${formatUsd(h.valueUsd)}`, + ]); + const total = d.totalValueUsd === null || d.totalValueUsd === undefined ? "-" : `$${formatUsd(d.totalValueUsd)}`; + const lines = [ + `${quote(acct(ctx, d.address ?? d.account))} Portfolio`, + table(["Token", "Balance", "Price (USD)", "Value (USD)"], rows), + `Total ≈ ${total}`, + ]; + if (d.priceError) lines.push(`${warn()} price warning: ${String(d.priceError)}`); + return lines.join("\n"); + }) satisfies TextFormatter, + + tokenBalance: ((data, ctx) => { + const d = asObj(data); + const balance = d.decimals !== undefined ? fromBaseUnits(String(d.balance ?? "0"), num(d.decimals, 0)) : formatScalar(d.balance); + return query([ + identity(ctx, d.address), + ["Name", String(d.name ?? "")], + ["Symbol", String(d.symbol ?? "")], + ["Balance", balance], + ]); + }) satisfies TextFormatter, + tokenInfo: ((data) => { + const d = asObj(data); + return query([ + ["Name", String(d.name ?? d.token_name ?? d.id ?? "")], + ["Symbol", String(d.symbol ?? d.abbr ?? "")], + ["Decimals", String(d.decimals ?? d.precision ?? "")], + ]); + }) satisfies TextFormatter, + + txReceipt: ((r) => renderTxReceipt(r)) satisfies TextFormatter, + txStatus: ((r) => { + // `failed` is computed by the command (tron: result ≠ SUCCESS) — no family branch. + const status = r.failed ? `failed ${fail()}` : r.confirmed ? `confirmed ${ok()}` : `pending ${pending()}`; + return query([ + ["TxID", r.txid], + ["Status", status], + ["Block", r.blockNumber === undefined ? "" : `#${formatInt(r.blockNumber)}`], + ]); + }) satisfies TextFormatter, + txInfo: ((r) => { + return query(FAMILY_RENDER[r.family].txInfoRows(r)); + }) satisfies TextFormatter, + + contractCall: ((data) => { + const d = asObj(data); + return query([ + ["Method", methodName(String(d.method ?? ""))], + ["Result", `${formatResult(d.result)} (raw)`], + ]); + }) satisfies TextFormatter, + contractInfo: ((data) => renderContractInfo(asObj(data))) satisfies TextFormatter, + + messageSign: ((data) => { + const d = asObj(data); + return receipt(ok(), "Signed", [ + ["Address", String(d.address ?? "")], + ["Signature", String(d.signature ?? "")], + ]); + }) satisfies TextFormatter, + block: ((data) => { + const block = asObj(asObj(data).block); + const header = asObj(asObj(block.block_header).raw_data); + const n = block.number ?? header.number; + const ts = block.timestamp ?? header.timestamp; + const txs = Array.isArray(block.transactions) ? block.transactions.length : 0; + return query([ + ["Number", n === undefined ? "" : `#${formatInt(n)}`], + ["Time", ts ? formatUtc(ts) : "unknown"], + ["Transactions", String(txs)], + ]); + }) satisfies TextFormatter, +}; + +export function renderGenericText(command: string, net: NetworkDescriptor | undefined, data: unknown): string { + const lines: string[] = [`${ok()} ${command}`]; + if (net) lines.push(` network: ${net.id}`); + if (data && typeof data === "object" && !Array.isArray(data)) { + for (const [k, v] of Object.entries(data as Obj)) { + if (Array.isArray(v) && v.length > 0) { + lines.push(` ${k}:`); + for (const item of v) lines.push(` - ${formatScalar(item)}`); + } else { + lines.push(` ${k}: ${formatScalar(v)}`); + } + } + } else if (Array.isArray(data)) { + for (const item of data) lines.push(` - ${formatScalar(item)}`); + } else if (data !== undefined && data !== null) { + lines.push(` ${String(data)}`); + } + return lines.join("\n"); +} + +function renderWalletCreated(verb: "Created" | "Imported", d: Obj, notes: string[]): string { + const existing = d.status === "existing"; + const title = existing ? "Existing wallet" : `${verb} wallet`; + const lines = [ + receipt(existing ? warn() : ok(), `${title} ${quote(displayName(d))}`, [ + ["Account ID", String(d.accountId ?? "")], + ["Type", typeLabel(d.type)], + ...addressPairs(d), + ["Active", d.active === true ? "yes" : ""], + ]), + ]; + if (notes.length) lines.push("", ...notes.map((n) => `${warn()} ${n}`)); + return lines.join("\n"); +} + +function renderLedgerImported(d: Obj): string { + const existing = d.status === "existing"; + return [ + receipt(existing ? warn() : ok(), `${existing ? "Existing Ledger account" : "Registered Ledger account"} ${quote(displayName(d))}`, [ + ["Account ID", String(d.accountId ?? "")], + ["App", String(d.family ?? "")], + ["Path", String(d.path ?? "")], + ...addressPairs(d), + ]), + "", + `${warn()} No private key is stored locally. Signing requires device confirmation.`, + ].join("\n"); +} + +function renderWalletList(items: Obj[]): string { + if (items.length === 0) return "No wallets found."; + const rows = items.map((d) => [ + displayName(d), + typeLabel(d.type), + firstAddress(d), + d.active ? "*" : "", + ]); + return table(["Label", "Type", "Address", "Active"], rows); +} + +function renderAccountInfo(d: Obj, ctx: TextRenderContext): string { + const account = asObj(d.account); + const owner = asObj(account.owner_permission); + const active = Array.isArray(account.active_permission) ? account.active_permission.length : 0; + const created = account.create_time ? new Date(Number(account.create_time)).toISOString().slice(0, 10) : ""; + const ownerKeys = Array.isArray(owner.keys) ? owner.keys.length : "?"; + const resources = asObj(d.resources); + const bandwidth = asObj(resources.bandwidth); + const energy = asObj(resources.energy); + const pairs: Pair[] = []; + if (ctx.accountLabel) pairs.push(["Label", ctx.accountLabel]); + pairs.push(["Address", String(d.address ?? "")]); + pairs.push(["Balance", `${formatSun(account.balance)} TRX`]); + const staked = stakedSummary(account); + if (staked) pairs.push(["Staked", staked]); + if (resources.energy) pairs.push(["Energy", `used ${formatInt(energy.used)} / ${formatInt(energy.limit)}`]); + if (resources.bandwidth) pairs.push(["Bandwidth", `used ${formatInt(bandwidth.used)} / ${formatInt(bandwidth.limit)}`]); + pairs.push(["Created", created]); + pairs.push(["Permissions", `owner ${String(owner.threshold ?? "?")}-of-${ownerKeys}, ${active} active group${active === 1 ? "" : "s"}`]); + return query(pairs); +} + +/** Sum FreezeBalanceV2 stakes into a " TRX (energy + bandwidth )" summary. */ +function stakedSummary(account: Obj): string { + const frozen = Array.isArray(account.frozenV2) ? account.frozenV2.map(asObj) : []; + // frozenV2's bandwidth entries carry no `type`, so an unrecognized code folds into bandwidth. + const sums = new Map(RESOURCES.map((r) => [r, 0n])); + for (const f of frozen) { + const r = resourceOfRpcCode(String(f.type ?? "")) ?? "bandwidth"; + sums.set(r, (sums.get(r) ?? 0n) + BigInt(Number(f.amount ?? 0))); + } + const total = RESOURCES.reduce((t, r) => t + (sums.get(r) ?? 0n), 0n); + if (total === 0n) return ""; + const parts = RESOURCES.map((r) => `${r} ${formatSun(sums.get(r) ?? 0n)}`).join(" + "); + return `${formatSun(total)} TRX (${parts})`; +} + +function renderContractInfo(d: Obj): string { + let names: string[]; + let count: number; + if (Array.isArray(d.methods)) { + names = d.methods.map(String); + count = num(d.functionCount, names.length); + } else { + const contract = asObj(d.contract); + const info = asObj(d.info); + const abi = contract.abi ?? info.abi ?? contract.ABI ?? info.ABI; + const nestedEntries = asObj(abi).entrys; + const entries: unknown[] = Array.isArray(abi) ? abi : Array.isArray(nestedEntries) ? nestedEntries : []; + const methods = entries.map(asObj).filter((e) => e.type === "Function" || e.type === "function"); + names = methods.map((e) => e.name).filter(Boolean).map(String); + count = methods.length; + } + const name = String(d.name ?? asObj(d.contract).name ?? asObj(d.info).name ?? ""); + const preview = names.slice(0, 3).join(" / "); + return query([ + ["Contract", String(d.address ?? "")], + ["Name", name], + ["Methods", `${count}${preview ? ` (${preview}${count > 3 ? " …" : ""})` : ""}`], + ]); +} + +function renderConfig(d: Obj): string { + if ("input" in d) { + return receipt(ok(), "Set config", [ + ["Key", String(d.key)], + ["Value", configValue(d.value)], + ]); + } + if ("key" in d) return kv([[String(d.key), configValue(d.value)]], ""); + return kv(Object.entries(d).map(([k, v]) => [k, configValue(v)] as Pair), ""); +} + +/** config values keep their literal form (no thousands grouping, raw key names). */ +function configValue(v: unknown): string { + if (Array.isArray(v)) return v.map(String).join(", "); + return v === null || v === undefined ? "" : String(v); +} + +/** Default-mode broadcast/dry-run/sign-only receipt for tx/stake/contract signing commands. + * Narrows on the typed `kind`/`family` — no stringly command-id matching, no alias probing. */ +function renderTxReceipt(r: TxReceiptView): string { + if (r.mode === "dry-run") { + return receipt(pending(), `Dry run ${actionLabel(r.kind)}`, [ + ["Fee", formatFee(r.fee, r.family)], + ["Tx", summarizeTx(r.tx)], + ]); + } + if (r.mode === "sign-only") { + return receipt(ok(), `Signed ${actionLabel(r.kind)}`, [ + ["Fee", formatFee(r.fee, r.family)], + ["Signed", summarizeTx(r.signed)], + ]); + } + const txid = String(r.txId ?? r.hash ?? ""); + const stage = r.stage ?? "submitted"; + const summary = receiptSummary(r); + const pairs: Pair[] = [...receiptRows(r)]; + if (txid) pairs.push(["TxID", txid]); + + // submitted (default, non-blocking): txid only, no fee/energy yet — those need confirmation. + if (stage === "submitted") { + pairs.push(["Status", "pending — not yet on-chain"]); + const body = receipt(pending(), summary, pairs); + return txid ? `${body}\n! Track it: wallet-cli tx info --txid ${txid}` : body; + } + + // confirmed / failed (after --wait): real on-chain block / fee / energy / result. + if (r.blockNumber) pairs.push(["Block", `#${formatInt(r.blockNumber)}`]); + if (r.energyUsed) pairs.push(["Energy", formatInt(r.energyUsed)]); + if (r.feeSun) pairs.push(["Fee", `${formatSun(r.feeSun)} TRX`]); + if (r.kind === "stake-unfreeze") pairs.push(["Withdrawable", "after the unlock period — then run `stake withdraw`"]); + if (stage === "failed") { + pairs.push(["Status", "failed"]); + if (r.result) pairs.push(["Reason", String(r.result)]); + return receipt(fail(), summary, pairs); + } + pairs.push(["Status", "success"]); + return receipt(ok(), summary, pairs); +} + +/** the verb-phrase summary for a broadcast receipt, by action kind. */ +function receiptSummary(r: TxReceiptView): string { + const stakeAmt = r.amountSun !== undefined ? `${formatSun(r.amountSun)} TRX` : "TRX"; + const resource = r.resource ? String(r.resource) : ""; + switch (r.kind) { + case "stake-freeze": return `Staked ${stakeAmt}${resource ? ` for ${resource}` : ""}`; + case "stake-unfreeze": return `Unstaked ${stakeAmt}`; + case "stake-delegate": return `Delegated ${stakeAmt}${resource ? ` of ${resource}` : ""}`; + case "stake-undelegate": return `Reclaimed ${stakeAmt}${resource ? ` of ${resource}` : ""}`; + case "stake-withdraw": return r.withdrawnSun ? `Withdrew ${formatSun(r.withdrawnSun)} TRX to balance` : "Withdrew expired TRX to balance"; + case "stake-cancel": return "Cancelled pending unstakes"; + case "contract-send": return `Called ${methodName(String(r.method ?? ""))}`; + case "contract-deploy": return "Contract deployed"; + case "send": { + const amount = receiptAmount(r); + return amount ? `Sent ${amount}` : "Sent"; + } + case "broadcast": return "Broadcast"; + } +} + +/** action-specific extra rows (To/From/Address/Contract), by kind. */ +function receiptRows(r: TxReceiptView): Pair[] { + const rows: Pair[] = []; + if (r.kind === "stake-delegate") rows.push(["To", String(r.receiver ?? "")]); + else if (r.kind === "stake-undelegate") rows.push(["From", String(r.receiver ?? "")]); + else if (r.kind === "contract-deploy") rows.push(["Address", String(r.contractAddress ?? "")]); + else if (r.to ?? r.receiver) rows.push(["To", String(r.to ?? r.receiver)]); + if (r.kind === "contract-send") rows.push(["Contract", String(r.contract ?? "")]); + return rows; +} + +/** broadcast-receipt amount: token-aware (symbol/decimals when known, else the contract/asset-id + * identifier for raw-amount sends), native smallest-unit → coin only when no token is involved. */ +function receiptAmount(r: TxReceiptView): string { + if (r.rawAmount !== undefined && r.rawAmount !== null && r.rawAmount !== "") { + const raw = String(r.rawAmount); + const isToken = r.token !== undefined || r.contract !== undefined || r.assetId !== undefined; + if (isToken) { + const human = r.decimals !== undefined && r.decimals !== null ? fromBaseUnits(raw, num(r.decimals, 0)) : formatScalar(raw); + const label = r.token ?? r.contract ?? (r.assetId !== undefined ? `asset ${String(r.assetId)}` : ""); + return label ? `${human} ${String(label)}` : human; + } + return FAMILY_RENDER[r.family].nativeAmount(raw); + } + if (r.amountSun) return `${formatSun(r.amountSun)} TRX`; + return ""; +} + +/** human label for an action kind, e.g. "send" → "tx send" (for dry-run/sign-only headers). */ +function actionLabel(kind: TxReceiptKind): string { + switch (kind) { + case "send": return "tx send"; + case "broadcast": return "tx broadcast"; + case "stake-freeze": return "stake freeze"; + case "stake-unfreeze": return "stake unfreeze"; + case "stake-delegate": return "stake delegate"; + case "stake-undelegate": return "stake undelegate"; + case "stake-withdraw": return "stake withdraw"; + case "stake-cancel": return "stake cancel-unfreeze"; + case "contract-send": return "contract send"; + case "contract-deploy": return "contract deploy"; + } +} + +function historyRow(r: Obj): string[] { + const ts = r.time ?? r.block_timestamp ?? r.timestamp; + const type = r.type ?? r.transfer_type ?? r.direction ?? ""; + const amount = r.amount ?? r.value ?? r.quant ?? ""; + const symbol = r.symbol ?? (r.token_info && typeof r.token_info === "object" ? asObj(r.token_info).symbol : undefined); + const counterparty = r.counterparty ?? r.to ?? r.from ?? ""; + const status = r.status === "failed" || r.confirmed === false ? "failed" : "ok"; + return [formatTime(ts), String(type), `${formatScalar(amount)}${symbol ? ` ${String(symbol)}` : ""}`, String(counterparty), status === "ok" ? ok() : fail()]; +} + +/** account display id for receipts: the centrally-injected --account label when present, + * else the full on-chain address. Callers add their own quoting where wanted. */ +function acct(ctx: TextRenderContext, address: unknown): string { + return ctx.accountLabel ?? String(address ?? ""); +} + +/** identity field pair: prefer the account label, else show the full address — the field + * name tracks the value's real meaning (§0.4). */ +function identity(ctx: TextRenderContext, address: unknown): Pair { + return ctx.accountLabel ? ["Label", ctx.accountLabel] : ["Address", String(address ?? "")]; +} + +function displayName(d: Obj): string { + return String(d.label ?? d.accountId ?? d.id ?? "unnamed"); +} + +/** non-empty address entries — drops families whose address is blank/absent. */ +function nonEmptyAddressEntries(d: Obj): Pair[] { + return Object.entries(asObj(d.addresses)) + .filter(([, address]) => typeof address === "string" && address.length > 0) + .map(([family, address]) => [family, String(address)] as Pair); +} + +function firstAddress(d: Obj): string { + const first = nonEmptyAddressEntries(d)[0]; + return first ? first[1] : ""; +} + +/** per-family address field pairs, addresses shown in full (§0.4 ②). */ +function addressPairs(d: Obj): Pair[] { + return nonEmptyAddressEntries(d).map(([family, address]) => [familyAddressLabel(family), address] as Pair); +} + +function familyAddressLabel(family: string): string { + return FAMILY_RENDER[family as ChainFamily]?.addressLabel ?? `${family} address`; +} + +function typeLabel(v: unknown): string { + return sourceLabel(v); +} + +function secretLabel(v: unknown): string { + switch (v) { + case "mnemonic": return "recovery phrase"; + case "privateKey": return "private key"; + default: return String(v ?? "secret"); + } +} + +function methodName(sig: string): string { + return sig.replace(/\(.*/, "") || sig; +} + +function formatResult(v: unknown): string { + if (Array.isArray(v)) return v.map((x) => formatScalar(x)).join(", "); + return formatScalar(v); +} + +function formatFee(fee: unknown, family: ChainFamily): string { + if (!fee) return "unknown"; + if (typeof fee === "object") { + const f = asObj(fee); + if (f.feeSun) return `${formatSun(f.feeSun)} TRX`; + if (f.bandwidthBurnSunIfNoFreeze) return `${formatSun(f.bandwidthBurnSunIfNoFreeze)} TRX`; + // energy estimate (TRC20/contract via estimateResources): no sun figure — staked energy may + // cover it. Report the estimated energy + whether the account's available energy covers it. + if (f.energy !== undefined) { + const energy = Number(f.energy); + const avail = f.availableEnergy === undefined ? undefined : Number(f.availableEnergy); + const covered = avail !== undefined && avail >= energy ? " (covered by staked energy)" : ""; + return `~${energy.toLocaleString()} energy${covered}`; + } + if (f.note) return String(f.note); + } + return FAMILY_RENDER[family].feeFallback(fee); +} + +function summarizeTx(tx: unknown): string { + if (!tx || typeof tx !== "object") return formatScalar(tx); + const o = asObj(tx); + return shorten(String(o.txid ?? o.txID ?? o.hash ?? JSON.stringify(o))); +} diff --git a/ts/src/adapters/inbound/cli/render/layout.ts b/ts/src/adapters/inbound/cli/render/layout.ts new file mode 100644 index 000000000..ee1a71676 --- /dev/null +++ b/ts/src/adapters/inbound/cli/render/layout.ts @@ -0,0 +1,46 @@ +/** + * Layout primitives — structural composition of label/value blocks and tables, + * plus status glyphs. The "§0.4 字段独占一行" vocabulary; no scalar or domain knowledge. + */ +export type Obj = Record; +export type Pair = [string, string]; + +/** aligned `