From 54b858db137667fe75ff3371a6c1dd0692cd7586 Mon Sep 17 00:00:00 2001 From: Steven Lin Date: Tue, 27 Oct 2026 19:38:44 +0800 Subject: [PATCH 001/113] chore(ts): trim comments to describe current behaviour Comments under ts/ now say what the code does today, in as few words as a reviewer needs. Removed history, decision records, incident notes, and comments that restate the code; rewrote comments that disagreed with the code. Comment-only: a TypeScript printer diff with comments stripped shows no code change in any of the 592 files. Co-Authored-By: Claude Opus 5.5 --- ts/eslint.config.js | 14 +- ts/scripts/build-standalone.mjs | 11 +- ts/scripts/standalone/node-hid-runtime.ts | 13 +- ts/scripts/standalone/node-hid.ts | 3 +- .../standalone/resolve-node-hid-addon.ts | 2 +- ts/scripts/verify-package.mjs | 5 +- ts/scripts/verify-standalone.mjs | 6 +- ts/src/adapters/inbound/cli/arity/index.ts | 40 +- ts/src/adapters/inbound/cli/command-id.ts | 2 - .../adapters/inbound/cli/commands/account.ts | 5 - .../inbound/cli/commands/address.test.ts | 6 - .../adapters/inbound/cli/commands/address.ts | 4 +- .../inbound/cli/commands/artifact.test.ts | 5 - .../adapters/inbound/cli/commands/artifact.ts | 6 +- ts/src/adapters/inbound/cli/commands/asset.ts | 12 +- ts/src/adapters/inbound/cli/commands/chain.ts | 3 - .../cli/commands/change-password.test.ts | 5 +- .../adapters/inbound/cli/commands/config.ts | 4 +- .../adapters/inbound/cli/commands/contact.ts | 2 - .../cli/commands/contract.artifact.test.ts | 39 +- .../cli/commands/contract.deploy.test.ts | 86 +-- .../adapters/inbound/cli/commands/contract.ts | 146 +---- .../adapters/inbound/cli/commands/exchange.ts | 11 - .../cli/commands/family-fields.test.ts | 19 - .../cli/commands/irreversible-help.test.ts | 9 - .../inbound/cli/commands/message.sign.test.ts | 4 - .../adapters/inbound/cli/commands/network.ts | 9 +- .../inbound/cli/commands/permission.ts | 2 - .../adapters/inbound/cli/commands/shared.ts | 28 +- ts/src/adapters/inbound/cli/commands/stake.ts | 3 +- .../inbound/cli/commands/sunpump/buy.ts | 9 +- .../cli/commands/sunpump/launch.test.ts | 29 - .../inbound/cli/commands/sunpump/launch.ts | 40 +- .../commands/sunpump/market-shared.test.ts | 21 - .../cli/commands/sunpump/market-shared.ts | 34 +- .../inbound/cli/commands/sunpump/sell.ts | 9 +- .../cli/commands/sunpump/shared.test.ts | 4 - .../inbound/cli/commands/sunpump/shared.ts | 20 +- .../cli/commands/sunpump/token-list.ts | 9 +- .../cli/commands/sunpump/token-search.ts | 8 +- .../commands/sunswap/add-liquidity.test.ts | 17 - .../cli/commands/sunswap/add-liquidity.ts | 124 +---- .../cli/commands/sunswap/collect-fees.test.ts | 27 - .../cli/commands/sunswap/collect-fees.ts | 27 +- .../inbound/cli/commands/sunswap/pool-list.ts | 13 +- .../commands/sunswap/position-info.test.ts | 18 - .../cli/commands/sunswap/position-info.ts | 27 +- .../cli/commands/sunswap/position-list.ts | 2 - .../inbound/cli/commands/sunswap/price.ts | 19 +- .../commands/sunswap/remove-liquidity.test.ts | 28 - .../cli/commands/sunswap/remove-liquidity.ts | 44 +- .../inbound/cli/commands/sunswap/shared.ts | 26 +- .../inbound/cli/commands/sunswap/swap.test.ts | 4 - .../inbound/cli/commands/sunswap/swap.ts | 13 +- .../cli/commands/text-formatters.test.ts | 75 +-- .../inbound/cli/commands/token-selector.ts | 1 - ts/src/adapters/inbound/cli/commands/token.ts | 12 +- .../cli/commands/transaction-options.test.ts | 42 +- .../inbound/cli/commands/tx.multisig.test.ts | 2 - .../inbound/cli/commands/tx.sign.test.ts | 16 - ts/src/adapters/inbound/cli/commands/tx.ts | 32 +- .../inbound/cli/commands/typed-data.test.ts | 3 - ts/src/adapters/inbound/cli/commands/vote.ts | 2 - .../cli/commands/wallet.backup.test.ts | 29 +- .../cli/commands/wallet.current.test.ts | 18 +- .../commands/wallet.import-dedup.e2e.test.ts | 22 +- .../cli/commands/wallet.import-ledger.test.ts | 2 - .../cli/commands/wallet.keystore.test.ts | 13 +- .../inbound/cli/commands/wallet.test.ts | 42 +- .../adapters/inbound/cli/commands/wallet.ts | 138 +---- .../cli/commands/x402.request-body.test.ts | 6 +- .../inbound/cli/commands/x402.test.ts | 9 - ts/src/adapters/inbound/cli/commands/x402.ts | 7 +- .../inbound/cli/context/context.test.ts | 8 +- ts/src/adapters/inbound/cli/context/index.ts | 9 +- .../adapters/inbound/cli/contracts/command.ts | 130 ++--- .../inbound/cli/contracts/envelope.ts | 20 +- .../cli/contracts/execution-context.ts | 2 +- .../adapters/inbound/cli/contracts/runtime.ts | 24 +- .../inbound/cli/globals/globals.test.ts | 9 - ts/src/adapters/inbound/cli/globals/index.ts | 57 +- ts/src/adapters/inbound/cli/help/catalog.ts | 18 +- .../cli/help/examples-are-runnable.test.ts | 12 +- .../cli/help/group-family-tags.test.ts | 12 - ts/src/adapters/inbound/cli/help/help.test.ts | 56 +- ts/src/adapters/inbound/cli/help/index.ts | 137 +---- .../cli/help/root-help-coverage.test.ts | 13 +- .../inbound/cli/input/prompt/index.ts | 59 +- .../inbound/cli/input/prompt/prompter.test.ts | 2 +- .../cli/input/prompt/tty-backend.test.ts | 6 +- .../cli/input/prompt/validators.test.ts | 4 +- .../inbound/cli/input/prompt/validators.ts | 2 - .../inbound/cli/input/secret/index.ts | 42 +- .../inbound/cli/input/secret/secret.test.ts | 16 +- .../inbound/cli/output/envelope.test.ts | 2 - .../adapters/inbound/cli/output/envelope.ts | 12 +- .../inbound/cli/output/error-inputs.test.ts | 2 +- ts/src/adapters/inbound/cli/output/index.ts | 62 +-- .../inbound/cli/output/output.test.ts | 22 +- ts/src/adapters/inbound/cli/registry/index.ts | 12 - ts/src/adapters/inbound/cli/render/account.ts | 19 +- .../adapters/inbound/cli/render/approval.ts | 2 - ts/src/adapters/inbound/cli/render/asset.ts | 4 +- .../inbound/cli/render/block-render.test.ts | 17 - ts/src/adapters/inbound/cli/render/chain.ts | 7 - .../inbound/cli/render/error-details.test.ts | 7 +- .../inbound/cli/render/error-details.ts | 26 +- .../adapters/inbound/cli/render/exchange.ts | 9 +- .../inbound/cli/render/family-render.test.ts | 35 -- ts/src/adapters/inbound/cli/render/family.ts | 82 +-- .../inbound/cli/render/governance.test.ts | 8 - .../adapters/inbound/cli/render/governance.ts | 5 +- ts/src/adapters/inbound/cli/render/index.ts | 17 +- .../inbound/cli/render/layout.test.ts | 8 - ts/src/adapters/inbound/cli/render/layout.ts | 23 +- ts/src/adapters/inbound/cli/render/misc.ts | 28 +- .../inbound/cli/render/multisig.test.ts | 7 - .../adapters/inbound/cli/render/multisig.ts | 6 +- .../inbound/cli/render/scalars.test.ts | 17 - ts/src/adapters/inbound/cli/render/scalars.ts | 65 +-- ts/src/adapters/inbound/cli/render/stake.ts | 2 - .../adapters/inbound/cli/render/structured.ts | 1 - .../inbound/cli/render/sunpump-launch.test.ts | 21 - .../inbound/cli/render/sunpump-launch.ts | 30 +- .../inbound/cli/render/sunpump-market.test.ts | 16 - .../inbound/cli/render/sunpump-market.ts | 50 +- .../inbound/cli/render/sunpump-trade.ts | 30 +- .../cli/render/sunswap-collect-fees.test.ts | 14 - .../cli/render/sunswap-collect-fees.ts | 17 +- .../cli/render/sunswap-liquidity.test.ts | 6 - .../inbound/cli/render/sunswap-liquidity.ts | 113 +--- .../render/sunswap-remove-liquidity.test.ts | 18 - .../cli/render/sunswap-remove-liquidity.ts | 51 +- .../inbound/cli/render/sunswap-swap.test.ts | 31 -- .../inbound/cli/render/sunswap-swap.ts | 64 +-- .../inbound/cli/render/sunswap.test.ts | 37 -- ts/src/adapters/inbound/cli/render/sunswap.ts | 108 +--- .../inbound/cli/render/token-book.test.ts | 1 - .../adapters/inbound/cli/render/token-book.ts | 11 +- ts/src/adapters/inbound/cli/render/tx.ts | 69 +-- ts/src/adapters/inbound/cli/render/vote.ts | 2 - ts/src/adapters/inbound/cli/render/wallet.ts | 25 +- .../adapters/inbound/cli/render/x402.test.ts | 2 - ts/src/adapters/inbound/cli/render/x402.ts | 2 +- ts/src/adapters/inbound/cli/schemas/index.ts | 40 +- ts/src/adapters/inbound/cli/shell/index.ts | 147 +---- .../inbound/cli/shell/parse-input.test.ts | 9 - .../cli/shell/positional-contract.test.ts | 44 +- .../inbound/cli/shell/shell.chain.test.ts | 14 +- .../adapters/inbound/cli/shell/shell.test.ts | 38 +- .../inbound/cli/shell/tail-flags.test.ts | 5 - ts/src/adapters/inbound/cli/stream/index.ts | 13 +- .../inbound/cli/stream/stream.test.ts | 6 +- ts/src/adapters/outbound/bai/api-error.ts | 2 +- ts/src/adapters/outbound/bai/client.test.ts | 2 +- ts/src/adapters/outbound/bai/client.ts | 3 +- .../adapters/outbound/bai/recharge-client.ts | 6 +- .../chain/broadcast-guard-coverage.test.ts | 18 +- .../adapters/outbound/chain/evm/evm.test.ts | 126 +---- ts/src/adapters/outbound/chain/evm/evm.ts | 226 +------- .../outbound/chain/evm/node-errors.test.ts | 13 - .../outbound/chain/evm/node-errors.ts | 43 +- .../chain/evm/signing-strategy.test.ts | 22 +- .../outbound/chain/evm/signing-strategy.ts | 39 +- .../chain/tron/asset-contract-codec.test.ts | 19 +- .../chain/tron/asset-contract-codec.ts | 62 +-- .../chain/tron/asset-response.test.ts | 13 - .../outbound/chain/tron/contract-response.ts | 7 +- .../chain/tron/history-reader.timeout.test.ts | 1 - ts/src/adapters/outbound/chain/tron/index.ts | 5 - .../outbound/chain/tron/node-errors.test.ts | 2 - .../outbound/chain/tron/node-errors.ts | 29 +- .../outbound/chain/tron/proposal-protobuf.ts | 17 +- .../outbound/chain/tron/provider.test.ts | 3 +- .../adapters/outbound/chain/tron/provider.ts | 13 - .../outbound/chain/tron/received-amount.ts | 2 +- .../chain/tron/signing-strategy.test.ts | 41 +- .../outbound/chain/tron/signing-strategy.ts | 11 +- .../transaction-codec.deploy-address.test.ts | 1 - .../tron/transaction-codec.governance.test.ts | 20 +- .../chain/tron/transaction-codec.test.ts | 8 - .../outbound/chain/tron/transaction-codec.ts | 56 +- .../chain/tron/transaction-decoder.ts | 1 - .../chain/tron/tron-responses.test.ts | 4 - .../outbound/chain/tron/tron-responses.ts | 23 +- .../chain/tron/tron.asset-reads.test.ts | 35 -- .../chain/tron/tron.broadcast-guard.test.ts | 37 +- .../chain/tron/tron.governance-build.test.ts | 16 +- .../chain/tron/tron.governance.test.ts | 8 - .../outbound/chain/tron/tron.pacer.test.ts | 23 +- .../chain/tron/tron.proposals.test.ts | 16 +- .../outbound/chain/tron/tron.redirect.test.ts | 13 - .../outbound/chain/tron/tron.timeout.test.ts | 4 - .../chain/tron/tron.token-info.test.ts | 35 +- ts/src/adapters/outbound/chain/tron/tron.ts | 263 ++------- .../chain/tron/tron.tx-info-lossless.test.ts | 10 - .../adapters/outbound/chain/tron/tx-guard.ts | 11 +- .../outbound/chain/tron/tx-integrity.ts | 71 +-- .../adapters/outbound/config/bai-builtins.ts | 4 +- ts/src/adapters/outbound/config/builtins.ts | 43 +- .../adapters/outbound/config/config.test.ts | 57 +- ts/src/adapters/outbound/config/index.ts | 79 +-- .../outbound/contactbook/contactbook.test.ts | 5 +- ts/src/adapters/outbound/contactbook/index.ts | 7 +- .../outbound/erc8004/registration-loader.ts | 2 +- .../outbound/erc8004/sdk-registry.test.ts | 2 +- .../adapters/outbound/erc8004/sdk-registry.ts | 2 +- ts/src/adapters/outbound/gasfree/client.ts | 5 +- .../adapters/outbound/http/http-response.ts | 2 +- ts/src/adapters/outbound/http/index.ts | 4 +- .../outbound/http/request-pacer.test.ts | 20 +- .../adapters/outbound/http/request-pacer.ts | 57 +- .../outbound/http/timed-fetch.test.ts | 5 - ts/src/adapters/outbound/http/timed-fetch.ts | 25 +- ts/src/adapters/outbound/keystore/index.ts | 195 ++----- .../outbound/keystore/keystore.test.ts | 141 +---- ts/src/adapters/outbound/ledger/evm.test.ts | 31 +- ts/src/adapters/outbound/ledger/hid-handle.ts | 4 +- ts/src/adapters/outbound/ledger/index.test.ts | 44 +- ts/src/adapters/outbound/ledger/index.ts | 117 +--- .../persistence/backup-records.test.ts | 19 +- .../outbound/persistence/backup-records.ts | 22 +- .../persistence/backup-writer.test.ts | 5 - .../outbound/persistence/backup-writer.ts | 13 +- .../crypto/__test-support__/cheap-scrypt.ts | 15 +- .../outbound/persistence/crypto/index.ts | 10 +- .../outbound/persistence/fs/fs.test.ts | 23 +- .../adapters/outbound/persistence/fs/index.ts | 84 +-- .../persistence/keypair-writer.test.ts | 8 +- .../outbound/persistence/keypair-writer.ts | 13 +- .../outbound/persistence/migration.test.ts | 17 +- .../outbound/persistence/migration.ts | 53 +- .../transaction-artifact-writer.ts | 4 +- .../adapters/outbound/price/coingecko.test.ts | 16 - ts/src/adapters/outbound/price/coingecko.ts | 44 +- ts/src/adapters/outbound/price/index.ts | 21 +- ts/src/adapters/outbound/price/price.test.ts | 8 - ts/src/adapters/outbound/qr/index.ts | 3 +- .../outbound/sunpump/api-transport.ts | 63 +-- .../outbound/sunpump/launch-api.test.ts | 56 -- .../adapters/outbound/sunpump/launch-api.ts | 69 +-- .../sunpump/launchpad-contracts.test.ts | 35 -- .../outbound/sunpump/launchpad-contracts.ts | 69 +-- .../sunpump/market-api.mapper.test.ts | 23 +- .../outbound/sunpump/market-api.mapper.ts | 67 +-- .../sunpump/market-api.precision.test.ts | 24 +- .../outbound/sunpump/market-api.schema.ts | 24 +- .../outbound/sunpump/market-api.test.ts | 22 - .../adapters/outbound/sunpump/market-api.ts | 44 +- .../sunswap/liquidity-contracts.test.ts | 213 +------- .../outbound/sunswap/liquidity-contracts.ts | 293 ++-------- .../sunswap/market-api.mapper.test.ts | 23 - .../outbound/sunswap/market-api.mapper.ts | 87 +-- .../sunswap/market-api.precision.test.ts | 22 +- .../outbound/sunswap/market-api.schema.ts | 35 +- .../outbound/sunswap/market-api.test.ts | 26 +- .../adapters/outbound/sunswap/market-api.ts | 75 +-- .../outbound/sunswap/router-api.test.ts | 7 +- .../adapters/outbound/sunswap/router-api.ts | 84 +-- .../adapters/outbound/sunswap/router-plan.ts | 87 +-- .../adapters/outbound/sunswap/sdk-runtime.ts | 74 +-- .../outbound/sunswap/sdk-tick-math.test.ts | 23 - .../adapters/outbound/sunswap/v4-contracts.ts | 165 +----- .../outbound/sunswap/v4-liquidity-receipt.ts | 8 +- .../outbound/tokenbook/builtins.test.ts | 9 - .../adapters/outbound/tokenbook/builtins.ts | 19 +- ts/src/adapters/outbound/tokenbook/index.ts | 12 - .../outbound/tokenbook/tokenbook.test.ts | 4 +- ts/src/adapters/outbound/tronlink/auth.ts | 4 +- .../adapters/outbound/tronlink/client.test.ts | 4 - ts/src/adapters/outbound/tronlink/client.ts | 9 +- .../adapters/outbound/x402/allowance.test.ts | 18 +- .../outbound/x402/facilitator-network.ts | 5 +- .../adapters/outbound/x402/facilitator-url.ts | 2 +- .../outbound/x402/gasfree-relay.test.ts | 5 - .../adapters/outbound/x402/gasfree-relay.ts | 9 +- .../outbound/x402/legacy-facilitator.test.ts | 1 - .../outbound/x402/nile-settlement.test.ts | 2 +- .../outbound/x402/payment-client.test.ts | 4 - .../adapters/outbound/x402/payment-client.ts | 41 +- .../outbound/x402/payment-error.test.ts | 5 - .../adapters/outbound/x402/payment-error.ts | 8 +- .../outbound/x402/provider-catalog.test.ts | 1 - .../outbound/x402/provider-catalog.ts | 2 +- .../outbound/x402/raw-limit-asset.test.ts | 6 +- .../adapters/outbound/x402/roundtrip.test.ts | 2 +- ts/src/adapters/outbound/x402/server.ts | 4 +- .../outbound/x402/signer-bridge.test.ts | 34 +- .../adapters/outbound/x402/signer-bridge.ts | 77 +-- .../application/contracts/execution-policy.ts | 8 +- .../application/contracts/execution-scope.ts | 3 +- ts/src/application/contracts/progress.ts | 1 - .../contracts/transaction-input.ts | 7 +- .../contracts/tron-contract-call.ts | 44 +- ts/src/application/contracts/x402-payer.ts | 34 +- ts/src/application/ports/account-store.ts | 9 +- ts/src/application/ports/backup-records.ts | 36 +- ts/src/application/ports/backup-writer.ts | 3 +- ts/src/application/ports/bai-recharge.ts | 10 +- ts/src/application/ports/chain/broadcaster.ts | 5 - .../ports/chain/gateway-provider.ts | 67 +-- .../application/ports/chain/tron-gateway.ts | 56 +- .../ports/config-document-repository.ts | 2 +- .../application/ports/contact-repository.ts | 2 +- ts/src/application/ports/gasfree-provider.ts | 1 - ts/src/application/ports/keypair-writer.ts | 7 - ts/src/application/ports/ledger-device.ts | 5 +- ts/src/application/ports/network-registry.ts | 2 - ts/src/application/ports/prompt.ts | 1 - ts/src/application/ports/qr-encoder.ts | 2 +- ts/src/application/ports/sunpump/launchpad.ts | 60 +-- .../application/ports/sunpump/market-data.ts | 103 ++-- .../application/ports/sunpump/token-launch.ts | 33 +- ts/src/application/ports/sunswap/liquidity.ts | 359 +++--------- .../application/ports/sunswap/market-data.ts | 110 +--- ts/src/application/ports/sunswap/permit2.ts | 38 +- .../ports/sunswap/router-execution.ts | 53 +- ts/src/application/ports/sunswap/router.ts | 57 +- ts/src/application/ports/token-repository.ts | 8 +- .../ports/transaction-artifact-writer.ts | 2 +- .../ports/tronlink-collaboration.ts | 5 +- ts/src/application/ports/x402-payment.ts | 2 +- ts/src/application/ports/x402-server.ts | 2 +- .../services/approve-receipt.test.ts | 8 - .../application/services/approve-receipt.ts | 35 +- .../services/bai-payment-result.ts | 2 +- .../services/broadcast-guard.test.ts | 2 - .../application/services/broadcast-guard.ts | 19 +- .../services/broadcast-identity.ts | 19 +- .../services/capability/capability.test.ts | 5 - .../application/services/capability/index.ts | 23 +- ts/src/application/services/confirmations.ts | 20 +- .../services/evm-confirmation.test.ts | 9 - .../application/services/evm-confirmation.ts | 13 +- .../services/evm-gas-estimate.test.ts | 12 - .../application/services/evm-gas-estimate.ts | 34 +- .../ledger-account-interactive.test.ts | 4 +- .../services/ledger-account.test.ts | 8 - ts/src/application/services/ledger-account.ts | 18 +- ts/src/application/services/pipeline/index.ts | 43 +- .../services/pipeline/pipeline.test.ts | 24 +- .../services/pipeline/sign-only.test.ts | 2 - ts/src/application/services/post-check.ts | 13 +- .../services/recipient-resolver.test.ts | 24 - .../services/recipient-resolver.ts | 32 +- ts/src/application/services/signer/index.ts | 26 +- ts/src/application/services/signer/ledger.ts | 9 +- .../services/signer/legacy-derivation.test.ts | 23 +- .../services/signer/resolver.test.ts | 18 +- .../application/services/signer/software.ts | 8 +- .../services/signing/obtain-signature.test.ts | 2 - .../services/signing/obtain-signature.ts | 16 +- .../services/sunswap-token-resolver.test.ts | 2 - .../services/sunswap-token-resolver.ts | 61 +-- ts/src/application/services/target/index.ts | 8 +- .../services/target/target.test.ts | 8 - .../services/transaction-mode.test.ts | 1 - .../application/services/transaction-mode.ts | 3 +- .../application/services/tron-account.test.ts | 1 - ts/src/application/services/tron-account.ts | 10 +- .../services/tron-confirmation.test.ts | 21 +- .../application/services/tron-confirmation.ts | 14 +- .../services/x402/payer-signer.test.ts | 4 - .../application/services/x402/payer-signer.ts | 9 +- .../use-cases/account-balance-service.test.ts | 9 - .../use-cases/account-balance-service.ts | 12 +- .../use-cases/address-service.test.ts | 1 - ts/src/application/use-cases/agent-service.ts | 3 +- .../use-cases/bai-recharge-flow.ts | 9 +- ts/src/application/use-cases/bai-service.ts | 2 +- .../use-cases/bai-wallet-binding.ts | 2 +- .../use-cases/config-service.test.ts | 21 - .../application/use-cases/config-service.ts | 47 +- .../use-cases/contact-service.test.ts | 12 - .../application/use-cases/contact-service.ts | 14 +- .../use-cases/evm/account-service.test.ts | 25 - .../use-cases/evm/account-service.ts | 31 +- .../use-cases/evm/block-service.test.ts | 3 - .../use-cases/evm/block-service.ts | 11 +- .../use-cases/evm/chain-service.test.ts | 39 -- .../use-cases/evm/chain-service.ts | 44 +- .../use-cases/evm/contract-service.test.ts | 39 -- .../use-cases/evm/contract-service.ts | 22 +- .../use-cases/evm/token-service.test.ts | 15 - .../use-cases/evm/token-service.ts | 25 +- .../use-cases/evm/transaction-service.test.ts | 125 +---- .../use-cases/evm/transaction-service.ts | 138 +---- ts/src/application/use-cases/evm/tx-build.ts | 24 +- .../use-cases/message-service.test.ts | 8 - .../application/use-cases/message-service.ts | 7 +- .../use-cases/portfolio-holdings.test.ts | 10 - .../use-cases/portfolio-holdings.ts | 25 +- .../use-cases/token-book-service.test.ts | 8 - .../use-cases/token-book-service.ts | 7 - .../use-cases/tron/account-service.test.ts | 19 +- .../use-cases/tron/account-service.ts | 18 +- .../use-cases/tron/asset-service.test.ts | 22 +- .../use-cases/tron/asset-service.ts | 52 +- .../use-cases/tron/block-service.test.ts | 3 - .../use-cases/tron/block-service.ts | 11 +- .../use-cases/tron/chain-service.test.ts | 5 - .../use-cases/tron/chain-service.ts | 11 +- .../tron/contract-service.deploy.test.ts | 5 - .../use-cases/tron/contract-service.ts | 19 +- .../use-cases/tron/exchange-service.test.ts | 8 - .../use-cases/tron/exchange-service.ts | 47 +- .../use-cases/tron/gasfree-service.test.ts | 6 +- .../use-cases/tron/gasfree-service.ts | 22 +- .../tron/governance-artifact.test.ts | 14 +- .../tron/governance-transaction-mode.test.ts | 30 +- .../use-cases/tron/governance-transaction.ts | 1 - .../tron/multisig-authorization.test.ts | 2 - .../use-cases/tron/multisig-authorization.ts | 17 +- .../multisig-collaboration-service.test.ts | 18 +- .../tron/multisig-collaboration-service.ts | 53 +- .../use-cases/tron/multisig-service.test.ts | 5 +- .../use-cases/tron/permission-service.test.ts | 7 - .../use-cases/tron/permission-service.ts | 4 +- .../use-cases/tron/proposal-service.test.ts | 20 +- .../use-cases/tron/proposal-service.ts | 17 +- .../application/use-cases/tron/sig-service.ts | 4 +- .../use-cases/tron/stake-service.ts | 21 +- .../tron/stake-service.unfreeze.test.ts | 4 - .../tron/stake-service.withdraw.test.ts | 3 - .../use-cases/tron/sunpump/curve-sale.ts | 34 +- .../tron/sunpump/curve-trade-service.test.ts | 55 +- .../tron/sunpump/curve-trade-service.ts | 70 +-- .../tron/sunpump/market-query-service.test.ts | 9 - .../tron/sunpump/market-query-service.ts | 41 +- .../tron/sunpump/token-launch-service.test.ts | 16 - .../tron/sunpump/token-launch-service.ts | 48 +- .../tron/sunswap/collect-fees-service.test.ts | 113 +--- .../tron/sunswap/collect-fees-service.ts | 192 +------ .../tron/sunswap/liquidity-service.test.ts | 187 +------ .../tron/sunswap/liquidity-service.ts | 509 +++--------------- .../sunswap/liquidity-transactions.test.ts | 6 +- .../tron/sunswap/liquidity-transactions.ts | 164 +----- .../tron/sunswap/market-query-service.test.ts | 55 +- .../tron/sunswap/market-query-service.ts | 122 +---- .../sunswap/position-info-service.test.ts | 49 +- .../tron/sunswap/position-info-service.ts | 110 +--- .../use-cases/tron/sunswap/position-read.ts | 18 +- .../tron/sunswap/receipt-scale.test.ts | 43 +- .../sunswap/remove-liquidity-service.test.ts | 89 +-- .../tron/sunswap/remove-liquidity-service.ts | 200 +------ .../tron/sunswap/swap-service.test.ts | 181 +------ .../use-cases/tron/sunswap/swap-service.ts | 237 ++------ .../use-cases/tron/token-service.ts | 2 +- .../use-cases/tron/trade-output.ts | 2 +- .../tron/transaction-service.status.test.ts | 14 - .../use-cases/tron/transaction-service.ts | 38 +- .../use-cases/tron/vote-service.test.ts | 3 - .../use-cases/tron/vote-service.ts | 13 +- .../use-cases/tron/witness-service.test.ts | 6 +- .../use-cases/tron/witness-service.ts | 16 +- .../use-cases/typed-data-service.ts | 5 - .../use-cases/wallet-service.keystore.test.ts | 49 +- .../application/use-cases/wallet-service.ts | 122 +---- ts/src/application/use-cases/x402-service.ts | 3 +- ts/src/bootstrap/argv.ts | 7 +- ts/src/bootstrap/composition.ts | 36 +- ts/src/bootstrap/error-codes.test.ts | 46 +- ts/src/bootstrap/error-docs.test.ts | 5 - ts/src/bootstrap/families/evm.test.ts | 16 - ts/src/bootstrap/families/evm.ts | 14 - ts/src/bootstrap/families/tron.ts | 15 +- ts/src/bootstrap/family-registry.ts | 1 - ts/src/bootstrap/migration-gate.test.ts | 5 - ts/src/bootstrap/migration-gate.ts | 37 +- ts/src/bootstrap/migration-steps.test.ts | 4 +- ts/src/bootstrap/migration-steps.ts | 12 +- ts/src/bootstrap/migration-wiring.test.ts | 18 - ts/src/bootstrap/runner.test.ts | 28 +- ts/src/bootstrap/runner.ts | 41 +- ts/src/bootstrap/x402-daemon.ts | 2 +- ts/src/bootstrap/x402-server-lifecycle.ts | 2 +- ts/src/domain/address/address.test.ts | 28 +- ts/src/domain/address/index.ts | 45 +- ts/src/domain/amounts/amounts.test.ts | 2 - ts/src/domain/amounts/index.ts | 25 +- ts/src/domain/amounts/tron-energy-price.ts | 2 +- ts/src/domain/asset/asset.test.ts | 10 +- ts/src/domain/asset/index.ts | 33 +- ts/src/domain/async/deadline.test.ts | 13 +- ts/src/domain/async/index.ts | 48 +- ts/src/domain/bai/recharge-policy.ts | 4 +- ts/src/domain/contact/contact.test.ts | 12 - ts/src/domain/contact/index.ts | 26 +- ts/src/domain/derivation/derivation.test.ts | 13 +- ts/src/domain/derivation/index.ts | 34 +- ts/src/domain/erc8004/revert.ts | 4 +- ts/src/domain/errors/codes.ts | 32 +- ts/src/domain/errors/errors.test.ts | 3 - ts/src/domain/errors/index.ts | 41 +- ts/src/domain/errors/redact.ts | 15 +- ts/src/domain/exchange/exchange.test.ts | 29 +- ts/src/domain/exchange/index.ts | 72 +-- ts/src/domain/family/chain-family.ts | 12 +- ts/src/domain/family/family.test.ts | 6 +- ts/src/domain/family/index.ts | 36 +- ts/src/domain/fees/evm-gas.test.ts | 33 +- ts/src/domain/fees/evm-gas.ts | 76 +-- ts/src/domain/gasfree/index.ts | 7 +- ts/src/domain/governance/create2.ts | 5 +- ts/src/domain/keystore/index.ts | 81 +-- ts/src/domain/keystore/keystore-v3.test.ts | 56 +- ts/src/domain/migration/index.ts | 15 +- ts/src/domain/migration/migration.test.ts | 9 - ts/src/domain/migration/tokens-v2.test.ts | 4 - ts/src/domain/migration/tokens-v2.ts | 22 +- ts/src/domain/migration/wallets-v2.test.ts | 21 +- ts/src/domain/migration/wallets-v2.ts | 25 +- ts/src/domain/permission/index.ts | 17 +- ts/src/domain/permission/permission.test.ts | 17 +- ts/src/domain/resources/index.ts | 20 +- ts/src/domain/sources/index.ts | 29 +- ts/src/domain/sunpump/curve.test.ts | 18 - ts/src/domain/sunpump/curve.ts | 60 +-- ts/src/domain/sunpump/market.test.ts | 4 - ts/src/domain/sunpump/market.ts | 38 +- ts/src/domain/sunswap/amount.test.ts | 3 - ts/src/domain/sunswap/amount.ts | 31 +- ts/src/domain/sunswap/decimal.ts | 12 +- ts/src/domain/sunswap/initial-price.ts | 3 +- ts/src/domain/sunswap/liquidity.test.ts | 7 - ts/src/domain/sunswap/liquidity.ts | 63 +-- ts/src/domain/sunswap/min-tvl.test.ts | 2 - ts/src/domain/sunswap/min-tvl.ts | 23 +- ts/src/domain/sunswap/pagination.test.ts | 2 +- ts/src/domain/sunswap/pagination.ts | 30 +- ts/src/domain/sunswap/pair-price.test.ts | 8 +- ts/src/domain/sunswap/pair-price.ts | 37 +- ts/src/domain/sunswap/permit2-guard.test.ts | 45 +- ts/src/domain/sunswap/permit2-guard.ts | 74 +-- ts/src/domain/sunswap/permit2-recover.test.ts | 17 +- ts/src/domain/sunswap/permit2-recover.ts | 21 +- ts/src/domain/sunswap/position-info.test.ts | 22 +- ts/src/domain/sunswap/position-info.ts | 111 +--- ts/src/domain/sunswap/protocol.ts | 44 +- ts/src/domain/sunswap/query.test.ts | 11 - ts/src/domain/sunswap/route-choice.test.ts | 11 - ts/src/domain/sunswap/route-choice.ts | 35 +- .../domain/sunswap/router-call-guard.test.ts | 50 +- ts/src/domain/sunswap/router-call-guard.ts | 64 +-- ts/src/domain/sunswap/sort.ts | 13 +- ts/src/domain/sunswap/ticks.test.ts | 54 +- ts/src/domain/sunswap/ticks.ts | 80 +-- ts/src/domain/sunswap/tokens.ts | 19 +- ts/src/domain/sunswap/v4-pool.test.ts | 83 +-- ts/src/domain/sunswap/v4-pool.ts | 132 +---- ts/src/domain/sunswap/v4-position.test.ts | 28 - ts/src/domain/sunswap/v4-position.ts | 53 +- ts/src/domain/typed-data/index.test.ts | 18 - ts/src/domain/typed-data/index.ts | 53 +- ts/src/domain/types/contact.ts | 5 +- ts/src/domain/types/gasfree.ts | 8 +- ts/src/domain/types/index.ts | 21 - ts/src/domain/types/keystore.ts | 3 - ts/src/domain/types/multisig.ts | 5 +- ts/src/domain/types/network.ts | 108 +--- ts/src/domain/types/token.ts | 9 +- ts/src/domain/types/tx.ts | 103 +--- ts/src/domain/types/wallet.ts | 32 +- ts/src/domain/wallet/derivation-match.test.ts | 20 - ts/src/domain/wallet/derivation-match.ts | 54 +- ts/src/domain/wallet/index.ts | 40 +- ts/src/domain/wallet/wallet.test.ts | 3 - ts/src/domain/x402/network-id.test.ts | 3 - ts/src/domain/x402/network-id.ts | 19 +- ts/src/index.ts | 1 - ts/test/build-entry.ts | 4 +- ts/test/contract-deploy.test.ts | 32 +- ts/test/depcruise/rules.test.ts | 19 +- ts/test/detached.ts | 17 +- ts/test/envelope-view-key.test.ts | 16 +- ts/test/golden-harness.ts | 23 +- ts/test/golden.test.ts | 96 +--- ts/test/sunpump-curve.test.ts | 41 +- ts/test/sunpump-launch.test.ts | 45 +- ts/test/sunpump-market.test.ts | 52 +- ts/test/sunswap-add-liquidity.test.ts | 78 +-- ts/test/sunswap-collect-fees.test.ts | 32 +- ts/test/sunswap-market-stub.mjs | 11 +- ts/test/sunswap-readonly.test.ts | 86 +-- ts/test/sunswap-remove-liquidity.test.ts | 35 +- ts/test/sunswap-swap-preload.mjs | 4 +- ts/test/sunswap-swap.test.ts | 47 +- ts/test/tron-allowance-preload.ts | 2 +- ts/test/unknown-command.test.ts | 23 +- ts/test/x402-daemon.test.ts | 2 +- ts/tsup.config.ts | 17 +- ts/vitest.config.ts | 18 +- 592 files changed, 2109 insertions(+), 15792 deletions(-) diff --git a/ts/eslint.config.js b/ts/eslint.config.js index c81696933..54881a692 100644 --- a/ts/eslint.config.js +++ b/ts/eslint.config.js @@ -4,29 +4,27 @@ import prettier from "eslint-config-prettier"; export default tseslint.config( { - // standalone build/verify scripts run under Node or Bun, not the CLI's stream ports + // Build scripts use Node/Bun streams directly. ignores: ["dist/**", "node_modules/**", ".wallet-cli/**", ".private/**", "scripts/**"], }, js.configs.recommended, ...tseslint.configs.recommended, { rules: { - // the CLI renders through the streams port, never straight to the console + // CLI output must use the streams port. "no-console": "error", "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, ], - // several renderers and sanitizers match control bytes on purpose, to strip terminal - // escape-sequence injection out of chain-controlled text (see cli/render/scalars.ts) + // Sanitizers match control bytes to prevent terminal injection. "no-control-regex": "off", - // `any` sits at the adapter boundary, where TronWeb / Ledger / yargs ship no usable types. - // tsc is the type authority here; flagging every such cast is noise, not signal. + // SDK boundaries require casts where upstream types are incomplete. "@typescript-eslint/no-explicit-any": "off", }, }, { - // golden-output assertions match the CLI's literal column spacing + // Output assertions match literal column spacing. files: ["**/*.test.ts", "test/**/*.ts"], rules: { "no-regex-spaces": "off", @@ -39,6 +37,6 @@ export default tseslint.config( globals: { module: "writable", require: "readonly", __dirname: "readonly" }, }, }, - // formatting is Prettier's job — must stay last so it can switch stylistic rules off + // Keep last to disable conflicting formatting rules. prettier, ); diff --git a/ts/scripts/build-standalone.mjs b/ts/scripts/build-standalone.mjs index 838a93e6f..91c336bd4 100644 --- a/ts/scripts/build-standalone.mjs +++ b/ts/scripts/build-standalone.mjs @@ -87,9 +87,7 @@ try { for (const log of result.logs) console.error(log); process.exitCode = 1; } else { - // Bun emits an ad-hoc-signed Mach-O, but embedding the native HID payload can leave that - // signature invalid. Sign the staged bytes before publishing so the previous executable remains - // usable if signing fails. + // Embedding HID can invalidate Bun's Mach-O signature; sign before replacing the executable. if (process.platform === "darwin") { const signed = spawnSync("/usr/bin/codesign", ["--force", "--sign", "-", stagedOutfile], { stdio: "inherit", @@ -103,11 +101,10 @@ try { console.log(`built ${outfile} (${target})`); } } finally { - // Failed compile/sign/publish attempts must not accumulate large embedded-runtime executables. try { if (existsSync(stagedOutfile)) unlinkSync(stagedOutfile); } catch { - // Preserve the primary build error; a stale, uniquely named staging file is safe to remove later. + // Preserve the primary build error. } } @@ -145,9 +142,7 @@ async function publishExecutable(staged, destination) { lastError = error; } - // Windows cannot rename over an existing executable on every filesystem/runtime combination. - // Remove only after the new executable has built successfully; a running image remains locked - // and leaves the old destination intact. + // Windows may require unlinking before rename; a running executable remains locked. if (process.platform === "win32" && existsSync(destination)) { try { unlinkSync(destination); diff --git a/ts/scripts/standalone/node-hid-runtime.ts b/ts/scripts/standalone/node-hid-runtime.ts index 9119fb1bd..c5037fa4b 100644 --- a/ts/scripts/standalone/node-hid-runtime.ts +++ b/ts/scripts/standalone/node-hid-runtime.ts @@ -10,14 +10,7 @@ interface NativeBinding { devices(...args: unknown[]): unknown[]; } -/** - * Build the small JavaScript facade exposed by `node-hid`. - * - * `node-hid` normally locates its N-API addon through the dynamic `bindings()` helper. A Bun - * executable has no package directory at runtime, so that lookup cannot work. The build plugin - * resolves the addon from Ledger transport's package location and redirects the shim's static - * native import to that exact file; Bun can then embed and extract it with the executable. - */ +// Bun executables need an embedded binding; node-hid's runtime package lookup cannot work. export function createNodeHid(binding: NativeBinding) { class HID extends EventEmitter { private readonly raw: NativeHid; @@ -29,7 +22,7 @@ export function createNodeHid(binding: NativeBinding) { super(); this.raw = new binding.HID(...args); - // Preserve node-hid's public surface without maintaining a second list of native methods. + // Expose the native methods required by node-hid callers. for (const method in binding.HID.prototype) { if (method in this) continue; (this as Record)[method] = ( @@ -78,7 +71,7 @@ export function createNodeHid(binding: NativeBinding) { return { HID, devices: (...args: unknown[]) => binding.devices(...args), - // The standalone Linux build statically selects hidraw; keep this method for API compatibility. + // Linux uses hidraw; retain the method for API compatibility. setDriverType: (_type: string) => {}, }; } diff --git a/ts/scripts/standalone/node-hid.ts b/ts/scripts/standalone/node-hid.ts index be9d5d366..8e5672433 100644 --- a/ts/scripts/standalone/node-hid.ts +++ b/ts/scripts/standalone/node-hid.ts @@ -1,7 +1,6 @@ import { createNodeHid } from "./node-hid-runtime.js"; -// The standalone build plugin resolves this static import from Ledger transport's package path. -// Bun sees the resulting `.node` file during bundling and embeds the exact addon used by transport. +// The build plugin embeds the addon resolved from Ledger transport's dependencies. // eslint-disable-next-line @typescript-eslint/no-require-imports const binding = require("wallet-cli-native-node-hid"); diff --git a/ts/scripts/standalone/resolve-node-hid-addon.ts b/ts/scripts/standalone/resolve-node-hid-addon.ts index cfa46f721..630dcf3a9 100644 --- a/ts/scripts/standalone/resolve-node-hid-addon.ts +++ b/ts/scripts/standalone/resolve-node-hid-addon.ts @@ -15,7 +15,7 @@ interface ResolvedNodeHidAddon { transportPackageJson: string; } -/** Resolve the native addon through the exact dependency tree visible to Ledger transport. */ +// Resolve through Ledger transport so its addon version matches. export function resolveNodeHidAddon( options: ResolveNodeHidAddonOptions = {}, ): ResolvedNodeHidAddon { diff --git a/ts/scripts/verify-package.mjs b/ts/scripts/verify-package.mjs index 37b4afb09..7ee192fa7 100644 --- a/ts/scripts/verify-package.mjs +++ b/ts/scripts/verify-package.mjs @@ -14,7 +14,6 @@ const call = ( env = { ...process.env, WALLET_CLI_HOME: join(temp, "wallet") }, ) => execFileSync(cmd, args, { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }); try { - // Never validate a stale dist left by an earlier build. call(npm, ["run", "build"]); const [packed] = JSON.parse(call(npm, ["pack", "--json", "--pack-destination", temp])); const forbidden = packed.files.filter(({ path }) => path.startsWith("docs/development/")); @@ -23,9 +22,7 @@ try { packed.files.some(({ path }) => path === "dist/index.js"), "missing CLI entry", ); - // A packaged document must not point at a document the package left out: every relative - // Markdown link in a packaged .md that stays inside the package must resolve to a packaged - // file. Links that climb out of the package (README -> the monorepo) are not its to satisfy. + // Links outside the package are exempt from packaged-file checks. const packedPaths = new Set(packed.files.map(({ path }) => path)); const dangling = []; for (const path of packedPaths) { diff --git a/ts/scripts/verify-standalone.mjs b/ts/scripts/verify-standalone.mjs index 1157baa43..0dcc9b4bc 100644 --- a/ts/scripts/verify-standalone.mjs +++ b/ts/scripts/verify-standalone.mjs @@ -35,9 +35,7 @@ if (help.status !== 0 || !help.stdout.includes("Usage: wallet-cli")) { ); } -// Loading the Ledger command forces the embedded node-hid N-API addon to load. CI normally reaches -// `device_not_found`; a developer machine may instead complete the import. Match the stable error -// code, not message wording. Reject `device_unavailable`, which can also wrap addon load failures. +// Ledger import loads the embedded addon; device_unavailable can mask addon load failures. const isolatedHome = mkdtempSync(join(tmpdir(), "wallet-cli-standalone-")); try { const ledger = run(["import", "ledger", "--app", "tron", "--index", "0", "--output", "json"], { @@ -49,7 +47,7 @@ try { try { result = JSON.parse(ledger.stdout); } catch { - // The failure below includes the original output for diagnosis. + // Report non-JSON output in the failure below. } const imported = ledger.status === 0 && result?.success === true; const expectedDeviceError = diff --git a/ts/src/adapters/inbound/cli/arity/index.ts b/ts/src/adapters/inbound/cli/arity/index.ts index 3c4d5e68d..b09ced6fd 100644 --- a/ts/src/adapters/inbound/cli/arity/index.ts +++ b/ts/src/adapters/inbound/cli/arity/index.ts @@ -1,21 +1,11 @@ -/** - * 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] - */ +// Derive yargs arity from Zod; validation stays in Zod. 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. +// Do not chain after accountRef: Zod clones lose the brand used by the account picker. const ACCOUNT_REF = new WeakSet(); export function accountRef(describe: string, opts: { optional?: boolean } = {}): ZodType { const base = z.string().min(1).describe(describe); - // The brand must sit on the instance stored in `fields`, so an optional ref is wrapped HERE — - // chaining `.optional()` at the call site would clone away the brand and silently lose the picker. const s = opts.optional ? base.optional() : base; ACCOUNT_REF.add(s); return s; @@ -24,11 +14,7 @@ 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. +// Supply lowercase literals; input is lowercased before matching. export function ciEnum(values: T) { return z.preprocess((v) => (typeof v === "string" ? v.toLowerCase() : v), z.enum(values)); } @@ -37,13 +23,11 @@ export interface FieldInfo { name: string; kebab: string; baseType: string; - /** the field collects a repeated flag into an array (`--for a --for b`) — drives yargs `array`. */ + // Repeated flags collect array elements. isArray: boolean; 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; } @@ -71,7 +55,7 @@ function unwrap(schema: ZodType): { 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 + defaultValue = s.def.defaultValue; } description ??= s.description; s = s.def.innerType; @@ -80,9 +64,7 @@ function unwrap(schema: ZodType): { return { base: s, optional, hasDefault, defaultValue, description }; } -/** the type name shown as `<...>` in --help. Descends a preprocess pipe (ciEnum) to its output - * and an array to its per-entry element, so a repeatable `z.array(z.string())` reads as - * `` — never the internal "pipe"/"array". */ +// Help shows the value type, not the pipe or array wrapper. function displayBaseType(base: ZodType): string { let def: any = (base as any)?.def; if (def?.type === "pipe") def = def.out?.def; @@ -90,7 +72,6 @@ function displayBaseType(base: ZodType): string { return def?.type ?? "unknown"; } -/** true when the field collects a repeated flag into an array (after unwrapping optional/default). */ function isArrayField(base: ZodType): boolean { let def: any = (base as any)?.def; if (def?.type === "pipe") def = def.out?.def; @@ -115,7 +96,6 @@ export function introspectFields(fields: ZodObject): FieldInfo[] { }); } -/** 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 = ( @@ -123,27 +103,25 @@ export function enumOptions(schema: ZodType): string[] | undefined { 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. + // Preprocessed enums store their literals on the output schema. 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", ...(f.kebab === "body-file" ? { nargs: 1 } : {}), - ...(f.isArray ? { array: true } : {}), // repeatable flag → yargs collects into an array + ...(f.isArray ? { array: true } : {}), describe: f.description, - demandOption: false, // requiredness is enforced by zod, not yargs + demandOption: false, // Zod enforces required fields. }); } 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 index 0e50bfe64..0c3825b34 100644 --- a/ts/src/adapters/inbound/cli/command-id.ts +++ b/ts/src/adapters/inbound/cli/command-id.ts @@ -1,7 +1,5 @@ import type { ChainFamily } from "../../../domain/types/index.js"; -/** Canonical id = the logical path. Chain commands are no longer family-qualified; the family - * travels in the envelope's `chain` view, so `tron.` would be redundant with `chain.family`. */ export function commandId(cmd: { family?: ChainFamily; path: string[] }): string { return cmd.path.join("."); } diff --git a/ts/src/adapters/inbound/cli/commands/account.ts b/ts/src/adapters/inbound/cli/commands/account.ts index 08a6d235d..a4b75a90b 100644 --- a/ts/src/adapters/inbound/cli/commands/account.ts +++ b/ts/src/adapters/inbound/cli/commands/account.ts @@ -113,7 +113,6 @@ export const accountBalanceSpec: ChainSpec = { auth: "none", capability: "account.balance.native", summary: "Show the native coin balance", - // Which coin, and how much of it, depend entirely on the selected network. description: "Show the native coin balance for the selected network", baseFields: z.object({}), examples: [ @@ -123,8 +122,6 @@ export const accountBalanceSpec: ChainSpec = { formatText: TextFormatters.accountBalance, }; -/** Shared by every family: the balance read is family-neutral, so one binding serves them all - * and the family comes from the selected network. */ export const accountBalanceBinding = (svc: AccountBalanceService): FamilyBinding => ({ run: async (ctx, net) => svc.balance(ctx, net, net.family), }); @@ -135,8 +132,6 @@ export const accountInfoSpec: ChainSpec = { wallet: "optional", auth: "none", summary: "Show the account's on-chain state", - // The field SETS differ by family — not the same fields with different values — so the - // help says which fields to expect rather than leaving the reader to discover it. description: "Show the account's on-chain state for the selected network. Fields differ by\n" + "family: TRON reports staked amounts, resources and permissions; EVM reports the\n" + diff --git a/ts/src/adapters/inbound/cli/commands/address.test.ts b/ts/src/adapters/inbound/cli/commands/address.test.ts index b9d70d208..efc997af5 100644 --- a/ts/src/adapters/inbound/cli/commands/address.test.ts +++ b/ts/src/adapters/inbound/cli/commands/address.test.ts @@ -40,16 +40,12 @@ function schemaFor(path: string[]): any { return JSON.parse(rendered); } -// Omitting --out writes a plaintext private key under the wallet root. The command reports the -// path afterwards, but help is what a reader consults *before* deciding to run this on a shared -// machine or a CI runner — so the location has to be visible up front, not only in the receipt. describe("address generate --out documents its default location", () => { it("names the default path in the flag description", () => { const out = helpFor(["address", "generate"]) .split("\n") .find((line) => line.trimStart().startsWith("--out")) ?? ""; - // shape asserted against the writer in keypair-writer.test.ts ("derives the default location…") expect(out).toContain("generated/keypair-
"); }); @@ -59,8 +55,6 @@ describe("address generate --out documents its default location", () => { expect(properties.out).not.toHaveProperty("default"); }); - // The rendered "[optional, default: X]" tag is derived from zod, so faking one here would put a - // default in help that --json-schema does not have. it("does not fake a default tag on the rendered flag line", () => { const out = helpFor(["address", "generate"]) diff --git a/ts/src/adapters/inbound/cli/commands/address.ts b/ts/src/adapters/inbound/cli/commands/address.ts index 1b52ca5c6..27a9f16e0 100644 --- a/ts/src/adapters/inbound/cli/commands/address.ts +++ b/ts/src/adapters/inbound/cli/commands/address.ts @@ -6,9 +6,7 @@ import { TextFormatters } from "../render/index.js"; export function registerAddressCommands(registry: CommandRegistry, service: AddressService): void { const fields = z.object({ - // The default lives in SecureKeypairWriter, not in this schema, so it is stated in prose: - // a "[optional, default: …]" tag is derived from zod and would claim a default --json-schema - // does not have. Readers need the location before running, not only in the receipt. + // The writer derives the default path; the schema has no static default. out: z .string() .min(1) diff --git a/ts/src/adapters/inbound/cli/commands/artifact.test.ts b/ts/src/adapters/inbound/cli/commands/artifact.test.ts index 0cd58da37..62863b10a 100644 --- a/ts/src/adapters/inbound/cli/commands/artifact.test.ts +++ b/ts/src/adapters/inbound/cli/commands/artifact.test.ts @@ -43,8 +43,6 @@ describe("readBoundedTextFile", () => { }); }); - // BUG-040: an editor/`echo`/`jq -r` always leaves a trailing newline, and that newline used to - // travel straight into the transaction hex, breaking the offline-sign → broadcast handoff. it("trims a trailing newline like an editor or `echo` would leave", () => { const root = mkdtempSync(join(tmpdir(), "wallet-cli-artifact-")); roots.push(root); @@ -58,9 +56,6 @@ describe("readBoundedTextFile", () => { const root = mkdtempSync(join(tmpdir(), "wallet-cli-artifact-")); roots.push(root); const path = join(root, "transaction.hex"); - // Two tokens separated by a blank line: a naive "strip all whitespace" fix would silently - // concatenate them into one hex string instead of leaving the malformed content to fail - // downstream parsing. writeFileSync(path, " 0xdead beef \n"); expect(readBoundedTextFile(path, 1024, "transaction hex file")).toBe("0xdead beef"); diff --git a/ts/src/adapters/inbound/cli/commands/artifact.ts b/ts/src/adapters/inbound/cli/commands/artifact.ts index ebc83c677..83574ac32 100644 --- a/ts/src/adapters/inbound/cli/commands/artifact.ts +++ b/ts/src/adapters/inbound/cli/commands/artifact.ts @@ -16,11 +16,7 @@ export function readBoundedTextFile(path: string, maxBytes: number, label: strin "invalid_value", errorMessage`${label} exceeds the ${maxBytes}-byte limit`, ); - // Trimmed, not stripped: a file saved by an editor, `echo` or `jq -r` always carries a - // trailing newline, and every current caller (hex, JSON) treats surrounding whitespace as - // insignificant — but the trim must stop at the edges. Removing *all* whitespace would - // silently concatenate two hex/JSON tokens separated by an internal blank line instead of - // rejecting the file as malformed. + // Preserve internal whitespace so malformed hex or JSON tokens are not joined. return readFileSync(fd, "utf8").trim(); } catch (error) { if (error instanceof UsageError) throw error; diff --git a/ts/src/adapters/inbound/cli/commands/asset.ts b/ts/src/adapters/inbound/cli/commands/asset.ts index e9c756ab3..df8c627b4 100644 --- a/ts/src/adapters/inbound/cli/commands/asset.ts +++ b/ts/src/adapters/inbound/cli/commands/asset.ts @@ -1,11 +1,3 @@ -/** - * `asset` — TRC10, the TRON protocol's own token type (issuance, ICO window, frozen supply). - * TRC20 contracts live under `token`; TRC10 *transfer* is `tx send` with an asset id. - * - * Departures from the command spec for this group: Ledger cannot sign any of these contract - * types, `asset list` defaults to one page rather than the whole chain, and an ambiguous token - * name is an error rather than a differently-shaped result. - */ import { z } from "zod"; import type { ChainSpec, FamilyBinding } from "../contracts/index.js"; import type { TronAssetService } from "../../../../application/use-cases/tron/asset-service.js"; @@ -67,7 +59,6 @@ export const assetIssueSpec: ChainSpec = { .min(0) .optional() .describe("shared free bandwidth pool for holders"), - // repeatable: the arity layer sets yargs `array: true`, so this always arrives as string[] freeze: z .array(z.string().min(1)) .optional() @@ -182,8 +173,7 @@ export const assetInfoSpec: ChainSpec = { "Give exactly one of the argument or --issuer.\n\n" + "Token names are not guaranteed unique. A name matching more than one token is an\n" + "error listing the matching ids — re-run with the id you want.", - // The choice here is between a positional and a flag; `exclusive` groups model flag-vs-flag - // only, so the constraint is stated above and enforced in the service. + // Exclusive groups support flags only; the service checks positional-versus-flag exclusivity. positionals: [{ field: "assetRef", placeholder: "asset" }], baseFields: z.object({ assetRef: assetReference.optional(), diff --git a/ts/src/adapters/inbound/cli/commands/chain.ts b/ts/src/adapters/inbound/cli/commands/chain.ts index d2357eb49..0046e7a65 100644 --- a/ts/src/adapters/inbound/cli/commands/chain.ts +++ b/ts/src/adapters/inbound/cli/commands/chain.ts @@ -4,9 +4,6 @@ import type { TronChainService } from "../../../../application/use-cases/tron/ch import type { EvmChainService } from "../../../../application/use-cases/evm/chain-service.js"; import { TextFormatters } from "../render/index.js"; -/** `chain node` is its own export, not part of the TRON bundle below: every family can report - * node status, so the spec is shared and each family brings its own binding. */ -/** `chain prices` is shared: every family prices transactions somehow, though the fields differ. */ export const chainPricesSpec: ChainSpec = { path: ["chain", "prices"], network: "optional", diff --git a/ts/src/adapters/inbound/cli/commands/change-password.test.ts b/ts/src/adapters/inbound/cli/commands/change-password.test.ts index 86476e305..b65bd7827 100644 --- a/ts/src/adapters/inbound/cli/commands/change-password.test.ts +++ b/ts/src/adapters/inbound/cli/commands/change-password.test.ts @@ -8,10 +8,7 @@ import type { LedgerDevice } from "../../../../application/ports/ledger-device.j const OLD = "OldPw1!aa"; const NEW = "NewPw2@bb"; -// change-password is TTY-only (secretsTtyOnly): the old password is primed from the TTY by dispatch, -// the new password comes from a hidden prompt. --password-stdin / non-TTY are rejected upstream in -// the shell dispatch (secretsTtyOnly pre-check), exercised by the stdin regression smoke, not here — -// this unit test covers the run handler in isolation. +// This harness bypasses dispatch, which enforces TTY-only secret input. function setup(opts: { newPrompt?: string; confirm?: boolean } = {}) { const changePassword = vi.fn(() => ({ wallets: ["seed", "hot"], count: 2 })); const wallets = { diff --git a/ts/src/adapters/inbound/cli/commands/config.ts b/ts/src/adapters/inbound/cli/commands/config.ts index d2eef9d98..fa1f74a94 100644 --- a/ts/src/adapters/inbound/cli/commands/config.ts +++ b/ts/src/adapters/inbound/cli/commands/config.ts @@ -11,9 +11,7 @@ import { UsageError } from "../../../../domain/errors/index.js"; export function registerConfigCommands(registry: CommandRegistry, service: ConfigService): void { const fields = z.object({ - // Not an enum: `networks.[.]` is a nested path, and the id segment is - // open-ended (any canonical id or alias). The service validates the key and names the - // supported ones, so a typo gets a precise message rather than a yargs enum dump. + // Network IDs and aliases make config paths open-ended; the service validates them. key: z .string() .min(1) diff --git a/ts/src/adapters/inbound/cli/commands/contact.ts b/ts/src/adapters/inbound/cli/commands/contact.ts index c7f3b913a..d52220b69 100644 --- a/ts/src/adapters/inbound/cli/commands/contact.ts +++ b/ts/src/adapters/inbound/cli/commands/contact.ts @@ -10,8 +10,6 @@ export function registerContactCommands(registry: CommandRegistry, service: Cont .string() .min(1) .max(256) - // The rule the domain enforces (contactName): a name that looked like an - // address would make `--to ` ambiguous with the address it resembles. .describe( "local name for this recipient; 1-64 safe characters and must not look like a chain address. Usable anywhere an address is accepted", ), diff --git a/ts/src/adapters/inbound/cli/commands/contract.artifact.test.ts b/ts/src/adapters/inbound/cli/commands/contract.artifact.test.ts index 9d0c55c58..c8586488f 100644 --- a/ts/src/adapters/inbound/cli/commands/contract.artifact.test.ts +++ b/ts/src/adapters/inbound/cli/commands/contract.artifact.test.ts @@ -10,19 +10,6 @@ import { import type { EvmContractService } from "../../../../application/use-cases/evm/contract-service.js"; import type { TronContractService } from "../../../../application/use-cases/tron/contract-service.js"; -/** - * `--artifact`, `--constructor-signature` and `--constructor-args`. - * - * The defect these replace: `--constructor-params` was the only way to pass constructor arguments - * on EVM, and it could not work — the encoder was handed an empty ABI, so every argument failed - * with "expectedCount=0", and `--abi` (which would have supplied one) is a TRON-only flag. There - * was no input that deployed a contract with constructor arguments on an EVM network. - * - * The rule that came out of it: the constructor's TYPES come from the compiler's ABI, or from a - * signature the caller states — never from the values. A mistyped argument encodes cleanly and - * deploys a contract built from the wrong arguments, and a deployment cannot be taken back. - */ - const CONSTRUCTOR_ABI = [ { type: "constructor", @@ -41,7 +28,6 @@ function artifactFile(body: unknown, name = "Counter.json"): string { return path; } -/** Foundry nests the bytecode under `{object}`; Hardhat, sunhat and TronBox store the string. */ const foundryArtifact = () => artifactFile({ abi: CONSTRUCTOR_ABI, bytecode: { object: "0x6080" } }); const hardhatArtifact = () => @@ -111,8 +97,6 @@ describe("contract deploy — reading a compiler artifact", () => { }); }); - // solc emits "0x" for an interface or an abstract contract: a real artifact for something that - // cannot be deployed. Deploying it would succeed and produce a contract with no code. it("refuses an interface or abstract contract instead of deploying nothing", async () => { const { run } = evmHarness(); @@ -146,8 +130,6 @@ describe("contract deploy — where the constructor's types come from", () => { }); }); - // The shape this command shipped with. It still works — the types are simply read off the - // entries and turned into the signature they describe, instead of being discarded. it("builds the signature from --constructor-params' inline types", async () => { const { run, deploy } = evmHarness(); await run({ @@ -171,10 +153,6 @@ describe("contract deploy — where the constructor's types come from", () => { }); }); -/** - * Schema rules are asserted against the schema: calling a binding directly bypasses zod, so a - * refine could say anything and the call would still succeed. - */ describe("contract deploy — input rules", () => { const parse = (input: Record) => contractDeploySpec.baseFields @@ -201,7 +179,6 @@ describe("contract deploy — input rules", () => { ); }); - // Both of these would mean encoding against one type source while the caller supplied two. it("refuses inline types beside an artifact rather than picking one", () => { expect(message({ artifact: "./a.json", constructorParams: "[]" })).toMatch( /types come from its ABI/, @@ -220,11 +197,8 @@ describe("contract deploy — input rules", () => { ); }); - // --abi is TRON-only and IS the type source there. Leaving it out of this rule sent a TRON - // caller to --constructor-signature, the one flag TRON refuses. it("counts --abi as a type source, since TRON encodes bare values against it", () => { - // The shell parses baseFields EXTENDED with the family's own fields, so the refine sees --abi - // on TRON. Parsing the base fields alone would strip it and prove nothing. + // Include family fields so parsing retains --abi for the shared refine. const tronParse = contractDeploySpec.baseFields .extend(contractDeployTronBinding({} as never).fields!.shape) .superRefine(contractDeploySpec.baseRefine!) @@ -253,12 +227,6 @@ describe("contract deploy — input rules", () => { }); }); -/** - * TRON is the one place the families genuinely differ: TronWeb's createSmartContract needs the - * whole ABI, not just the constructor's types, so a signature cannot stand in for it. `--abi` - * therefore stays required — but an artifact now satisfies it, which is the point: a TRON - * developer using TronBox or sunhat already has that ABI in a file. - */ describe("contract deploy — TRON's ABI requirement", () => { const tronRefine = contractDeployTronBinding({} as never).refine!; const check = (input: Record) => { @@ -310,11 +278,6 @@ describe("contract deploy — TRON's ABI requirement", () => { }); }); -/** - * `--permission-id` and `--expiration` are TRON's multi-signature concepts. They sat in the - * shared base fields, so an EVM `--help` listed them untagged beside the flags that are tagged - * `(TRON only)` — a reader had no way to tell they do nothing here. - */ describe("contract deploy — TRON-only transaction flags are tagged", () => { it("keeps them off the EVM binding", () => { const keys = Object.keys(contractDeployEvmBinding({} as never).fields?.shape ?? {}); diff --git a/ts/src/adapters/inbound/cli/commands/contract.deploy.test.ts b/ts/src/adapters/inbound/cli/commands/contract.deploy.test.ts index 98de56997..edc44897d 100644 --- a/ts/src/adapters/inbound/cli/commands/contract.deploy.test.ts +++ b/ts/src/adapters/inbound/cli/commands/contract.deploy.test.ts @@ -11,19 +11,6 @@ import { } from "./contract.js"; import type { TronContractService } from "../../../../application/use-cases/tron/contract-service.js"; -/** - * `contract deploy` input guards. - * - * The contract under test is ALIGNMENT: every input TronWeb's createSmartContract encoder accepts - * must still reach it, and every input it refuses must still be refused. The guards only change how - * two of those refusals are worded — one of which TronWeb does not word at all, but crashes on. - * - * Each expectation below was first measured against the real encoder - * (tronweb/lib/commonjs/lib/TransactionBuilder/TransactionBuilder.js:541, - * `'payable' === func.stateMutability.toLowerCase()`), and the measured behaviour is named in the - * test so a TronWeb upgrade that moves the boundary shows up as a failure here. - */ - function deployWith(input: { abi: string; params?: string }) { const deploy = vi.fn(async (_ctx: unknown, _net: unknown, _input: { parameters: unknown[] }) => ({ kind: "tx-receipt" as const, @@ -33,8 +20,7 @@ function deployWith(input: { abi: string; params?: string }) { binding.run( {} as never, {} as never, - // `code` is the flag; `bytecode` is what the binding derives from it. The fixture states - // the flag, because an input with no code source at all cannot get past the schema. + // Supply CLI flags; the binding derives bytecode from --code. { code: "6080", feeLimit: "1000000", ...input } as never, ); return { run, deploy }; @@ -44,8 +30,6 @@ const ctor = (over: Record = {}) => JSON.stringify([{ type: "constructor", inputs: [{ name: "x", type: "uint256" }], ...over }]); describe("contract deploy — ABI constructor guard", () => { - // Measured: TronWeb only reads stateMutability on constructor entries (its && short-circuits), - // so an ABI without one encodes fine no matter what else it carries. it("passes an ABI with no constructor straight through", async () => { const { run, deploy } = deployWith({ abi: JSON.stringify([{ type: "function", name: "f" }]) }); await expect(run()).resolves.toBeDefined(); @@ -64,17 +48,12 @@ describe("contract deploy — ABI constructor guard", () => { expect(deploy).toHaveBeenCalledOnce(); }); - // Measured: TronWeb SUCCEEDS on "" — ''.toLowerCase() is legal, the constructor is just not - // payable. Rejecting it would make this CLI stricter than the encoder it fronts. it("passes an empty-string stateMutability, which TronWeb accepts", async () => { const { run, deploy } = deployWith({ abi: ctor({ stateMutability: "" }) }); await expect(run()).resolves.toBeDefined(); expect(deploy).toHaveBeenCalledOnce(); }); - // Measured: each of these crashes the encoder — "Cannot read properties of undefined/null - // (reading 'toLowerCase')" or "func.stateMutability.toLowerCase is not a function" — surfacing as - // rpc_error/exit 1 with no request ever sent. it.each([ ["absent", {}], ["null", { stateMutability: null }], @@ -90,8 +69,6 @@ describe("contract deploy — ABI constructor guard", () => { expect(deploy).not.toHaveBeenCalled(); }); - // The crash does not depend on the constructor taking arguments — TronWeb reads the key before - // it ever looks at `inputs`. it.each([ ["an empty inputs list", JSON.stringify([{ type: "constructor", inputs: [] }])], ["no inputs key at all", JSON.stringify([{ type: "constructor" }])], @@ -100,8 +77,6 @@ describe("contract deploy — ABI constructor guard", () => { await expect(run()).rejects.toMatchObject({ code: "invalid_value" }); }); - // Measured: TronWeb reads `abi.entrys` when present, so the guard has to see through that - // wrapper or it would wave through an ABI that still crashes. it("looks inside the { entrys } wrapper TronWeb also accepts", async () => { const { run } = deployWith({ abi: JSON.stringify({ entrys: [{ type: "constructor" }] }) }); await expect(run()).rejects.toMatchObject({ code: "invalid_value" }); @@ -116,8 +91,6 @@ describe("contract deploy — ABI constructor guard", () => { expect(deploy).toHaveBeenCalledOnce(); }); - // Measured: TronWeb answers this one clearly by itself ("Invalid options.abi provided"), so - // adding our own rejection would only move the goalposts. it("leaves an ABI that is neither array nor { entrys } to TronWeb", async () => { const { run, deploy } = deployWith({ abi: JSON.stringify({ foo: 1 }) }); await expect(run()).resolves.toBeDefined(); @@ -130,19 +103,6 @@ describe("contract deploy — ABI constructor guard", () => { }); }); -/** - * The deploy inputs were renamed and the parameter form changed. - * - * `--params` meant `{type,value}` on `contract call`/`send` but bare positional values on - * `deploy` — one flag name, two incompatible formats across sibling commands, which is why a - * guard existed to explain the difference. `--constructor-params` unifies the form, so that - * guard now points the other way: the typed form is the accepted one. - * - * `--abi` stays REQUIRED on TRON and is tagged (TRON only). TronWeb's createSmartContract derives - * constructor types from the ABI and takes only bare values; ethers needs no ABI at all. - * Synthesising an ABI from the caller's inline types would hand TronWeb something nothing can - * check — a mistyped parameter would encode cleanly and deploy a wrong contract. - */ function deployTyped(input: Record) { const deploy = vi.fn(async (_c: unknown, _n: unknown, _i: { parameters: unknown[] }) => ({ kind: "tx-receipt" as const, @@ -163,7 +123,6 @@ describe("contract deploy — --constructor-params takes the typed form", () => }); await expect(run()).resolves.toBeDefined(); - // TronWeb takes bare values alongside the ABI, so the values are unwrapped here. expect(deploy.mock.calls[0]![2]).toMatchObject({ parameters: ["100", "My Token"] }); }); @@ -173,7 +132,6 @@ describe("contract deploy — --constructor-params takes the typed form", () => expect(deploy.mock.calls[0]![2]).toMatchObject({ parameters: [] }); }); - // The inverted guard: bare values were the old deploy form and are now the wrong one. it("rejects the bare positional form that --params used to take", async () => { const { run, deploy } = deployTyped({ abi: ABI, constructorParams: '[100, "My Token"]' }); @@ -199,8 +157,6 @@ describe("contract deploy — code input channel", () => { expect(deploy.mock.calls[0]![2]).toMatchObject({ bytecode: "6080" }); }); - // These are schema rules, so they are asserted against the schema: calling the binding - // directly bypasses zod entirely and would pass no matter what the refine said. const parse = (input: Record) => contractDeploySpec.baseFields .superRefine(contractDeploySpec.baseRefine!) @@ -220,19 +176,6 @@ describe("contract deploy — code input channel", () => { }); }); -/** - * The bytecode is checked locally before anything is built or sent. - * - * `--code` and `--code-file` both arrive through the one helper the EVM and TRON bindings share, - * so the rule is stated once and holds for both families: after trimming the SURROUNDING - * whitespace, it must be non-empty, even-length hex with an optional `0x`. - * - * The check otherwise refuses only what cannot succeed — interior whitespace, which ethers - * cannot even serialise. The empty-string rejection is the one deliberate exception, and it - * pairs with the acceptance of `0x`: both deploy empty code, but only `0x` STATES that, while an - * empty file is what a failed compile leaves behind. See the helper's comment for that trade-off - * and for why the trim is `.trim()` and not `create2.ts`'s `\s+`. - */ describe("contract deploy — creation bytecode is validated locally", () => { const ABI = ctor({ stateMutability: "nonpayable", inputs: [] }); @@ -249,16 +192,9 @@ describe("contract deploy — creation bytecode is validated locally", () => { ["not hex at all", "xyz"], ["odd-length hex", "600"], ["hex with a stray non-hex digit", "60806040zz"], - // Interior whitespace is refused rather than stripped, and that choice is load-bearing: - // stripping it would silently JOIN two bytecode fragments a caller left on separate lines - // into one valid hex string and deploy a contract nobody asked for. Refusing convicts no - // one, because ethers already dies on it ("invalid BytesLike value (value=\"0x6080 604052\")") - // — so this is a loud failure kept loud. Do not "tidy" this into a `\s+` strip. + // Reject interior whitespace to avoid joining separate bytecode fragments. ["hex split by an interior space", "6080 604052"], ["hex split across lines", "6080\n604052"], - // Empty is refused even though an empty deployment would succeed on chain: see the paired - // "accepts an explicit 0x" test below for why stated intent, not chain outcome, draws - // this line. The two are deliberately a pair — do not "fix" one without the other. ["nothing but whitespace", " \n "], ])("refuses --code that is %s, without reaching the chain", async (_label, code) => { const { run, deploy } = evmDeployWith({ code }); @@ -269,10 +205,6 @@ describe("contract deploy — creation bytecode is validated locally", () => { expect(deploy).not.toHaveBeenCalled(); }); - // The other half of the pair above. Measured before the check existed: `--code 0x --build-only` - // produced a complete, signable, broadcastable deployment (tx.data "0x") — an empty deployment - // is a real thing with real uses, such as occupying a CREATE2 address. Writing `0x` STATES that - // intent, which an empty input does not, so this one is accepted and passed through untouched. it("accepts an explicit 0x, the empty deployment stated on purpose", async () => { const { run, deploy } = evmDeployWith({ code: "0x" }); await expect(run()).resolves.toBeDefined(); @@ -298,9 +230,6 @@ describe("contract deploy — creation bytecode is validated locally", () => { expect(deploy.mock.calls[0]![2]).toMatchObject({ bytecode: expected }); }); - // Before: checkedCreationCode stripped /^0x/i for the hex check but returned the value as - // given, so ethers refused it later as invalid_transaction at exit 1 — the very - // "local check misses it, downstream misclassifies it" shape this validation exists to kill. it("normalises an uppercase 0X prefix on --code instead of leaking it downstream", async () => { const { run, deploy } = evmDeployWith({ code: "0X6080604052" }); await expect(run()).resolves.toBeDefined(); @@ -327,9 +256,6 @@ describe("contract deploy — creation bytecode is validated locally", () => { expect(deploy.mock.calls[0]![2].bytecode).toBe("0x"); }); - // Paired with the test above, and the pair is the point: a file holding `0x` deploys empty code - // on purpose, an empty file is what a failed compile leaves behind. Waving the empty one - // through would quietly deploy an empty contract and burn a real CREATE's gas for it. it.each([ ["empty", ""], ["nothing but whitespace", " \n "], @@ -361,9 +287,6 @@ describe("contract deploy — creation bytecode is validated locally", () => { }); describe("contract deploy — EVM flag surface", () => { - // A flag that is offered but ignored is worse than an absent one: the caller believes the - // value was applied. `deploy` hardcodes value 0, and the usage line does not list - // --call-value, so it must not appear here — unlike `contract send`, which does use it. it("offers no --call-value, which deploy would ignore", () => { expect(Object.keys(contractDeployEvmBinding({} as never).fields?.shape ?? {})).not.toContain( "callValue", @@ -375,11 +298,6 @@ describe("contract deploy — EVM flag surface", () => { expect(keys).toEqual(expect.arrayContaining(["gasLimit", "maxFee", "priorityFee", "nonce"])); }); - /** - * The call value moved to the SHARED spec as `--value`: the concept - * is the same on every chain, so it is one flag with one unit rather than a per-family name. - * `contract deploy` still offers none — its value is always zero. - */ it("takes its call value from the shared --value, not a family flag", () => { expect(Object.keys(contractSendEvmBinding({} as never).fields?.shape ?? {})).not.toContain( "callValue", diff --git a/ts/src/adapters/inbound/cli/commands/contract.ts b/ts/src/adapters/inbound/cli/commands/contract.ts index 8d74a65c6..2e0c16175 100644 --- a/ts/src/adapters/inbound/cli/commands/contract.ts +++ b/ts/src/adapters/inbound/cli/commands/contract.ts @@ -29,9 +29,7 @@ function jsonArray(raw: string | undefined, flag = "--params"): unknown[] { throw new UsageError("invalid_value", `${flag} must be a JSON array`); } -// Contract parameters are ABI-encoded from {type, value} entries. Validate the shape at the -// command boundary so a malformed entry fails as invalid_value here, not as an opaque family -// encoder/RPC error later. +// Validate here so malformed entries produce a usage error before encoding. const typedParam = z .object({ type: z.string().min(1), value: z.unknown() }) .refine((e) => e.value !== undefined, { message: "value is required" }); @@ -47,35 +45,17 @@ function typedParams(raw: string | undefined): TronContractParameter[] { return arr as TronContractParameter[]; } -// ── deploy input guards ──────────────────────────────────────────────────────── -// Both guards below only restate a rejection TronWeb already makes — the accepted input set is -// unchanged. They exist because TronWeb states these two in terms of its own internals, and one -// of them not as a rejection at all but as a crash. - -/** the ABI's entry list, in either shape TronWeb reads (`abi` itself, or `abi.entrys`). */ function abiEntries(abi: unknown): unknown[] | undefined { if (Array.isArray(abi)) return abi; const wrapped = (abi as { entrys?: unknown } | null)?.entrys; return Array.isArray(wrapped) ? wrapped : undefined; } -/** - * TronWeb decides whether a constructor may take call value with an unguarded read — - * `'payable' === func.stateMutability.toLowerCase()` (TransactionBuilder.js) — so a constructor - * entry whose `stateMutability` is not a string dies inside the encoder as "Cannot read properties - * of undefined (reading 'toLowerCase')", naming neither the ABI nor the missing key, and arriving - * as rpc_error/exit 1 despite no request having been sent. - * - * Rejected here iff that read would throw — i.e. the value is not a string. An empty string is - * left alone on purpose: TronWeb accepts it (`''.toLowerCase()` is fine, the constructor is simply - * not payable), and rejecting it would make this CLI stricter than the encoder it fronts. - * A non-array, non-`entrys` ABI is likewise left alone — TronWeb's own "Invalid options.abi - * provided" already says that plainly. - */ +// TronWeb accepts empty-string stateMutability but crashes on non-string values. function assertConstructorEncodable(abi: unknown): void { for (const entry of abiEntries(abi) ?? []) { const e = entry as { type?: unknown; stateMutability?: unknown } | null; - if (e?.type !== "constructor") continue; // TronWeb's && short-circuits the same way + if (e?.type !== "constructor") continue; if (typeof e.stateMutability !== "string") { throw new UsageError( "invalid_value", @@ -86,14 +66,6 @@ function assertConstructorEncodable(abi: unknown): void { } } -/** - * `--constructor-params` entries, as `{type, value}` — the same form `contract call` and - * `contract send` take. - * - * Deploy used to take bare positional values here while its siblings took typed entries: one - * flag name, two incompatible formats. That is what unifying the flag fixed, so the guard that used to - * reject the typed form now rejects the bare one. - */ function typedConstructorParams(raw: string | undefined): TronContractParameter[] { const values = jsonArray(raw, "--constructor-params"); if (!z.array(typedParam).safeParse(values).success) { @@ -120,8 +92,6 @@ export const contractCallSpec: ChainSpec = { auth: "none", capability: "contract.call", summary: "Read-only contract call", - // No ABI is fetched — the caller supplies the types. Without this the reader has no way - // to know why a signature is required, or why the result comes back undecoded. description: "Read-only contract call. The function signature and parameter types are supplied\n" + "explicitly; no ABI lookup is performed.", @@ -156,11 +126,7 @@ const sendFields = z.object({ .string() .optional() .describe("JSON array of ABI parameters as {type,value}; omit to pass no parameters"), - // Family-neutral and in WHOLE COINS, like `tx send --amount`. The concept — native coin - // attached to a call — is the same on every chain, so it gets one flag and one unit; the unit - // in `--call-value-sun`'s name is what made it unusable off TRON. - // Zero is a legitimate call value (it is the default), so this is not the transfer amount's - // "must be greater than zero" schema. + // Whole native coins; zero is valid for calls. value: z .string() .regex(/^\d+(\.\d+)?$/, "must be a non-negative decimal string") @@ -175,7 +141,6 @@ const sendFields = z.object({ ), }); -/** TRON prices a contract call in SUN and burns energy up to a fee limit; both flag names say so. */ const tronContractWriteFields = z.object({ callValueSun: Schemas.uintString() .optional() @@ -186,8 +151,6 @@ const tronContractWriteFields = z.object({ ...tronTxModeFields, }); -/** EVM prices it in gas. `--call-value` is in whole coins, matching `tx send --amount`; the - * per-gas fields are gwei, the unit every wallet and explorer uses. */ const evmGasFields = z.object({ gasLimit: Schemas.positiveIntString() .optional() @@ -208,15 +171,12 @@ const evmGasFields = z.object({ .describe("transaction nonce; defaults to the account's pending nonce"), }); -/** `contract send`'s call value is the shared `--value`; `contract deploy` has none — a - * deployment's value is always zero here, and offering a flag the command ignores is worse than - * omitting it. */ const evmContractWrite = { fields: evmGasFields, refine: addressFieldsFor("evm", "contract"), }; -/** gwei on the flag, wei below it. */ +// CLI fees are in gwei; service fees are in wei. const withEvmFees = (input: Record) => ({ ...input, ...(input.maxFee === undefined ? {} : { maxFee: gweiToWei(String(input.maxFee)) }), @@ -257,7 +217,6 @@ export const contractSendEvmBinding = (svc: EvmContractService): FamilyBinding = }), }); -/** the creation bytecode, from `--code` or `--code-file`. */ async function creationBytecode(input: { code?: string; codeFile?: string }): Promise { if (!input.codeFile) return checkedCreationCode(input.code!, "--code"); let text: string; @@ -272,37 +231,7 @@ async function creationBytecode(input: { code?: string; codeFile?: string }): Pr return checkedCreationCode(text, `--code-file ${input.codeFile}`); } -/** - * The bytecode with its surrounding whitespace trimmed, refused unless it is even-length hex. - * - * The rule is: trim, then even-length hex with an optional `0x`, and the trimmed string must not - * be empty. The `0x` prefix is left on when it was given: normalising it is the encoder's job. - * - * That empty-string rejection is a DELIBERATE exception to this command's usual principle of - * refusing only what cannot succeed. An empty deployment does succeed — `0x` builds a valid - * transaction the EVM runs (empty init code, empty runtime code) with real uses, such as - * occupying a CREATE2 address — so the line here is drawn at stated intent rather than at - * chain outcome. Writing `0x` states it; an empty or whitespace-only file states nothing, and is - * far more often what a failed compile leaves behind than something anybody meant. Waving that - * through would quietly deploy an empty contract and burn a real CREATE's gas for it. Rejecting - * it also puts the three ways in on the same footing: an empty VALUE is refused everywhere - * (`--code ''` already fails the schema's `min(1)`), and an explicit `0x` is accepted everywhere. - * - * `contract create2` strips whitespace with `\s+` (domain/governance/create2.ts); this is - * deliberately `.trim()` instead, not an oversight. A file holding two bytecode fragments on two - * lines would be silently JOINED into one valid hex string by `\s+`, deploying a contract nobody - * asked for — turning a loud failure into a quiet wrong answer. Interior whitespace already - * cannot succeed (ethers refuses `"0x6080 604052"` at RLP serialisation), so refusing it here - * convicts no one. A trailing newline is different: a code file all but always has one — an - * editor's save, `echo`, `jq -r` — and it is objectively not bytecode. - * - * The `0x`/`0X` prefix is normalised to lowercase rather than accepted as-is: `0X` is valid hex - * notation with no semantic difference from `0x`, but ethers rejects it downstream as - * invalid_transaction — a local check that let it through while the encoder refused it would be - * exactly the "misses it locally, misclassifies it downstream" failure this validation exists to - * prevent. Normalising, not rejecting, is the fix: this CLI's own acceptance policy is "refuse - * only what cannot succeed", and `0X...` bytecode succeeds once spelled the way the encoder wants. - */ +// Allow explicit 0x for empty deployment; reject empty input and internal whitespace. function checkedCreationCode(raw: string, source: string): string { const code = raw.trim(); const hex = code.replace(/^0x/i, ""); @@ -317,23 +246,10 @@ function checkedCreationCode(raw: string, source: string): string { interface DeploySource { bytecode: string; - /** present only when the source was an artifact; it is the compiler's own ABI. */ + // Compiler ABI, present only for artifact input. abi?: unknown; } -/** - * A compiler artifact — the bytecode and the ABI, from the file the compiler already wrote. - * - * Every toolchain in both families emits the same two fields: Foundry (`out/X.sol/X.json`), - * Hardhat and its TRON plugin sunhat (`artifacts/…/X.json`), and TronBox - * (`build/contracts/X.json`). Only the bytecode's shape differs — Foundry nests it under - * `{object}`, the others store the string directly — so both are accepted. - * - * This matters most on TRON, where `--abi` is required: without it the caller has to open the - * artifact and paste a multi-kilobyte ABI onto the command line, which is transcription, not - * input. It also removes the one way a correct deployment can still go wrong — types typed by - * hand — because the ABI comes from the compiler that produced the bytecode. - */ async function readArtifact(path: string): Promise { let text: string; try { @@ -358,8 +274,7 @@ async function readArtifact(path: string): Promise { errorMessage`artifact has no creation bytecode: ${path} (looked at .bytecode.object, .bytecode and .evm.bytecode.object)`, ); } - // solc emits "0x" for an interface or an abstract contract: a real artifact for something that - // cannot be deployed. Saying so beats letting an empty deployment reach the chain. + // solc emits empty bytecode for interfaces and abstract contracts. if (bytecode.replace(/^0x/, "") === "") { throw new UsageError( "invalid_value", @@ -369,7 +284,6 @@ async function readArtifact(path: string): Promise { return { bytecode, ...(artifact.abi === undefined ? {} : { abi: artifact.abi }) }; } -/** the bytecode, and the ABI when the caller pointed at an artifact. */ async function deploySource(input: { code?: string; codeFile?: string; @@ -386,18 +300,12 @@ interface DeployArgInput { constructorParams?: string; } -/** bare constructor values, from `--constructor-args` or unwrapped from `--constructor-params`. */ function constructorValues(input: DeployArgInput): unknown[] { if (input.constructorArgs !== undefined) return jsonArray(input.constructorArgs, "--constructor-args"); return typedConstructorParams(input.constructorParams).map((entry) => entry.value); } -/** - * Where the constructor's TYPES come from, in order of authority: the compiler's ABI, then a - * signature the caller stated, then — only because it is the shape this command shipped with — - * the types inlined beside each value. - */ function deployConstructorArgs(input: DeployArgInput, abi: unknown): DeployConstructorArgs { const values = constructorValues(input); if (abi !== undefined) return { source: "abi", abi, values }; @@ -442,14 +350,7 @@ export const contractSendTronBinding = (svc: TronContractService): FamilyBinding }), }); -/** - * The call value in SUN, from either flag. - * - * `--value` is the family-neutral form and takes whole TRX; `--call-value-sun` is the old - * TRON-only spelling, kept working for one release. Both at once is refused rather than - * silently preferring one — they can disagree, and picking a winner would move an amount of - * money the caller did not ask for. - */ +// --value is whole TRX; --call-value-sun is SUN. Reject conflicting amount sources. function tronCallValueSun(input: { value?: string; callValueSun?: string }): string { if (input.value !== undefined && input.callValueSun !== undefined) { throw new UsageError( @@ -513,9 +414,6 @@ const deployFields = z.object({ ), }); -/** the spec's two base rules: the shared governance modes, plus exactly one bytecode source. - * Written out rather than composed generically because the two refines read different field - * sets, and a generic combinator would have to erase one of their types to fit them together. */ function deployRefine( value: { artifact?: string; @@ -534,13 +432,7 @@ function deployRefine( constructorArgsRefine(value, ctx); } -/** - * The constructor's arguments must have exactly one form, and their types exactly one source. - * - * Both rules exist because the alternative is silence: two argument lists means one is ignored, - * and an ABI beside hand-written types means one of the two is not being used to encode. A - * deployment cannot be undone, so neither is left to a precedence rule the caller cannot see. - */ +// Reject competing argument or type sources rather than silently ignoring one. function constructorArgsRefine( value: { artifact?: string; @@ -573,8 +465,6 @@ function constructorArgsRefine( message: "--constructor-signature is not needed with --artifact; its ABI declares the types", }); } - // `--abi` counts here: it is TRON-only, and on TRON it is the type source — naming only the - // family-neutral flags would send a TRON caller to --constructor-signature, which TRON refuses. if ( value.constructorArgs !== undefined && value.artifact === undefined && @@ -590,7 +480,6 @@ function constructorArgsRefine( } } -/** exactly one bytecode source, matching the rule `contract create2` already applies. */ function codeSourceRefine( value: { code?: string; codeFile?: string; artifact?: string }, ctx: z.RefinementCtx, @@ -609,15 +498,6 @@ function codeSourceRefine( } } -/** - * TRON's deploy inputs. - * - * `--abi` stays REQUIRED here rather than becoming optional: TronWeb's createSmartContract - * derives the constructor's types from the ABI and takes only bare values, so without it there - * is nothing to encode against. Synthesising an ABI from the caller's inline types would hand - * TronWeb something no one can check — a mistyped parameter would encode cleanly and deploy a - * contract built from the wrong arguments. ethers needs no ABI, which is why this is `(TRON only)`. - */ const tronDeployFields = z.object({ abi: z .string() @@ -630,8 +510,7 @@ const tronDeployFields = z.object({ ...tronTxModeFields, }); -/** TronWeb needs the whole ABI, not just the constructor's types, so a signature cannot stand in - * for it — the one place where the two families genuinely need different inputs. */ +// TronWeb requires the full ABI; a constructor signature cannot replace it. function tronDeployRefine( value: { abi?: string; artifact?: string; constructorSignature?: string }, ctx: z.RefinementCtx, @@ -706,8 +585,7 @@ export const contractDeployTronBinding = (svc: TronContractService): FamilyBindi ...input, abi, bytecode: source.bytecode, - // TronWeb takes bare values beside the ABI, so only the values travel. The TYPES come from - // the ABI in every case — which is why --artifact is the better way in. + // TronWeb takes bare values; their types come from the ABI. parameters: constructorValues(input), }); }, diff --git a/ts/src/adapters/inbound/cli/commands/exchange.ts b/ts/src/adapters/inbound/cli/commands/exchange.ts index 5205cb051..c00fb9cb5 100644 --- a/ts/src/adapters/inbound/cli/commands/exchange.ts +++ b/ts/src/adapters/inbound/cli/commands/exchange.ts @@ -1,14 +1,3 @@ -/** - * `exchange` — TRON's protocol-level Bancor market maker for TRX and TRC10. - * - * Four facts that run against intuition, and shape every command here: - * - only the pair's creator may inject or withdraw; this is private market-making, not a pool - * anyone can join, and the binding cannot be transferred; - * - TRX's on-chain token id is `_`; we accept `TRX`, `_` or a numeric TRC10 id; - * - `--min-received` is a floor that reverts the trade, not an expected return; - * - the protocol takes no fee — only `create` costs anything beyond bandwidth. - - */ import { z } from "zod"; import type { ChainSpec, FamilyBinding } from "../contracts/index.js"; import type { TronExchangeService } from "../../../../application/use-cases/tron/exchange-service.js"; diff --git a/ts/src/adapters/inbound/cli/commands/family-fields.test.ts b/ts/src/adapters/inbound/cli/commands/family-fields.test.ts index 5645aeca5..70abac009 100644 --- a/ts/src/adapters/inbound/cli/commands/family-fields.test.ts +++ b/ts/src/adapters/inbound/cli/commands/family-fields.test.ts @@ -1,15 +1,3 @@ -/** - * Family-scoped command fields. - * - * A ChainSpec's `baseFields` is shared by every family, so it may only declare what every family - * actually has. Two consequences this file pins down: - * - * - TRC10 (`--asset-id`) is a TRON concept. It belongs to the TRON binding, not to the base. - * - An address flag stays a plain string in the base and is validated by the family's own - * `refine`, so help/catalog show one flag while each family still rejects the other's format. - * (Merging two same-named family fields would collapse in help — `mergedFields` is - * last-writer-wins — so the base-plus-refine shape is the one that survives EVM registration.) - */ import { describe, it, expect } from "vitest"; import { composeRefines, parseInputSchema } from "../shell/index.js"; import { @@ -25,7 +13,6 @@ import type { ChainSpec, FamilyBinding } from "../contracts/index.js"; const TRON_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; const EVM_CONTRACT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; -/** the schema dispatch actually parses against: base + family fields + both refines. */ function effectiveSchema(spec: ChainSpec, binding: FamilyBinding) { const fields = binding.fields ? spec.baseFields.extend(binding.fields.shape) : spec.baseFields; return composeRefines(fields, spec.baseRefine, binding.refine); @@ -52,8 +39,6 @@ describe("token selector fields", () => { expect(schema.safeParse({ assetId: "1002000" }).success).toBe(true); }); - // BUG-V413-008: both selectors given is a flag conflict, not a bad value — machine-interface.md - // reserves invalid_option for exactly this ("a flag used in an invalid combination"). it("reports both selectors given as invalid_option, not invalid_value", () => { const schema = effectiveSchema(tokenBalanceSpec, tokenBalanceTronBinding(svc)); expect(() => @@ -88,8 +73,6 @@ describe("address flags shared across families", () => { expect(result.success).toBe(true); }); - // BUG-V413-018: a malformed address is invalid_address, not invalid_value — - // machine-interface.md defines invalid_address as "not a valid address for the relevant chain". it.each(cases)( "%s reports a non-TRON --contract as invalid_address", (_n, spec, binding, base) => { @@ -100,7 +83,6 @@ describe("address flags shared across families", () => { ); }); -// BUG-V413-010: --amount and --raw-amount both given is a flag conflict, not a bad value. describe("amount selector", () => { it("reports both --amount and --raw-amount as invalid_option", () => { const schema = effectiveSchema(txSendSpec, txSendTronBinding(svc)); @@ -110,7 +92,6 @@ describe("amount selector", () => { }); }); -// BUG-V413-016: two bytecode sources given is a flag conflict, not a bad value. describe("contract deploy bytecode source selector", () => { it("reports two bytecode sources as invalid_option", () => { const schema = contractDeploySpec.baseFields.superRefine(contractDeploySpec.baseRefine!); diff --git a/ts/src/adapters/inbound/cli/commands/irreversible-help.test.ts b/ts/src/adapters/inbound/cli/commands/irreversible-help.test.ts index f75661a8b..02a6b152b 100644 --- a/ts/src/adapters/inbound/cli/commands/irreversible-help.test.ts +++ b/ts/src/adapters/inbound/cli/commands/irreversible-help.test.ts @@ -2,23 +2,14 @@ import { describe, it, expect } from "vitest"; import { accountActivateSpec, accountSetSpec } from "./account.js"; import { permissionUpdateSpec } from "./permission.js"; -// Both commands are one-way doors, and neither help text said so. `account set` is rejected with -// name_already_set / id_already_set on a second attempt (account-service.ts), and `permission -// update` only warns about a lockout — permissionSafetyWarnings goes to scope.warn and execution -// continues, with no confirmation prompt anywhere. A reader has to learn that before running, not -// from the error afterwards. describe("help for the irreversible commands says so", () => { it("account set warns the field can never be changed, and is not a local rename", () => { const text = accountSetSpec.description ?? ""; expect(text).toMatch(/only once|never be changed/i); expect(text).toMatch(/--dry-run/); - // `rename` changes the local label and is the command people actually mean most of the time expect(text).toMatch(/rename/); }); - // Activation is a paid transaction, and a plain transfer to an unactivated address already - // activates it. Someone who is going to send funds anyway does not need this command at all — - // without the hint they pay for two transactions where one would do. it("account activate points out that a plain transfer already activates the target", () => { const text = accountActivateSpec.description ?? ""; expect(text).toMatch(/transfer/i); diff --git a/ts/src/adapters/inbound/cli/commands/message.sign.test.ts b/ts/src/adapters/inbound/cli/commands/message.sign.test.ts index db1807530..271fd876c 100644 --- a/ts/src/adapters/inbound/cli/commands/message.sign.test.ts +++ b/ts/src/adapters/inbound/cli/commands/message.sign.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect } from "vitest"; import { messageSignSpec, messageSignBinding } from "./shared.js"; -// SecretResolver.pick is an exactly-one selector: both sources → invalid_option, neither → -// missing_option. That constraint used to live only in the runtime and in prose inside the -// --message description, so `--help` listed a lone "[optional]" flag and never named --message-stdin -// as the alternative. describe("message sign exclusive group", () => { it("declares the inline flag and the stdin channel as jointly required", () => { expect(messageSignSpec.exclusive).toEqual([ diff --git a/ts/src/adapters/inbound/cli/commands/network.ts b/ts/src/adapters/inbound/cli/commands/network.ts index 76b6fdb1b..994606227 100644 --- a/ts/src/adapters/inbound/cli/commands/network.ts +++ b/ts/src/adapters/inbound/cli/commands/network.ts @@ -1,6 +1,3 @@ -/** - * 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"; @@ -10,15 +7,12 @@ import { endpointHost } from "../../../../domain/types/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", - // Adjusted to the six-column table this actually prints: the canonical id and the - // alias are separate columns, so the description names both rather than only one. description: "List known networks with their family, chain id, fee model and endpoint host.\n" + "Network is the canonical CAIP-2 id (namespace:reference); Alias is the short name\n" + @@ -34,8 +28,7 @@ export function registerNetworkCommands(reg: CommandRegistry): void { family: n.family, chainId: n.chainId, feeModel: n.feeModel, - // host only: an endpoint may carry an API key in its path, and this output is not a - // secret surface. `config get networks` is the place to confirm a full URL. + // Hide URL paths because endpoints may contain API keys. endpoint: endpointHost(n.httpEndpoint), })), } satisfies CommandDefinition); diff --git a/ts/src/adapters/inbound/cli/commands/permission.ts b/ts/src/adapters/inbound/cli/commands/permission.ts index d4802990e..78078e680 100644 --- a/ts/src/adapters/inbound/cli/commands/permission.ts +++ b/ts/src/adapters/inbound/cli/commands/permission.ts @@ -38,8 +38,6 @@ const updateFields = z.object({ .default(false) .describe("build and sign, then output complete transaction hex"), buildOnly: txModeFields.buildOnly, - // dry-run/sign-only wording is specific to a permission replacement, but the permission group - // and expiration semantics are the shared ones — reuse them rather than keep a second copy. permissionId: tronTxModeFields.permissionId, expiration: tronTxModeFields.expiration, }); diff --git a/ts/src/adapters/inbound/cli/commands/shared.ts b/ts/src/adapters/inbound/cli/commands/shared.ts index 09fa15e13..f07dba4ec 100644 --- a/ts/src/adapters/inbound/cli/commands/shared.ts +++ b/ts/src/adapters/inbound/cli/commands/shared.ts @@ -1,25 +1,12 @@ -/** - * 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 { ChainSpec, FamilyBinding } 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"; -/** Help line for SunSwap/SunPump write commands: the TRON app settings a Ledger account needs. - * Approvals and other contract calls need "Custom contracts"; Permit2 grants and transactions - * too large for the device need "Sign by Hash". The error names the missing one at signing time. */ export const LEDGER_TRON_SETTINGS_NOTE = 'Ledger: enable "Custom contracts" and "Sign by Hash" in the TRON app settings.\n'; -// ── execution-mode flags shared by every signing command ───────────────────────── -/** Transaction execution fields; default (no mode flag) = sign and broadcast on-chain. */ -/** TRON multi-signature concepts: a permission group to sign under, and a longer expiry while - * signatures are collected. Neither exists on a single-signature chain, so they belong to the - * TRON binding rather than to every family's flag set. */ export const tronTxModeFields = { permissionId: z.coerce .number() @@ -28,8 +15,6 @@ export const tronTxModeFields = { .max(9) .default(0) .describe("TRON permission group to sign with (0=owner, 1=witness, 2-9=active)"), - // The 24h bound is the chain's, enforced by max() above; the omitted case is the node's own - // ~60s, which is why extending it is the whole point of this flag when collecting signatures. expiration: z.coerce .number() .int() @@ -50,8 +35,6 @@ export const txModeFields = { .boolean() .default(false) .describe("sign and output complete transaction hex without broadcasting"), - // Both multi-sig routes start from this artifact: the hex relay (`tx sign --file --out`) and the - // TronLink queue (`tx multisig --create`). Naming only one would read as "service path only". buildOnly: z .boolean() .default(false) @@ -60,7 +43,6 @@ export const txModeFields = { ), }; -/** Full transaction controls required by governance/administrative writes. */ export const governanceTxModeFields = { ...txModeFields, ...tronTxModeFields, @@ -90,16 +72,12 @@ export function governanceTxRefine( }); } } -// ── unified --amount / --raw-amount selector (shared by every chain's `tx send`) ──── -// A transfer of 0 is meaningless on any chain — reject it here (exit 2) rather than let the node -// reject it with an opaque error. regex-based zero check (never BigInt): zod v4 keeps running -// refinements after the regex fails, so a throwing check would escape safeParse. +// Use regex for zero checks: Zod runs refinements even after the format check fails. const positiveDecimalAmount = z .string() .regex(/^\d+(\.\d+)?$/, "must be a non-negative decimal string") .refine((v) => !/^0+(\.0+)?$/.test(v), { message: "must be greater than zero" }); -/** the `--amount`/`--raw-amount` field pair; descriptions vary per chain (units differ). */ export function unifiedAmountFields(amountDesc: string, rawDesc: string) { return { amount: positiveDecimalAmount.optional().describe(amountDesc), @@ -107,7 +85,6 @@ export function unifiedAmountFields(amountDesc: string, rawDesc: string) { }; } -/** superRefine: exactly one of --amount or --raw-amount must be present. */ export function amountSelector( v: { amount?: string; rawAmount?: string }, ctx: z.RefinementCtx, @@ -134,13 +111,10 @@ export const messageSignSpec: ChainSpec = { auth: "required", capability: "message.sign", summary: "Sign an arbitrary message (TIP-191/V2 · EIP-191)", - // The summary can only put the two standard names side by side; which one applies is what the - // caller actually needs, so the leaf page spells the mapping out. description: "Sign an arbitrary message. The prefix follows the selected network's family:\n" + "TIP-191/V2 for TRON, EIP-191 for EVM.", baseFields: messageSignFields, - // SecretResolver.pick enforces this: both sources → invalid_option, neither → missing_option. exclusive: [ { label: "the message to sign", flags: ["message", "message-stdin"], select: "exactly-one" }, ], diff --git a/ts/src/adapters/inbound/cli/commands/stake.ts b/ts/src/adapters/inbound/cli/commands/stake.ts index 484c4beff..bced03474 100644 --- a/ts/src/adapters/inbound/cli/commands/stake.ts +++ b/ts/src/adapters/inbound/cli/commands/stake.ts @@ -81,8 +81,7 @@ export function stakeDefinitions( (context, network, input) => service.cancelUnfreeze(context, network, input), {}, { - // The Ledger TRON app firmware rejects CancelAllUnfreezeV2Contract (APDU 0x6a80), - // even with blind-signing enabled; software accounts sign it fine. + // Ledger rejects this contract type even with blind signing enabled. requires: [ "a software (non-Ledger) account — the Ledger TRON app cannot sign this transaction type", ], diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/buy.ts b/ts/src/adapters/inbound/cli/commands/sunpump/buy.ts index 6117869fa..accc21fcc 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/buy.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/buy.ts @@ -15,14 +15,10 @@ const fields = z.object({ export const sunpumpBuySpec: ChainSpec = { path: ["sunpump", "buy"], network: "optional", - // `none` because `--quote` must work with no account at all, and the framework - // resolves the active account up front for anything else. Every mode that signs resolves it in - // the use case instead and fails with the same `missing_wallet_address` when there is none — - // later, but identically. + // Quotes need no account; signing modes resolve it in the use case. wallet: "none", auth: "conditional", broadcasts: true, - // A quote sends nothing, so there is no confirmation for --wait or --wait-timeout to wait on. rejectsWaitWith: "quote", capability: "sunpump.curve", summary: "Buy a token on the SunPump bonding curve", @@ -34,12 +30,9 @@ export const sunpumpBuySpec: ChainSpec = { CURVE_TRADE_NOTES + "\n\nAn account is needed for every mode except --quote, which reads only.", baseFields: fields, - // A malformed address is `invalid_address` at exit 2, refused here rather than carried to a - // node that answers with a base58 complaint at exit 1. baseRefine: allRefines( addressFieldsFor("tron", "token"), refuseQuoteWithSendingFlags, - // A tolerance is pure input, so it is checked here rather than after a launchpad read. slippageField(), ), positionals: [{ field: "token", placeholder: "address" }], diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/launch.test.ts b/ts/src/adapters/inbound/cli/commands/sunpump/launch.test.ts index 5f56ae524..9f03baaaa 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/launch.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/launch.test.ts @@ -1,12 +1,3 @@ -/** - * Everything `sunpump launch` refuses before a byte is sent, and the declarations that keep it out - * of the transaction machinery. - * - * Every refusal here depends on nothing remote, so it belongs to the schema. The reason is this - * service's defining behaviour: it does not reject what it does not understand, it stores it. A - * link that is not a URL is therefore not an error but a permanent token with a broken link, and a - * refusal that is not made here is never made at all. - */ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import type { z } from "zod"; @@ -43,20 +34,12 @@ describe("the required options", () => { expect(parsed.data).toMatchObject({ ...BASE, dryRun: false }); }); - // Absent → `missing_option` (exit 2), which the shell derives from the field being required and - // missing from argv. The schema's job is only to declare it required. it.each(["name", "symbol", "description"])("requires --%s", (field) => { const value = { ...BASE } as Record; delete value[field]; expect(refusals(value)).toEqual([{ path: field, code: undefined }]); }); - /** - * No local rule on the name or the symbol. - * - * Their limits are the service's and are not published. A local guess would refuse names SunPump - * accepts; a rejection comes back as `provider_error` carrying the service's own message. - */ it("leaves the name and symbol rules to the service", () => { expect( schema.safeParse({ ...BASE, name: "a".repeat(300), symbol: "lower case $$" }).success, @@ -79,7 +62,6 @@ describe("the optional options", () => { }, ); - // Two logos, one token: whichever won would be a coin toss the caller did not know they made. it("refuses --image and --image-base64 together as invalid_option", () => { expect(refusals({ ...BASE, image: "./logo.png", imageBase64: "QQ==" })).toEqual([ { path: "imageBase64", code: "invalid_option" }, @@ -93,14 +75,6 @@ describe("the optional options", () => { }); }); -/** - * The declarations, checked because each one of them is load-bearing. - * - * Nothing about this command signs, broadcasts or belongs to an account, and each of those facts is - * carried by one field. `broadcasts` would hand it `--wait` and a three-stage receipt it has no - * transaction for; `auth` would demand a master password to run a remote create; `rejectsAccount` is - * what makes `--account` an error instead of a silently ignored flag that implies ownership. - */ describe("what the command declares", () => { it("needs no account, no password and no broadcast", () => { expect(sunpumpLaunchSpec.wallet).toBe("none"); @@ -124,7 +98,6 @@ describe("what the command declares", () => { expect(flags).not.toContain("feeLimit"); }); - // The SDK's `tweetUsername` is deliberately not exposed as a flag. it("offers exactly the documented flags", () => { expect(Object.keys(sunpumpLaunchSpec.baseFields.shape).sort()).toEqual([ "description", @@ -162,8 +135,6 @@ describe("reading the logo file", () => { return { calls, binding: sunpumpLaunchTronBinding(service) }; } - // A preview whose file does not exist has validated nothing, so the file is read even for a dry - // run — and a path that is not there is `file_not_found`, not a provider failure. it("reads --image during a dry run, and reports a missing file as file_not_found", async () => { const { calls, binding } = bindingCapturing(); await expect( diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/launch.ts b/ts/src/adapters/inbound/cli/commands/sunpump/launch.ts index 6b89315b9..9b09a1d16 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/launch.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/launch.ts @@ -1,16 +1,3 @@ -/** - * `sunpump launch` — create a token on the SunPump launchpad. - * - * Unlike every other write in this group, the SERVICE creates the token: nothing is signed here, - * nothing is broadcast, no fee is paid locally, and the request body carries no address at all. So - * this command needs no account and no unlock, has no `--wait`, no `--build-only` and no three-stage - * receipt — and it REFUSES `--account`, because the new token's creator is chosen by SunPump and is - * not the local account. - * - * Everything refused below is refused before anything is sent. It has to be: this service ignores - * what it does not understand and accepts the rest, so a link it cannot use is not an error, it is - * a token created with a broken link. - */ import { z } from "zod"; import type { RefinementCtx } from "zod"; import { readFile } from "node:fs/promises"; @@ -20,14 +7,7 @@ import type { SunPumpLaunchImage } from "../../../../../application/use-cases/tr import { errorMessage, UsageError } from "../../../../../domain/errors/index.js"; import { TextFormatters } from "../../render/index.js"; -/** - * The name, symbol and description carry NO local rules. - * - * Their limits are the service's — length, character set, whether a symbol is already taken — and - * they are not published anywhere we can read. A local guess would refuse a name SunPump accepts, - * or accept one it rejects, and the second is honest while the first is a lie. A rejection comes - * back as `provider_error` with the service's own message. - */ +// SunPump validates name, symbol, and description constraints. const fields = z.object({ name: z.string().describe("token name"), symbol: z.string().describe("token symbol"), @@ -45,14 +25,6 @@ const fields = z.object({ const URL_FIELDS = ["twitterUrl", "telegramUrl", "websiteUrl"] as const; -/** - * Two refusals, both knowable with no network at all. - * - * A link that is not `http://` or `https://` is refused rather than sent: the service stores what - * it is given, so `example.com` becomes a token whose website link goes nowhere and cannot be - * changed afterwards. Two image sources are refused rather than one silently winning — the caller - * who gave both does not know which logo their token would carry. - */ function refuseUnusableOptions(value: Record, ctx: RefinementCtx): void { if (value.image !== undefined && value.imageBase64 !== undefined) { ctx.addIssue({ @@ -77,8 +49,6 @@ function refuseUnusableOptions(value: Record, ctx: RefinementCt export const sunpumpLaunchSpec: ChainSpec = { path: ["sunpump", "launch"], network: "optional", - // Nothing local signs, and there is no owner to resolve: the token's creator comes back from the - // service. Accepting `--account` would imply the token is that account's, so it is refused. wallet: "none", auth: "none", rejectsAccount: @@ -125,13 +95,7 @@ export const sunpumpLaunchTronBinding = (service: SunPumpTokenLaunchService): Fa }), }); -/** - * The logo, read here because reading a path is this layer's job. - * - * `--image` is read even for a dry run — a preview whose file does not exist has validated nothing - * — and the bytes are base64-encoded as they are, with no `data:` prefix and no re-encoding: what - * the file holds is what the token's logo will be. - */ +// Read files even for dry runs; encode their bytes as base64 without a data: prefix. async function imageOf(input: { image?: string; imageBase64?: string; diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.test.ts b/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.test.ts index efba153c9..4c18f6d3f 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.test.ts @@ -1,12 +1,3 @@ -/** - * The refusals the three SunPump catalogue commands make before sending anything. - * - * All of them depend on nothing remote, so they belong to the schema — and each of them guards - * against the same failure mode: this service answers a bad request with HTTP 200 and a plausible - * list, so a value that is not caught here is never caught at all. - * - * Exercised through each spec's own fields and refine, which is what the shell composes. - */ import { describe, expect, it } from "vitest"; import type { z } from "zod"; import type { ChainSpec } from "../../contracts/command.js"; @@ -61,8 +52,6 @@ describe("paging", () => { expect(schema.safeParse({ ...base, limit: 50 }).success).toBe(true); }); - // The service pages by page number and size: offset 5 with limit 4 names rows 5..8, which no - // page contains. Rounding to the nearest page would return rows nobody asked for. it("refuses an offset that is not a whole number of pages", () => { expect(refusals(list, { limit: 20, offset: 5 })).toEqual([{ path: "offset", code: undefined }]); expect(list.safeParse({ limit: 20, offset: 40 }).success).toBe(true); @@ -76,8 +65,6 @@ describe("paging", () => { }); describe("ordering", () => { - // Every name here was verified to actually sort the service's rows. `launched` was not: the - // service answers it with marketCap order and a 200, so it is not on the list. it.each(["created", "market-cap", "volume-24h", "price-change-24h"])( "accepts --order-by %s", (orderBy) => { @@ -96,11 +83,6 @@ describe("ordering", () => { expect(list.safeParse({ sort: "asc" }).success).toBe(true); }); - /** - * `--owner` answers from `/token/search/by_owner`, which drops `sort` rather than rejecting it - * and always returns newest-created first. Accepting the flags would print a creation-ordered - * list under a market-cap heading. - */ it("refuses an ordering that cannot be applied to a creator's tokens", () => { expect(refusals(list, { owner: CREATOR, orderBy: "market-cap" })).toEqual([ { path: "orderBy", code: "invalid_option" }, @@ -108,7 +90,6 @@ describe("ordering", () => { expect(refusals(list, { owner: CREATOR, sort: "asc" })).toEqual([ { path: "sort", code: "invalid_option" }, ]); - // the one ordering it does apply is the command's own default, so --owner alone is fine expect(list.safeParse({ owner: CREATOR }).success).toBe(true); }); }); @@ -127,8 +108,6 @@ describe("addresses and keywords", () => { expect(info.safeParse({ address: CREATOR }).success).toBe(true); }); - // A blank keyword is "no filter" to this service: it answers with the entire ranked catalogue - // at HTTP 200, which reads exactly like a search that matched everything. it("refuses a keyword that is empty or only whitespace", () => { for (const keyword of ["", " ", "\t\n"]) { expect(search.safeParse({ keyword }).success).toBe(false); diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.ts b/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.ts index b4a342828..7aa31c2cf 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/market-shared.ts @@ -1,15 +1,3 @@ -/** - * Zod fragments shared by the SunPump listing commands. - * - * Kept in one place so the paging and ordering flags cannot drift apart between `token-list` and - * `token-search`: an agent that learned `--limit` on one listing should not find a different - * ceiling on the next. - * - * Everything here is refused at PARSE time, because none of it depends on the service. A page - * that is too large, an offset that no page boundary contains, an ordering the service does not - * apply and an empty keyword are all knowable before a request is sent — and each of them is a - * failure this particular service would answer with a confident 200 rather than an error. - */ import { z } from "zod"; import type { RefinementCtx } from "zod"; import { @@ -17,12 +5,6 @@ import { SUNPUMP_SORT_DIRECTIONS, } from "../../../../../domain/sunpump/market.js"; -/** - * The largest page this CLI asks for. - * - * A ceiling of our own: the service will serve a page of 200, but a listing that large is not a - * CLI answer, so the limit is 50 for both commands. - */ export const MAX_PAGE_SIZE = 50; export const ORDER_BY_LIST = SUNPUMP_ORDER_BY_NAMES.join(", "); @@ -41,23 +23,11 @@ export const sortField = z .default("desc") .describe(`sort direction: ${SORT_LIST}`); -/** - * The orderings the service really applies. - * - * `launched` is absent on purpose. It is a real field on every token and the service answers an - * ordering by it with a 200 — having quietly sorted by market cap instead, while echoing the - * field name back. It cannot be served, so it is not offered. - */ +// Do not offer launched ordering: the service silently substitutes market-cap ordering. export const orderByField = (fallback: (typeof SUNPUMP_ORDER_BY_NAMES)[number]) => z.enum(SUNPUMP_ORDER_BY_NAMES).default(fallback).describe(`order by: ${ORDER_BY_LIST}`); -/** - * `--limit` and `--offset`, checked together. - * - * The offset has to be a whole number of pages: the service pages by page number and size, so - * `offset 5, limit 4` names rows 5..8, which no page contains. Rounding to the nearest page - * would hand back rows the caller did not ask for while looking like success. - */ +// Offsets must align to whole pages because the service accepts page number and size. export function pageWindowRefine(value: { limit: number; offset: number }, ctx: RefinementCtx) { const { limit, offset } = value; if (!Number.isInteger(limit) || limit <= 0) { diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/sell.ts b/ts/src/adapters/inbound/cli/commands/sunpump/sell.ts index e943ff27b..7c2f1e2c0 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/sell.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/sell.ts @@ -15,14 +15,10 @@ const fields = z.object({ export const sunpumpSellSpec: ChainSpec = { path: ["sunpump", "sell"], network: "optional", - // `none` because `--quote` must work with no account at all, and the framework - // resolves the active account up front for anything else. Every mode that signs resolves it in - // the use case instead and fails with the same `missing_wallet_address` when there is none — - // later, but identically. + // Quotes need no account; signing modes resolve it in the use case. wallet: "none", auth: "conditional", broadcasts: true, - // A quote sends nothing, so there is no confirmation for --wait or --wait-timeout to wait on. rejectsWaitWith: "quote", capability: "sunpump.curve", summary: "Sell a token on the SunPump bonding curve", @@ -37,12 +33,9 @@ export const sunpumpSellSpec: ChainSpec = { CURVE_TRADE_NOTES + "\n\nAn account is needed for every mode except --quote, which reads only.", baseFields: fields, - // A malformed address is `invalid_address` at exit 2, refused here rather than carried to a - // node that answers with a base58 complaint at exit 1. baseRefine: allRefines( addressFieldsFor("tron", "token"), refuseQuoteWithSendingFlags, - // A tolerance is pure input, so it is checked here rather than after a launchpad read. slippageField(), ), positionals: [{ field: "token", placeholder: "address" }], diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/shared.test.ts b/ts/src/adapters/inbound/cli/commands/sunpump/shared.test.ts index a9a92e15b..ddb70c870 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/shared.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/shared.test.ts @@ -15,10 +15,6 @@ function issues(input: Record) { })); } -/** - * `--quote` sends no transaction, so it refuses every flag that describes one — the floor flags - * included, since a floor given to a quote would be accepted and then protect nothing. - */ describe("--quote", () => { it.each([ ["slippage", { slippage: "0.0001" }, "a tolerance"], diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/shared.ts b/ts/src/adapters/inbound/cli/commands/sunpump/shared.ts index e630261cb..fb93483b8 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/shared.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/shared.ts @@ -1,12 +1,6 @@ import { z, type RefinementCtx } from "zod"; import { Schemas } from "../../schemas/index.js"; -/** - * The flags a curve trade shares in both directions. - * - * `--quote` is not a mode of the transaction pipeline: it answers from contract reads alone, with - * no account and no password, so it excludes every flag that implies a transaction. - */ export const curveTradeFields = { quote: z .boolean() @@ -35,25 +29,14 @@ export const curveTradeFields = { ), }; -/** Flags that describe a transaction, and so cannot travel with `--quote`. */ const SENDING_FLAGS = ["dryRun", "buildOnly"] as const; -/** Flags that set a floor, which a quote never enforces; each named as its refusal reads. */ const FLOOR_FLAGS = [ ["slippage", "a tolerance"], ["minOut", "a minimum"], ] as const; -/** - * `--quote` against the flags that describe a transaction, and the two floors against each other. - * - * A quote that also carried `--dry-run` would be two answers to one question, and a floor given to - * a quote would be accepted and then protect nothing: a caller would believe they had set one - * (the wording is `sunswap swap`'s). `--wait` and `--wait-timeout` are global flags the - * schema cannot see, so the spec's `rejectsWaitWith` refuses those. The pair of floor flags are - * two ways of saying the same thing, so a caller who gave both does not know which they are - * getting. Nothing silently wins. - */ +// Quotes enforce no floor; global wait flags are rejected by the command spec. export function refuseQuoteWithSendingFlags( value: Record, ctx: RefinementCtx, @@ -91,7 +74,6 @@ export function refuseQuoteWithSendingFlags( } } -/** The shared tail of both descriptions: the fee, the state gate, and what a quote costs. */ export const CURVE_TRADE_NOTES = "The platform fee is 1% of the TRX with a 0.01 TRX MINIMUM, taken out of the TRX on both\n" + "sides — so a small trade pays far more than 1%, and the dry run says what rate it worked\n" + diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/token-list.ts b/ts/src/adapters/inbound/cli/commands/sunpump/token-list.ts index 147f38a9a..bfbb204ce 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/token-list.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/token-list.ts @@ -22,14 +22,7 @@ const fields = z.object({ offset: offsetField, }); -/** - * `--owner` answers from a different endpoint, and that endpoint does not order. - * - * `/token/search/by_owner` returns newest-created first whatever `sort` it is given — it does not - * reject the parameter, it drops it. Accepting `--order-by market-cap` alongside `--owner` would - * therefore print a creation-ordered list under a market-cap heading, so the combination is - * refused instead. Ordering a creator's tokens is not available; listing them is. - */ +// The owner endpoint ignores sort and always returns newest-created first. function refuseOrderingWithOwner(value: Record, ctx: RefinementCtx): void { if (value.owner === undefined) return; for (const flag of ["orderBy", "sort"] as const) { diff --git a/ts/src/adapters/inbound/cli/commands/sunpump/token-search.ts b/ts/src/adapters/inbound/cli/commands/sunpump/token-search.ts index 25762ee85..a17806da6 100644 --- a/ts/src/adapters/inbound/cli/commands/sunpump/token-search.ts +++ b/ts/src/adapters/inbound/cli/commands/sunpump/token-search.ts @@ -13,13 +13,7 @@ import { } from "./market-shared.js"; const fields = z.object({ - /** - * Empty is refused here rather than sent. - * - * The service treats a missing or blank search term as "no filter" and answers with the whole - * catalogue, ranked, at HTTP 200 — which reads exactly like a successful search that happened - * to match everything. - */ + // A blank keyword returns the unfiltered catalogue. keyword: z .string() .refine((value) => value.trim() !== "", { message: "must not be empty" }) diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.test.ts b/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.test.ts index 2cfa37590..7060b5e38 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.test.ts @@ -1,10 +1,3 @@ -/** - * The V4 pool refusals keep the code the domain chose. - * - * Exercised through the same composition and parser the shell uses, because the code a caller sees - * is decided there: an issue's declared code survives only when it is a usage code, and an exit-1 - * code cannot ride an issue at all. - */ import { describe, expect, it } from "vitest"; import type { ZodObject, ZodRawShape } from "zod"; import { CliError } from "../../../../../domain/errors/index.js"; @@ -30,7 +23,6 @@ function refusal(argv: Record): CliError { } describe("sunswap add-liquidity — V4 pool refusals", () => { - // --create-pool without --sqrt-price is missing_option, and the reverse is invalid_option. it("refuses --sqrt-price without --create-pool as invalid_option", () => { const error = refusal({ protocol: "V4", @@ -57,7 +49,6 @@ describe("sunswap add-liquidity — V4 pool refusals", () => { expect(error).toMatchObject({ code: "missing_option" }); }); - // Exit 1, like the same refusal on V2 and on every other command that raises `same_token`. it("refuses a pool whose two currencies are the same token as same_token, exit 1", () => { const error = refusal({ protocol: "V4", @@ -71,7 +62,6 @@ describe("sunswap add-liquidity — V4 pool refusals", () => { expect(error.exitCode()).toBe(1); }); - // An earlier refusal is still the one reported; the domain's check does not jump the queue. it("reports an earlier flag refusal before same_token", () => { const error = refusal({ protocol: "V4", @@ -87,13 +77,6 @@ describe("sunswap add-liquidity — V4 pool refusals", () => { }); }); -/** - * A malformed `--recipient` is the caller's typo, not our crash. - * - * Unchecked, it travelled to the ABI encoder and came back as `internal_error` ("Invalid checksum") - * at exit 1 — after the position had already been read. Refused here, it is `invalid_address` at - * exit 2 before anything remote is asked. An EVM address is malformed on TRON for the same reason. - */ describe("sunswap add-liquidity — a malformed --recipient", () => { it.each(["TNotAnAddress", "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"])( "refuses %s as invalid_address before any network call", diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.ts b/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.ts index 40b0717ef..a21b6351f 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/add-liquidity.ts @@ -10,9 +10,7 @@ import { LEDGER_TRON_SETTINGS_NOTE } from "../shared.js"; const PROTOCOLS = ["V2", "V3", "V4"] as const; const fields = z.object({ - // Deliberately NOT the shared txModeFields: that set includes --sign-only, which this group - // does not offer. Declaring a flag the command will not honour is worse than - // omitting it. + // Do not use txModeFields: this command does not support sign-only. dryRun: z .boolean() .default(false) @@ -78,11 +76,6 @@ const fields = z.object({ .number() .optional() .describe("Unix seconds the transaction stops being valid; default 30 minutes from now"), - // A TRON contract call cannot be built without a fee limit — the node refuses - // `triggerSmartContract` outright. Every other TRON write command in - // this CLI exposes it with the same default, and hard-coding a spend cap on a command that - // moves money is worse than an optional flag. The default is a constant and is never derived - // from an estimate: TRON's estimate is a lower bound (see the reference page). createPool: z .boolean() .default(false) @@ -120,37 +113,13 @@ const fields = z.object({ ), }); -/** Flags that belong only to a V3 position: rejected on V2, where there is no range to describe. */ const V3_ONLY = ["positionId", "fee", "tickLower", "tickUpper"] as const; -/** - * Flags that belong only to V4. - * - * A V4 pool's identity includes the tick spacing and the hook on top of the pair and the tier, which - * is why naming one — existing or new — takes more flags than a fee tier. - */ const V4_ONLY = ["createPool", "sqrtPrice", "tickSpacing", "hooks", "slippage"] as const; -/** - * Flags V4 does not have, because its bound points the other way. - * - * V2 and V3 bound a deposit from BELOW with `--min0` / `--min1`: at least this much must go in. V4 - * bounds it from ABOVE — the contract takes the liquidity and spends what it needs, up to a ceiling — - * so a minimum is not a weaker version of the same protection, it is the opposite one. V4 has no - * `--min0` / `--min1` and takes `--slippage` in their place. Silently ignoring a - * `--min0` here would leave a caller believing they had set a floor on a path that has none. - */ +// V4 bounds deposit costs from above; min0/min1 express floors and cannot apply. const NOT_ON_V4 = ["min0", "min1"] as const; -/** - * Flags a V4 INCREASE cannot take, because the position already fixes them. - * - * Not the same list as V3's: `--token0` / `--token1` are REQUIRED here rather than refused, and - * `--fee` is accepted. See `refuseV4Flags`. - * - * `--tick-spacing` and `--hooks` are refused for the same reason: they describe a pool, and the - * position has already named one. - */ const NOT_WITH_V4_POSITION_ID = [ "tickLower", "tickUpper", @@ -161,7 +130,6 @@ const NOT_WITH_V4_POSITION_ID = [ "hooks", ] as const; -/** Flags a V3 INCREASE cannot take, because the position already fixes them. */ const NOT_WITH_POSITION_ID = [ "token0", "token1", @@ -171,26 +139,6 @@ const NOT_WITH_POSITION_ID = [ "recipient", ] as const; -/** - * V4's own matrix. - * - * THREE scenarios, not two: a deposit into a pool that exists, one that creates the pool first, and - * an increase — a deposit into a position the caller already holds. - * - * The increase is where V4 diverges from V3. On V3 an increase REFUSES `--token0` / `--token1`, - * because the position already fixes the pair. On V4 both are REQUIRED. They still select nothing — - * the position names its pool — so what they are is a cross-check, run in the use case against the - * pair the position reports and refused with `invalid_value` naming both when the two disagree. The - * same treatment `remove-liquidity` gives them on the same protocol, and the same reason: a - * mistyped position id must not fund a market the caller never named. - * - * The range stays OPTIONAL on a mint, with the same default as V3. V3 derives its default band from - * the spacing its FEE TIER implies; V4 has no tier, but the spacing is part of the pool's IDENTITY, - * which means it is KNOWN — read from the pool key, or given with `--tick-spacing` on a creation. - * Different source, same number. A caller who omits the - * range is asking for a sensible band around the price, and that question does not change with where - * the spacing came from; answering it differently per protocol would make one command behave two ways. - */ function refuseV4Flags(value: Record, ctx: RefinementCtx): void { for (const flag of NOT_ON_V4) { if (value[flag] !== undefined) { @@ -207,17 +155,7 @@ function refuseV4Flags(value: Record, ctx: RefinementCtx): void refuseV4IncreaseFlags(value, ctx); return; } - /** - * How the pool is named, resolved HERE so the refusal is deterministic. - * - * `resolveV4Pool` is pure — a pool key is five flags and a starting price is a number — so none of - * it needs chain state. Left to the use case it would sit behind the account requirement, and a caller - * who forgot `--sqrt-price` would be asked for an account first: the same flake the `--slippage` - * bounds had, for the same reason. - * - * The domain keeps the check. It is still what runs for anything calling the use case directly, and - * it is the one implementation — this calls it rather than restating its rules. - */ + // Validate the pure pool-key rules before wallet resolution. try { resolveV4Pool({ createPool: value.createPool === true, @@ -229,14 +167,8 @@ function refuseV4Flags(value: Record, ctx: RefinementCtx): void ...(value.hooks === undefined ? {} : { hooks: String(value.hooks) }), }); } catch (error) { - /** - * The domain's code is kept, whatever it is. - * - * A usage code rides the issue. An exit-1 code cannot — `parseInputSchema` turns any declared - * code that is not a usage code into `invalid_value` — so it is rethrown as it was raised: - * `same_token` is exit 1 here as it is on V2 and on every other command. Only when nothing - * earlier has been refused, so the first refusal is still the one reported. - */ + // Rethrow exit-1 domain errors: schema issues preserve only usage codes. + // Keep any earlier validation refusal first. if (error instanceof CliError && error.exitCode() === 1) { if (ctx.issues.length === 0) throw error; return; @@ -244,35 +176,18 @@ function refuseV4Flags(value: Record, ctx: RefinementCtx): void const code = (error as { code?: string }).code; ctx.addIssue({ code: "custom", - // Pointed at the flag the message is about, where the message names one, so the error reads as - // being about that flag rather than about the command. path: [flagFrom(error) ?? "token0"], - /** - * The domain's message, whole. - * - * The framework prefixes the path's flag, so this reads slightly redundantly — - * "invalid --sqrt-price: --sqrt-price is required with --create-pool". Stripping the leading - * flag was tried and is worse: these messages are written to be read standalone, so - * "--token0 and --token1 are the wrong way round" becomes "and --token1 are the wrong way - * round". Redundant beats ungrammatical, and neither of them misleads. - */ + // Keep whole messages: removing a leading flag can break compound subjects. message: messageOf(error), params: { errorCode: code ?? "invalid_value" }, }); } } -/** - * The V4 increase scenario, whose two halves point in opposite directions. - * - * What the position fixes is REFUSED — its range, its holder, its pool. What the position is checked - * AGAINST is REQUIRED — the pair. Neither half is something to ignore: a dropped `--tick-lower` would - * suggest a position's range can be moved, and a dropped `--token0` would remove the only guard - * against adding to a position the caller did not mean. - */ +// The pair cross-check prevents funding a mistyped position ID. function refuseV4IncreaseFlags(value: Record, ctx: RefinementCtx): void { for (const flag of NOT_WITH_V4_POSITION_ID) { - // `--create-pool` is a boolean with a default, so "not given" is `false` rather than undefined. + // An omitted createPool defaults to false. if (value[flag] !== undefined && value[flag] !== false) { ctx.addIssue({ code: "custom", @@ -295,7 +210,6 @@ function refuseV4IncreaseFlags(value: Record, ctx: RefinementCt } } -/** The flag a domain refusal is about, taken from its own message rather than re-derived. */ function flagFrom(error: unknown): string | undefined { const named = /--([a-z0-9-]+)/.exec(messageOf(error))?.[1]; if (named === undefined) return undefined; @@ -306,17 +220,6 @@ function messageOf(error: unknown): string { return error instanceof Error ? error.message : String(error); } -/** - * The flag × scenario matrix. A flag outside its scenario is `invalid_option`, not - * something to ignore: silently dropping `--fee` would deposit at a tier the caller did not - * choose, and silently dropping `--tick-lower` on an increase would suggest a position's range - * can be changed, which it cannot. - * - * V4's flags ARE declared now, so a V2 or V3 deposit that carries one is refused here by name — and - * refused first, because a caller who passed `--tick-spacing` to V3 has the wrong protocol rather - * than a stray flag, and hearing about the flag alone would send them hunting for a V3 setting that - * does not exist. - */ function refuseFlagsOutsideScenario(value: Record, ctx: RefinementCtx): void { const protocol = String(value.protocol).toUpperCase(); if (!PROTOCOLS.includes(protocol as (typeof PROTOCOLS)[number])) { @@ -328,9 +231,7 @@ function refuseFlagsOutsideScenario(value: Record, ctx: Refinem }); return; } - // V4's flags on a V2 or V3 deposit, said before anything else: a caller who passed --tick-spacing - // to V3 has the wrong protocol rather than a stray flag, and hearing about the flag first would - // send them looking for a V3 setting that does not exist. + // Report protocol mismatches before other scenario errors. if (protocol !== "V4") { for (const flag of V4_ONLY) { if (value[flag] !== undefined && value[flag] !== false) { @@ -426,8 +327,7 @@ export const sunswapAddLiquiditySpec: ChainSpec = { "--dry-run validates everything — balances, the pool, the amounts, the allowances — without a\n" + "password, and works for a watch-only account.", baseFields: fields, - // The scenario matrix first, so a flag refused outright is reported as such; then a malformed - // `--recipient` is `invalid_address` at exit 2 rather than an encoder crash at exit 1. + // Reject unsupported flags before validating recipient addresses. baseRefine: allRefines(refuseFlagsOutsideScenario, addressFieldsFor("tron", "recipient")), examples: [ { @@ -476,10 +376,6 @@ export const sunswapAddLiquidityTronBinding = ( ...(input.feeLimit === undefined ? {} : { feeLimit: input.feeLimit }), ...(input.dryRun === undefined ? {} : { dryRun: input.dryRun }), ...(input.buildOnly === undefined ? {} : { buildOnly: input.buildOnly }), - // V4's own. Forwarded explicitly like the rest rather than by spreading `input`, so a flag that - // is declared but never passed on is a visible omission instead of a silent one — which is - // exactly what happened here: the flags parsed, the help advertised them, and the service saw - // none of them until this block named them. ...(input.createPool === undefined ? {} : { createPool: input.createPool }), ...(input.sqrtPrice === undefined ? {} : { sqrtPrice: input.sqrtPrice }), ...(input.tickSpacing === undefined ? {} : { tickSpacing: input.tickSpacing }), diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.test.ts b/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.test.ts index f9250d3a8..63de48682 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.test.ts @@ -1,10 +1,3 @@ -/** - * The flag × scenario matrix, checked where it is decided. - * - * These refusals depend on nothing remote, so they belong to the schema: they fire - * deterministically, before a wallet is opened or a node is asked anything. Exercised through the - * spec's own fields and refine, which is exactly what the shell composes. - */ import { describe, expect, it } from "vitest"; import type { z } from "zod"; import type { ZodObject, ZodRawShape } from "zod"; @@ -39,12 +32,6 @@ describe("sunswap collect-fees — which protocols have claimable fees", () => { expect(schema.safeParse({ protocol, positionId: "1" }).success).toBe(true); }); - /** - * V2 is `invalid_value`, not an unknown protocol. - * - * Its fees are real; they are simply not separable. "Unknown protocol" would send a caller - * looking for a spelling error instead of telling them how a V2 pool works. - */ it("refuses V2 by explaining that its fees are not separable", () => { const parsed = schema.safeParse({ protocol: "V2", positionId: "1" }); expect(refusals({ protocol: "V2", positionId: "1" })).toContainEqual({ @@ -71,12 +58,6 @@ describe("sunswap collect-fees — V4's flag matrix", () => { expect(schema.safeParse({ ...V4, deadline: 1790000000 }).success).toBe(true); }); - /** - * `--recipient` is refused rather than ignored. - * - * A V4 collection settles to the signing account and nowhere else, so a flag that was accepted - * and dropped would tell a caller their money was going somewhere it was not. - */ it("refuses --recipient", () => { expect(refusals({ ...V4, recipient: "TM56HhEWoaw2UevQh86k9AUjJqj9QVvmFC" })).toContainEqual({ path: "recipient", @@ -84,7 +65,6 @@ describe("sunswap collect-fees — V4's flag matrix", () => { }); }); - // One side names no pair at all. The pair is optional; half of it is a mistake. it.each([ ["token0", "token1"], ["token1", "token0"], @@ -118,13 +98,6 @@ describe("sunswap collect-fees — V4's flags elsewhere", () => { }); }); -/** - * A malformed `--recipient` is the caller's typo, not our crash. - * - * Unchecked, it travelled to the ABI encoder and came back as `internal_error` ("Invalid checksum") - * at exit 1 — after the position had already been read. Refused here, it is `invalid_address` at - * exit 2 before anything remote is asked. An EVM address is malformed on TRON for the same reason. - */ describe("sunswap collect-fees — a malformed --recipient", () => { it.each(["TNotAnAddress", "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"])( "refuses %s as invalid_address before any network call", diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.ts b/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.ts index 385c471a4..09def0542 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/collect-fees.ts @@ -5,10 +5,9 @@ import type { SunSwapCollectFeesService } from "../../../../../application/use-c import { TextFormatters } from "../../render/index.js"; import { LEDGER_TRON_SETTINGS_NOTE } from "../shared.js"; -/** Flags V4 adds, and which mean nothing on V3. */ const V4_ONLY = ["token0", "token1", "fee", "deadline"] as const; -/** Flags V4 does not have. A V4 collection settles to the signing account and nowhere else. */ +// V4 settles fees to the signing account only. const NOT_ON_V4 = ["recipient"] as const; const fields = z.object({ @@ -52,18 +51,6 @@ const fields = z.object({ ), }); -/** - * The flag × scenario matrix, and why V2 is absent. - * - * V2 is `invalid_value`, not an unknown protocol: a V2 pool's fees are real; they are simply not - * separable — they accrue into the LP token's own value and come out when the liquidity does. - * Telling a caller "unknown protocol" would suggest they mistyped, when what they need to know is - * that there is nothing to claim. - * - * Every refusal here depends on nothing remote, so it fires deterministically and without a - * wallet. The checks that need the chain — the pair and the tier against what the position - * reports — belong to the use case. - */ function refuseFlagsOutsideScenario(value: Record, ctx: RefinementCtx): void { const protocol = String(value.protocol).toUpperCase(); if (protocol !== "V3" && protocol !== "V4") { @@ -95,13 +82,6 @@ function refuseFlagsOutsideScenario(value: Record, ctx: Refinem refuseV4Flags(value, ctx); } -/** - * V4's own matrix. - * - * The pair is OPTIONAL here, unlike on `remove-liquidity` — it is a cross-check a caller may - * skip, because a collection cannot be sized wrong the way a withdrawal can. What it - * cannot be is half-given: one side names no pair at all, and `--fee` on its own checks nothing. - */ function refuseV4Flags(value: Record, ctx: RefinementCtx): void { for (const flag of NOT_ON_V4) { if (value[flag] !== undefined) { @@ -162,8 +142,7 @@ export const sunswapCollectFeesSpec: ChainSpec = { "On V3, 'sunswap remove-liquidity' already collects the fees alongside the principal, so this\n" + "command is for the case where the principal should stay in the pool.", baseFields: fields, - // The scenario matrix first, so a flag refused outright is reported as such; then a malformed - // `--recipient` is `invalid_address` at exit 2 rather than an encoder crash at exit 1. + // Reject unsupported flags before validating recipient addresses. baseRefine: allRefines(refuseFlagsOutsideScenario, addressFieldsFor("tron", "recipient")), examples: [ { cmd: "wallet-cli sunswap collect-fees --protocol V3 --position-id 686 --dry-run" }, @@ -183,8 +162,6 @@ export const sunswapCollectFeesTronBinding = ( protocol: String(input.protocol).toUpperCase(), positionId: input.positionId, ...(input.recipient === undefined ? {} : { recipient: input.recipient }), - // V4's own. Forwarded explicitly like the rest rather than by spreading `input`, so a flag - // that is declared but never passed on is a visible omission instead of a silent one. ...(input.token0 === undefined ? {} : { token0: input.token0 }), ...(input.token1 === undefined ? {} : { token1: input.token1 }), ...(input.fee === undefined ? {} : { fee: input.fee }), diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/pool-list.ts b/ts/src/adapters/inbound/cli/commands/sunswap/pool-list.ts index 849ca5658..d2cde70dd 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/pool-list.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/pool-list.ts @@ -28,24 +28,13 @@ const fields = z.object({ offset: offsetField, }); -/** - * The service refuses `poolAddress` and `tokenAddress` together, and so must we — but as a usage - * error, not as its error. - * - * Passed through, the refusal arrives as `provider_error`: exit 1, retry "same". That tells a - * caller retrying the identical command might work, when it never can, and blames the service - * for what the command line got wrong. Only this layer knows whose fault it is. - */ +// Reject combined pool and token filters locally; the service cannot apply both. function refuseBothFilters(value: { pool?: string; token?: string }, ctx: RefinementCtx): void { if (value.pool !== undefined && value.token !== undefined) { ctx.addIssue({ code: "custom", - // the path names the offending flag, which the parser puts in front of this message path: ["pool"], message: "cannot be combined with --token; narrow by one or by neither", - // invalid_option is the code for two flags that cannot be given together. Left - // undeclared, a refine arrives as invalid_value, which describes a bad VALUE rather than a - // bad combination of flags that are each fine on their own. params: { errorCode: "invalid_option" }, }); } diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/position-info.test.ts b/ts/src/adapters/inbound/cli/commands/sunswap/position-info.test.ts index 760ab37b1..81e324a03 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/position-info.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/position-info.test.ts @@ -1,9 +1,3 @@ -/** - * What the schema refuses, checked where it is decided. - * - * These refusals reach nothing remote: they fire before a node is asked anything, which is the - * point of putting them in the schema rather than in the use case. - */ import { describe, expect, it } from "vitest"; import { sunswapPositionInfoSpec } from "./position-info.js"; @@ -18,10 +12,6 @@ describe("sunswap position-info — the schema", () => { } }); - /** - * V2, V1, V1_5 and CURVE are not "unsupported" — their positions are not NFTs and have no id at - * all, so there is nothing for `--position-id` to name. Refused here rather than at a contract. - */ it.each(["V2", "V1", "V1_5", "CURVE", "ALL", ""])("refuses --protocol %s", (protocol) => { expect(schema.safeParse({ protocol, positionId: "88" }).success).toBe(false); }); @@ -34,7 +24,6 @@ describe("sunswap position-info — the schema", () => { const huge = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; const parsed = schema.safeParse({ protocol: "V4", positionId: huge }); expect(parsed.success).toBe(true); - // still a string: coercing it to a number would name a different position expect((parsed.data as { positionId: string }).positionId).toBe(huge); }); @@ -52,13 +41,6 @@ describe("sunswap position-info — the spec", () => { expect(sunswapPositionInfoSpec.rejectsAccount).toBeTruthy(); }); - /** - * The gate is the CONTRACTS, not the market API. - * - * The data API is mainnet-only; this implementation reads the chain instead, so it is gated the - * way the other contract-reading commands are and works on Nile too. - * Using `sunswap.market` here would switch it off on every network but mainnet. - */ it("is gated on the liquidity contracts rather than on the market API", () => { expect(sunswapPositionInfoSpec.capability).toBe("sunswap.liquidity"); }); diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/position-info.ts b/ts/src/adapters/inbound/cli/commands/sunswap/position-info.ts index 085577107..4179cd144 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/position-info.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/position-info.ts @@ -3,16 +3,6 @@ import type { ChainSpec, FamilyBinding } from "../../contracts/command.js"; import type { SunSwapPositionInfoService } from "../../../../../application/use-cases/tron/sunswap/position-info-service.js"; import { TextFormatters } from "../../render/index.js"; -/** - * V3 or V4, and nothing else — refused before anything is asked of a node. - * - * V2, V1, V1_5 and CURVE positions are not NFTs and have no id, so there is no "unsupported - * protocol" here to fall through to at runtime: naming one is a mistake about what a position id - * IS, and the message says so rather than listing four more values that would never work. - * - * Case is normalised because `v4` is what a person types and the value is an enum, not an - * identifier — but the accepted set is still exactly two. - */ const protocolField = z .string() .transform((value) => value.trim().toUpperCase()) @@ -24,13 +14,7 @@ const protocolField = z ) .describe("protocol version: V3 or V4"); -/** - * The NFT id, as a STRING. - * - * Not `z.coerce.number()`: an id is an unbounded uint256, and a number would round one past fifteen - * digits into a different position. What is checked is the shape — digits only, no sign, no point — - * so a malformed id is a usage error at parse time rather than a revert a caller pays for. - */ +// Keep uint256 IDs as strings to avoid rounding to another position. const positionIdField = z .string() .refine((value) => /^\d+$/.test(value.trim()), { @@ -48,14 +32,7 @@ export const sunswapPositionInfoSpec: ChainSpec = { network: "optional", wallet: "none", auth: "none", - /** - * The CONTRACTS, not the market API. - * - * The data API is mainnet-only. This implementation reads the position manager and the pool - * instead, and those exist on every network the liquidity commands - * already work on — so the gate is the same one they use, and the USD fields are absent where - * there is no price source rather than the whole command being refused. - */ + // Gate on contracts: position reads do not require the mainnet market API. capability: "sunswap.liquidity", rejectsAccount: "position-info looks up a position by its id and reports whoever holds it, so it is not about any account of yours", diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/position-list.ts b/ts/src/adapters/inbound/cli/commands/sunswap/position-list.ts index 62d10ea01..ab02efa25 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/position-list.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/position-list.ts @@ -19,8 +19,6 @@ const fields = z.object({ export const sunswapPositionListSpec: ChainSpec = { path: ["sunswap", "position-list"], network: "optional", - // Read-only and address-scoped, like `account balance`: the active account by default, and - // `--account` takes a label, an account id, or any TRON address. wallet: "optional", auth: "none", capability: "sunswap.market", diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/price.ts b/ts/src/adapters/inbound/cli/commands/sunswap/price.ts index d32fd0885..47ba02d7d 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/price.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/price.ts @@ -3,13 +3,7 @@ import type { ChainSpec, FamilyBinding } from "../../contracts/command.js"; import type { SunSwapMarketQueryService } from "../../../../../application/use-cases/tron/sunswap/market-query-service.js"; import { TextFormatters } from "../../render/index.js"; -/** - * One schema drives arity, validation, help and `--json-schema`. - * - * The two inputs are deliberately not merged into one "token or address" field. Which one the - * caller meant decides whether a local lookup happens at all, and giving both is an error rather - * than a silent preference. - */ +// Symbol inputs use the token book; explicit addresses bypass it. const fields = z.object({ token: z .string() @@ -50,13 +44,7 @@ export const sunswapPriceSpec: ChainSpec = { formatText: TextFormatters.sunswapPrice, }; -/** - * The binding decides what the envelope publishes, and it is not the whole service result. - * - * `warnings` goes through `ctx.warn`, which is the only route to `meta.warnings` — the place a - * caller reads. Returning it inside the payload instead would put - * the notice somewhere nobody looks, which is how the degraded symbol lookup went silent. - */ +// Route warnings through ctx.warn so they reach envelope metadata. export const sunswapPriceTronBinding = (service: SunSwapMarketQueryService): FamilyBinding => ({ run: async (ctx, net, input) => { const view = await service.prices(net, { @@ -64,8 +52,7 @@ export const sunswapPriceTronBinding = (service: SunSwapMarketQueryService): Fam ...(input.address === undefined ? {} : { addresses: input.address.split(",") }), }); for (const warning of view.warnings) ctx.warn(warning); - // `view` is stripped before the JSON envelope is written: the symbol stays out of json - // because a symbol is self-reported and an agent must key on the address instead. + // Symbols are display-only; JSON identifies tokens by address. return { prices: view.prices, view: { symbols: Object.fromEntries(view.symbols) } }; }, }); diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.test.ts b/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.test.ts index 1054bb244..47bd1fd49 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.test.ts @@ -1,10 +1,3 @@ -/** - * The flag × scenario matrix, checked where it is decided. - * - * These refusals depend on nothing remote, so they belong to the schema: they fire deterministically, - * before a wallet is opened or a node is asked anything. Exercised through the spec's own fields and - * refine, which is exactly what the shell composes. - */ import { describe, expect, it } from "vitest"; import type { z } from "zod"; import type { ZodObject, ZodRawShape } from "zod"; @@ -44,19 +37,11 @@ describe("sunswap remove-liquidity — V4's flag matrix", () => { expect(schema.safeParse(V4).success).toBe(true); }); - // V4 is the one scenario that needs both, because the pair is a cross-check rather than a - // selector. A missing side is a missing option, not something to infer from the position. it.each(["token0", "token1", "positionId"])("requires --%s", (flag) => { const rest = Object.fromEntries(Object.entries(V4).filter(([key]) => key !== flag)); expect(refusals(rest)).toContainEqual({ path: flag, code: "missing_option" }); }); - /** - * `--recipient` is refused rather than ignored. - * - * A V4 withdrawal settles to the signing account and nowhere else, so a flag that was accepted and - * dropped would tell a caller their money was going somewhere it was not. - */ it("refuses --recipient", () => { expect(refusals({ ...V4, recipient: "TNmoJ3Be59WFEq5dsW6eCkZjveiL3G8HVB" })).toContainEqual({ path: "recipient", @@ -71,11 +56,6 @@ describe("sunswap remove-liquidity — V4's flag matrix", () => { expect(schema.safeParse({ ...V4, slippage: "0.005" }).success).toBe(true); }); - /** - * They COMBINE: --slippage is a tolerance that lowers --min0/--min1 further. Refusing the pair - * would refuse the one shape a careful caller wants — a floor they chose, with a little room - * under it. - */ it("accepts --slippage together with an explicit minimum", () => { expect(refusals({ ...V4, slippage: "0.005", min0: "1" })).toEqual([]); }); @@ -102,18 +82,10 @@ describe("sunswap remove-liquidity — V4's flags elsewhere", () => { path: "protocol", code: "invalid_value", }); - // Lower case too: the binding upper-cases it before the service ever sees it. expect(schema.safeParse({ ...V4, protocol: "v4" }).success).toBe(true); }); }); -/** - * A malformed `--recipient` is the caller's typo, not our crash. - * - * Unchecked, it travelled to the ABI encoder and came back as `internal_error` ("Invalid checksum") - * at exit 1 — after the position had already been read. Refused here, it is `invalid_address` at - * exit 2 before anything remote is asked. An EVM address is malformed on TRON for the same reason. - */ describe("sunswap remove-liquidity — a malformed --recipient", () => { it.each(["TNotAnAddress", "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"])( "refuses %s as invalid_address before any network call", diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.ts b/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.ts index 276a8186b..5c5c16b21 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/remove-liquidity.ts @@ -7,22 +7,9 @@ import { LEDGER_TRON_SETTINGS_NOTE } from "../shared.js"; const PROTOCOLS = ["V2", "V3", "V4"] as const; -/** - * Flags that belong only to V4. - * - * `--fee` is a cross-check on V4 rather than a selector, and `--slippage` is a tolerance on the - * FLOOR. Neither means anything on V2 or V3, where the tier comes from the pair or the position and - * the floor is set directly with `--min0` / `--min1`. - */ const V4_ONLY = ["fee", "slippage"] as const; -/** - * Flags V4 does not have. - * - * `--recipient` is absent by design: a V4 withdrawal settles to the signing account and - * nowhere else. Accepting the flag and ignoring it would send someone's money to the wrong place - * while telling them otherwise. - */ +// V4 settles withdrawals to the signing account only. const NOT_ON_V4 = ["recipient"] as const; const fields = z.object({ @@ -35,8 +22,6 @@ const fields = z.object({ .default(false) .describe("emit the unsigned transactions in execution order without signing them"), protocol: z.string().describe("protocol version: V2, V3 or V4"), - // One flag, two meanings, and the difference is invisible in the value itself — so it is - // spelled out here rather than left to a reader to infer from the protocol they chose. liquidity: z .string() .describe( @@ -90,16 +75,6 @@ const fields = z.object({ ), }); -/** - * The flag × scenario matrix. - * - * A V2 withdrawal names the pair; a V3 one names the position, which already knows its pair, so - * accepting both would let a caller believe the two could disagree. V4 is the exception and - * deliberately so: it requires BOTH, because there the pair is not a selector but a cross-check the - * use case makes against what the position reports. - * - * Every refusal here depends on nothing remote, so it fires deterministically and without a wallet. - */ function refuseFlagsOutsideScenario(value: Record, ctx: RefinementCtx): void { const protocol = String(value.protocol).toUpperCase(); if (!PROTOCOLS.includes(protocol as (typeof PROTOCOLS)[number])) { @@ -111,8 +86,7 @@ function refuseFlagsOutsideScenario(value: Record, ctx: Refinem }); return; } - // V4's own flags on a V2 or V3 withdrawal, said before anything else: a caller who passed - // --slippage to V3 has the wrong protocol rather than a stray flag. + // Report protocol mismatches before other scenario errors. if (protocol !== "V4") { for (const flag of V4_ONLY) { if (value[flag] !== undefined) { @@ -170,14 +144,7 @@ function refuseFlagsOutsideScenario(value: Record, ctx: Refinem } } -/** - * V4's own matrix. - * - * Both the position id AND the pair are required, which is the one place this command asks for more - * than V3 does. The pair does not choose the pool — the position does — so it is checked against - * what the position reports and a disagreement is refused. That check needs the chain and therefore - * lives in the use case; what lives here is that both were given at all. - */ +// Require the pair for an on-chain cross-check against the selected position. function refuseV4Flags(value: Record, ctx: RefinementCtx): void { for (const flag of NOT_ON_V4) { if (value[flag] !== undefined) { @@ -243,8 +210,7 @@ export const sunswapRemoveLiquiditySpec: ChainSpec = { "--dry-run validates everything — the position, the balance, the amounts — without a\n" + "password, and works for a watch-only account.", baseFields: fields, - // The scenario matrix first, so a flag refused outright is reported as such; then a malformed - // `--recipient` is `invalid_address` at exit 2 rather than an encoder crash at exit 1. + // Reject unsupported flags before validating recipient addresses. baseRefine: allRefines(refuseFlagsOutsideScenario, addressFieldsFor("tron", "recipient")), examples: [ { @@ -274,8 +240,6 @@ export const sunswapRemoveLiquidityTronBinding = ( ...(input.min1 === undefined ? {} : { min1: input.min1 }), ...(input.recipient === undefined ? {} : { recipient: input.recipient }), ...(input.deadline === undefined ? {} : { deadline: input.deadline }), - // V4's own. Forwarded explicitly like the rest rather than by spreading `input`, so a flag - // that is declared but never passed on is a visible omission instead of a silent one. ...(input.fee === undefined ? {} : { fee: input.fee }), ...(input.slippage === undefined ? {} : { slippage: input.slippage }), ...(input.feeLimit === undefined ? {} : { feeLimit: input.feeLimit }), diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/shared.ts b/ts/src/adapters/inbound/cli/commands/sunswap/shared.ts index 11eac3cc4..9ce713db5 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/shared.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/shared.ts @@ -1,15 +1,3 @@ -/** - * Zod fragments shared by the SunSwap listing commands. - * - * Kept in one place so the paging and scope flags cannot drift apart between commands: an agent - * that learned `--limit` on one listing should not find a different default on the next. - * - * Note what is deliberately ABSENT. There is no `--include-blacklisted`: pool and token records - * carry no blacklist marker, so listing them "without marking" would put impersonations into the - * table unlabelled, and the service's filter is always left on. And `token-list` has no `--sort`, - * because the token endpoints expose a sort field but no direction — offering a flag the service - * ignores would be a promise the output does not keep. - */ import { z } from "zod"; import { SUNSWAP_PROTOCOL_FILTERS, @@ -18,10 +6,7 @@ import { const SCOPE_LIST = SUNSWAP_SCOPES.join(", "); -/** - * The scope is always sent, never left to the service's default: omitting it makes the - * service mix statistic scopes and return the same token more than once. - */ +// Always send the scope; omitting it can mix scopes and duplicate tokens. export const protocolField = z.string().default("ALL").describe(`protocol scope: ${SCOPE_LIST}`); export const limitField = z.coerce @@ -31,14 +16,7 @@ export const limitField = z.coerce export const offsetField = z.coerce.number().default(0).describe("number of rows to skip"); -/** - * `--protocol` where it FILTERS rows rather than choosing a statistic scope. - * - * `ALL` is not offered: "all of them" is spelled by omitting the flag, and there is no such - * protocol for a pool or a position to belong to. The list is generated from the same constant - * the validator checks against, so help, `--json-schema` and the runtime cannot drift apart — - * which is exactly how `ALL` came to be accepted here while the schema said it was not a value. - */ +// Omit the filter for all protocols; ALL is only a statistic scope. export const protocolFilterField = z .string() .optional() diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/swap.test.ts b/ts/src/adapters/inbound/cli/commands/sunswap/swap.test.ts index 682484b3a..039a35e1b 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/swap.test.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/swap.test.ts @@ -2,10 +2,6 @@ import { describe, expect, it } from "vitest"; import { sunswapSwapSpec } from "./swap.js"; describe("sunswap swap --quote", () => { - // A quote submits nothing, so there is no confirmation to wait for — the same rule - // `tx send` applies to `--dry-run --wait`. `--wait` and `--wait-timeout` are global, so the - // shell refuses them from the spec before the binding runs; positional-contract.test.ts drives - // that refusal end to end for every command that declares it. it("declares --wait and --wait-timeout refused under --quote", () => { expect(sunswapSwapSpec.rejectsWaitWith).toBe("quote"); }); diff --git a/ts/src/adapters/inbound/cli/commands/sunswap/swap.ts b/ts/src/adapters/inbound/cli/commands/sunswap/swap.ts index 8fc9642ba..93cf7f021 100644 --- a/ts/src/adapters/inbound/cli/commands/sunswap/swap.ts +++ b/ts/src/adapters/inbound/cli/commands/sunswap/swap.ts @@ -36,11 +36,6 @@ const fields = z.object({ ), }); -/** - * `--quote` sends no transaction, so it excludes everything that describes one — including - * `--slippage`, since a tolerance only means something when a floor will be enforced. `--all` - * lists candidate routes, which only a quote has. - */ function refuseFlagsOutsideMode(value: Record, ctx: RefinementCtx): void { const sending = ["dryRun", "buildOnly"] as const; if (value.quote === true) { @@ -78,16 +73,12 @@ function refuseFlagsOutsideMode(value: Record, ctx: RefinementC export const sunswapSwapSpec: ChainSpec = { path: ["sunswap", "swap"], network: "optional", - // `none` because `--quote` must work with no account at all; the use case resolves - // the account itself on the paths that sign. An account is needed for every other mode. + // Quotes need no account; other modes resolve it in the use case. wallet: "none", auth: "conditional", - // A quote sends nothing, so there is no confirmation for --wait or --wait-timeout to wait on. rejectsWaitWith: "quote", broadcasts: true, - // Its own key, not sunpump's: this command needs a launchpad address OR a route service, and - // `swap` must not be switched on merely because SunPump is configured — nor hidden on a network - // that has a router but no curve. + // Swap availability depends on a curve or router, not SunPump configuration alone. capability: "sunswap.swap", summary: "Swap tokens through SunSwap or a SunPump curve", description: diff --git a/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts b/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts index 7471a1c2e..3fdf081ba 100644 --- a/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts +++ b/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts @@ -17,9 +17,7 @@ import { registerContactCommands } from "./contact.js"; import { registerAddressCommands } from "./address.js"; import { registerEncodingCommands } from "./encoding.js"; -// A chain command always has a resolved network by the time its formatter runs, so the default -// carries one. renderFamily() now refuses to guess (it used to silently default to tron, which -// would render wei as TRX), and a fixture without `net` would not represent any real invocation. +// Chain-command formatters require a resolved network to interpret units. const ctx = (over: Partial = {}): TextRenderContext => ({ command: "x", net: { id: "tron:3448148188", family: "tron", nativeSymbol: "TRX" } as never, @@ -48,9 +46,6 @@ describe("text formatters", () => { expect(missing).toEqual([]); }); - // Help renders a field's description for both flags and positionals (help/index.ts Args + Flags). - // A field without one prints a bare name and leaves the reader — often an agent — guessing what - // to pass. Registry-wide so a new command cannot quietly ship undocumented inputs. it("every registered command field carries a help description", () => { const registry = new CommandRegistry(); registerWalletCommands(registry, {} as Parameters[1]); @@ -118,8 +113,6 @@ describe("permissionShow formatter", () => { ], } as any; - // operationsHex stays in json — it is a machine value, and the human column already - // carries the decoded operation labels next to it. it("does not print the operations bitmap in the text card", () => { const out = TextFormatters.permissionShow(view, ctx()) as string; expect(out).not.toContain("Operations Hex"); @@ -141,8 +134,6 @@ describe("accountBalance formatter", () => { { address: "TXaddress", balance: "1983993000", decimals: 6, symbol: "TRX" }, ctx(), ); - // Grouped, like every other amount the CLI prints — this now goes through the shared - // formatter rather than a bare unit conversion. expect(out).toContain("1,983.993 TRX"); expect(out).not.toContain("sun"); }); @@ -162,9 +153,6 @@ describe("accountBalance formatter", () => { expect(out).not.toContain("TXaddress"); }); - // An 18-decimal balance printed in full is 18 fractional digits of noise around the two or - // three that carry meaning. json keeps every one of them; text is for a person reading a - // terminal, so it obeys the same display rule as every other amount the CLI prints. it("caps an 18-decimal balance at six fractional digits", () => { const out = TextFormatters.accountBalance( { address: "0xabc", balance: "12345678901234567890", decimals: 18, symbol: "ETH" }, @@ -244,7 +232,6 @@ describe("stakeInfo unfreezing list", () => { expect(branch).toHaveLength(2); expect(branch[0]).toContain("├─ 500 TRX withdrawable"); expect(branch[1]).toContain("└─ 300 TRX withdrawable"); - // no legacy " 1) "/" 2) " line-leading numbering survives expect(out).not.toMatch(/^\s*\d+\)\s/m); }); @@ -445,7 +432,7 @@ describe("txReceipt formatter (typed kind, narrowed — no command-id matching)" 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 + expect(out).toContain("covered by staked energy"); }); it("dry-run energy estimate with insufficient available energy: no 'covered' note", () => { const out = TextFormatters.txReceipt( @@ -463,9 +450,6 @@ describe("txReceipt formatter (typed kind, narrowed — no command-id matching)" expect(out).toContain("29,650 energy"); expect(out).not.toContain("covered by staked energy"); }); - // `account activate` estimates its fee from two chain parameters, so its fee object carries - // neither `feeSun` nor `energy`. Every such shape must still render as TRX — and no fee shape, - // known or not, may ever reach the user as a stringified object. const activateFee = { feeModel: "tron-resource", createAccountFeeSun: "100000", @@ -489,15 +473,13 @@ describe("txReceipt formatter (typed kind, narrowed — no command-id matching)" it("account activate dry-run: renders the total creation fee, not [object Object]", () => { const out = dryRun(activateFee); expect(out).not.toContain("[object Object]"); - // The confirmed receipt shows `Fee 1.1 TRX`; dry-run must be comparable expect(out).toContain("1.1 TRX"); }); it.each([ ["createAccountFeeSun alone", { minimumFeeSun: "100000" }, "0.1 TRX"], ["a zero fee", { minimumFeeSun: "0" }, "0 TRX"], - // fees use fromBaseUnits (exact decimal, no thousands separators) like every other Fee row - ["a large fee", { minimumFeeSun: "9000000000" }, "9,000 TRX"], // grouped + ["a large fee", { minimumFeeSun: "9000000000" }, "9,000 TRX"], ])("account activate dry-run: %s", (_name, fee, expected) => { expect(dryRun(fee)).toContain(expected); }); @@ -506,8 +488,6 @@ describe("txReceipt formatter (typed kind, narrowed — no command-id matching)" expect(dryRun({ feeSun: "2000000", minimumFeeSun: "1100000" })).toContain("2 TRX"); }); - // The regression net: every fee shape the renderer already understood must keep working, so - // adding a branch cannot silently reorder or shadow an existing one. it.each([ ["feeSun", { feeSun: "268000" }, "0.268 TRX"], ["bandwidth burn", { bandwidthBurnSunIfNoFreeze: "1000000" }, "1 TRX"], @@ -517,9 +497,6 @@ describe("txReceipt formatter (typed kind, narrowed — no command-id matching)" expect(dryRun(fee)).toContain(expected); }); - // The root cause of the activate bug: an unrecognized fee object fell through to a fallback that - // stringified it. Failing honestly is required; leaking "[object Object]" is not acceptable for - // any shape, including ones added later. it.each([ ["an unrecognized fee object", { someFutureFeeModel: "42" }], ["an empty fee object", {}], @@ -530,10 +507,6 @@ describe("txReceipt formatter (typed kind, narrowed — no command-id matching)" expect(out).toContain("unknown"); }); - // `tx broadcast` carries the full approval view in json, but text projected none of it: the - // dry-run receipt was a bare header plus a fee line, and the fee was printed twice whenever the - // multi-sign fee was non-zero (receiptRows pushed one row, the dry-run branch another). A sample - // fee of 0 hides the duplicate because 0 is falsy. const broadcastApproval = { txId: "abc123", contractType: "TransferContract", @@ -661,9 +634,6 @@ describe("local multisig formatters", () => { expect(out).toContain("Tsigner"); }); - // The default `tx sign` receipt is the action block plus the same transaction and - // signature-progress block `tx approvals` prints — permission group name and threshold, a - // Progress line, and the per-signer weight table. The offline receipt cannot carry any of it. it("prints the documented progress block on the default (checked) sign receipt", () => { const out = TextFormatters.txSign({ kind: "tx-sign", @@ -682,16 +652,14 @@ describe("local multisig formatters", () => { approval, }) as string; expect(out).toContain("Signature added"); - expect(out).toContain("Tsigner (weight 1)"); // signer weight on the action block + expect(out).toContain("Tsigner (weight 1)"); expect(out).toContain('Permission active "operations" (id 2) threshold 2'); expect(out).toContain("Progress 1 / 2"); - expect(out).toContain("Approved signer"); // weight table header + expect(out).toContain("Approved signer"); expect(out).not.toContain("local inspection"); expect(out).not.toContain("was not checked online"); }); - // Text gets the human operation name; the machine-readable contractType enum stays in - // json. Printing both put a machine value in a human column. it("prints the human operation name without the contract-type enum", () => { const out = TextFormatters.txApprovals({ ...approval, operation: "Transfer TRX" }) as string; expect(out).toContain("Transfer TRX"); @@ -876,8 +844,6 @@ describe("accountInfo staking summary", () => { ); it("preserves staking amounts above Number.MAX_SAFE_INTEGER when supplied as strings", () => { - // grouped; the point of this test is that the fraction survives intact past - // Number.MAX_SAFE_INTEGER, which it still does. expect(accountInfo("9007199254740993")).toContain("9,007,199,254.740993 TRX"); }); @@ -943,9 +909,6 @@ describe("sign-only receipt", () => { net: { family: "tron", nativeSymbol: "TRX", id: "nile" }, } as never; - // The signature is the product of a signing command and has to be copied somewhere, so it must - // never be shortened. Before this it showed a truncated txID — redundant with the TxID row and - // useless as output. it("prints the signature in full", () => { const sig = "16a2ec10".repeat(16) + "1C"; const out = TextFormatters.txReceipt( @@ -966,8 +929,6 @@ describe("sign-only receipt", () => { expect(out).toContain("Signature 2"); }); - // tx sign estimates nothing, so there is no fee to report and the row is dropped entirely - // rather than rendered as "unknown". it("omits the fee row when nothing was estimated", () => { const out = TextFormatters.txReceipt( { ...base, signed: { signature: ["aa".repeat(65)] } }, @@ -977,8 +938,6 @@ describe("sign-only receipt", () => { }); }); -// `config networks` used to be a list of ids (an array, which rendered fine). It is now a map of -// id -> endpoint, and `aliases` is a map too — both printed as "[object Object]" until this. describe("config renders map-valued keys", () => { it("renders a single-key read as a titled block", () => { const out = TextFormatters.config({ @@ -986,17 +945,12 @@ describe("config renders map-valued keys", () => { value: { nile: "tron:3448148188", sepolia: "eip155:11155111" }, }); - // `titled` is the house shape: bare title line, then indented fields (no colon) — see - // asset.ts / exchange.ts / governance.ts for the same form. expect(out.split("\n")[0]).toBe("aliases"); expect(out).toMatch(/^ {2}nile\s+tron:3448148188$/m); expect(out).toMatch(/^ {2}sepolia\s+eip155:11155111$/m); expect(out).not.toContain("[object Object]"); }); - // The whole-config view used to SUMMARISE a map by listing its keys ("networks tron:3448148188, - // eip155:1"), which said a network existed but never what it was configured with. The revised shape: - // config renders every configurable value, nested — the file's own shape, indented. it("expands map-valued keys in the whole-config view", () => { const out = TextFormatters.config({ defaultOutput: "text", @@ -1006,8 +960,6 @@ describe("config renders map-valued keys", () => { }, }); - // No trailing colon: a network id already contains one, so `tron:3448148188:` would hide where the - // id ends — and the id is what a reader copies into `--network` / `config networks.`. expect(out).toMatch(/^networks$/m); expect(out).toMatch(/^ {2}tron:3448148188$/m); expect(out).toMatch(/^ {4}httpEndpoint {2}nile\.trongrid\.io$/m); @@ -1015,7 +967,6 @@ describe("config renders map-valued keys", () => { expect(out).not.toContain("[object Object]"); }); - // Two levels deep, under a named read: the block below a network is its fields, indented once. it("renders a single network read as a nested block", () => { const out = TextFormatters.config({ key: "networks.tron:3448148188", @@ -1031,7 +982,6 @@ describe("config renders map-valued keys", () => { expect(out).toMatch(/^ {2}apiKey {8}\*{8}$/m); }); - // A scalar leaf keeps its one-line form; nesting must not swallow the simple case. it("keeps a scalar read on one line", () => { expect(TextFormatters.config({ key: "timeoutMs", value: 60_000 })).toMatch( /^timeoutMs {2}60000$/, @@ -1039,8 +989,6 @@ describe("config renders map-valued keys", () => { }); }); -// A distinction the renderer previously did not draw: a VALUATION gets 2 decimals, a UNIT -// PRICE gets 4. This column had no coverage at all, so the two were silently the same. describe("portfolio price vs valuation precision", () => { const portfolio = (priceUsd: string, valueUsd: string) => TextFormatters.accountPortfolio( @@ -1064,8 +1012,6 @@ describe("portfolio price vs valuation precision", () => { expect(portfolio("0.0001", "0.10")).toContain("$0.0001"); }); - // The Balance column reads the base-unit integer and the row's own decimals, so it obeys the - // same six-digit rule as `account balance`. It used to print the pre-scaled string verbatim. it("caps the balance column at six fractional digits", () => { const out = TextFormatters.accountPortfolio( { @@ -1089,8 +1035,6 @@ describe("portfolio price vs valuation precision", () => { }); }); -// The address already says which chain it is (T… / 0x…), so a Family column repeats it in -// vocabulary the user never needs otherwise. Externally the book is a flat name↔address map. describe("contact list is a flat name-to-address map", () => { const listed = () => TextFormatters.contactList({ @@ -1111,8 +1055,6 @@ describe("contact list is a flat name-to-address map", () => { }); }); -// The address column follows the SELECTED NETWORK's family. text never puts both families -// side by side — the table doubles in width and the user only cares about the chain in use. describe("list shows one family's addresses at a time", () => { const accounts = [ { @@ -1159,8 +1101,6 @@ describe("list shows one family's addresses at a time", () => { expect(out).not.toContain("TSRmq8kP9dEf"); }); - // A single-family account has nothing to show on the other family's network, and an empty row - // is worse than no row. it("hides single-family accounts that do not belong to the selected network", () => { expect(listed("tron")).not.toContain("ledger-evm"); expect(listed("evm")).not.toContain("team-vault"); @@ -1172,9 +1112,6 @@ describe("list shows one family's addresses at a time", () => { }); }); -// `--keystore` picks ONE of a seed account's two keys, and with --network omitted that choice -// comes from config.defaultNetwork. The receipt has to say which key was written, or the same -// command on two machines silently produces different secrets with nothing to tell them apart. describe("keystore receipt names the exported family", () => { const receipt = (extra: Record) => TextFormatters.walletBackup({ @@ -1191,8 +1128,6 @@ describe("keystore receipt names the exported family", () => { expect(receipt({ family: "evm" })).toMatch(/^\s*Family\s+evm$/m); }); - // A mnemonic covers every family, so there is nothing to disambiguate and a row would imply - // a choice that was never made. it("omits the row for a native backup", () => { const out = TextFormatters.walletBackup({ accountId: "wlt_a.0", diff --git a/ts/src/adapters/inbound/cli/commands/token-selector.ts b/ts/src/adapters/inbound/cli/commands/token-selector.ts index 45522a826..bd2e107af 100644 --- a/ts/src/adapters/inbound/cli/commands/token-selector.ts +++ b/ts/src/adapters/inbound/cli/commands/token-selector.ts @@ -1,6 +1,5 @@ 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, diff --git a/ts/src/adapters/inbound/cli/commands/token.ts b/ts/src/adapters/inbound/cli/commands/token.ts index c75c669ec..0207b8540 100644 --- a/ts/src/adapters/inbound/cli/commands/token.ts +++ b/ts/src/adapters/inbound/cli/commands/token.ts @@ -7,15 +7,11 @@ import { tokenSelector } from "./token-selector.js"; import type { TokenBookService } from "../../../../application/use-cases/token-book-service.js"; import type { EvmTokenService } from "../../../../application/use-cases/evm/token-service.js"; -/** Shared across families: every family has a token contract. TRC10 does not exist outside TRON, - * so `--asset-id` — and the "exactly one selector" rule it is half of — belong to the TRON - * binding below, not here. */ +// TRC10 selectors belong to the TRON binding; only contract addresses are shared. const selectorFields = z.object({ contract: Schemas.address().optional().describe("token contract address"), }); -/** the EVM half: no TRC10 equivalent exists, so `--contract` is simply required, and the shared - * neutral field is validated as an EVM address here. */ const evmSelector = { refine: allRefines( (v: { contract?: string }, ctx: z.RefinementCtx) => { @@ -27,7 +23,6 @@ const evmSelector = { ), }; -/** the TRON half of the selector: TRC10 asset id + the XOR rule + TRON address format. */ const tronSelector = { fields: z.object({ assetId: z @@ -106,8 +101,6 @@ export const tokenAddSpec: ChainSpec = { auth: "none", capability: "token.tokenbook", summary: "Add a token to the address book", - // The book is PER NETWORK, and the metadata comes from the contract — the one moment - // decimals are checked against the chain (see the token service). description: "Add a token to the address book of the selected network, fetching its name,\n" + "symbol and decimals from the contract", @@ -131,7 +124,6 @@ export const tokenListSpec: ChainSpec = { auth: "none", capability: "token.tokenbook", summary: "List the address book", - // Which book depends on the network, and it holds two layers. description: "List the address book of the selected network (official + user entries)", baseFields: z.object({}), examples: [ @@ -141,7 +133,6 @@ export const tokenListSpec: ChainSpec = { formatText: TextFormatters.tokenBookList, }; -/** Shared by every family: listing merges the book's two layers and touches no chain. */ export const tokenListBinding = (svc: TokenBookService): FamilyBinding => ({ run: async (ctx, net) => svc.list(ctx, net), }); @@ -153,7 +144,6 @@ export const tokenRemoveSpec: ChainSpec = { auth: "none", capability: "token.tokenbook", summary: "Remove a user-added token", - // The refusal on an official entry is a rule worth stating before it is hit. description: "Remove a user-added token from the address book. Official entries cannot be\n" + "removed.", baseFields: selectorFields, diff --git a/ts/src/adapters/inbound/cli/commands/transaction-options.test.ts b/ts/src/adapters/inbound/cli/commands/transaction-options.test.ts index d4b2f68a7..0fcf07aa0 100644 --- a/ts/src/adapters/inbound/cli/commands/transaction-options.test.ts +++ b/ts/src/adapters/inbound/cli/commands/transaction-options.test.ts @@ -23,8 +23,6 @@ import { voteCastSpec } from "./vote.js"; import type { ChainSpec } from "../contracts/index.js"; import { governanceTxModeFields, tronTxModeFields, txModeFields } from "./shared.js"; -// --permission-id and --expiration are TRON multi-signature concepts and live on the TRON -// binding's field set; these assertions are about their wording, which did not move. const txModeAndTron = { ...txModeFields, ...tronTxModeFields }; describe("transaction option argv coercion", () => { @@ -52,9 +50,6 @@ describe("transaction option argv coercion", () => { }); }); -// These two flags exist only for multi-sign, and their help used to state neither what the values -// mean nor what the limits are. A co-signer reading `--help` could not tell which permission group -// to pass, nor that the collection window they were extending is capped at 24h. describe("shared --permission-id / --expiration document their semantics", () => { const describeOf = (name: keyof typeof txModeAndTron): string => (txModeAndTron[name] as { description?: string }).description ?? ""; @@ -70,7 +65,6 @@ describe("shared --permission-id / --expiration document their semantics", () => const schema = z.object(txModeAndTron); expect(schema.safeParse({ buildOnly: true, expiration: 86_400_000 }).success).toBe(true); expect(schema.safeParse({ buildOnly: true, expiration: 86_400_001 }).success).toBe(false); - // the description must quote that same bound — a stale number here is worse than none expect(describeOf("expiration")).toContain("86400000"); expect(describeOf("expiration")).toContain("24h"); }); @@ -79,17 +73,12 @@ describe("shared --permission-id / --expiration document their semantics", () => expect(describeOf("expiration")).toMatch(/node default.*60s/); }); - // --build-only produces an artifact nobody can use without knowing what consumes it. Both - // multi-sig routes start here — the hex relay through `tx sign` and the TronLink queue through - // `tx multisig` (docs/commands/tx/index.md) — so naming only one of them would be worse than - // naming neither: it reads as "this flag is for the service path". it("says what the unsigned hex is for, and does not narrow it to one route", () => { const text = describeOf("buildOnly"); expect(text).toMatch(/tx sign/); expect(text).toMatch(/tx multisig/); }); - // permission update carries its own copies of these fields; two copies of a description drift. it("permission update shows the same text, not a stale copy", () => { const shape = permissionUpdateSpec.baseFields.shape as Record; expect(shape.permissionId?.description).toBe(describeOf("permissionId")); @@ -98,10 +87,6 @@ describe("shared --permission-id / --expiration document their semantics", () => }); }); -// `txModeFields` is shared, so adding a flag to it silently widens 14 commands at once while their -// individual reference pages stay behind — which is exactly how --build-only / --permission-id / -// --expiration ended up documented on only 3 of the 14. A page that documents the transaction modes -// at all must document all of them. describe("reference pages keep up with the shared transaction options", () => { const DOCS = fileURLToPath(new URL("../../../../../docs/commands/", import.meta.url)); const REQUIRED = ["--build-only", "--permission-id", "--expiration"]; @@ -116,11 +101,7 @@ describe("reference pages keep up with the shared transaction options", () => { ); } - // Presence is not enough. These two rows carried the pre-A-2/A-3 wording — a permission id with - // no key to its values, and a range with no readable bound — so a reader who went to the - // reference page instead of `--help` still could not tell which group to pass. it("every --permission-id row carries the same value key the flag's help does", () => { - // taken from the schema, not restated here: one wording, two surfaces const key = /\((0=owner[^)]*)\)/.exec( (tronTxModeFields.permissionId as { description?: string }).description ?? "", )?.[1]; @@ -158,8 +139,7 @@ describe("reference pages keep up with the shared transaction options", () => { it("every page offering --sign-only also documents the rest of the shared modes", () => { const behind = pages(DOCS) .map((path) => ({ path, text: readFileSync(path, "utf8") })) - // An Options-table ROW for --sign-only means the command itself carries the shared object; - // prose mentions (tx/index, tx/sign) merely refer to other commands and must not be flagged. + // Match option rows; prose may refer to flags on other commands. .filter(({ text }) => /^\| `--sign-only`/m.test(text)) .filter(({ text }) => REQUIRED.some((flag) => !text.includes(flag))) .map(({ path }) => relative(DOCS, path)); @@ -168,21 +148,11 @@ describe("reference pages keep up with the shared transaction options", () => { }); }); -/** - * The governance group re-declared `--permission-id` with an int32 ceiling, so `--help` advertised a - * range the application layer then refused: `transactionMode()` accepts 0..9 and rejects anything - * above it with `invalid_option`, after the command has already started. TRON has at most eight - * active permissions (ids 2..9) plus owner (0), so 0..9 is the real bound and the override was - * simply wrong — it also downgraded the failure from a schema `invalid_value` to a runtime - * `invalid_option`, classifying the same mistake differently from every non-governance command. - */ describe("governance --permission-id shares the protocol bound with every other command", () => { const field = (fields: Record) => z.object(fields as never); it("rejects a permission id above the protocol maximum, as the TRON field set does", () => { expect(field(governanceTxModeFields).safeParse({ permissionId: "10" }).success).toBe(false); - // The bound lives with the flag, which is TRON-only: a permission group is a TRON concept, - // so the shared set no longer declares it at all. expect(field(tronTxModeFields).safeParse({ permissionId: "10" }).success).toBe(false); expect(Object.keys(txModeFields)).not.toContain("permissionId"); }); @@ -194,12 +164,6 @@ describe("governance --permission-id shares the protocol bound with every other }); }); -/** - * Extracting `tronTxModeFields` out of the shared set silently dropped --permission-id and - * --expiration from every TRON-only write that did not re-add them: multi-signature use (signing - * under a non-owner active group) and extending the collection window both became impossible on - * those commands. They are single-family, so the fields belong in `baseFields` directly. - */ describe("TRON-only writes still carry the multi-signature transaction options", () => { const specs: Array<[string, ChainSpec]> = [ ["account activate", accountActivateSpec], @@ -214,7 +178,6 @@ describe("TRON-only writes still carry the multi-signature transaction options", ["exchange withdraw", exchangeWithdrawSpec], ["reward withdraw", rewardWithdrawSpec], ["vote cast", voteCastSpec], - // stake's read commands (info, delegated) are built separately and take no mode flags ...stakeDefinitions({} as never) .filter(({ spec }) => spec.broadcasts) .map(({ spec }) => [spec.path.join(" "), spec] as [string, ChainSpec]), @@ -225,15 +188,12 @@ describe("TRON-only writes still carry the multi-signature transaction options", expect(Object.keys(shape)).toEqual(expect.arrayContaining(["permissionId", "expiration"])); }); - // A flag whose help text drifts from the shared one is a second source of truth; these commands - // reuse the exported object precisely so there is only ever one. it.each(specs)("%s reuses the shared field definitions verbatim", (_name, spec) => { const shape = spec.baseFields.shape as Record; expect(shape.permissionId).toBe(tronTxModeFields.permissionId); expect(shape.expiration).toBe(tronTxModeFields.expiration); }); - // --build-only keeps the shared wording: these are not governance writes, which override it. it.each(specs)("%s keeps the shared --build-only description", (_name, spec) => { const shape = spec.baseFields.shape as Record; expect(shape.buildOnly?.description).toBe( diff --git a/ts/src/adapters/inbound/cli/commands/tx.multisig.test.ts b/ts/src/adapters/inbound/cli/commands/tx.multisig.test.ts index f3cdc9ac2..b974fd379 100644 --- a/ts/src/adapters/inbound/cli/commands/tx.multisig.test.ts +++ b/ts/src/adapters/inbound/cli/commands/tx.multisig.test.ts @@ -21,8 +21,6 @@ function harness() { } describe("tx multisig binding", () => { - // --create signs, so it must receive the execution scope. Handing it the bare address instead - // would still typecheck at the call site and fail only at runtime, in the signing step. it("routes --create through the signing scope, not the resolved address", async () => { const { service, ctx, run } = harness(); await run(ctx, NETWORK, { create: true, hex: "aabb" } as never); diff --git a/ts/src/adapters/inbound/cli/commands/tx.sign.test.ts b/ts/src/adapters/inbound/cli/commands/tx.sign.test.ts index 6f3cb077f..0674f6535 100644 --- a/ts/src/adapters/inbound/cli/commands/tx.sign.test.ts +++ b/ts/src/adapters/inbound/cli/commands/tx.sign.test.ts @@ -26,8 +26,6 @@ describe("tx sign spec", () => { expect(txSignSpec.baseFields.safeParse({ hex: "abcd", offline: true }).success).toBe(true); }); - // The online permission/approval check is the default; --offline opts out of it. - // --check is gone: it was the inverted spelling and never shipped in a release. it("exposes --offline and no longer exposes --check", () => { expect(Object.keys(txSignSpec.baseFields.shape)).toContain("offline"); expect(Object.keys(txSignSpec.baseFields.shape)).not.toContain("check"); @@ -38,7 +36,6 @@ describe("tx sign spec", () => { expect(parsed.offline).toBe(false); }); - // the payload is not a secret, so argv is the only channel — no --tx-stdin on this command. it("declares no stdin channel", () => { expect(txSignSpec.stdin).toBeUndefined(); }); @@ -99,9 +96,6 @@ describe("tx sign binding", () => { }, }; - // Default: verify signer permission and resulting weight online. A co-signer who is - // not in the permission group, or who already signed, must fail before a signature is produced — - // not silently emit a hex that only `tx broadcast` will reject, after it has been passed on. it("routes hex signing through the multisig authorization service by default", async () => { await expect( txSignTronBinding( @@ -189,9 +183,6 @@ describe("EVM tx sign binding", () => { }); }); -// `tx sign --transaction ` was once the only form, and that contract must survive the -// multisig work: the JSON route still returns the transaction service's result verbatim, and the -// newer `kind:"tx-sign"` shape appears only for --hex/--file. describe("tx sign legacy JSON compatibility", () => { it("returns the transaction service result unwrapped and unannotated", async () => { const legacy = { @@ -217,8 +208,6 @@ describe("tx sign legacy JSON compatibility", () => { }); }); -// tx send carries both flavours, and conflating them would be a new lie in the help: -// the amount pair is jointly required, the asset trio is not (omit all three → native TRX). describe("tx send exclusive groups", () => { const groups = () => Object.fromEntries((txSendSpec.exclusive ?? []).map((g) => [g.flags[0], g])); @@ -231,8 +220,6 @@ describe("tx send exclusive groups", () => { }); it("marks the asset selector optional, since omitting it sends the native coin", () => { - // `--asset-id` is TRON-only and is declared on that binding. An exclusive group is - // spec-level and therefore shared by every family, so it may only name flags they all have. expect(groups().token).toEqual({ label: "which asset to send; omit for the network's native coin", flags: ["token", "contract"], @@ -265,9 +252,6 @@ describe("tx broadcast binding", () => { expect(txBroadcastSpec.baseFields.safeParse({ file: "tx.hex" }).success).toBe(true); }); - // The exclusive group is the single source of truth for "pick exactly one of these four", - // on both the human help and the --json-schema catalog. Restating it inside a field - // description is a second copy that drifts the moment an input is added or renamed. it("declares every input in one exclusive group and nowhere else", () => { expect(txBroadcastSpec.exclusive).toEqual([ { diff --git a/ts/src/adapters/inbound/cli/commands/tx.ts b/ts/src/adapters/inbound/cli/commands/tx.ts index d60474217..22acba636 100644 --- a/ts/src/adapters/inbound/cli/commands/tx.ts +++ b/ts/src/adapters/inbound/cli/commands/tx.ts @@ -13,9 +13,6 @@ import { amountSelector, tronTxModeFields, txModeFields, unifiedAmountFields } f import { TextFormatters } from "../render/index.js"; import { exactlyOne, readBoundedTextFile } from "./artifact.js"; -// baseFields carry only what every family has: a recipient, an asset selector that is either a -// book symbol or a contract, and an amount. Everything priced or numbered per chain — TRON's -// fee limit and TRC10 asset id, EVM's gas flags and nonce — lives on that family's binding. const sendFields = z.object({ to: z .string() @@ -44,16 +41,12 @@ export const txSendSpec: ChainSpec = { summary: "Send native coins or tokens with human --amount", description: "Send the native coin, or a token selected with --token / --contract.\n" + - // A command whose Options show BOTH families' tags must say what the tags mean — - // help has to be readable on its own, without any other documentation. "Flags marked (TRON only) or (EVM only) are accepted only on networks of that family; using one on the other family is rejected.", baseFields: sendFields, exclusive: [ { label: "the amount to send", flags: ["amount", "raw-amount"], select: "exactly-one" }, - // omitting all three is the native-TRX path, so this set is optional as a whole. { - // `--asset-id` is TRON-only and is declared on that binding; this group is spec-level, so - // it may only name flags every family actually has. + // Spec-level groups name shared flags; --asset-id belongs to the TRON binding. label: "which asset to send; omit for the network's native coin", flags: ["token", "contract"], select: "at-most-one", @@ -82,8 +75,6 @@ const tronSendFields = z.object({ ...tronTxModeFields, }); -/** EVM pricing: gwei for the per-gas fields, because that is the unit every wallet, explorer and - * human uses for gas — wei would be nine zeros longer and a real typo risk. */ const evmSendFields = z.object({ gasLimit: Schemas.positiveIntString() .optional() @@ -104,15 +95,11 @@ const evmSendFields = z.object({ .describe("transaction nonce; defaults to the account's pending nonce"), }); -/** EVM has no multi-signature relay, so the artifact both ends exchange is raw hex: an unsigned - * serialisation in, a signed one out. TRON's `--transaction` JSON has no EVM meaning. */ function evmHexOnly( input: { transaction?: string; hex?: string; file?: string }, hasStdin = false, ): string { - // `--transaction` no longer reaches here — it is declared by the TRON binding, so the flag check - // refuses it first. `--tx-stdin` is not a flag but a channel, so it still needs saying: a piped - // payload that is silently ignored is worse than one that is refused. + // Reject the TRON JSON stdin channel instead of ignoring its payload. if (hasStdin) { throw new UsageError( "invalid_option", @@ -129,8 +116,6 @@ export const txSignEvmBinding = ( run: async (ctx, net, input) => { const result = await svc.sign(ctx, net, evmHexOnly(input)); if (!input.out) return result; - // The two families name this differently: TRON's `tx sign` writes `result.hex`, EVM's writes - // `result.signed.raw` — not a shared field, so nothing here can be written generically. writer.write(input.out, result.signed.raw); return { ...result, out: input.out }; }, @@ -151,7 +136,7 @@ export const txSendEvmBinding = (svc: EvmTransactionService): FamilyBinding => ( run: async (ctx, net, input) => svc.send(ctx, net, { ...input, - // gwei on the flag, wei everywhere below it. + // CLI fees are in gwei; service fees are in wei. ...(input.maxFee === undefined ? {} : { maxFee: gweiToWei(input.maxFee) }), ...(input.priorityFee === undefined ? {} : { priorityFee: gweiToWei(input.priorityFee) }), }), @@ -163,9 +148,6 @@ export const txSendTronBinding = (svc: TronTransactionService): FamilyBinding => run: async (ctx, net, input) => svc.send(ctx, net, input), }); -/** TRON's JSON form of a signed transaction. Declared by the TRON binding alone, which is what - * makes help tag it `(TRON only)` and every other family refuse it — the same treatment `--asset-id` - * gets. EVM has no JSON transaction: it exchanges RLP hex. */ const tronBroadcastFields = z.object({ transaction: z.string().optional().describe("signed transaction JSON"), }); @@ -188,9 +170,6 @@ const broadcastFields = z.object({ export const txBroadcastSpec: ChainSpec = { path: ["tx", "broadcast"], stdin: "tx", - // The channel carries TRON's transaction JSON, and only the TRON binding reads it. Declaring - // that is what tags it `(TRON only)` in help and lets any other family refuse it outright, instead - // of ignoring a payload the caller piped in. stdinFamily: "tron", network: "optional", wallet: "none", @@ -291,9 +270,6 @@ export const txApprovalsTronBinding = (service: TronMultisigService): FamilyBind run: async (_ctx, network, input) => service.approvals(network, hexInput(input)), }); -/** The TRON compatibility path, declared by the TRON binding so help tags it `(TRON only)`; see - * tronBroadcastFields. Its two companion rules (`--out` / `--offline` are hex-only) travel with - * it, because they are only meaningful where `--transaction` exists. */ const tronSignFields = z.object({ transaction: z .string() @@ -333,10 +309,8 @@ export const txSignSpec: ChainSpec = { "resulting approval weight; --offline skips the online permission checks. An EVM\n" + "transaction carries exactly one signature, so an already-signed one is refused.", baseFields: signFields, - // --hex/--file first: --transaction is the compatibility path, not the co-signing one. exclusive: [{ label: "the transaction to co-sign", flags: ["hex", "file", "transaction"] }], baseRefine: (input, context) => { - // On a family without `--transaction` this is the whole rule: one of --hex / --file. if ( input.transaction === undefined && [input.hex, input.file].filter((entry) => entry !== undefined).length !== 1 diff --git a/ts/src/adapters/inbound/cli/commands/typed-data.test.ts b/ts/src/adapters/inbound/cli/commands/typed-data.test.ts index ab19d6dc3..a125d8799 100644 --- a/ts/src/adapters/inbound/cli/commands/typed-data.test.ts +++ b/ts/src/adapters/inbound/cli/commands/typed-data.test.ts @@ -22,7 +22,6 @@ describe("typed-data sign spec", () => { expect(typedDataSignSpec.auth).toBe("required"); }); - // the payload is not a secret, so argv is the only channel — no --typed-data-stdin exists. it("declares no stdin channel", () => { expect(typedDataSignSpec.stdin).toBeUndefined(); }); @@ -55,8 +54,6 @@ describe("typed-data sign binding", () => { ).rejects.toMatchObject({ code: "invalid_value" }); }); - // BUG-V413-020: missing/malformed top-level fields (domain/types/message) don't decode as what - // --typed-data says they are, so they're invalid_payload (exit 2), not invalid_value. it("rejects a structurally invalid payload with invalid_payload", async () => { const svc = { sign: async () => stubResult }; await expect( diff --git a/ts/src/adapters/inbound/cli/commands/vote.ts b/ts/src/adapters/inbound/cli/commands/vote.ts index fce37c6b9..d626e085f 100644 --- a/ts/src/adapters/inbound/cli/commands/vote.ts +++ b/ts/src/adapters/inbound/cli/commands/vote.ts @@ -4,8 +4,6 @@ import type { TronVoteService } from "../../../../application/use-cases/tron/vot import { txModeFields, tronTxModeFields } from "./shared.js"; import { TextFormatters } from "../render/index.js"; -// repeatable flag: the arity layer sets yargs `array: true`, so `--for` always arrives as a -// string[] (single or repeated) — no preprocess needed to normalize. const voteForField = z .array(z.string().min(1)) .min(1) diff --git a/ts/src/adapters/inbound/cli/commands/wallet.backup.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.backup.test.ts index de913cfd3..ba7935351 100644 --- a/ts/src/adapters/inbound/cli/commands/wallet.backup.test.ts +++ b/ts/src/adapters/inbound/cli/commands/wallet.backup.test.ts @@ -1,8 +1,4 @@ -/** - * `backup` password gating, exercised through real dispatch — the ordering only exists there. - * A Ledger-only keystore has no master password sentinel, so verifying one can never succeed: - * the account-type check must therefore win over the password gate. - */ +// Ledger-only keystores may have no password; dispatch must check exportability first. import { describe, it, expect, vi } from "vitest"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -22,7 +18,7 @@ import { WalletService } from "../../../../application/use-cases/wallet-service. import { registerWalletCommands } from "./wallet.js"; import type { SessionRef } from "../contracts/index.js"; -// Cheap KDF for keystore encryption in this suite — see cheap-scrypt.ts. Production untouched. +// Use a cheap KDF for test fixtures. vi.mock( "@noble/hashes/scrypt.js", async () => import("../../../outbound/persistence/crypto/__test-support__/cheap-scrypt.js"), @@ -98,7 +94,7 @@ describe("backup password gating", () => { await secrets.primePassword({ mode: "set" }); const { accountId } = keystore.import({ secret: VALID_MNEMONIC, type: "seed" }); secrets.clearPrimed(); - spyPrime.mockClear(); // ignore the prime used to seed the wallet above + spyPrime.mockClear(); await buildCli(shellOpts).parseAsync(["backup", accountId]); @@ -107,11 +103,6 @@ describe("backup password gating", () => { }); }); -/** - * The log filters only mean anything in --records mode; accepting one on an export would silently - * ignore what the caller asked for. --offset is the one that has to be asserted deliberately: it - * reads as a plain pagination flag, so a default value would hide it from the guard entirely. - */ describe("backup --records flag gating", () => { for (const args of [ ["--from", "2026-08-01"], @@ -129,8 +120,6 @@ describe("backup --records flag gating", () => { }); } - // BUG-V413-033: --keystore/--out describe an export, so combining either with --records is a - // flag conflict, not a bad value — machine-interface.md reserves invalid_option for that. for (const args of [["--keystore"], ["--out", "./out.json"]]) { it(`rejects ${args[0]} with --records as invalid_option`, async () => { const { shellOpts, spyPrime } = fixture({ tty: false }); @@ -151,7 +140,6 @@ describe("backup --records flag gating", () => { expect(spy).toHaveBeenCalledWith(expect.objectContaining({ offset: 1, limit: 5 })); }); - // offset is optional now; the 0 has to come from the service, or the emitted pagination changes. it("still reports offset 0 when --offset is omitted", async () => { const { shellOpts, walletService } = fixture({ tty: false }); const spy = vi.spyOn(walletService, "backupRecords"); @@ -163,13 +151,8 @@ describe("backup --records flag gating", () => { }); }); -// A V3 keystore holds ONE private key, and a seed account has a different one per family (derivation puts -// TRON at coin 195, EVM at coin 60). The selected network picks which — `family` is -// never exposed as a flag, because it is an internal concept and --network already selects it -// everywhere else in the CLI. describe("backup --keystore exports the selected network's key", () => { - // --network reaches the shell through parseGlobals, not through argv, so it is supplied the - // way the real runner supplies it. + // Supply globals separately, as the runner does. async function exportWith(network?: string) { const f = fixture({ tty: true }); await f.secrets.primePassword({ mode: "set" }); @@ -193,9 +176,7 @@ describe("backup --keystore exports the selected network's key", () => { expect(await exportWith(network)).toBe(family); }); - // No --family flag to forget, and no error either: the default network decides. The receipt - // names the family so the choice is never silent. it("falls back to the configured default network", async () => { - expect(await exportWith()).toBe("tron"); // built-in default is tron:728126428 + expect(await exportWith()).toBe("tron"); }); }); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.current.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.current.test.ts index 9062a8696..470f38514 100644 --- a/ts/src/adapters/inbound/cli/commands/wallet.current.test.ts +++ b/ts/src/adapters/inbound/cli/commands/wallet.current.test.ts @@ -37,8 +37,6 @@ function command( if (!current || isChainCommand(current)) { throw new Error("current command missing"); } - // ExecutionContext always carries these; --qr now reads the selected network to choose which - // family's address to encode, so a fixture without them represents no real invocation. const tronNet = { id: "tron:728126428", family: "tron", @@ -71,8 +69,6 @@ describe("current --qr", () => { }); }); - // json gets the answer --qr was asked for, and no terminal art: the QR is the only text-shaped - // part of this command, so it is the only part the output format decides. it("gives JSON the receive address and never builds terminal art", async () => { const fixture = command({ output: "json" }); const result = await fixture.current.run(fixture.context as never, fixture.tronNet as never, { @@ -97,7 +93,6 @@ describe("current --qr", () => { const EVM_ADDRESS = "0xe2E1a54926527Fbb4E4420DE4c6BAb82beAEE24D"; -/** the same fixture, but with the network selector the QR now reads. */ function withNetwork( addresses: Record, selected: string | undefined, @@ -120,18 +115,12 @@ function withNetwork( output: "text" as "text" | "json", warn: vi.fn(), }; - // the shell resolves --network (else config.defaultNetwork) and hands it to run(). - // A namespace is not a family — `eip155` addresses the EVM family — so this maps the two - // builtin namespaces by hand. Production never parses an id for this: it reads the - // descriptor's own `family` field. + // Fixture mapping: eip155 is the EVM namespace; runtime uses the network descriptor. const familyOf = (id: string) => (id.startsWith("eip155:") ? "evm" : "tron"); const network = net(selected ? familyOf(selected) : defaultFamily); return { current, context, network, qr }; } -// --qr encodes the address for the SELECTED NETWORK's family. Handing someone a receive -// code for a different chain is a fund-loss shape, so this never falls back to whatever the -// account happens to have. describe("current --qr picks the address by network family", () => { const both = { tron: ADDRESS, evm: EVM_ADDRESS }; @@ -177,9 +166,6 @@ describe("current --qr picks the address by network family", () => { expect(f.qr.encode).not.toHaveBeenCalled(); }); - // The regression this replaces: the family check sat behind `output === "text"`, so the same - // command exited 2 with family_mismatch for a human and 0 with success for an agent — and the - // agent is the one that cannot see the QR it was supposedly refusing to draw. it("refuses under -o json too, not just in text", async () => { const f = withNetwork({ evm: EVM_ADDRESS }, "tron:3448148188"); f.context.output = "json"; @@ -190,8 +176,6 @@ describe("current --qr picks the address by network family", () => { expect(f.qr.encode).not.toHaveBeenCalled(); }); - // The error is scoped to --qr. Looking at an account is local and must not depend on which - // network happens to be selected. it("still shows a mismatched single-family account when --qr is absent", async () => { const f = withNetwork({ evm: EVM_ADDRESS }, "tron:3448148188"); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.import-dedup.e2e.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.import-dedup.e2e.test.ts index cb5bae7ed..095a98158 100644 --- a/ts/src/adapters/inbound/cli/commands/wallet.import-dedup.e2e.test.ts +++ b/ts/src/adapters/inbound/cli/commands/wallet.import-dedup.e2e.test.ts @@ -1,13 +1,3 @@ -/** - * End-to-end coverage for scoping import dedup to one source kind, exercised through real - * CLI dispatch rather than calling Keystore.import directly: import private-key, then import the - * mnemonic that derives the SAME EVM address at account #0, and confirm both accounts survive - * (list shows two) and a wallet-management command on that shared address refuses to guess - * (ambiguous_account — list/backup/delete/rename/derive never accept a family to narrow - * with). `import private-key` / `import mnemonic` are TTY-only, so this drives them through the - * same fake-TTY prompter pattern wallet.test.ts and wallet.backup.test.ts already use instead of a - * real PTY. - */ import { describe, it, expect, vi } from "vitest"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -29,7 +19,7 @@ import { WalletService } from "../../../../application/use-cases/wallet-service. import { registerWalletCommands } from "./wallet.js"; import type { SessionRef } from "../contracts/index.js"; -// Cheap KDF for keystore encryption in this suite — see cheap-scrypt.ts. Production untouched. +// Use a cheap KDF for test fixtures. vi.mock( "@noble/hashes/scrypt.js", async () => import("../../../outbound/persistence/crypto/__test-support__/cheap-scrypt.js"), @@ -37,17 +27,11 @@ vi.mock( const MNEMONIC = "test test test test test test test test test test test junk"; const VALID_PASSWORD = "Abcdef1!"; -// the raw private key MNEMONIC derives at m/44'/60'/0'/0/0 — importing this key and later -// importing MNEMONIC as a seed land on the same EVM address via two DIFFERENT source kinds. const EVM_KEY_OF_MNEMONIC_ACCOUNT_0 = bytesToHex( Derivation.derive(Derivation.mnemonicToSeed(MNEMONIC), Derivation.path("evm", 0)).privateKey, ); -/** - * Fresh CLI plumbing (StreamManager allows exactly one `result` write, so it — and everything - * built from it — must be rebuilt per command) sharing one keystore/root and one hidden-answer - * script consumed across calls, the way a single interactive session would. - */ +// Rebuild streams per command: each StreamManager allows only one result write. function fixture(root: string, hiddenAnswers: string[], hiddenIdx: { i: number }) { const store = new AtomicFileStore(); const streams = new StreamManager("text", false); @@ -93,8 +77,6 @@ function fixture(root: string, hiddenAnswers: string[], hiddenIdx: { i: number } describe("import private-key then import mnemonic on the same address (real CLI dispatch)", () => { it("keeps both accounts and makes wallet-management commands on the shared address ambiguous", async () => { - // Fresh $WALLET_CLI_HOME asks Set + Confirm master password before the first secret; the - // second import only asks to verify it (already established), then the mnemonic. const root = mkdtempSync(join(tmpdir(), "wallet-import-dedup-e2e-")); const hiddenIdx = { i: 0 }; const hiddenAnswers = [ 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 index 79d465fab..4f89b715b 100644 --- a/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts +++ b/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts @@ -28,8 +28,6 @@ describe("wallet import-ledger contract", () => { expect(r.success && (r.data as { index?: number }).index).toBe(2); }); - // Previously "rejects a hidden-family app (EVM is not currently exposed)". The app list is - // derived from FAMILIES[f].ledger, so wiring hw-app-eth exposes `--app ethereum` by itself. it("accepts the ethereum app now that the EVM family is ledger-wired", () => { expect(ok({ app: "ethereum", index: 0 })).toBe(true); }); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.keystore.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.keystore.test.ts index 069900050..6a08a9fb8 100644 --- a/ts/src/adapters/inbound/cli/commands/wallet.keystore.test.ts +++ b/ts/src/adapters/inbound/cli/commands/wallet.keystore.test.ts @@ -1,8 +1,3 @@ -/** - * The `backup` mode switch and `import keystore`, exercised through real dispatch — the parts that - * only exist there: which envelope `command` a mode reports, whether a password is demanded, whether - * the TTY is asked to pick an account, and the flag combinations each mode refuses. - */ import { describe, it, expect, vi } from "vitest"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -24,7 +19,7 @@ import { KeystoreV3 } from "../../../../domain/keystore/index.js"; import { registerWalletCommands } from "./wallet.js"; import type { SessionRef } from "../contracts/index.js"; -// Cheap KDF for keystore encryption in this suite — see cheap-scrypt.ts. Production untouched. +// Use a cheap KDF for test fixtures. vi.mock( "@noble/hashes/scrypt.js", async () => import("../../../outbound/persistence/crypto/__test-support__/cheap-scrypt.js"), @@ -55,7 +50,6 @@ function fixture(opts: { tty: boolean; records?: BackupRecord[] }) { isTTY: () => opts.tty, async question(prompt: string, _hidden: boolean) { asked.push(prompt); - // the keystore file's own password is a distinct prompt from the master password return /keystore/i.test(prompt) ? KEYSTORE_PW : VALID_PASSWORD; }, async readKey() { @@ -108,7 +102,6 @@ function fixture(opts: { tty: boolean; records?: BackupRecord[] }) { return { shellOpts, keystore, secrets, spyPrime, spySelect, root, asked, writes, envelope }; } -/** an account whose secret can be exported, with the master password already established. */ async function seedWallet( f: ReturnType, secret = VALID_MNEMONIC, @@ -215,8 +208,6 @@ describe("backup --records", () => { expect(f.envelope().command).toBe("backup.records"); }); - // The service returns `pagination` inside its view; the json formatter lifts it into envelope - // `meta` (and removes it from `data`) whenever it carries a full offset/limit/total triple. it("returns records, with pagination lifted into envelope meta", async () => { const f = fixture({ tty: false, @@ -235,8 +226,6 @@ describe("backup --records", () => { ["backup", "--records", "--keystore"], ["backup", "--records", "--out", "./x.json"], ]) { - // BUG-V413-033: --keystore/--out describe an export, so combining either with --records is - // a flag conflict (invalid_option), not a bad value. await expect(buildCli(f.shellOpts).parseAsync(argv)).rejects.toMatchObject({ code: "invalid_option", }); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.test.ts index 8fa7ed832..b3b4cdec4 100644 --- a/ts/src/adapters/inbound/cli/commands/wallet.test.ts +++ b/ts/src/adapters/inbound/cli/commands/wallet.test.ts @@ -17,19 +17,16 @@ import { isChainCommand } from "../contracts/index.js"; import type { CommandDefinition, Globals } from "../contracts/index.js"; import { WalletService } from "../../../../application/use-cases/wallet-service.js"; -// Cheap KDF for keystore encryption in this suite — see cheap-scrypt.ts. Production untouched. +// Use a cheap KDF for test fixtures. vi.mock( "@noble/hashes/scrypt.js", async () => import("../../../outbound/persistence/crypto/__test-support__/cheap-scrypt.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[]; @@ -47,7 +44,6 @@ function makeFakeBackend(opts: FakePromptOpts = {}): ConstructorParameters