diff --git a/.gitignore b/.gitignore index 648c86304..83de291e0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,9 @@ FileTest bin # Wallet keystore files created at runtime -Wallet/ -Mnemonic/ -wallet_data/ +/Wallet/ +/Mnemonic/ +/wallet_data/ # QA runtime output qa/results/ @@ -38,3 +38,6 @@ qa/.verify.lock/ graphify-out/ .vscode/ docs/superpowers + + +/ts-deprecated/ \ No newline at end of file diff --git a/README.md b/README.md index 5b9bcc643..f4b5aa70c 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ After the freezing time expires, funds can be unfroze. **Unfreeze operation is as follows:** ```console -> unfreezeBalance [OwnerAddress] ResourceCode(0 BANDWIDTH, 1 CPU) [receiverAddress] +> unfreezeBalance [OwnerAddress] ResourceCode(0 BANDWIDTH, 1 ENERGY) [receiverAddress] ``` ## How to vote @@ -1245,14 +1245,14 @@ frozen_duration > frezen duration, 3 days ResourceCode -> 0 BANDWIDTH;1 ENERGY +> 0 BANDWIDTH;1 ENERGY (TRON_POWER is not delegatable) receiverAddress > target account address ### unfreeze delegated resource - > unfreezeBalance [OwnerAddress] ResourceCode(0 BANDWIDTH, 1 CPU) [receiverAddress] + > unfreezeBalance [OwnerAddress] ResourceCode(0 BANDWIDTH, 1 ENERGY) [receiverAddress] The latter two parameters are optional. If they are not set, the BANDWIDTH resource is unfreeze by default; when the receiverAddress is set, the delegate resources are unfreezed. @@ -1279,7 +1279,7 @@ frozen_balance > The amount of frozen, the unit is the smallest unit (Sun), the minimum is 1000000sun. ResourceCode -> 0 BANDWIDTH;1 ENERGY +> 0 BANDWIDTH;1 ENERGY;2 TRON_POWER (only when getAllowNewResourceModel is enabled) Example: ```console @@ -1327,7 +1327,7 @@ unfreezeBalance > The amount of unfreeze, the unit is the smallest unit (Sun) ResourceCode -> 0 BANDWIDTH;1 ENERGY +> 0 BANDWIDTH;1 ENERGY;2 TRON_POWER (only when getAllowNewResourceModel is enabled) Example: ```console diff --git a/docs/standard-cli-contract-spec.md b/docs/standard-cli-contract-spec.md index c8a41b2cf..02a4f9e46 100644 --- a/docs/standard-cli-contract-spec.md +++ b/docs/standard-cli-contract-spec.md @@ -446,6 +446,12 @@ Examples: Command-specific validation that runs immediately after parsing may still surface as `usage_error` when the problem is malformed user input rather than runtime execution failure. +Staking resource-code validation is **network-aware and fail-open**: for freeze/unfreeze commands the code `2` +(TRON_POWER) is accepted only when the chain parameter `getAllowNewResourceModel` is enabled, and when that +parameter cannot be fetched (offline/timeout) the client-side pre-check is skipped so the node's `validate()` +remains the single source of truth at broadcast. Delegation commands reject `2` unconditionally (BANDWIDTH/ENERGY +only), matching the node actuator. + ### Rationale This contract prevents malformed input such as `--contract --method balanceOf(address)` from being treated as diff --git a/docs/standard-cli-user-manual.md b/docs/standard-cli-user-manual.md index b770c6b0c..6b999232e 100644 --- a/docs/standard-cli-user-manual.md +++ b/docs/standard-cli-user-manual.md @@ -130,6 +130,7 @@ TRON uses two resources to process transactions: You obtain these resources by **staking (freezing) TRX**. Resource type codes: - `0` = Bandwidth - `1` = Energy +- `2` = TRON_POWER -- accepted by freeze/unfreeze only when the network enables `getAllowNewResourceModel`; not delegatable --- @@ -748,7 +749,7 @@ Freeze TRX to obtain bandwidth or energy using **Stake 2.0** (the current stakin | Option | Required | Type | Description | |--------|----------|------|-------------| | `--amount` | Yes | number | Amount to freeze in SUN | -| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy (default: `0`) | +| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy, `2` = TRON_POWER (only when getAllowNewResourceModel enabled; default: `0`) | | `--owner` | No | address | Owner address | | `--permission-id` | No | number | Permission ID for multi-sig signing (default: 0) | | `--multi` | No | boolean | Multi-signature mode | @@ -775,7 +776,7 @@ Unfreeze previously frozen TRX under **Stake 2.0**. Unfrozen TRX enters a waitin | Option | Required | Type | Description | |--------|----------|------|-------------| | `--amount` | Yes | number | Amount to unfreeze in SUN | -| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy (default: `0`) | +| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy, `2` = TRON_POWER (only when getAllowNewResourceModel enabled; default: `0`) | | `--owner` | No | address | Owner address | | `--permission-id` | No | number | Permission ID for multi-sig signing (default: 0) | | `--multi` | No | boolean | Multi-signature mode | @@ -922,7 +923,7 @@ Freeze TRX using the legacy Stake 1.0 system. **Use `freeze-balance-v2` instead. |--------|----------|------|-------------| | `--amount` | Yes | number | Amount to freeze in SUN | | `--duration` | Yes | number | Freeze duration in days | -| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy (default: `0`) | +| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy, `2` = TRON_POWER (only when getAllowNewResourceModel enabled; default: `0`) | | `--receiver` | No | address | Delegate the frozen resources to this address | | `--owner` | No | address | Owner address | | `--multi` | No | boolean | Multi-signature mode | @@ -944,7 +945,7 @@ Unfreeze TRX under the legacy Stake 1.0 system. **Use `unfreeze-balance-v2` inst | Option | Required | Type | Description | |--------|----------|------|-------------| -| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy (default: `0`) | +| `--resource` | No | number | Resource type: `0` = Bandwidth, `1` = Energy, `2` = TRON_POWER (only when getAllowNewResourceModel enabled; default: `0`) | | `--receiver` | No | address | Receiver address (if resources were delegated) | | `--owner` | No | address | Owner address | | `--multi` | No | boolean | Multi-signature mode | @@ -2749,6 +2750,7 @@ Common error codes: |------|----------|---------| | `0` | Bandwidth | All transactions (data transfer) | | `1` | Energy | Smart contract calls | +| `2` | TRON_POWER | freeze/unfreeze only, when `getAllowNewResourceModel` is enabled (not delegatable) | ### C. Network Endpoints diff --git a/src/main/java/org/tron/core/manager/UpdateAccountPermissionInteractive.java b/src/main/java/org/tron/core/manager/UpdateAccountPermissionInteractive.java index 32548a834..753164ad7 100644 --- a/src/main/java/org/tron/core/manager/UpdateAccountPermissionInteractive.java +++ b/src/main/java/org/tron/core/manager/UpdateAccountPermissionInteractive.java @@ -44,6 +44,8 @@ public class UpdateAccountPermissionInteractive { 41, 42, 43, 44, 45, 46, 48, 49, 52, 53, 54, 55, 56, 57, 58, 59 ); + static final String DEFAULT_ACTIVE_OPERATIONS = + "7fff1fc0033ef30f000000000000000000000000000000000000000000000000"; public static final Map operationsMap = new HashMap<>(); static { @@ -86,6 +88,22 @@ public class UpdateAccountPermissionInteractive { operationsMap.put("59", "Cancel Unstake"); } + static String opLabel(int code) { + String name = operationsMap.get(String.valueOf(code)); + if (name != null) { + return name; + } + ContractType type = ContractType.forNumber(code); + return type != null + ? "Unlisted op " + code + " (" + type.name() + ")" + : "Unknown op " + code; + } + + private static String contractTypeName(int code) { + ContractType type = ContractType.forNumber(code); + return type != null ? type.name() : "UNKNOWN"; + } + public String start(String address) { System.out.println("\n=== UpdateAccountPermission Interactive Mode ==="); Response.Account account = WalletApi.queryAccount(WalletApi.decodeFromBase58Check(address)); @@ -105,7 +123,7 @@ public String start(String address) { active.setType(2); active.setPermissionName("active"); active.setThreshold(1L); - active.setOperations("7fff1fc0033efb0f000000000000000000000000000000000000000000000000"); + active.setOperations(DEFAULT_ACTIVE_OPERATIONS); active.setKeys(Lists.newArrayList(new Key(address, 1L))); activePermissions = Lists.newArrayList(active); } @@ -443,13 +461,12 @@ private void editActivePermissions() { Collections.sort(currentOps); while (true) { - List allowedOps = currentOps.stream() - .filter(i -> operationsMap.get(String.valueOf(i)) != null).sorted().collect(Collectors.toList()); + List allowedOps = currentOps.stream().sorted().collect(Collectors.toList()); System.out.println("\nCurrent allowed operations:"); for (int i = 0; i < allowedOps.size(); i++) { int code = allowedOps.get(i); - System.out.println((i + 1) + ". " + operationsMap.get(String.valueOf(code)) - + " -> " + ContractType.forNumber(code).name() + "(" + code + ")"); + System.out.println((i + 1) + ". " + opLabel(code) + + " -> " + contractTypeName(code) + "(" + code + ")"); } System.out.println("\nOperations editing (enter 'q' to finish editing operations):"); @@ -471,8 +488,8 @@ private void editActivePermissions() { System.out.println("Current operations that can be deleted:"); for (int i = 0; i < allowedOps.size(); i++) { int code = allowedOps.get(i); - System.out.println((i + 1) + ". " + operationsMap.get(String.valueOf(code)) - + " -> " + ContractType.forNumber(code).name() + "(" + code + ")"); + System.out.println((i + 1) + ". " + opLabel(code) + + " -> " + contractTypeName(code) + "(" + code + ")"); } System.out.print("Enter indexes to delete (comma separated), or 'q' to cancel: "); @@ -698,9 +715,7 @@ private void printPermissionSummary(Permission p, int index) { System.out.println(" Operations : (none)"); } else { String opsDisplay = ops.stream() - .map(String::valueOf) - .filter(operationsMap::containsKey) - .map(operationsMap::get) + .map(UpdateAccountPermissionInteractive::opLabel) .collect(Collectors.joining(", ")); System.out.println(" Operations : " + opsDisplay); } @@ -814,11 +829,7 @@ private void printPermissionDetail(Permission p) { } else { System.out.println(" Operations :"); for (Integer code : ops) { - String name = operationsMap.get(String.valueOf(code)); - if (name == null) { - continue; - } - System.out.printf(" - %-3d (%s)%n", code, name); + System.out.printf(" - %-3d (%s)%n", code, opLabel(code)); } } System.out.println(" Threshold : " + p.getThreshold()); @@ -922,4 +933,4 @@ public long getWeight() { } } -} \ No newline at end of file +} diff --git a/src/main/java/org/tron/walletcli/Client.java b/src/main/java/org/tron/walletcli/Client.java index 717a6723b..91f7687c6 100755 --- a/src/main/java/org/tron/walletcli/Client.java +++ b/src/main/java/org/tron/walletcli/Client.java @@ -1582,6 +1582,32 @@ private byte[] getAddressBytes(final String address) { return ownerAddress; } + // Network-aware staking resource guard (issue #939). Returns false (and prints guidance) when + // the code should be rejected. freezeContext=true for freeze/unfreeze (2=TRON_POWER allowed only + // when getAllowNewResourceModel is on); false for delegate/undelegate (never delegatable). + // Fail-open: when the chain parameter can't be fetched, allow 2 and let the node validate. + private boolean checkStakingResource(int resourceCode, boolean freezeContext) { + if (resourceCode == 0 || resourceCode == 1) { + return true; + } + if (resourceCode != 2 || !freezeContext) { + System.out.println("Invalid ResourceCode: " + resourceCode + ". Use 0 (BANDWIDTH), 1 (ENERGY)" + + (freezeContext ? ", or 2 (TRON_POWER, only when getAllowNewResourceModel is enabled)." : ".")); + return false; + } + Boolean enabled = walletApiWrapper.isNewResourceModelEnabled(); + if (Boolean.FALSE.equals(enabled)) { + System.out.println("ResourceCode 2 (TRON_POWER) is not enabled on this network " + + "(getAllowNewResourceModel is off)."); + return false; + } + if (enabled == null) { + System.out.println("[WARNING] Could not verify getAllowNewResourceModel; proceeding with " + + "TRON_POWER — the node validates at broadcast."); + } + return true; + } + private void freezeBalance(String[] parameters) throws IOException, CipherException, CancelException, IllegalException { boolean multi = isMulti(parameters); @@ -1618,6 +1644,10 @@ private void freezeBalance(String[] parameters) receiverAddress = WalletApi.decodeFromBase58Check(parameters[index]); } + // v1 freeze with a receiver is a delegated freeze; TRON_POWER is not delegatable. + if (!checkStakingResource(resourceCode, receiverAddress == null)) { + return; + } boolean result = walletApiWrapper.freezeBalance(ownerAddress, frozen_balance, frozen_duration, resourceCode, receiverAddress, multi); if (result) { @@ -1659,6 +1689,9 @@ private void freezeBalanceV2(String[] parameters) } } + if (!checkStakingResource(resourceCode, true)) { + return; + } boolean result = walletApiWrapper.freezeBalanceV2(ownerAddress, frozen_balance , resourceCode, multi); if (multi) { @@ -1703,6 +1736,10 @@ private void unfreezeBalance(String[] parameters) receiverAddress = WalletApi.decodeFromBase58Check(parameters[index++]); } + // v1 unfreeze with a receiver targets a delegated freeze; TRON_POWER is not delegatable. + if (!checkStakingResource(resourceCode, receiverAddress == null)) { + return; + } boolean result = walletApiWrapper.unfreezeBalance(ownerAddress, resourceCode, receiverAddress, multi); if (result) { System.out.println("UnfreezeBalance " + successfulHighlight() + " !!!"); @@ -1741,6 +1778,9 @@ private void unfreezeBalanceV2(String[] parameters) } } + if (!checkStakingResource(resourceCode, true)) { + return; + } boolean result = walletApiWrapper.unfreezeBalanceV2(ownerAddress, unfreezeBalance, resourceCode, multi); if (multi) { createMultiSignResult(result); @@ -1834,6 +1874,9 @@ private void delegateResource(String[] parameters) } } + if (!checkStakingResource(resourceCode, false)) { + return; + } boolean result = walletApiWrapper.delegateresource( ownerAddress, balance, resourceCode, receiverAddress, lock, lockPeriod, multi); if (multi) { @@ -1887,6 +1930,9 @@ private void unDelegateResource(String[] parameters) return; } } + if (!checkStakingResource(resourceCode, false)) { + return; + } boolean result = walletApiWrapper.undelegateresource(ownerAddress, balance, resourceCode, receiverAddress, multi); if (multi) { createMultiSignResult(result); diff --git a/src/main/java/org/tron/walletcli/WalletApiWrapper.java b/src/main/java/org/tron/walletcli/WalletApiWrapper.java index ac4eecc9d..1c797d131 100644 --- a/src/main/java/org/tron/walletcli/WalletApiWrapper.java +++ b/src/main/java/org/tron/walletcli/WalletApiWrapper.java @@ -1923,6 +1923,19 @@ public Response.ChainParameters getChainParametersForCli() { } } + /** TRUE = new resource model active, FALSE = off, null = couldn't determine (fail-open). */ + public Boolean isNewResourceModelEnabled() { + Response.ChainParameters params = getChainParameters(); + if (params == null) { + return null; // node unreachable / timeout → fail-open + } + return params.getChainParameterList().stream() + .filter(p -> "getAllowNewResourceModel".equals(p.getKey())) + .map(p -> p.getValue() == 1) + .findFirst() + .orElse(null); // key absent → unknown → fail-open + } + public boolean approveProposal(byte[] ownerAddress, long id, boolean is_add_approval, boolean multi) throws CipherException, IOException, CancelException, IllegalException { diff --git a/src/main/java/org/tron/walletcli/cli/commands/CommandSupport.java b/src/main/java/org/tron/walletcli/cli/commands/CommandSupport.java index bee51da88..d218f4f2f 100644 --- a/src/main/java/org/tron/walletcli/cli/commands/CommandSupport.java +++ b/src/main/java/org/tron/walletcli/cli/commands/CommandSupport.java @@ -1,5 +1,6 @@ package org.tron.walletcli.cli.commands; +import org.tron.walletcli.WalletApiWrapper; import org.tron.walletcli.cli.OutputFormatter; import java.util.LinkedHashMap; @@ -86,6 +87,36 @@ static void requireResourceCode(OutputFormatter out, String name, int value) { } } + // Network-aware staking resource guard (issue #939). freezeContext=true for + // freeze/unfreeze (2=TRON_POWER allowed only when getAllowNewResourceModel is on); + // false for delegate/undelegate (TRON_POWER is never delegatable). Fail-open: when the + // chain parameter can't be fetched, allow 2 and let the node validate at broadcast. + static void requireStakingResource(OutputFormatter out, WalletApiWrapper wrapper, + String name, int value, boolean freezeContext) { + if (value == 0 || value == 1) { + return; // common case: no chain-parameter RPC + } + if (value != 2) { + out.usageError(name + " must be 0 (BANDWIDTH), 1 (ENERGY)" + + (freezeContext ? ", or 2 (TRON_POWER)" : "") + ", got: " + value, null); + return; + } + if (!freezeContext) { + out.usageError(name + " for delegation must be 0 (BANDWIDTH) or 1 (ENERGY); " + + "TRON_POWER (2) is not delegatable, got: " + value, null); + return; + } + Boolean enabled = wrapper.isNewResourceModelEnabled(); // only reached when value == 2 + if (Boolean.FALSE.equals(enabled)) { + out.usageError(name + " = 2 (TRON_POWER) is not enabled on this network " + + "(getAllowNewResourceModel is off)", null); + } else if (enabled == null) { + out.info("[WARNING] Could not verify getAllowNewResourceModel (node unreachable); " + + "proceeding with resource=2 (TRON_POWER) — the node validates at broadcast."); + } + // TRUE or null (fail-open) → allow + } + static void requireForce(OutputFormatter out, String commandName, boolean force) { if (!force) { out.usageError(commandName + " requires --force in standard CLI mode.", null); diff --git a/src/main/java/org/tron/walletcli/cli/commands/StakingCommands.java b/src/main/java/org/tron/walletcli/cli/commands/StakingCommands.java index cbbd7fc6e..121d1642a 100644 --- a/src/main/java/org/tron/walletcli/cli/commands/StakingCommands.java +++ b/src/main/java/org/tron/walletcli/cli/commands/StakingCommands.java @@ -28,7 +28,7 @@ private static void registerFreezeBalance(CommandRegistry registry) { .description("Freeze TRX for bandwidth/energy (v1, deprecated)") .option("amount", "Amount to freeze in SUN", true, OptionDef.Type.LONG) .option("duration", "Freeze duration in days", true, OptionDef.Type.LONG) - .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY)", false, OptionDef.Type.LONG) + .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY, 2=TRON_POWER when getAllowNewResourceModel enabled)", false, OptionDef.Type.LONG) .option("receiver", "Receiver address (for delegated freeze)", false) .option("owner", "Owner address", false) .option("multi", "Multi-signature mode", false, OptionDef.Type.BOOLEAN) @@ -40,8 +40,9 @@ private static void registerFreezeBalance(CommandRegistry registry) { long duration = opts.getLong("duration"); CommandSupport.requirePositive(out, "duration", duration); int resource = opts.has("resource") ? opts.getInt("resource") : 0; - CommandSupport.requireResourceCode(out, "resource", resource); byte[] receiver = opts.has("receiver") ? opts.getAccountAddress("receiver") : null; + // v1 freeze with a receiver is a delegated freeze; TRON_POWER is not delegatable. + CommandSupport.requireStakingResource(out, wrapper, "resource", resource, receiver == null); boolean multi = opts.getBoolean("multi"); String txid = wrapper.freezeBalanceForCli(owner, amount, duration, resource, receiver, multi); CommandSupport.emitSuccess(out, @@ -58,7 +59,7 @@ private static void registerFreezeBalanceV2(CommandRegistry registry) { .aliases("freezebalancev2") .description("Freeze TRX for bandwidth/energy (Stake 2.0)") .option("amount", "Amount to freeze in SUN", true, OptionDef.Type.LONG) - .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY)", false, OptionDef.Type.LONG) + .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY, 2=TRON_POWER when getAllowNewResourceModel enabled)", false, OptionDef.Type.LONG) .option("owner", "Owner address", false) .option("permission-id", "Permission ID for signing (default: 0)", false, OptionDef.Type.LONG) .option("multi", "Multi-signature mode", false, OptionDef.Type.BOOLEAN) @@ -68,7 +69,7 @@ private static void registerFreezeBalanceV2(CommandRegistry registry) { long amount = opts.getLong("amount"); CommandSupport.requirePositive(out, "amount", amount); int resource = opts.has("resource") ? opts.getInt("resource") : 0; - CommandSupport.requireResourceCode(out, "resource", resource); + CommandSupport.requireStakingResource(out, wrapper, "resource", resource, true); int permissionId = opts.has("permission-id") ? opts.getInt("permission-id") : 0; CommandSupport.requirePermissionId(out, "permission-id", permissionId); boolean multi = opts.getBoolean("multi"); @@ -92,7 +93,7 @@ private static void registerUnfreezeBalance(CommandRegistry registry) { .name("unfreeze-balance") .aliases("unfreezebalance") .description("Unfreeze TRX (v1, deprecated)") - .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY)", false, OptionDef.Type.LONG) + .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY, 2=TRON_POWER when getAllowNewResourceModel enabled)", false, OptionDef.Type.LONG) .option("receiver", "Receiver address", false) .option("owner", "Owner address", false) .option("multi", "Multi-signature mode", false, OptionDef.Type.BOOLEAN) @@ -100,8 +101,9 @@ private static void registerUnfreezeBalance(CommandRegistry registry) { byte[] owner = opts.has("owner") ? opts.getAccountAddress("owner") : null; int resource = opts.has("resource") ? opts.getInt("resource") : 0; - CommandSupport.requireResourceCode(out, "resource", resource); byte[] receiver = opts.has("receiver") ? opts.getAccountAddress("receiver") : null; + // v1 unfreeze with a receiver targets a delegated freeze; TRON_POWER is not delegatable. + CommandSupport.requireStakingResource(out, wrapper, "resource", resource, receiver == null); boolean multi = opts.getBoolean("multi"); String txid = wrapper.unfreezeBalanceForCli(owner, resource, receiver, multi); CommandSupport.emitSuccess(out, @@ -118,7 +120,7 @@ private static void registerUnfreezeBalanceV2(CommandRegistry registry) { .aliases("unfreezebalancev2") .description("Unfreeze TRX (Stake 2.0)") .option("amount", "Amount to unfreeze in SUN", true, OptionDef.Type.LONG) - .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY)", false, OptionDef.Type.LONG) + .option("resource", "Resource type (0=BANDWIDTH, 1=ENERGY, 2=TRON_POWER when getAllowNewResourceModel enabled)", false, OptionDef.Type.LONG) .option("owner", "Owner address", false) .option("permission-id", "Permission ID for signing (default: 0)", false, OptionDef.Type.LONG) .option("multi", "Multi-signature mode", false, OptionDef.Type.BOOLEAN) @@ -128,7 +130,7 @@ private static void registerUnfreezeBalanceV2(CommandRegistry registry) { long amount = opts.getLong("amount"); CommandSupport.requirePositive(out, "amount", amount); int resource = opts.has("resource") ? opts.getInt("resource") : 0; - CommandSupport.requireResourceCode(out, "resource", resource); + CommandSupport.requireStakingResource(out, wrapper, "resource", resource, true); int permissionId = opts.has("permission-id") ? opts.getInt("permission-id") : 0; CommandSupport.requirePermissionId(out, "permission-id", permissionId); boolean multi = opts.getBoolean("multi"); @@ -185,7 +187,7 @@ private static void registerDelegateResource(CommandRegistry registry) { long amount = opts.getLong("amount"); CommandSupport.requirePositive(out, "amount", amount); int resource = opts.getInt("resource"); - CommandSupport.requireResourceCode(out, "resource", resource); + CommandSupport.requireStakingResource(out, wrapper, "resource", resource, false); byte[] receiver = opts.getAccountAddress("receiver"); boolean lock = opts.getBoolean("lock"); long lockPeriod = opts.has("lock-period") ? opts.getLong("lock-period") : 0; @@ -217,7 +219,7 @@ private static void registerUndelegateResource(CommandRegistry registry) { long amount = opts.getLong("amount"); CommandSupport.requirePositive(out, "amount", amount); int resource = opts.getInt("resource"); - CommandSupport.requireResourceCode(out, "resource", resource); + CommandSupport.requireStakingResource(out, wrapper, "resource", resource, false); byte[] receiver = opts.getAccountAddress("receiver"); boolean multi = opts.getBoolean("multi"); String txid = wrapper.undelegateResourceForCli(owner, amount, resource, receiver, multi); diff --git a/src/test/java/org/tron/core/manager/UpdateAccountPermissionInteractiveTest.java b/src/test/java/org/tron/core/manager/UpdateAccountPermissionInteractiveTest.java new file mode 100644 index 000000000..840f2e20a --- /dev/null +++ b/src/test/java/org/tron/core/manager/UpdateAccountPermissionInteractiveTest.java @@ -0,0 +1,28 @@ +package org.tron.core.manager; + +import static org.tron.common.utils.ByteUtil.hexStringToIntegerList; + +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class UpdateAccountPermissionInteractiveTest { + + @Test + public void defaultActiveOperationsExcludeShieldedTransferContract() { + List operations = + hexStringToIntegerList(UpdateAccountPermissionInteractive.DEFAULT_ACTIVE_OPERATIONS); + + Assert.assertFalse(operations.contains(51)); + Assert.assertTrue(operations.contains(49)); + Assert.assertTrue(operations.contains(52)); + } + + @Test + public void opLabelDistinguishesUnlistedAndUnknownOperations() { + Assert.assertEquals("Transfer TRX", UpdateAccountPermissionInteractive.opLabel(1)); + Assert.assertEquals("Unlisted op 51 (ShieldedTransferContract)", + UpdateAccountPermissionInteractive.opLabel(51)); + Assert.assertEquals("Unknown op 255", UpdateAccountPermissionInteractive.opLabel(255)); + } +} diff --git a/src/test/java/org/tron/walletcli/cli/commands/StakingResourceGuardTest.java b/src/test/java/org/tron/walletcli/cli/commands/StakingResourceGuardTest.java new file mode 100644 index 000000000..00eac4333 --- /dev/null +++ b/src/test/java/org/tron/walletcli/cli/commands/StakingResourceGuardTest.java @@ -0,0 +1,162 @@ +package org.tron.walletcli.cli.commands; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.enums.NetType; +import org.tron.walletcli.WalletApiWrapper; +import org.tron.walletcli.cli.CommandContext; +import org.tron.walletcli.cli.CommandDefinition; +import org.tron.walletcli.cli.CommandRegistry; +import org.tron.walletcli.cli.OutputFormatter; +import org.tron.walletcli.cli.ParsedOptions; +import org.tron.walletserver.WalletApi; + +/** + * Network-aware, fail-open resource-code guard for staking commands (issue #939). + * + * Covers the tri-state getAllowNewResourceModel behavior (enabled / disabled / unknown fail-open), + * unconditional TRON_POWER rejection for delegation, and the receiver-aware v1 semantics: a v1 + * freeze/unfreeze with a receiver is a delegated freeze, where TRON_POWER is not delegatable. + */ +public class StakingResourceGuardTest { + + private static final String RECEIVER = "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"; + + /** Stub wrapper: fixed getAllowNewResourceModel state; records whether a broadcast was attempted. */ + private static class GuardStubWrapper extends WalletApiWrapper { + private final Boolean modelEnabled; + boolean broadcastCalled = false; + + GuardStubWrapper(Boolean modelEnabled) { + this.modelEnabled = modelEnabled; + } + + @Override + public Boolean isNewResourceModelEnabled() { + return modelEnabled; + } + + @Override + public String freezeBalanceForCli(byte[] ownerAddress, long frozenBalance, long frozenDuration, + int resourceCode, byte[] receiverAddress, boolean multi) { + broadcastCalled = true; + return "faketxid"; + } + + @Override + public String unfreezeBalanceForCli(byte[] ownerAddress, int resourceCode, byte[] receiverAddress, + boolean multi) { + broadcastCalled = true; + return "faketxid"; + } + + @Override + public String delegateResourceForCli(byte[] ownerAddress, long balance, int resourceCode, + byte[] receiverAddress, boolean lock, long lockPeriod, boolean multi) { + broadcastCalled = true; + return "faketxid"; + } + } + + private static class Result { + final JsonObject json; + final boolean broadcastCalled; + + Result(JsonObject json, boolean broadcastCalled) { + this.json = json; + this.broadcastCalled = broadcastCalled; + } + } + + private Result run(GuardStubWrapper wrapper, String commandName, String... args) throws Exception { + NetType originalNetwork = WalletApi.getCurrentNetwork(); + PrintStream originalOut = System.out; + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + WalletApi.setCurrentNetwork(NetType.NILE); + System.setOut(new PrintStream(stdout)); + try { + CommandRegistry registry = new CommandRegistry(); + StakingCommands.register(registry); + CommandDefinition command = registry.lookup(commandName); + ParsedOptions opts = command.parseArgs(args); + OutputFormatter formatter = new OutputFormatter(OutputFormatter.OutputMode.JSON, false); + try { + command.getHandler().execute(CommandContext.empty(), opts, wrapper, formatter); + } catch (RuntimeException ignored) { + // usageError()/error() throw CliAbortException after recording the outcome + } + formatter.flush(); + String json = stdout.toString(StandardCharsets.UTF_8.name()); + return new Result(JsonParser.parseString(json).getAsJsonObject(), wrapper.broadcastCalled); + } finally { + WalletApi.setCurrentNetwork(originalNetwork); + System.setOut(originalOut); + } + } + + private void assertUsageError(Result r, String messageFragment) { + Assert.assertFalse("expected failure", r.json.get("success").getAsBoolean()); + Assert.assertEquals("usage_error", r.json.get("error").getAsString()); + Assert.assertTrue("message should contain: " + messageFragment, + r.json.get("message").getAsString().contains(messageFragment)); + Assert.assertFalse("must not broadcast when rejected", r.broadcastCalled); + } + + private void assertProceeded(Result r) { + Assert.assertTrue("expected success", r.json.get("success").getAsBoolean()); + Assert.assertTrue("should reach broadcast", r.broadcastCalled); + } + + // --- tri-state getAllowNewResourceModel for a self-freeze (no receiver) --- + + @Test + public void freezeBalanceAllowsTronPowerWhenModelEnabled() throws Exception { + Result r = run(new GuardStubWrapper(Boolean.TRUE), "freeze-balance", + "--amount", "1000000", "--duration", "3", "--resource", "2"); + assertProceeded(r); + } + + @Test + public void freezeBalanceRejectsTronPowerWhenModelDisabled() throws Exception { + Result r = run(new GuardStubWrapper(Boolean.FALSE), "freeze-balance", + "--amount", "1000000", "--duration", "3", "--resource", "2"); + assertUsageError(r, "not enabled on this network"); + } + + @Test + public void freezeBalanceAllowsTronPowerFailOpenWhenModelUnknown() throws Exception { + Result r = run(new GuardStubWrapper(null), "freeze-balance", + "--amount", "1000000", "--duration", "3", "--resource", "2"); + assertProceeded(r); + } + + // --- receiver-aware v1 semantics: freeze/unfreeze with a receiver is a delegation --- + + @Test + public void freezeBalanceWithReceiverRejectsTronPowerEvenWhenModelEnabled() throws Exception { + Result r = run(new GuardStubWrapper(Boolean.TRUE), "freeze-balance", + "--amount", "1000000", "--duration", "3", "--resource", "2", "--receiver", RECEIVER); + assertUsageError(r, "not delegatable"); + } + + @Test + public void unfreezeBalanceWithReceiverRejectsTronPowerEvenWhenModelEnabled() throws Exception { + Result r = run(new GuardStubWrapper(Boolean.TRUE), "unfreeze-balance", + "--resource", "2", "--receiver", RECEIVER); + assertUsageError(r, "not delegatable"); + } + + // --- delegation always rejects TRON_POWER, regardless of the chain parameter --- + + @Test + public void delegateResourceRejectsTronPowerEvenWhenModelEnabled() throws Exception { + Result r = run(new GuardStubWrapper(Boolean.TRUE), "delegate-resource", + "--amount", "1000000", "--resource", "2", "--receiver", RECEIVER); + assertUsageError(r, "not delegatable"); + } +} diff --git a/ts/.dependency-cruiser.cjs b/ts/.dependency-cruiser.cjs new file mode 100644 index 000000000..1a1bcea08 --- /dev/null +++ b/ts/.dependency-cruiser.cjs @@ -0,0 +1,49 @@ +/** + * Enforced dependency direction: + * + * bootstrap (composition) -> inbound/outbound adapters -> application -> domain + * inbound adapters -> application -> domain + * + * Inbound and outbound adapters are peers. They may meet only in bootstrap/composition. + */ +module.exports = { + forbidden: [ + { + name: "no-circular", + severity: "error", + from: {}, + to: { circular: true }, + }, + { + name: "domain-is-independent", + severity: "error", + from: { path: "^src/domain/" }, + to: { path: "^src/(application|adapters|bootstrap)/" }, + }, + { + name: "application-owns-ports", + severity: "error", + comment: "production application code depends on domain and its own ports, never adapters", + from: { path: "^src/application/", pathNot: "\\.test\\.ts$" }, + to: { path: "^src/(adapters|bootstrap)/" }, + }, + { + name: "inbound-does-not-know-outbound", + severity: "error", + comment: "CLI adapters call application ports/use-cases; bootstrap/composition supplies outbound implementations", + from: { path: "^src/adapters/inbound/", pathNot: "\\.test\\.ts$" }, + to: { path: "^src/(adapters/outbound|bootstrap)/" }, + }, + { + name: "outbound-does-not-know-inbound", + severity: "error", + from: { path: "^src/adapters/outbound/", pathNot: "\\.test\\.ts$" }, + to: { path: "^src/(adapters/inbound|bootstrap)/" }, + }, + ], + options: { + doNotFollow: { path: "node_modules" }, + tsConfig: { fileName: "tsconfig.json" }, + enhancedResolveOptions: { extensions: [".ts", ".js"] }, + }, +}; diff --git a/ts/.gitignore b/ts/.gitignore new file mode 100644 index 000000000..a0a00aedd --- /dev/null +++ b/ts/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +*.log +.wallet-cli/ +.env +.private/ +docs/superpowers \ No newline at end of file diff --git a/ts/LICENSE b/ts/LICENSE new file mode 100644 index 000000000..65c5ca88a --- /dev/null +++ b/ts/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/ts/README.md b/ts/README.md new file mode 100644 index 000000000..d6b854b3b --- /dev/null +++ b/ts/README.md @@ -0,0 +1,359 @@ +# wallet-cli + +An agent-first TypeScript CLI wallet for TRON, with deterministic commands, structured JSON, discoverable schemas, and secure secret input. Privacy-sensitive operations such as import, backup, and delete retain guided, human-friendly interactions. + +> Currently supports TRON mainnet, Nile, and Shasta. EVM chains are not yet supported by this version. + +## Contents + +- [Features](#features) +- [Quick Start](#quick-start) +- [Common Tasks](#common-tasks) +- [Automation and Agent Integration](#automation-and-agent-integration) +- [Security](#security) +- [Development](#development) + +## Features + +| Category | Capabilities | +| --- | --- | +| Wallets | Create BIP39 HD wallets, import mnemonics or private keys, derive accounts, rename, back up, and delete | +| External accounts | Watch-only addresses, Ledger TRON App, and on-device signing | +| Accounts | TRX balance, raw account data, transaction history, token portfolio, and best-effort USD valuation | +| Tokens | TRC10 and TRC20 metadata and balances, plus a custom token address book | +| Transactions | TRX, TRC10, and TRC20 transfers; dry-run; sign-only; broadcast; status and receipt queries | +| Contracts | Constant calls, state-changing calls, contract metadata, and deployment | +| Stake 2.0 | Stake, unstake, delegate and reclaim resources, cancel pending unstakes, and withdraw expired unstakes | +| Signing | TIP-191/V2 message signing | +| Automation | Stable JSON envelopes, deterministic exit codes, and a complete command JSON Schema | + +## Quick Start + +### 1. Install + +Node.js 20 or later is required. + +```bash +npm install -g walletcli +``` + +Verify the installation: + +```bash +wallet-cli --version +wallet-cli --help +``` + +### 2. Create your first wallet + +```bash +wallet-cli create --label main +``` + +The CLI prompts you to set a master password. It encrypts your local keys and cannot be recovered by this tool if lost. + +Inspect your wallets and make `main` the active account: + +```bash +wallet-cli list +wallet-cli use main +wallet-cli current +``` + +### 3. Start on testnet + +For your first transaction, set Nile as the default network: + +```bash +wallet-cli config defaultNetwork tron:nile +wallet-cli account balance +``` + +You can also select a network for one command without changing the default: + +```bash +wallet-cli account balance --network tron:nile +``` + +### 4. Dry-run before sending + +Build and estimate a transaction without signing or broadcasting it: + +```bash +wallet-cli tx send \ + --network tron:nile \ + --to T... \ + --amount 1 \ + --dry-run +``` + +After checking the recipient, amount, and estimated cost, send it: + +```bash +wallet-cli tx send \ + --network tron:nile \ + --to T... \ + --amount 1 \ + --wait +``` + +The CLI prompts for the master password when a signature is required. Without `--wait`, it returns the txid immediately after a successful broadcast. With `--wait`, it polls until the transaction is confirmed, fails, or reaches the wait timeout. + +## Common Tasks + +### Import an existing wallet + +In an interactive terminal, mnemonic and private-key prompts hide their input: + +```bash +wallet-cli import mnemonic --label imported +wallet-cli import private-key --label hot +``` + +Register an address that can be monitored but cannot sign: + +```bash +wallet-cli import watch --address T... --label treasury +``` + +Derive the next account from an HD wallet. Find its seed ID with `wallet-cli list`: + +```bash +wallet-cli derive --seed-id wlt_ab12cd34 --label operations +``` + +### Use a Ledger device + +Connect and unlock the Ledger, open the TRON App, and register its first account: + +```bash +wallet-cli import ledger --app tron --index 0 --label cold +wallet-cli use cold +wallet-cli account balance +``` + +Ledger private keys are never written locally. Signing requires confirmation on the device. You can also provide a derivation path with `--path`, or locate an account with `--address` and `--scan-limit`. + +### Query accounts and assets + +```bash +wallet-cli account balance +wallet-cli account portfolio +wallet-cli account history --limit 10 +wallet-cli account info --output json +``` + +Wallet-bound commands use the active account by default. Override it with a label, account ID, or address: + +```bash +wallet-cli account balance --account treasury +``` + +### Work with TRC10 and TRC20 tokens + +The mainnet address book includes USDT and USDC. Add a custom TRC20 contract to use its symbol in later commands: + +```bash +wallet-cli token add --contract TR7... +wallet-cli token list +wallet-cli token balance --contract TR7... +wallet-cli tx send --to T... --token USDT --amount 5 --dry-run +``` + +You can also use `--contract` directly. Use `--asset-id` for TRC10 tokens: + +```bash +wallet-cli tx send --to T... --contract TR7... --amount 5 +wallet-cli tx send --to T... --asset-id 1002000 --raw-amount 1000000 +``` + +### Inspect transactions and sign offline + +```bash +wallet-cli tx status --txid +wallet-cli tx info --txid --output json +``` + +Commands that change chain state support three execution modes: + +- Default: build, sign, and broadcast. +- `--dry-run`: build and estimate without signing or broadcasting. +- `--sign-only`: sign and output the transaction without broadcasting. Submit it later with `tx broadcast --tx-stdin`. + +Use command help for the complete set of options: + +```bash +wallet-cli tx send --help +wallet-cli tx broadcast --help +``` + +### Interact with smart contracts + +Inspect a contract and make a read-only call: + +```bash +wallet-cli contract info --contract T... +wallet-cli contract call \ + --contract T... \ + --method 'balanceOf(address)' \ + --params '[{"type":"address","value":"T..."}]' +``` + +Dry-run a state-changing call before submitting it: + +```bash +wallet-cli contract send \ + --contract T... \ + --method 'transfer(address,uint256)' \ + --params '[{"type":"address","value":"T..."},{"type":"uint256","value":"1000000"}]' \ + --dry-run +``` + +Deploy a contract: + +```bash +wallet-cli contract deploy \ + --abi '[...]' \ + --bytecode 60... \ + --fee-limit 1000000000 \ + --params '[100,"T..."]' \ + --dry-run +``` + +### Use Stake 2.0 + +Stake amounts are specified in SUN (1 TRX = 1,000,000 SUN): + +```bash +wallet-cli stake freeze --amount-sun 1000000 --resource energy --dry-run +wallet-cli stake delegate --amount-sun 1000000 --receiver T... --resource energy --dry-run +wallet-cli stake undelegate --amount-sun 1000000 --receiver T... --resource energy --dry-run +wallet-cli stake unfreeze --amount-sun 1000000 --resource energy --dry-run +wallet-cli stake withdraw --dry-run +``` + +### Sign a message + +```bash +wallet-cli message sign --message 'hello' +``` + +## Automation and Agent Integration + +### JSON output + +```bash +wallet-cli account balance --output json +wallet-cli tx info --txid --output json +``` + +JSON mode uses the `wallet-cli.result.v1` envelope and writes exactly one terminal frame to stdout. Exit codes are deterministic: + +| Exit code | Meaning | +| --- | --- | +| `0` | Success | +| `1` | Execution, authentication, device, or chain error | +| `2` | Invalid command usage or arguments | + +### Discover commands + +Agents do not need to parse human-readable help. Retrieve every command, input schema, example, and prerequisite in one call: + +```bash +wallet-cli --json-schema +``` + +Retrieve the input schema for one command: + +```bash +wallet-cli tx send --json-schema +``` + +### Provide secrets safely + +Use stdin flags in non-interactive environments. Do not put passwords, mnemonics, or private keys directly in argv or exported environment variables: + +```bash +printf '%s\n' "$WALLET_PASSWORD" | wallet-cli message sign --message 'hello' --password-stdin --output json +printf '%s\n' "$MNEMONIC" | wallet-cli import mnemonic --label main --mnemonic-stdin +printf '%s\n' "$PRIVATE_KEY" | wallet-cli import private-key --label hot --private-key-stdin +``` + +These examples assume the shell variables are populated securely and are not exported. Only one `*-stdin` flag may consume stdin in each invocation. Use an interactive terminal when one operation requires two secrets. + +### Source secrets from a password manager + +Because secrets are read from stdin, you can pipe them straight from a password manager. The secret is never written to argv, an environment variable, a temp file, or shell history — the manager keeps it encrypted at rest, and the CLI consumes it once and discards it. This pairs well with the no-`MASTER_PASSWORD`-env design: the password manager is where the secret lives, the pipe is how it travels. + +**1Password (`op read`):** + +```bash +# 1. Store the master password once (op stores it encrypted). +op item create --category=password --title='wallet-cli master' password='' + +# 2. Use it via pipe — nothing sensitive touches argv or history. +op read 'op://Private/wallet-cli master/password' | \ + wallet-cli create --label main --password-stdin +``` + +**macOS Keychain (`security`):** + +```bash +# 1. Store the master password once (omit the value after -w to be prompted, so it stays out of history). +security add-generic-password -s wallet-cli-master -a "$USER" -w + +# 2. Use it via pipe. +security find-generic-password -s wallet-cli-master -w | \ + wallet-cli create --label main --password-stdin +``` + +Only one `*-stdin` flag may consume stdin per invocation, so commands that need two secrets at once (for example `import mnemonic`, which needs both a mnemonic and a password) can pipe one secret and must supply the other interactively. + +Use `WALLET_CLI_HOME` to isolate test or automation data. The default data directory is `~/.wallet-cli`: + +```bash +WALLET_CLI_HOME=/tmp/wallet-cli-demo wallet-cli list --output json +``` + +## Security + +- Mnemonics and private keys are encrypted locally using scrypt, AES-128-CTR, and a Keccak MAC. +- The keystore uses one master password. Secrets are not accepted through argv or CLI-specific environment variables. +- Configuration, keystore, and backup data are written with restricted permissions. `backup` creates `0600` files and never overwrites an existing file. +- Ledger accounts store only the address and derivation path locally; signing remains on the hardware device. +- Watch-only accounts can query data but cannot sign. +- Test on Nile and use `--dry-run` before sending production transactions. Backup files contain secret material capable of restoring an account and must be protected like private keys. + +Back up an account: + +```bash +wallet-cli backup main --out ~/main-backup.json +``` + +## Configuration and Command Reference + +```bash +wallet-cli config +wallet-cli networks +wallet-cli COMMAND --help +``` + +Global options include `--network`, `--account`, `--output text|json`, `--timeout`, and `--verbose`. Broadcasting commands also support `--wait` and `--wait-timeout`. + +## Development + +```bash +npm ci +npm run typecheck +npm run depcruise +npm test +npm run build +``` + +The Nile live suite uses an isolated `WALLET_CLI_HOME` and does not copy or print private material: + +```bash +npm run test:live:nile +``` + +For the design and CLI contracts, see the [architecture source of truth](./docs/typescript-wallet-cli-architecture-source-of-truth.md) and the [architecture overview](./docs/architecture.md). diff --git a/ts/docs/architecture.md b/ts/docs/architecture.md new file mode 100644 index 000000000..fe3b89850 --- /dev/null +++ b/ts/docs/architecture.md @@ -0,0 +1,230 @@ +# wallet-cli architecture — English summary + +This document is a non-normative English overview. The single, authoritative architecture and +behavior contract is +[typescript-wallet-cli-architecture-source-of-truth.zh-TW.md](./typescript-wallet-cli-architecture-source-of-truth.zh-TW.md). + +## 1. Dependency model + +```mermaid +flowchart LR + BOOTSTRAP[bootstrap
process lifecycle + composition root] --> INBOUND[adapters/inbound
CLI] + BOOTSTRAP --> OUTBOUND[adapters/outbound
filesystem / TRON / Ledger / price] + INBOUND --> APPLICATION[application
contracts / use cases / ports] + OUTBOUND --> APPLICATION + APPLICATION --> DOMAIN[domain
pure rules and values] +``` + +Rules: + +1. `domain` imports neither application, adapters nor bootstrap. +2. Production `application` imports neither adapters nor bootstrap. +3. Inbound and outbound adapters never import each other or bootstrap. +4. `bootstrap/composition.ts` is the only general composition root. +5. Chain-specific adapter and use-case construction belongs to a family plugin. +6. Circular dependencies are forbidden. +7. Type-only dependencies must follow the same conceptual direction even when the dependency + checker would ignore their runtime edge. + +## 2. Package tree + +```text +src/ +├── index.ts +├── bootstrap/ +│ ├── argv.ts +│ ├── runner.ts +│ ├── composition.ts +│ ├── family-registry.ts +│ └── families/ +│ ├── types.ts +│ └── tron.ts +├── domain/ +│ ├── address/ +│ ├── amounts/ +│ ├── derivation/ +│ ├── errors/ +│ ├── family/ +│ ├── resources/ +│ ├── sources/ +│ ├── types/ +│ └── wallet/ +├── application/ +│ ├── contracts/ +│ │ ├── execution-policy.ts +│ │ ├── execution-scope.ts +│ │ └── progress.ts +│ ├── ports/ +│ │ ├── chain/ +│ │ ├── network-registry.ts +│ │ └── prompt.ts +│ ├── services/ +│ │ ├── capability/ +│ │ ├── pipeline/ +│ │ ├── signer/ +│ │ └── target/ +│ └── use-cases/ +│ └── tron/ +└── adapters/ + ├── inbound/cli/ + │ ├── commands/ + │ ├── contracts/ + │ ├── context/ + │ ├── help/ + │ ├── input/ + │ ├── output/ + │ ├── registry/ + │ ├── render/ + │ └── shell/ + └── outbound/ + ├── chain/tron/ + ├── config/ + ├── keystore/ + ├── ledger/ + ├── persistence/ + ├── price/ + └── tokenbook/ +``` + +## 3. Responsibilities + +### Domain + +Contains pure wallet, account, address, amount, derivation, source, family and transaction value +rules. Domain code performs no filesystem, network, device, terminal or process I/O. + +### Application + +Defines what the product does and which external capabilities it requires. + +- `contracts`: adapter-neutral execution policy, scopes and progress events. +- `ports`: capabilities required by application workflows, implemented by inbound or outbound adapters. +- `services`: reusable orchestration such as signer resolution and transaction pipeline. +- `use-cases`: wallet/config and family-specific workflows. + +Application services consume ports such as `WalletRepository`, `ChainGatewayProvider`, +`LedgerDevice`, `TokenRepository`, `PriceProvider`, `NetworkRegistry`, `PromptPort` and +`BackupWriter`. They never construct or import filesystem, TronWeb, Ledger transport, CoinGecko, +Zod, yargs, CLI rendering or output-envelope implementations. + +### Inbound CLI adapter + +Owns yargs, zod command schemas, help/catalog generation, TTY input, stream discipline, output +envelopes and human rendering. A command definition translates CLI input into a use-case call; it +does not implement persistence or provider transport. CLI-only contracts (`CommandDefinition`, +`ExecutionContext`, globals, session and output envelopes) live under `inbound/cli/contracts`. + +### Outbound adapters + +Implement application ports: + +- encrypted file keystore and atomic persistence; +- YAML configuration document; +- secure backup writer; +- TRON gateway and TronGrid history reader; +- Ledger device transport; +- token repository and price provider. + +### Bootstrap + +- `argv.ts`: pre-yargs global scan required to select output and secret channels. +- `composition.ts`: constructs process-scoped adapters and shared services. +- `runner.ts`: executes one invocation and owns the terminal error boundary. +- `family-registry.ts`: enabled plugin list and family-keyed projections. +- `families/*.ts`: family-specific gateway/signing/use-case/command composition. + +## 4. Command flow + +```mermaid +flowchart LR + ARGV[argv] --> SHELL[yargs shell] + SHELL --> DEF[CommandDefinition] + DEF --> TARGET[target + capability gates] + TARGET --> USECASE[application use case] + USECASE --> PORT[application port] + PORT --> ADAPTER[outbound adapter] + USECASE --> RESULT[typed result] + RESULT --> FORMAT[text or JSON formatter] + FORMAT --> STREAM[one terminal frame] +``` + +Zod remains the single source for command input shape, defaults, validation, help fields and JSON +Schema. Global flags remain table-driven. Business execution modes and confirmation behavior have +one implementation in application services, not duplicate command helpers. + +## 5. Transaction flow + +```mermaid +flowchart LR + RESOLVE[resolve signer] --> BUILD[build] + BUILD --> ESTIMATE[estimate] + ESTIMATE --> MODE{mode} + MODE -->|dry-run| PLAN[plan] + MODE -->|sign| SIGN[software or Ledger sign] + SIGN --> BROADCAST{broadcast?} + BROADCAST -->|no| SIGNED[signed transaction] + BROADCAST -->|yes| SUBMIT[submitted receipt] + SUBMIT --> CONFIRM{wait?} + CONFIRM --> FINAL[submitted / confirmed / failed] +``` + +Chain-specific builders and confirmation readers are provided by the family gateway. The shared +pipeline knows only signer and broadcaster ports. + +## 6. Chain-family extension + +A family plugin owns the concrete composition for one chain family: + +The complete EVM implementation order, public discovery requirements, network contract and +acceptance checklist are defined in [evm-development-plan.zh-TW.md](./evm-development-plan.zh-TW.md). + +```ts +interface FamilyPlugin { + meta: FamilyMeta & { family: F } + signStrategy: SignStrategy + createGateway(network: NetworkDescriptor): ChainGatewayMap[F] + createModule(dependencies: FamilyApplicationDependencies): ChainModule +} +``` + +Adding EVM requires: + +1. Add `evm` to `ChainFamily` and `FamilyMeta` facts. +2. Extend the discriminated `NetworkDescriptor` union. +3. Extend `ChainGatewayMap` with an `EvmGateway` port. +4. Implement EVM address codec, gateway and signing strategy. +5. Implement EVM use cases and CLI command module without changing TRON use cases. +6. Add `bootstrap/families/evm.ts` and one registry entry. +7. Add routing, signer, capability, output and contract tests. + +Only genuinely shared capabilities receive shared ports. TRON staking/resource methods and EVM +gas/nonce methods stay in their family gateways; no universal gateway may accumulate unrelated +chain operations. + +## 7. Behavioral invariants + +- JSON success and error use `wallet-cli.result.v1`. +- Usage errors exit 2; execution errors exit 1; success/meta requests exit 0. +- JSON stdout contains exactly one terminal frame. +- Progress and diagnostics use stderr. +- Unknown exceptions are redacted. +- A run consumes stdin through at most one secret channel. +- Secrets never enter logs, envelopes, argv or environment variables. +- Dry-run does not decrypt, sign or broadcast. +- Watch-only accounts never sign. +- Every persistent mutation is locked and atomically replaced. +- Already-broadcast transactions do not become command failures solely because confirmation times + out. + +## 8. Verification gates + +```bash +npm run typecheck +npm run depcruise +npm test +npm run build +npm run test:live:nile +``` + +`test:live:nile` exercises the live surface in an isolated wallet home and emits a raw log without +exposing the test secret. diff --git a/ts/docs/evm-development-plan.md b/ts/docs/evm-development-plan.md new file mode 100644 index 000000000..2fc00530b --- /dev/null +++ b/ts/docs/evm-development-plan.md @@ -0,0 +1,593 @@ +# wallet-cli EVM Support Development Plan + +> Status: to be implemented +> Applies to: `ts-refactor` +> Purpose: list the full scope of everything that must be modified, added, and accepted when extending the current TRON-only CLI to TRON + EVM. This document deliberately does not expand into the implementation details of functions and RPC payloads. + +## 1. Definition of Done + +EVM support cannot merely mean "can connect to an Ethereum RPC". When complete, all of the following must hold simultaneously: + +1. `evm` is a formal `ChainFamily`, no longer simulated by a type cast in tests. +2. It can resolve Ethereum mainnet `evm:1`, as well as any `evm:` added in the config file, e.g. `evm:11155111`, `evm:8453`, `evm:31337`. +3. An EVM network can be selected by canonical id or a unique alias, and can be set as `defaultNetwork`. +4. seed, private key, watch-only, and the Ledger Ethereum app all have explicit EVM behavior. +5. EVM has its own gateway, signing strategy, use cases, and CLI command module, and does not stuff EVM methods into the TRON gateway. +6. `wallet-cli --help`, family help, command help, `--json-schema`, and `wallet-cli networks` all let a user/agent discover EVM. +7. Text and JSON output correctly present EVM address, chain id, native currency, gas, fee, nonce, transaction hash, and receipt. +8. The existing TRON commands, output, secret handling, and exit-code contract remain unchanged. + +Development dependency order: + +```text +public contract → Domain → Persistence/Config → Application ports → EVM adapters + → EVM use cases → CLI commands → Bootstrap → Help/Output → Tests/Docs +``` + +### 1.1 The Real Code-Change Classification + +The "locations involved" that appear later in this document include modifications, references, and verification — it does not mean every file must change. Based on the current code, the actual classification is as follows. + +#### Definitely added + +- `application/ports/chain/evm-gateway.ts` +- `adapters/outbound/chain/evm/*` +- `application/use-cases/evm/*` +- `adapters/inbound/cli/commands/evm/*` +- `bootstrap/families/evm.ts` +- EVM confirmation, fixtures, unit/integration/live tests +- Explorer/history adapter (added only if EVM history/ABI metadata is to be provided) + +#### Definitely modified + +- Domain family/address/network/wallet/transaction types +- Config builtins, custom-network validation, and network display +- Keystore schema migration and the EVM-address backfill for old wallets +- `ChainGatewayMap` and the family plugin registry +- The file location of the generic gateway registry (currently misplaced in `chain/tron/provider.ts`) +- The outbound Ledger dispatcher (if Ledger EVM is exposed) +- Token builtin/normalization and the CoinGecko network mapping +- Root/family/merged command help, the global `--network` description, and the wallet help examples +- The family renderer, EVM transaction/fee text output +- Dependencies, architecture docs, help/golden baselines + +#### In principle not modified, only adding EVM tests to verify reusability + +- `application/services/transaction-mode.ts` +- `application/services/pipeline/index.ts` +- `application/services/signer/index.ts` +- `application/services/signer/software.ts` +- `application/services/signer/ledger.ts` +- `application/services/target/index.ts` +- `application/contracts/execution-policy.ts` +- `application/contracts/execution-scope.ts` +- `application/ports/chain/broadcaster.ts` +- `application/ports/ledger-device.ts` +- `application/ports/network-registry.ts` +- `application/ports/token-repository.ts` +- `application/ports/price-provider.ts` +- `adapters/inbound/cli/registry/index.ts` +- `adapters/inbound/cli/shell/index.ts` +- `adapters/inbound/cli/command-id.ts` +- `adapters/inbound/cli/context/index.ts` +- `adapters/inbound/cli/arity/index.ts` +- `adapters/inbound/cli/output/envelope.ts` +- stream, secret, prompt, and output formatter infrastructure + +#### Modified only if a public decision requires it + +- `CapabilityRegistry` and bootstrap capability composition: needed only if history/indexer, legacy/EIP-1559, etc. require per-network gating. +- Help catalog: EVM commands enter the catalog automatically via the registry; modification is needed only to add a families/networks summary at the top level of the catalog. +- `WalletService`: the existing family detection and repository delegation are reusable; usually only tests are needed, and migration should stay in the persistence adapter. +- Shared transaction types/pipeline: types must be extended with display fields, but the pipeline control flow is in principle unchanged. + +If the implementation must modify one of the above "in principle not modified" modules, you should first point out which family-neutral capability the current abstraction lacks; you must not add `if (family === "evm")` just because the EVM adapter is awkward to write. + +## 2. Public Decisions to Lock Before Development + +The following decisions must be written into the architecture contract first, otherwise help, the network schema, the command schema, and the tests will be revised repeatedly. + +### 2.1 Network identity + +Network aliases are deferred while selector input accepts canonical ids only. Alias-related requirements below remain future work for when that feature is restored. + +- The EVM canonical network id is fixed as `evm:`. +- The `xxx` in `evm:xxx` is the EIP-155 chain id, not a name; the actual value must be a positive-integer string. +- Minimum builtin networks: + - `evm:1`, with recommended aliases `eth`, `ethereum`. + - One public testnet, recommended `evm:11155111`, alias `sepolia`. +- Other networks are added via `config.yaml`; whether to additionally build in Base, Polygon, BSC, etc. is a product decision. +- An alias must be globally unique; a duplicate alias must keep the `ambiguous_network_alias` error. +- The chain id reported by RPC must match the configured value; the mismatch must not be ignored before signing or broadcast. +- `defaultNetwork` is still recommended to remain `tron:mainnet`, unless there is a separate product migration decision. + +The recommended public shape of a custom network: + +```yaml +defaultNetwork: evm:1 +networks: + evm:1: + family: evm + chainId: "1" + aliases: [eth, ethereum] + rpcUrl: https://example.invalid + feeModel: eip1559 + nativeCurrency: + name: Ether + symbol: ETH + decimals: 18 + + evm:31337: + family: evm + chainId: "31337" + aliases: [local] + rpcUrl: http://127.0.0.1:8545 + feeModel: eip1559 + nativeCurrency: + name: Ether + symbol: ETH + decimals: 18 +``` + +### 2.2 The First-Version Command Surface + +The same logical path selects the TRON or EVM implementation via `--network`. A separate top-level `ethereum ...` execution grammar must not be created for EVM. + +| Command group | First-version EVM requirement | Notes | +| --- | --- | --- | +| wallet lifecycle | supported | create/import/derive show the EVM address afterward; watch can recognize `0x...`. | +| `account balance` | supported | uses the network native currency. | +| `account info` | supported | EVM semantics are defined by the EVM use case, not by imitating the TRON resource fields. | +| `account history` | explicit decision | standard JSON-RPC has no address history; an explorer/indexer adapter is required, otherwise it must not claim availability in the help/capability of an unsupported network. | +| `account portfolio` | supported | native coin + ERC-20 token book + price provider. | +| `token add/list/remove/balance/info` | supported | the EVM token kind is `erc20`. | +| `tx send/broadcast/status/info` | supported | native/ERC-20, legacy/EIP-1559, raw signed tx. | +| `contract call/send/deploy/info` | supported or explicitly degraded | if `contract info` needs ABI metadata, there must be an explorer source; with only bytecode, the help must describe it truthfully. | +| `message sign` | supported | software and Ledger behavior are consistent. | +| `block` | supported | latest or a specified block. | +| `stake ...` | no EVM implementation | stays TRON-only; root/family help must mark it. | + +### 2.3 SDK and Hardware Wallet + +- Select a single EVM SDK; the current architecture recommends `viem`, added to production dependencies. +- If full Ledger EVM support is claimed, add an Ethereum Ledger app adapter and the corresponding package (e.g. `@ledgerhq/hw-app-eth`). +- If the first version does not do Ledger EVM, `FAMILIES.evm.ledger`, `import ledger --help`, and the README must not show the Ethereum app. + +## 3. Phased Development Checklist + +### Phase 0: Contract and Test Baseline + +- [ ] Confirm the network id, builtin networks, command matrix, Ledger, and history/indexer decisions in Section 2. +- [ ] Build EVM address, transaction, receipt, legacy fee, EIP-1559 fee, ERC-20, block, and RPC error fixtures. +- [ ] Establish a new multichain help/golden baseline; the existing TRON-only help parity must no longer serve as the sole truth for the entire root help. +- [ ] Define the upgrade strategy and rollback behavior for the old `wallets.json` version. + +### Phase 1: Domain + +Locations involved: + +- `src/domain/family/index.ts` +- `src/domain/address/index.ts` +- `src/domain/types/network.ts` +- `src/domain/types/tx.ts` +- `src/domain/types/token.ts` +- `src/domain/types/wallet.ts` +- `src/domain/sources/index.ts` +- `src/domain/derivation/index.ts` +- `src/domain/wallet/index.ts` +- `src/domain/errors/index.ts` + +Work list: + +- [ ] Add `evm` to `ChainFamily` and `FAMILIES`. +- [ ] Register the EVM BIP44 coin type `60`, smallest unit `wei`, address codec, and Ledger metadata. +- [ ] Add EVM address derive, validate, normalize/checksum rules and tests. +- [ ] Restore `NetworkDescriptor` to a discriminated union keyed by `family`. +- [ ] Add `EvmNetworkDescriptor`: `rpcUrl`, decimal chain id, fee model, native currency, and optional explorer/history config. +- [ ] Verify that the canonical id's family and chain id are consistent with the descriptor. +- [ ] Extend the transaction view: gas, gas price, max fee, priority fee, effective gas price, nonce, EVM receipt/status, and other fields. +- [ ] Remove TRON-only naming assumptions from the shared view; when keeping backward-compatible TRON JSON fields, document them explicitly. +- [ ] Confirm the family constraint of the `erc20` token kind and contract-address normalization. +- [ ] Update seed/private-key address derivation, dedup, account projection, and family-detection tests. +- [ ] Complete typed errors for network/chain mismatch, invalid chain id, invalid EVM address, etc. + +### Phase 2: Wallet Persistence and Migration + +Locations involved: + +- `src/adapters/outbound/keystore/index.ts` +- `src/domain/types/wallet.ts` +- `src/domain/wallet/index.ts` +- `src/application/ports/account-store.ts` +- `src/application/ports/wallet-repository.ts` +- `src/application/use-cases/wallet-service.ts` +- `src/adapters/outbound/persistence/backup-writer.ts` +- wallet/keystore tests and migration fixtures + +Work list: + +- [ ] Raise the `wallets.json` schema version. +- [ ] Handle existing seed/private-key wallets that have only a TRON cached address; adding `evm` to `ChainAddresses` must not invalidate old files. +- [ ] Define the timing for the EVM address's lazy backfill / explicit migration, and the UX when a master password is needed. +- [ ] Ensure migration uses an atomic write and lock, and a failure does not corrupt the original file. +- [ ] Newly created and newly imported seed/private-key wallets generate both a TRON and an EVM address. +- [ ] `derive` for a new account generates addresses for both families. +- [ ] watch-only automatically recognizes TRON/EVM; an EVM address is normalized before storage. +- [ ] Ledger/watch remain single-family sources. +- [ ] list/current/use/rename/delete/backup and address lookup support EVM addresses. +- [ ] Backup metadata includes both known TRON/EVM addresses; for an old wallet not yet backfilled, the field must not be fabricated. +- [ ] Verify behavior for the same private key, a different BIP44 seed path, old-data migration, and dedup. + +### Phase 3: Config and NetworkRegistry + +Locations involved: + +- `src/adapters/outbound/config/builtins.ts` +- `src/adapters/outbound/config/index.ts` +- `src/adapters/outbound/config/yaml-config-document.ts` +- `src/application/ports/network-registry.ts` +- `src/application/use-cases/config-service.ts` +- `src/adapters/inbound/cli/commands/config.ts` +- `src/adapters/inbound/cli/commands/network.ts` + +Work list: + +- [ ] Add the `evm:1` and the chosen-testnet builtin descriptors. +- [ ] Apply runtime schema validation to a user-defined network, no longer casting directly to `NetworkDescriptor`. +- [ ] Support any valid `evm:`, and reject inconsistencies among `family`, id, and chain id. +- [ ] Validate `rpcUrl`, native currency, fee model, aliases, and the optional indexer/explorer fields. +- [ ] When alias support is restored, `NetworkRegistry.resolve()` supports the EVM canonical id, case-insensitive aliases, and the ambiguity check. +- [ ] `config defaultNetwork evm:1`, alias setting, and persistence work. +- [ ] `wallet-cli networks` displays builtin and custom EVM networks, the native symbol, and a safe summary of RPC/fee model/capabilities. +- [ ] Do not leak any API key that the RPC URL may contain in ordinary output; a redaction rule is needed. +- [ ] The network RPC client verifies the remote chain id on first use. + +### Phase 4: Application Ports and Shared Services + +Locations involved: + +- `src/application/ports/chain/evm-gateway.ts` (added) +- `src/application/ports/chain/gateway-provider.ts` +- `src/application/services/evm-confirmation.ts` (added) +- `src/application/services/capability/index.ts` (modified only if per-network capability is needed) + +The following shared modules are expected to only gain EVM reuse tests, with no production-code change: `Broadcaster`, `LedgerDevice`, +`TxPipeline`, `SignerResolver`, software/device signer, `TargetResolver`, `transactionMode`. + +Work list: + +- [ ] Define `EvmGateway`, holding only the read/build/estimate/broadcast capabilities EVM needs. +- [ ] Add `evm` to `ChainGatewayMap`, preserving the typed family lookup. +- [ ] Use EVM fixtures to verify that the shared `Broadcaster`, `TxPipeline`, `Signer`, and `SignStrategy` can carry an EVM transaction directly; the control flow is expected to be unchanged. +- [ ] Implement EVM confirmation normalization, preserving the existing contract of returning a submitted receipt after a `--wait` timeout. +- [ ] Support the legacy and EIP-1559 fee models; a network-specific trait must not be misjudged by a family-wide command capability as supported by all EVM networks. +- [ ] Adjust the capability registration to distinguish "the family has this command" from "this network has an indexer / EIP-1559 / etc. capability". +- [ ] Preserve the invariants: watch-only cannot sign, a wrong-family account is blocked, and dry-run does not decrypt the private key. + +### Phase 5: EVM Outbound Adapters + +Recommended new locations: + +```text +src/adapters/outbound/chain/evm/ +├── index.ts +├── provider.ts +├── evm.ts +├── signing-strategy.ts +├── evm-responses.ts +└── history-reader.ts # added only if history/indexer support is decided +``` + +Work list: + +- [ ] Implement a per-network EVM JSON-RPC client/gateway. +- [ ] Implement native balance, nonce/code, block, transaction, receipt, and fee/gas reads. +- [ ] Implement native/ERC-20 transfer, contract call/send/deploy, estimate, and raw transaction broadcast. +- [ ] Implement software transaction signing and personal-message signing. +- [ ] Validate every RPC response before normalizing, to avoid passing a provider-specific shape into a use case. +- [ ] Uniformly handle errors such as revert reason, replacement/nonce, insufficient funds, underpriced fee, chain mismatch, and timeout. +- [ ] If supporting account history / ABI metadata, add a separate explorer/indexer adapter; do not pretend standard JSON-RPC can provide it. +- [ ] Add EVM adapter unit tests with a mocked transport, not relying on a public RPC. + +### Phase 6: Ledger EVM Adapter + +Locations involved: + +- `src/adapters/outbound/ledger/index.ts` or split into family-specific device adapters +- `src/application/ports/ledger-device.ts` +- `src/application/services/signer/ledger.ts` +- `src/application/services/ledger-account.ts` +- `src/adapters/inbound/cli/commands/wallet.ts` +- `src/bootstrap/composition.ts` +- package dependencies and tsup bundling config + +Work list: + +- [ ] Add Ethereum Ledger app transport, address, transaction signing, and message signing. +- [ ] The EVM derivation path uses coin type 60, and supports the existing flow for index/path/address scan. +- [ ] `import ledger --app ethereum` appears in the schema, help, interactive choices, and tests. +- [ ] The precheck compares the device address with the cached address. +- [ ] Classify user rejection, wrong app, locked device, wrong seed, and transport error. +- [ ] Update the tsup `noExternal` / native addon config and the Ledger emulator/real-device verification. + +### Phase 7: EVM Application Use Cases + +Recommended new locations: + +```text +src/application/use-cases/evm/ +├── account-service.ts +├── token-service.ts +├── transaction-service.ts +├── contract-service.ts +└── block-service.ts +``` + +Work list: + +- [ ] Implement EVM account balance/info/portfolio; handle history per the Section 2 decision. +- [ ] Implement ERC-20 metadata, balance, and token book workflows. +- [ ] Implement native/ERC-20 send, signed raw tx broadcast, status/info. +- [ ] Implement the agreed first-version semantics of contract call/send/deploy/info. +- [ ] Implement EVM block query. +- [ ] Reuse `MessageService`, `TxPipeline`, `TransactionMode`, the token repository, and the price port; do not reuse a use case carrying TRON semantics. +- [ ] All returned shapes use a family-aware, stably-emittable normalized view. + +### Phase 8: EVM CLI Command Module + +Recommended new locations: + +```text +src/adapters/inbound/cli/commands/evm/ +├── index.ts +├── account.ts +├── token.ts +├── tx.ts +├── contract.ts +├── message.ts +└── block.ts +``` + +Shared locations involved: + +- `src/adapters/inbound/cli/commands/shared.ts` +- `src/adapters/inbound/cli/schemas/index.ts` +- `src/adapters/inbound/cli/arity/index.ts` +- `src/adapters/inbound/cli/registry/index.ts` +- `src/adapters/inbound/cli/shell/index.ts` +- `src/adapters/inbound/cli/context/index.ts` +- `src/adapters/inbound/cli/command-id.ts` + +Work list: + +- [ ] Each EVM command registers `family: "evm"`, the logical path, capability, requirements, Zod fields, examples, and formatter. +- [ ] EVM address, hash, hex data, ABI, quantity, gas, fee, nonce, and block identifier use an EVM-specific schema. +- [ ] `tx send` handles both native and ERC-20, but does not expose TRC10/TRC20 flags. +- [ ] The legacy/EIP-1559 command flags, mutual-exclusion conditions, and defaults are driven by a single schema that drives both help and JSON Schema. +- [ ] EVM does not register `stake` commands. +- [ ] Logical routing selects the EVM implementation via `--network evm:`; the same path for TRON/EVM does not pollute each other's fields or examples. +- [ ] The command id is stable as `evm.`, e.g. `evm.tx.send`. + +### Phase 9: Bootstrap and Family Composition + +Locations involved: + +- `src/bootstrap/families/evm.ts` (added) +- `src/bootstrap/families/types.ts` +- `src/bootstrap/family-registry.ts` +- `src/bootstrap/composition.ts` +- `src/bootstrap/runner.test.ts` +- `src/adapters/outbound/chain/tron/provider.ts` (may be renamed to a family-neutral gateway-registry location) + +Work list: + +- [ ] Build the `evmFamily` plugin: meta, gateway factory, sign strategy, use cases, command module. +- [ ] Add `evmFamily` to `FAMILY_REGISTRY`. +- [ ] `familyMap()` is complete for both TRON/EVM factories and signing strategies. +- [ ] The gateway cache is still isolated by canonical network id, not sharing a client across different chains. +- [ ] Capability composition is produced correctly from family commands + per-network traits. +- [ ] Bootstrap tests expect the enabled families to be `tron`, `evm`, and verify the command registration of both. + +### Phase 10: Help, Discovery, and Machine Catalog + +This phase is a necessary condition for publicly supporting EVM; it must not be treated as a documentation wrap-up. + +Locations involved: + +- `src/adapters/inbound/cli/help/index.ts` +- `src/adapters/inbound/cli/help/catalog.ts` +- `src/adapters/inbound/cli/globals/index.ts` +- `src/adapters/inbound/cli/registry/index.ts` +- `src/adapters/inbound/cli/commands/network.ts` +- help/golden tests and baselines + +Must support and test: + +- [ ] `wallet-cli --help` + - Shows the supported families: TRON, EVM. + - Explains that the command implementation is selected by `--network`. + - Has at least one `--network evm:1` example. + - `stake` is clearly marked TRON-only. +- [ ] `wallet-cli evm --help` + - Shows the EVM-available command tree, without `stake`. + - If the `evm` prefix is used only for help/catalog discovery and cannot be used for ordinary execution, it must say so explicitly in the output. +- [ ] `wallet-cli evm tx send --help` + - Shows only EVM fields, EVM examples, the fee-model explanation, and the EVM address format. +- [ ] `wallet-cli tx send --help` + - The merged logical help must clearly mark family-specific flags/examples; it must not just take the metadata of the registry's first family. +- [ ] `wallet-cli tx send --network evm:1 --help` + - Meta parsing must correctly consume the `--network` value and parse it into EVM help; it must not treat `evm:1` as a command positional. +- [ ] `wallet-cli networks --help` + - Explains the canonical id `evm:`, aliases, and the source of custom networks. +- [ ] `wallet-cli --json-schema` + - The full catalog includes the `evm.*` commands. + - The top level of the catalog should add an enabled-families and available-networks summary. +- [ ] `wallet-cli evm --json-schema` + - Emits only EVM chain commands; the schema and examples contain no TRON-only flags. +- [ ] `--json-schema` for each EVM leaf + - The input schema, requires, capability, examples, and stdin flags are correct. +- [ ] global `--network` description + - The examples include at least `tron:nile`, `evm:1`, an alias, and the config fallback. +- [ ] unknown/disabled family, unknown EVM network, and family/network mismatch all emit a clear usage error and exit 2. + +### Phase 11: Text and JSON Output + +Locations involved: + +- `src/adapters/inbound/cli/render/index.ts` +- `src/adapters/inbound/cli/render/scalars.ts` +- `src/adapters/inbound/cli/output/envelope.ts` +- `src/adapters/inbound/cli/contracts/envelope.ts` +- formatter/envelope/golden tests + +Work list: + +- [ ] Add EVM hooks to `FAMILY_RENDER`. +- [ ] Display the native amount per the network `nativeCurrency`, not assuming every EVM network is ETH. +- [ ] EVM transaction info/receipt shows hash, from/to/value, nonce, gas, fee, status, block, and contract address. +- [ ] Both legacy and EIP-1559 fee text render correctly. +- [ ] wallet/list/current/import/derive show both TRON and EVM addresses, and the address is not wrongly abbreviated or mislabeled by family. +- [ ] The `networks` text table shows the EVM chain id, native symbol, and fee model. +- [ ] The JSON envelope keeps `wallet-cli.result.v1` and emits: + - `command: "evm...."` + - `chain.family: "evm"` + - `chain.network: "evm:"` + - `chain.chainId: ""` +- [ ] All wei, gas, fee, nonce, and block quantities avoid JavaScript number precision loss; JSON uses a stable string rule. +- [ ] error, warning, and progress still obey the stdout/stderr and single-terminal-frame contract. + +### Phase 12: Token Book and Price Provider + +Locations involved: + +- `src/adapters/outbound/tokenbook/builtins.ts` +- `src/adapters/outbound/tokenbook/index.ts` +- `src/application/ports/token-repository.ts` +- `src/adapters/outbound/price/coingecko.ts` +- `src/application/ports/price-provider.ts` + +Work list: + +- [ ] Add official ERC-20 token entries for the chosen builtin EVM networks; a testnet may keep an empty list. +- [ ] The ERC-20 contract id uses a consistent normalized/checksummed comparison to avoid case-based duplicates. +- [ ] Confirm the token book scope is still `(networkId, accountRef)`, so different EVM chains do not share a list. +- [ ] The CoinGecko native-coin id and asset platform must not be derived from the `evm:` prefix alone; they must be decided by the actual network mapping/config. +- [ ] When a custom EVM network has no price mapping, return null/warning, and do not fail the portfolio command. +- [ ] token price lookup, official/user merge, remove protection, and portfolio tests cover EVM. + +### Phase 13: Tests and Quality Gates + +#### Unit tests + +- [ ] EVM address derive/validate/checksum. +- [ ] BIP44 coin type 60 and seed/private-key address derivation. +- [ ] network descriptor validation, canonical id, arbitrary chain id, aliases, RPC chain mismatch. +- [ ] wallet migration, backfill, dedup, watch/Ledger family pinning. +- [ ] EVM gateway RPC normalization and typed errors. +- [ ] software/Ledger transaction and message signing. +- [ ] legacy/EIP-1559 transaction build, estimate, broadcast, confirmation. +- [ ] ERC-20, contract, block, account, and portfolio use cases. +- [ ] EVM commands, registry routing, target/capability gates, renderers, envelopes. + +#### CLI/golden tests + +- [ ] root/family/group/leaf `--help`. +- [ ] root/family/leaf `--json-schema`. +- [ ] `networks` text + JSON include `evm:1` and custom `evm:31337`. +- [ ] `config defaultNetwork evm:1` and alias round trip. +- [ ] `--network evm:1` routes to an `evm.*` command id. +- [ ] The same logical command yields a different schema, client, and output under TRON/EVM. +- [ ] wrong-family account, unknown chain, alias collision, unsupported network trait. +- [ ] JSON one-frame, exit `0/1/2`, stderr progress, secret redaction. +- [ ] All old TRON golden tests still pass; the expected output of the root help switches to the new multichain baseline. + +#### Integration/live tests + +- [ ] Add a local EVM suite, recommended Anvil, covering account, native/ERC-20 send, contract, block, sign-only, broadcast, `--wait`. +- [ ] Add a public EVM testnet smoke suite, using an isolated wallet home and secret source, not logging the private key. +- [ ] Keep the Nile live suite, confirming the EVM changes cause no TRON regression. +- [ ] If supporting Ledger EVM, add Speculos or real-device smoke tests. + +#### Required commands + +```bash +npm run typecheck +npm run depcruise +npm test +npm run build +npm run test:parity:help +npm run test:live:nile +# new: EVM local integration suite +# new: EVM public-testnet smoke suite +``` + +### Phase 14: User Documentation and Release + +Locations involved: + +- `README.md` +- `docs/architecture.md` +- network/config example docs +- command/help baselines +- release notes and migration notes + +Work list: + +- [ ] Change the README to TRON + EVM, adding `evm:1`, custom chain, wallet, and send examples. +- [ ] Mark the architecture diagram and the family-extension section as EVM-implemented, no longer writing it as a future item. +- [ ] The docs list the builtin networks, canonical id, aliases, custom-network schema, and how to set `defaultNetwork`. +- [ ] Explain that the same wallet has different TRON/EVM derivation paths, and that watch/Ledger are single-family. +- [ ] Explain optional capabilities such as EVM history, contract metadata, price, and Ledger, and their network requirements. +- [ ] Provide the behavior of upgrading from the old wallets schema, the backup recommendation, and the failure-recovery method. +- [ ] Before release, run end-to-end acceptance once each with a brand-new home and an old-version home. + +## 4. Master Table of File Changes + +| Layer | Modified | Added | +| --- | --- | --- | +| Domain | family, address, network, wallet, tx, token, errors | EVM codec/types (may cohere within existing modules) | +| Application contracts/ports | gateway map, ledger/transaction contracts, capabilities | `evm-gateway.ts` | +| Application services | signer, pipeline, target, capability | `evm-confirmation.ts` | +| Application use cases | shared message/wallet integration | `use-cases/evm/*` | +| Outbound adapters | config, keystore, ledger, tokenbook, price, gateway registry | `chain/evm/*` | +| Inbound CLI | schemas, shell, registry, help, render, output, wallet/network commands | `commands/evm/*` | +| Bootstrap | family types, registry, composition, tests | `families/evm.ts` | +| Tooling | dependencies, tsup, test scripts, baselines | EVM local/live scripts and fixtures | +| Docs | README, architecture, network/config docs | migration/release notes | + +## 5. Unacceptable Shortcuts + +- Do not merely add `evm` to the union without handling the old-wallet address-cache migration. +- Do not type-cast a custom network directly in `ConfigLoader` without validation. +- Do not add EVM RPC methods into `TronGateway` or create a universal gateway that contains all chains' methods. +- Do not let all EVM networks automatically gain explorer, history, or EIP-1559 capability just because the family has the command. +- Do not let root help, leaf help, and JSON Schema still show only TRON examples. +- Do not hardcode all EVM native currencies as ETH. +- Do not convert a bigint fee/value into an unsafe JavaScript number. +- Do not leak an API key / private key in the RPC URL, an error, a verbose log, the JSON envelope, or a test artifact. +- Do not break TRON command ids, the JSON envelope, exit codes, or the stdout/stderr discipline by adding EVM. + +## 6. Final Acceptance Examples + +EVM may be declared publicly supported only when all of the following behaviors hold: + +```bash +wallet-cli --help +wallet-cli evm --help +wallet-cli evm tx send --help +wallet-cli tx send --network evm:1 --help +wallet-cli --json-schema +wallet-cli evm --json-schema + +wallet-cli networks +wallet-cli config defaultNetwork evm:1 +wallet-cli account balance --network evm:1 +wallet-cli account balance --network evm:31337 +wallet-cli tx send --network evm:1 --to 0x... --amount 0.01 +wallet-cli token balance --network evm:1 --contract 0x... +wallet-cli contract call --network evm:1 --contract 0x... ... +wallet-cli block --network evm:1 +wallet-cli message sign --network evm:1 --message hello +``` + +Here `evm:31337` must be providable by the user's config; it is not required that every chain id become a builtin network. diff --git a/ts/docs/typescript-wallet-cli-architecture-source-of-truth.md b/ts/docs/typescript-wallet-cli-architecture-source-of-truth.md new file mode 100644 index 000000000..6c13d906f --- /dev/null +++ b/ts/docs/typescript-wallet-cli-architecture-source-of-truth.md @@ -0,0 +1,723 @@ +# TypeScript Wallet CLI Architecture Specification (Source of Truth) + +```mermaid +flowchart LR + USER([User / Agent]):::ext + + subgraph INB["📥 inbound · CLI"] + IN["parse argv ▶
◀ render output"] + end + + subgraph CORE["Core (independently testable)"] + direction TB + APP["🎯 application
use cases · ports"] + DOM["💎 domain
pure rules"] + APP --> DOM + end + + subgraph OUTB["📤 outbound"] + OUT["Keystore · TronWeb
Ledger · CoinGecko"] + end + + USER ==>|drives| IN + IN ==>|calls| APP + APP ==>|"calls port"| OUT + OUT -.->|"implements port (dependency points inward)"| APP + IN ==>|renders result| USER + + classDef ext fill:#e8e8e8,stroke:#888,color:#333 + classDef inb fill:#d4edff,stroke:#1f78b4,color:#0b3d66 + classDef core fill:#d5f5e3,stroke:#27ae60,color:#145a32 + classDef outb fill:#fadbd8,stroke:#c0392b,color:#641e16 + class USER ext + class IN inb + class APP,DOM core + class OUT outb +``` + +> Status: the single current architecture contract +> Applies to version: `wallet-cli 0.1.x` +> Runtime: Node.js 20+, ESM, TypeScript +> Current chain support: TRON (mainnet, Nile, Shasta) + +This document fully defines the system boundaries, dependency direction, composition, command routing, application ports, wallet and transaction flows, persistence, output, and extension rules of the TypeScript Wallet CLI. The document itself is the single specification for architecture and behavior; it does not require any other design document to be understood. + +If the implementation and this document disagree, the change must fix one side or the other — the document must not describe an abstraction that does not exist for any length of time. + +--- + +## 1. System Goals and Boundaries + +### 1.1 Goals + +1. Provide humans and agents with the same stable CLI, JSON envelope, command id, and exit codes. +2. Keep Domain and Application as the core; isolate external I/O behind ports and adapters. +3. inbound CLI and outbound infrastructure are peers, assembled only in Bootstrap. +4. Keep chain-family differences inside the family plugin, family use cases, gateway, and signing strategy. +5. Encrypt private keys, mnemonics, and BIP39 passphrases at rest; Ledger/watch-only hold no secrets. +6. Each execution produces exactly one terminal result on stdout; progress and diagnostics go to stderr. +7. A single Zod schema drives validation, yargs arity, help, and JSON Schema. +8. Use dependency-cruiser, typecheck, contract tests, unit tests, and build to prevent architectural and behavioral regression. + +### 1.2 Current Boundaries + +- The only formal `ChainFamily` is currently `tron`; EVM is a planned but not-yet-public family. +- Ledger currently implements only the TRON app. +- Network transport is TRON FullNode HTTP / TronWeb; `httpEndpoint` is not an Ethereum JSON-RPC or gRPC endpoint. +- `create`, the various `import` commands, `delete`, and `backup` may be interactive in a controlled way; other commands fail fast when arguments are missing. +- Secrets are not accepted from argv plaintext or ordinary files; only a dedicated stdin channel or hidden TTY prompt is allowed. + +--- + +## 2. Architecture and Dependency Rules + +### 2.1 The Four Architectural Areas + +```mermaid +flowchart LR + BOOTSTRAP[bootstrap
process lifecycle and assembly] --> INBOUND[adapters/inbound
drives Application] + BOOTSTRAP --> OUTBOUND[adapters/outbound
implements Application ports] + INBOUND --> APPLICATION[application
use cases, orchestration, ports] + OUTBOUND --> APPLICATION + APPLICATION --> DOMAIN[domain
pure rules and values] +``` + +| Area | May depend on | Must not depend on | +| --- | --- | --- | +| `domain` | Node / third-party pure libraries, same area | `application`, `adapters`, `bootstrap` | +| `application` | `domain`, application-internal contracts/ports | `adapters`, `bootstrap` | +| `adapters/inbound` | `application`, `domain`, inbound-internal | `adapters/outbound`, `bootstrap` | +| `adapters/outbound` | `application` ports, `domain`, outbound-internal | `adapters/inbound`, `bootstrap` | +| `bootstrap` | all areas | none; but it only does assembly and process lifecycle | + +These are conceptual dependency rules. Even when a type-only import produces no runtime edge, it must still follow the same direction. Circular dependencies are always forbidden. + +The diagram below is a detailed view (dependency view) of the same rules. **Solid lines are the runtime call direction (left to right); dashed lines are the compile-time dependency/implements direction (always pointing inward).** Their opposite directions are exactly what dependency inversion looks like in concrete form: application calls outbound (rightward), but outbound depends on application's port (leftward). This diagram depicts responsibilities and dependencies, not the order of process execution; the real runtime entry/exit is wrapped by `bootstrap/runner.ts` (see §3.1). + +```mermaid +flowchart LR + USER([User / Agent]):::ext + + subgraph INB["inbound · CLI (driving side)"] + direction TB + IN_PARSE["Controller
shell · arity · Zod schemas"] + IN_CMD["commands
argv to use case"] + IN_OUT["Presenter
envelope · render · stream"] + IN_PARSE --> IN_CMD --> IN_OUT + end + + subgraph CORE["Core (independently testable)"] + direction TB + subgraph APP["application"] + direction TB + UC["use-cases
TronTransactionService · WalletService"] + SVC["services
TxPipeline · SignerResolver · Target"] + CON["contracts
ExecutionPolicy · TransactionScope"] + PORT{{"ports (owned by application)
WalletRepository · TronGateway · LedgerDevice"}} + UC --> SVC + UC -.in/out.-> CON + UC --> PORT + SVC --> PORT + end + subgraph DOM["domain (pure rules · zero I/O)"] + DM["address · amounts · derivation
wallet · family · errors"] + end + APP --> DOM + end + + subgraph OUTB["outbound (implements ports)"] + direction TB + O_KS["keystore to WalletRepository"] + O_TRON["chain/tron to TronGateway"] + O_PRICE["price to PriceProvider"] + O_LED["ledger to LedgerDevice"] + O_CFG["config · persistence · tokenbook"] + end + + subgraph EXT["Frameworks & Drivers"] + E["TRON nodes · filesystem
Ledger · CoinGecko"] + end + + %% call direction (runtime, solid) — L→R spine + USER ==>|drives| IN_PARSE + IN_CMD ==>|calls| UC + PORT ==>|resolved to adapter| OUTB + OUTB ==>|I/O| EXT + IN_OUT -. result .-> USER + + %% dependency / implements (compile-time, dashed, points inward) + O_KS -.->|implements| PORT + O_TRON -.->|implements| PORT + O_PRICE -.->|implements| PORT + O_LED -.->|implements| PORT + O_CFG -.->|implements| PORT + + %% bootstrap: bottom rail, injects upward + subgraph BOOT["bootstrap (single composition root)"] + direction LR + COMP["composition.ts
new + inject"] + PLUG["family-registry
FamilyPlugin (TRON · EVM later)"] + end + PLUG -.-> COMP + COMP -.->|inject| UC + COMP -.->|inject| O_KS + COMP -.->|inject| O_TRON + COMP -.->|wire| IN_CMD + + classDef ext fill:#e8e8e8,stroke:#888,color:#333 + classDef boot fill:#fff3cd,stroke:#d4a017,color:#5c4500 + classDef inb fill:#d4edff,stroke:#1f78b4,color:#0b3d66 + classDef app fill:#d5f5e3,stroke:#27ae60,color:#145a32 + classDef dom fill:#fdebd0,stroke:#e67e22,color:#7e3f0b + classDef outb fill:#fadbd8,stroke:#c0392b,color:#641e16 + + class USER,E ext + class COMP,PLUG boot + class IN_PARSE,IN_CMD,IN_OUT inb + class UC,SVC,CON,PORT app + class DM dom + class O_KS,O_TRON,O_PRICE,O_LED,O_CFG outb +``` + +### 2.2 Why inbound and outbound do not depend on each other + +A CLI command should not know about Keystore, TronWeb, CoinGecko, or the Ledger transport; it only calls a use case. An outbound adapter likewise should not know about Zod, yargs, the CLI envelope, or the renderer; it only implements an application port. The two are injected into the same object graph only in `bootstrap/composition.ts`. + +### 2.3 Actual Directory Responsibilities + +```text +src/ +├── index.ts # process entry +├── bootstrap/ +│ ├── argv.ts # global/secret flags scan before yargs +│ ├── runner.ts # invocation lifecycle + terminal error funnel +│ ├── composition.ts # the single general composition root +│ ├── family-registry.ts # enabled family plugins and familyMap +│ └── families/ +│ ├── types.ts # FamilyPlugin contract +│ └── tron.ts # TRON gateway/use cases/commands assembly +├── domain/ +│ ├── address/ amounts/ derivation/# pure value rules +│ ├── errors/ # typed errors + exit semantics +│ ├── family/ resources/ sources/ # exhaustive facts registries +│ ├── types/ # domain data shapes +│ └── wallet/ # account refs, address projections, vault codec +├── application/ +│ ├── contracts/ # execution policy/scope/progress +│ ├── ports/ # required external capabilities +│ ├── services/ # target/capability/signer/pipeline/confirmation +│ └── use-cases/ # wallet/config/message/TRON workflows +└── adapters/ + ├── inbound/cli/ + │ ├── commands/ # schema + use-case translation + │ ├── contracts/ context/ # CLI-only command/runtime contracts + │ ├── globals/ arity/ schemas/ # flag single source + Zod projections + │ ├── shell/ registry/ help/ # routing and discovery + │ ├── input/ # secret + prompt + │ └── output/ render/ stream/ # terminal presentation + └── outbound/ + ├── chain/tron/ # gateway, history, signing strategy + ├── config/ keystore/ # config and wallet persistence + ├── ledger/ # device adapter + ├── persistence/ # crypto, atomic FS, backup writer + ├── tokenbook/ # TokenRepository + └── price/ # PriceProvider +``` + +--- + +## 3. Startup, Composition, and Process Lifecycle + +### 3.1 Startup Flow + +```mermaid +flowchart LR + ARGV[process.argv] --> PRE[parseGlobals] + PRE --> COMPOSE[composeCliRuntime] + COMPOSE --> META{help/version/schema
or bare invocation?} + META -->|yes| HELP[HelpService] + META -->|no| SHELL[buildCli + parseAsync] + HELP --> FUNNEL[Runner terminal boundary] + SHELL --> FUNNEL + FUNNEL --> CLOSE[close Prompter] +``` + +1. `src/index.ts` only calls `main(process.argv)` and sets `process.exitCode`; it does not call `process.exit()`. +2. `bootstrap/argv.ts` scans globals before yargs, because the output mode and secret source must be decided first. +3. `composeCliRuntime()` loads config and builds streams, formatter, outbound adapters, application services/use cases, the command registry, and the target/capability gates. +4. `FAMILY_REGISTRY` assembles each family's metadata, sign strategy, gateway factory, and command module into a plugin. +5. The family plugin builds family-specific use cases and then injects them into the inbound `ChainModule`. +6. Command-backed capabilities are derived from the registry's `capability` field and merged with network traits. +7. A meta request short-circuits before building the yargs execution, but uses the same streams and error-output rules. +8. The Runner catches all typed/unknown errors, normalizes them, emits output, decides the exit code, and finally closes the `/dev/tty` handle. + +### 3.2 The `FamilyPlugin` Contract + +```ts +interface FamilyPlugin { + readonly meta: FamilyMeta & { family: F } + readonly signStrategy: SignStrategy + createGateway(network: NetworkDescriptor): ChainGatewayMap[F] + createModule(deps: FamilyApplicationDependencies): ChainModule +} +``` + +`bootstrap/families/tron.ts` is TRON's concrete composition: it builds the `TronRpcClient`, the TronGrid history reader, the TRON use cases, and the `TronModule`. Application and adapters must not import the family registry in reverse. + +--- + +## 4. Command Contract and Dispatch + +### 4.1 `CommandDefinition` + +`CommandDefinition` is the contract of the inbound CLI adapter, not a Domain/Application model. + +| Field | Contract | +| --- | --- | +| `path` | Neutral commands use the full path; chain commands use a cross-family logical path. | +| `family` | Omitted for neutral commands; when present, the resolved network selects the family implementation. | +| `stdin` | A dedicated stdin channel for `privateKey`, `mnemonic`, `tx`, `message`. | +| `network` | `none`, `optional`, `required`; today both optional/required can fall back to the default network. | +| `wallet` | `none` or `optional`; optional can override the active account with `--account`. | +| `auth` | An unlock declaration for help/catalog; actual software signing uses lazy decrypt. | +| `broadcasts` | Controls whether help reveals `--wait`. | +| `passwordMode` | `establish` or `verify`, controls interactive master-password priming. | +| `interactive` | Only commands that explicitly opt in may open a TTY prompt. | +| `capability` | A per-network capability that must pass before execution. | +| `fields` / `input` | Zod field metadata and the complete validation schema. | +| `run` | Translates CLI input/context into a use-case call and returns structured data. | +| `formatText` | Optional text renderer; JSON does not use it. | + +The stable command id is derived from metadata: a neutral command is `path.join(".")`, e.g. `import.mnemonic`; a chain command is `family.path`, e.g. `tron.tx.send`. + +### 4.2 The Two Command Classes and Routing + +```mermaid +flowchart LR + PATH[Parsed path] --> KIND{neutral exact match?} + KIND -->|yes| NEUTRAL[resolveNeutral] + KIND -->|no| CAND[resolveCandidates] + CAND --> NET[resolve explicit/default network] + NET --> FAMILY[choose candidate by network.family] + NEUTRAL --> EXEC[common executeCommand] + FAMILY --> EXEC +``` + +- `tron` is not a public prefix for ordinary execution commands; `--network` decides the family. +- Help/JSON Schema may use the family prefix to address a concrete implementation precisely. +- An unknown top-level/subcommand/flag must return `unknown_command` or `invalid_option`; yargs must not silently succeed. + +### 4.3 The Fixed Dispatch Order + +```mermaid +flowchart LR + ROUTE[Route] --> FLAGS[Reject unknown flags] + FLAGS --> TARGET[Resolve target] + TARGET --> CAP[Capability gate] + CAP --> PASSWORD[Optional password prime] + PASSWORD --> GAP[Optional TTY gap-fill] + GAP --> ZOD[Zod parse] + ZOD --> CTX[Build ExecutionContext] + CTX --> ACCOUNT[Resolve account if wallet-bound] + ACCOUNT --> RUN[Command → use case] + RUN --> FORMAT[Text or JSON] + FORMAT --> RESULT[Stream result exactly once] +``` + +`ExecutionContext` is the CLI context; an Application workflow receives only the narrower `ExecutionPolicy`, `ExecutionSelection`, `AccountScope`, or `TransactionScope`, and does not depend on the full picture of CLI streams/config/envelope. + +--- + +## 5. The Public Command Surface + +```text +wallet-cli +├── create +├── import mnemonic | private-key | ledger | watch +├── list | use | current | rename | derive | backup | delete +├── config | networks +├── account balance | info | history | portfolio +├── token balance | info | add | list | remove +├── tx send | broadcast | status | info +├── contract call | send | deploy | info +├── stake freeze | unfreeze | withdraw | cancel-unfreeze | delegate | undelegate +├── message sign +└── block [number] +``` + +Neutral commands do not touch a chain. Chain commands are currently all provided by the TRON plugin. All transaction-creating commands jointly support: + +- `--dry-run`: build + estimate, no decrypt, no sign, no broadcast. +- `--sign-only`: build + estimate + sign, returns a signed transaction. +- No mode flag: sign + broadcast. +- `--wait`: wait for confirmation only after broadcast. + +### 5.1 Global Flags + +| Flag | Runtime semantics | +| --- | --- | +| `--output` / `-o` | `text` or `json`; defaults from config. | +| `--network` | Canonical network id; a chain command falls back to `defaultNetwork` when omitted. | +| `--account` | Account ref/label/address; overrides only for this execution. | +| `--timeout` | Timeout for a single RPC/device operation. | +| `--verbose` / `-v` | Additional diagnostics. | +| `--wait` | Poll for confirmation after broadcast. | +| `--wait-timeout` | Upper bound for confirmation polling, default 60000 ms. | +| `--password-stdin` | Read the master password from fd 0. | +| `--help` / `--version` / `--json-schema` | Meta requests. | + +The single registration point for global flags is `adapters/inbound/cli/globals/GLOBAL_FLAG_SPECS`; the argv scan, yargs options, and help/catalog are all projected from it. + +--- + +## 6. Domain Model + +### 6.1 Wallet, Account, and Source + +```mermaid +flowchart LR + WALLET[Wallet wlt_x] --> SOURCE[One Source] + SOURCE -->|seed| HD[wlt_x.0 / .1 / ...] + SOURCE -->|privateKey| PK[one account wlt_x] + SOURCE -->|ledger| LEDGER[one single-family account] + SOURCE -->|watch| WATCH[one single-family account] +``` + +```ts +type Source = + | { type: "seed"; vaultId: string; addresses: Record } + | { type: "privateKey"; keyId: string; addresses: ChainAddresses } + | { type: "ledger"; family: ChainFamily; path: string; address: string } + | { type: "watch"; family: ChainFamily; address: string } +``` + +| Source | HD | Local secret | Family scope | Signing | +| --- | --- | --- | --- | --- | +| seed | yes | encrypted entropy/passphrase | all enabled families | software | +| privateKey | no | encrypted raw key | all enabled families | software | +| ledger | no | none | single family/path | device | +| watch | no | none | single family | forbidden | + +The account is the unit of selection and operation. `--account` accepts a canonical ref, a unique label, or a unique address; for a multi-account seed, when only a wallet ref is given, the index must not be guessed. + +### 6.2 Derivation and Addresses + +- BIP39 English wordlist; `create` generates 128-bit entropy (12 words). +- HD path: `m/44'/{coinType}'/{account}'/0/0`; the TRON coin type is 195. +- secp256k1 derives the address from an uncompressed 65-byte public key. +- The seed vault stores encrypted entropy and an optional BIP39 passphrase, not the mnemonic string directly. +- The public address cache lives in wallet metadata; read/build/estimate do not require decrypting secrets. +- The Domain `family`, `sources`, and `resources` registries must be exhaustively keyed; adding a union member forces the type system to fill in the related facts. + +### 6.3 Active Account + +- A successful `create`, `import`, or `derive` persistently makes its target account active. This + also applies when `import` or `derive` resolves to an existing account rather than creating a new + one. +- `use` persistently changes `activeAccount`; `--account` does not persist. +- When the active account is deleted, the first remaining account is chosen; if none, it is set to `null`. +- `current` returns only the persistent active account. + +--- + +## 7. Application: Use Cases, Services, and Ports + +### 7.1 Ports + +Application defines capabilities, not concrete technologies: + +| Port | Purpose | Current adapter | +| --- | --- | --- | +| `WalletRepository` / `AccountStore` | wallet/account query, mutation, decrypt | `Keystore` | +| `BackupWriter` | safely write a plaintext backup | `SecureBackupWriter` | +| `ConfigDocumentRepository` | atomic config document update | `YamlConfigDocument` | +| `NetworkRegistry` | canonical network id/default resolution | outbound config registry | +| `LedgerDevice` | address, tx/message signing, app config | `Ledger` | +| `ChainGatewayProvider` | obtain a gateway by network/family | `ChainGatewayRegistry` | +| `TronGateway` | TRON reads/build/estimate/broadcast | `TronRpcClient` | +| `TronHistoryReader` | TronGrid transaction history | `TronGridHistoryReader` | +| `TokenRepository` | official/user token book | `TokenBook` | +| `PriceProvider` | best-effort USD price | CoinGecko/Null provider | +| `PromptPort` | the minimal interaction capability Application needs | inbound Prompter | + +`PromptPort` is one of the few ports implemented by an inbound adapter and consumed by Application; this does not change the dependency direction, because Application owns only the interface. + +### 7.2 Use Cases + +- `WalletService`: create/import/list/use/current/rename/derive/delete/backup, with no knowledge of JSON/Zod/yargs. +- `ConfigService`: effective config view, key validation, canonical network normalization, and document update. +- `MessageService`: sign a message via the signer port. +- TRON use cases: account, token, transaction, contract, stake, block; they use only the TRON gateway and the necessary shared ports. + +An inbound command's responsibility is to turn argv/Zod input and `ExecutionContext` into use-case input and then choose a stable output view; it must not do persistence or provider transport itself. + +### 7.3 Reusable Services + +- `TargetResolver`: network selection and single-family account compatibility. +- `CapabilityRegistry`: per-network feature gate. +- `SignerResolver`: source → software/device signer. +- `TxPipeline`: shared build/estimate/sign/broadcast lifecycle. +- `transactionMode`: decides `dryRun`/`signOnly`/broadcast mode. +- `tronConfirmation`: TRON-specific polling/receipt normalization, not pushed into the generic pipeline. + +--- + +## 8. Network, Gateway, and Capability + +The current descriptor: + +```ts +interface TronNetworkDescriptor { + id: string + family: "tron" + chainId: string + aliases: string[] + httpEndpoint?: string + feeModel?: "tron-resource" + capabilities: string[] +} +``` + +| ID | Alias | Endpoint | +| --- | --- | --- | +| `tron:mainnet` | `tron` | `https://api.trongrid.io` | +| `tron:nile` | `nile` | `https://nile.trongrid.io` | +| `tron:shasta` | `shasta` | `https://api.shasta.trongrid.io` | + +Canonical-id resolution is case-insensitive. Aliases remain descriptor metadata but are not accepted as network selectors. Both `network: optional/required` adopt `config.defaultNetwork` when not specified, and that value must be a canonical id. Ledger/watch pin a single family, and a family mismatch must fail before any RPC. + +`ChainGatewayRegistry` is injected with the family factory by Bootstrap and caches the client by network id. Its generic `client()` may only use the truly common minimal capabilities; a family use case obtains the `TronGateway` via the guarded `get(net, "tron")`. TRON staking and the future EVM gas/nonce must not be forced into a universal gateway. + +Capabilities consist of two parts: the command-backed keys declared by registered commands, plus the network traits in `NetworkDescriptor.capabilities`. The gate must happen before the use case. + +--- + +## 9. Signer and Transaction Flow + +### 9.1 Signer Resolution + +```mermaid +flowchart LR + REF[account ref] --> SOURCE{source.type} + SOURCE -->|seed| SEED[lazy decrypt vault + derive] + SOURCE -->|privateKey| KEY[lazy decrypt key] + SOURCE -->|ledger| DEVICE[Ledger signer + path] + SOURCE -->|watch| REJECT[watch_only_no_signer] + SEED --> SOFT[SoftwareSigner + family strategy] + KEY --> SOFT +``` + +The software signer obtains the key only at the moment of an actual `sign()`; a dry-run does not trigger decryption. The Ledger signer verifies the app/address before signing, and if the cached address does not match the device it returns `wrong_device_seed`. + +### 9.2 Pipeline + +```mermaid +flowchart LR + RESOLVE[resolve signer] --> BUILD[build + timeout] + BUILD --> EST[estimate + timeout] + EST --> MODE{mode} + MODE -->|dry-run| PLAN[plan] + MODE -->|sign| SIGN[software / Ledger sign] + SIGN --> CAST{broadcast?} + CAST -->|no| SIGNED[signed] + CAST -->|yes| SEND[broadcast] + SEND --> WAIT{--wait?} + WAIT -->|no| SUB[submitted] + WAIT -->|yes| CONFIRM[family confirmation] + CONFIRM --> FINAL[confirmed / failed
or timeout → submitted] +``` + +The pipeline knows only the signer and the `Broadcaster` port; the family use case provides build, estimate, and confirm callbacks. `timeoutMs` limits a single operation; `waitTimeoutMs` limits confirmation polling. Once a transaction has been broadcast, a polling error/timeout must not reclassify the command as not-broadcast — it returns `submitted`. + +### 9.3 Ledger + +- When `SPECULOS_PORT` is present, use the Speculos HTTP transport; otherwise USB/HID. +- Transports and `hw-app-trx` are lazily imported and closed after each operation. +- The `m/` is stripped from the Ledger path before it is passed to the app. +- APDU `0x6985` → `signing_rejected`; an unavailable device/app/transport → `auth_required`. + +--- + +## 10. Persistence and Cryptography + +### 10.1 Root and Files + +The root uses a non-empty `WALLET_CLI_HOME` in order of preference, otherwise `$HOME/.wallet-cli`. + +```text +/ +├── config.yaml +├── wallets.json +├── tokens.json +├── verifier.json +├── vaults/vlt_.json +├── keys/key_.json +└── backups/-.json +``` + +`AtomicFileStore` writes use a unique temp file in the same directory, mode `0600`, and an atomic rename. Mutations are serialized with `.lock` + `O_EXCL`; a dead PID/stale lock can be reclaimed. + +### 10.2 `wallets.json` + +```json +{ + "version": 1, + "activeAccount": "wlt_abcd1234.0", + "wallets": [{ + "id": "wlt_abcd1234", + "source": { + "type": "seed", + "vaultId": "vlt_efgh5678", + "addresses": { "0": { "tron": "T..." } } + } + }], + "labels": { "wlt_abcd1234.0": "main" } +} +``` + +IDs are random 5-byte Crockford base32 lowercase strings. Labels are case-insensitively unique and must not begin with `wlt_`. The seed's known indices equal the `addresses` keys; Ledger/watch are deduplicated by source identity and are not merged with a software wallet that has the same address. + +### 10.3 Token and Config + +The user entries in `tokens.json` are partitioned by `|`; the effective list is official first, then user-only, deduplicated by `(kind,id)`. Official entries cannot be deleted/overwritten. + +`config.yaml` is shallow-merged with the builtin config. The only writable keys are `defaultNetwork`, `defaultOutput`, `timeoutMs`; `networks` is a CLI read-only view. Runtime globals are not written back to config. + +### 10.4 Encrypted Blobs + +`verifier.json`, vaults, and keys use scrypt (N=262144, r=8, p=1, dkLen=32), AES-128-CTR, and a `keccak256(derivedKey[16..31] + ciphertext)` MAC. Each blob has its own 32-byte salt and 16-byte IV but shares the keystore master password. A MAC mismatch returns `auth_failed`; the password is never written to disk. + +Backup is allowed only for seed/private-key; the plaintext secret file must be `0600` and must not overwrite an existing file; the terminal/envelope returns only metadata, not the secret. + +--- + +## 11. Secret and Interaction Policy + +```mermaid +flowchart LR + NEED[Secret needed] --> SOURCE{source} + SOURCE -->|--kind-stdin| STDIN[fd 0 once] + SOURCE -->|interactive TTY| HIDDEN[hidden prompt] + SOURCE -->|none| ERROR[missing_option / auth_required] +``` + +- A handler must not read `process.stdin` directly; `StreamManager.readStdinOnce()` reads at most once per execution. +- A single invocation may use fd 0 through only one `--*-stdin` channel. +- Secret argv, `MASTER_PASSWORD`, `--*-file`, and ordinary env secrets are not supported. +- A secret must not enter logs, diagnostics, error details, or the result envelope. +- Interactive allowlist: create, the four imports, delete, backup; the order is password → field gap-fill/account selection → command confirm. + +--- + +## 12. Output, Stream, and Error + +| Data | Text mode | JSON mode | +| --- | --- | --- | +| Successful terminal result | stdout once | one result envelope on stdout | +| Failed terminal result | stderr once | one error envelope on stdout | +| Progress | stderr | stderr JSON event | +| Warning | stderr/collected | `meta.warnings` | +| Debug | verbose stderr | verbose stderr | + +The JSON schema is fixed as `wallet-cli.result.v1`. A chain command envelope includes family, network id/name, and chain id; a neutral command omits chain. `bigint` is converted to a decimal string and `Uint8Array` to hex. A second terminal result must throw `internal_error`. + +Exit codes: success/meta = 0; execution error = 1; usage error = 2. An unknown exception is normalized into a redacted `internal_error`; the raw text of a third-party error must not enter the public envelope. + +--- + +## 13. Help and Machine-Readable Introspection + +Supports root/group/leaf help, version, the full catalog JSON Schema, and a single-command JSON Schema. The data flow: + +```mermaid +flowchart LR + ZOD[fields + input] --> ARITY[yargs arity] + ZOD --> HELP[help flags] + ZOD --> SCHEMA[JSON Schema] + DEF[CommandDefinition] --> HELP + DEF --> CATALOG[machine catalog] + GLOBAL[GLOBAL_FLAG_SPECS] --> ARITY & HELP & CATALOG +``` + +A hand-maintained command flag table must not be created separately. The public help/output is a stable contract; when it changes, automated tests must verify root, group, leaf, JSON Schema, and functional scenarios. + +--- + +## 14. Rules for Adding Features + +### 14.1 Adding a Command + +1. Decide whether it is a neutral or a family logical command. +2. Application first creates/extends the use case and the required ports. +3. Outbound capabilities implement the port with an adapter; the use case must not import the adapter. +4. The inbound command defines the Zod fields/input, policy metadata, use-case translation, and renderer. +5. Register it with the neutral registrar or the family `ChainModule`. +6. Add use-case, adapter, registry/dispatch, and output/help tests, and update the inventory in this document. + +A command is forbidden to build TronWeb/Keystore directly, write to process stdout, perform a filesystem mutation, or treat a provider wire response as the renderer's business model. + +### 14.2 Adding a Chain Family + +```mermaid +flowchart LR + DOMAIN[1 Domain family/address/network] --> PORT[2 family gateway port] + PORT --> ADAPTER[3 gateway + signing adapters] + ADAPTER --> USECASE[4 family use cases] + USECASE --> CLI[5 family CLI module] + CLI --> PLUGIN[6 bootstrap plugin + registry] + PLUGIN --> TEST[7 routing/output/contract tests] +``` + +Adding a family must extend `ChainFamily`/`FAMILIES`, the discriminated network/address types, `ChainGatewayMap`, the sign strategy, the gateway, use cases, commands, the family plugin, and networks/render/tests. Only a genuinely identical intent and I/O shape may be factored into a shared port; the TRON resource model and the EVM gas/nonce must remain separate. + +### 14.3 Adding a Wallet Source + +Synchronously update the `Source` union, `SOURCE_KINDS`, the import workflow, repository persistence/migration, dedup, signer resolution, cleanup, descriptor rendering, and tests. An unknown source must not fall into a silent default. + +--- + +## 15. Invariants That Must Be Maintained + +### 15.1 Architecture + +- Domain has no external I/O and does not depend on upper layers. +- Production Application does not import adapters/bootstrap. +- Inbound/Outbound adapters do not import each other. +- `bootstrap/composition.ts` is the single general composition root; family-specific composition lives in plugins. +- Application owns the ports; adapters implement the ports. +- No circular dependencies and no use of type-only imports to bypass a conceptual boundary. + +### 15.2 Behavior and Security + +- JSON stdout is exactly one terminal frame, schema `wallet-cli.result.v1`. +- The usage/execution/success exit codes are fixed at 2/1/0. +- Secrets do not enter argv/env/log/envelope; stdin uses at most one channel per execution, read once. +- Watch-only never signs; dry-run never decrypts, signs, or broadcasts. +- All persistent mutations are locked, and all replacement writes use an atomic rename. +- A broadcast transaction does not become a command failure because of a confirmation timeout. +- An unknown exception is redacted from the user. + +### 15.3 Verification Gates + +```bash +npm run typecheck +npm run depcruise +npm test +npm run build +``` + +When real TRON behavior is involved, additionally run `npm run test:live:nile` with an isolated wallet home; test secrets must not be logged or copied. An architectural change must at minimum pass typecheck, dependency-cruiser, unit tests, and build. + +--- + +## 16. Architectural Judgment Criteria + +When ownership is disputed, decide in order: + +1. No I/O, describes business values and invariants: Domain. +2. Describes what the product does or what external capability it needs: Application use case/service/port. +3. Turns terminal/argv/Zod into application input: Inbound CLI adapter. +4. Implements filesystem, HTTP, device, price, etc. as a port: Outbound adapter. +5. Chooses a concrete implementation and wires the object graph: Bootstrap. + +If a single module is parsing argv, calling a provider, writing a file, and rendering output all at once, the responsibilities have not yet been separated. The core standard is not the directory name, but whether dependencies point from the outside in, whether external details are replaceable, and whether the use case can be tested with only ports. diff --git a/ts/docs/wallet-cli-v1-failed-cases-retest-qa-report.md b/ts/docs/wallet-cli-v1-failed-cases-retest-qa-report.md new file mode 100644 index 000000000..797cfff8b --- /dev/null +++ b/ts/docs/wallet-cli-v1-failed-cases-retest-qa-report.md @@ -0,0 +1,118 @@ +# Wallet-CLI v1 失败用例复测补充报告 + +## 一、测试概要 + +| 项目 | 内容 | +|---|---| +| 测试对象 | Wallet-CLI TypeScript CLI,分支 feat/ts-version | +| 测试用例来源 | /Users/admin/Documents/wallet_cil/testcases/wallet-cli-v1-command-modules/Wallet-CLI-完整测试用例.md | +| 测试数据来源 | /Users/admin/Documents/wallet_cil/testcases/wallet-cli-v1-command-modules/wallet-cli-v1-test-data.env | +| 执行入口 | node dist/index.js | +| 执行网络 | tron:mainnet | +| 首轮执行目录 | /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/qa/execution-results/wallet-cli-v1-command-modules-no-ledger-20260702-144259 | +| 复测执行目录 | /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/qa/execution-results/wallet-cli-v1-failed-cases-retest-no-ledger-20260702-151024 | +| 复测时间 | 2026-07-02T07:13:16.264Z | +| 复测策略 | 仅复测首轮失败的 13 条用例;每条用例先执行 help,再执行实际命令;单条实际命令超时 120000ms;Ledger 仍按要求排除;敏感信息已脱敏 | +| 前置处理 | 复测使用独立 WALLET_CLI_HOME,执行器自动创建测试钱包、watch-only 账户并设置默认网络;未修改源码 | + +## 二、测试范围 + +包含:首轮失败的全局参数与帮助、通用命令、本地命令、Token、交易、合约、管理命令全局参数相关用例。 + +不包含:Ledger 硬件设备相关用例、首轮已通过用例、主网真实资产写入验证、主网广播/质押/合约写入正向提交验证。 + +## 三、执行结果汇总 + +| 指标 | 数量 | +|---|---:| +| 首轮失败用例 | 13 | +| 本轮纳入复测 | 13 | +| 排除用例 | 0 | +| 复测通过 | 1 | +| 复测失败 | 12 | +| 复测阻塞 | 0 | +| 复测通过率 | 7.69% | + +## 四、模块测试结果汇总 + +| 模块 | 用例总数 | 执行数 | 通过 | 失败 | 阻塞 | 通过率 | 是否通过 | 备注 | +|---|---:|---:|---:|---:|---:|---:|---|---| +| 全局参数与帮助 | 1 | 1 | 0 | 1 | 0 | 0.00% | 未通过 | 仍存在失败用例 | +| 通用命令 | 3 | 3 | 0 | 3 | 0 | 0.00% | 未通过 | 仍存在失败用例 | +| 本地命令 | 3 | 3 | 0 | 3 | 0 | 0.00% | 未通过 | 仍存在失败用例 | +| Token | 3 | 3 | 0 | 3 | 0 | 0.00% | 未通过 | 仍存在失败用例 | +| 交易 | 1 | 1 | 1 | 0 | 0 | 100.00% | 通过 | 复测通过 | +| 合约 | 1 | 1 | 0 | 1 | 0 | 0.00% | 未通过 | 仍存在失败用例 | +| 管理命令全局参数 | 1 | 1 | 0 | 1 | 0 | 0.00% | 未通过 | 仍存在失败用例 | + +## 五、详细测试结果 + +| 测试用例ID | 模块 | 子模块 | 测试场景 | 前置条件 | 执行命令 | 优先级 | 预期结果 | 实际结果 | 执行结论 | 用例类型 | 回報 | +|---|---|---|---|---|---|---|---|---|---|---|---| +| GLOB-017 | 全局参数与帮助 | 密码 stdin | 密码错误 | 已存在软件钱包 | printf '%s' "wrong-password" \| wallet-cli message sign --message "qa" --account "$QA_ACCOUNT" --password-stdin | P1 | 命令失败;返回 `wrong_password`;不返回签名 | 退出码=1;错误码=auth_failed;error code mismatch; expected one of wrong_password;输出=error [auth_failed]: incorrect master password | 失败 | 异常 | 預期結果偏差 實際結果正常| +| COM-015 | 通用命令 | import private-key | master password 错误后重试 | 已设置 master password;TTY 可交互 | wallet-cli import private-key --label qa-pk-retry | P1 | 第一次密码错误提示 `wrong_password` 或等价错误;允许重新输入;正确密码后继续导入 | 退出码=2;error code mismatch; expected one of wrong_password;输出=spawn node /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/dist/index.js import private-key --label qa-pk-retry ? Master password (hidden): warning: incorrect master password ? Master password (hidden): auth_failed | 失败 | 异常 | 沒復現 | +| COM-025 | 通用命令 | import watch | 当前版本不支持登记 EVM watch-only 地址 | 本地无同名 label | wallet-cli import watch --address "$QA_EVM_ADDRESS" --label qa-watch-evm | P2 | 命令失败;返回 `invalid_option`;不创建账户 | 退出码=2;错误码=invalid_value;error code mismatch; expected one of invalid_option;输出=error [invalid_value]: unrecognised address format: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 失败 | 异常 | 預期結果偏差 實際結果正常| +| COM-027 | 通用命令 | import watch | 地址格式非法 | 无 | wallet-cli import watch --address "abc123" --label qa-watch-invalid | P1 | 命令失败;返回 `invalid_option`;不创建账户 | 退出码=2;错误码=invalid_value;error code mismatch; expected one of invalid_option;输出=error [invalid_value]: unrecognised address format: abc123 | 失败 | 异常 | 預期結果偏差 實際結果正常| +| LOC-013 | 本地命令 | derive | 密码错误 | `$QA_SEED_ACCOUNT` 存在 | printf '%s' "wrong-password" \| wallet-cli derive --account "$QA_SEED_ACCOUNT" --label qa-derived-wrong --password-stdin | P1 | 命令失败;返回 `wrong_password`;不派生账户 | 退出码=1;错误码=auth_failed;error code mismatch; expected one of wrong_password;输出=error [auth_failed]: incorrect master password | 失败 | 异常 | 預期結果偏差 實際結果正常| +| LOC-019 | 本地命令 | backup | 密码错误 | `$QA_ACCOUNT` 为软件钱包 | wallet-cli backup --account "$QA_ACCOUNT" --out "$BACKUP_DIR/wrong-password.json" | P1 | 命令失败;返回 `wrong_password`;不生成文件 | 退出码=1;错误码=auth_failed;error code mismatch; expected one of wrong_password;输出=error [auth_failed]: incorrect master password | 失败 | 异常 | 預期結果偏差 實際結果正常| +| LOC-029 | 本地命令 | config | timeout 边界值 | 无 | wallet-cli config timeoutMs 0 | P2 | 当前实现允许非负数 timeoutMs;写入成功并返回配置结果 | 退出码=2;错误码=invalid_value;expected success but exit=2;输出=error [invalid_value]: timeoutMs must be a positive number | 失败 | 边界 | 預期結果偏差 實際結果正常| +| MGT-TOKEN-007 | Token | token add | 添加 TRC20 到地址簿 | 合约未添加 | wallet-cli token add --contract $QA_CONTRACT_TRC20 --network $NETWORK_TRON | P1 | 添加成功;随后 `token list` 可见 | 退出码=2;错误码=token_already_listed;expected success but exit=2;输出=error [token_already_listed]: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t is already an official token on tron:mainnet | 失败 | 正常 | 預期結果偏差 實際結果正常| +| MGT-TOKEN-008 | Token | token add | 重复添加 | 合约已添加 | wallet-cli token add --contract $QA_CONTRACT_TRC20 --network $NETWORK_TRON | P2 | 返回已存在或幂等成功,不产生重复项 | 退出码=2;错误码=token_already_listed;expected success but exit=2;输出=error [token_already_listed]: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t is already an official token on tron:mainnet | 失败 | 边界 | 預期結果偏差 實際結果正常| +| MGT-TOKEN-012 | Token | token remove | 移除用户 TRC20 | 已添加用户 token | wallet-cli token remove --contract $QA_CONTRACT_TRC20 --network $NETWORK_TRON | P1 | 移除成功;`token list` 不再展示用户项 | 退出码=2;错误码=token_is_official;expected success but exit=2;输出=error [token_is_official]: cannot remove an official token: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t | 失败 | 正常 | 預期結果偏差 實際結果正常| +| MGT-TX-007 | 交易 | tx send | 发送 TRC10 代币 | asset-id 有效 | wallet-cli tx send --to $QA_TRON_ADDRESS --amount 1 --asset-id $QA_ASSET_ID --network $NETWORK_TRON --dry-run | P1 | 返回 TRC10 transfer plan | 退出码=0;expected success observed;输出=⏳ Dry run tx send Fee TRC10 transfer uses bandwidth only Tx bddf4e6302...56921342 | 通过 | 正常 | 預期結果偏差 實際結果正常| +| MGT-CONTRACT-010 | 合约 | contract deploy | 带 constructor 参数部署 | ABI/bytecode 有效 | wallet-cli contract deploy --abi "$QA_ABI_JSON" --bytecode $QA_BYTECODE_HEX --fee-limit 100000000 --constructor-sig 'constructor(uint256)' --params '[{"type":"uint256","value":"1"}]' --network $NETWORK_TRON --dry-run | P1 | constructor 参数编码正确,返回部署预览 | 退出码=2;错误码=invalid_option;expected success but exit=2;输出=error [invalid_option]: unknown option(s): --constructor-sig, --constructor-sig | 失败 | 正常 | | +| MGT-GLOBAL-003 | 管理命令全局参数 | global | timeout 设置 | 网络可访问 | wallet-cli account info --account $QA_ACCOUNT --network $NETWORK_TRON --timeout 1 | P2 | 当前查询在 1ms 参数下仍可能快速成功;命令不应卡死,成功或 timeout 均需稳定返回 | 退出码=1;错误码=rpc_error;expected success but exit=1;输出=error [rpc_error]: TRON getAccount failed: The operation was aborted due to timeout | 失败 | 边界 | 預期結果偏差 實際結果正常 | + +## 六、问题清单 + +| 问题ID | 严重级别 | 问题描述 | 影响用例数 | 关联用例 | 影响说明 | 修复建议 | +|---|---|---|---:|---|---|---| +| RETEST-BUG-001 | 中 | 密码错误场景错误码仍为 auth_failed,与用例期望 wrong_password 不一致 | 4 | GLOB-017, COM-015, LOC-013, LOC-019 | 错误码契约仍未对齐,调用方无法按 wrong_password 分支处理。 | 统一产品错误码为 wrong_password,或将 PRD/用例预期调整为 auth_failed 并补充 message 断言。 | +| RETEST-BUG-002 | 中 | watch 地址格式异常错误码仍为 invalid_value,与预期 invalid_option 不一致 | 2 | COM-025, COM-027 | 参数校验错误分类仍不一致。 | 明确地址格式错误归类,并同步 CLI 实现、help 文档与测试用例。 | +| RETEST-BUG-003 | 中 | 配置 timeoutMs=0 被实现拒绝,与边界用例预期不一致 | 1 | LOC-029 | 配置边界规则不一致,当前实现要求 timeoutMs 为正数。 | 若产品定义允许 0,需放宽校验;若不允许,更新用例预期为 invalid_value。 | +| RETEST-BUG-004 | 中 | 官方 Token 地址簿添加/删除行为与用例预期不一致 | 3 | MGT-TOKEN-007, MGT-TOKEN-008, MGT-TOKEN-012 | 官方 Token 重复添加返回 token_already_listed,删除官方 Token 返回 token_is_official,正向用例仍失败。 | 确认官方 Token 是否允许用户重复添加/删除;按产品规则修订用例或调整实现为幂等成功。 | +| RETEST-BUG-005 | 高 | contract deploy constructor 参数命令契约不一致 | 1 | MGT-CONTRACT-010 | 当前 CLI 不支持 --constructor-sig,带 constructor 参数部署 dry-run 无法通过。 | 补齐 --constructor-sig 兼容参数,或将用例改为当前 CLI 支持的 constructor 参数表达方式。 | +| RETEST-BUG-006 | 中 | 1ms timeout 链上查询仍返回 rpc_error,边界行为未满足用例预期 | 1 | MGT-GLOBAL-003 | 极短 timeout 下行为稳定失败,不能满足“成功或稳定 timeout”预期中的成功分支。 | 明确 1ms timeout 的标准预期;建议用例接受 timeout/rpc_error,或实现返回统一 timeout 错误码。 | + +## 七、复测通过清单 + +| 用例ID | 模块 | 子模块 | 测试场景 | 说明 | +|---|---|---|---|---| +| MGT-TX-007 | 交易 | tx send | 发送 TRC10 代币 | 由失败转为通过 | + +## 八、补充质量门禁 + +| 检查项 | 结果 | 说明 | +|---|---|---| +| npm run build | 通过 | 本轮复测前已完成构建,dist/index.js 可执行 | +| npm run typecheck | 通过 | 上轮完整测试后已执行 tsc --noEmit,退出码 0 | +| npm test | 未通过 | 上轮验证中存在 crypto、keystore、wallet create、golden backup 相关超时失败,本轮未重复执行单元测试 | +| Ledger 专项 | 未执行 | 按用户要求排除 Ledger/硬件钱包相关用例 | + +## 九、Ledger 排除清单 + +| 用例ID | 子模块 | 场景 | 处理说明 | +|---|---|---|---| +| COM-017 | import ledger | 按 index 导入 TRON Ledger 账户 | 需 Ledger 硬件/设备确认,本轮按范围排除 | +| COM-018 | import ledger | 按 path 导入 Ledger | 需 Ledger 硬件/设备确认,本轮按范围排除 | +| COM-019 | import ledger | 按地址扫描导入 | 需 Ledger 硬件/设备确认,本轮按范围排除 | +| COM-020 | import ledger | 导入 EVM Ledger 账户 | 需 Ledger 硬件/设备确认,本轮按范围排除 | +| COM-021 | import ledger | index/path 互斥 | Ledger 模块整体排除,参数校验可后续单独补测 | +| COM-022 | import ledger | scan-limit 缺少 address | Ledger 模块整体排除,参数校验可后续单独补测 | +| COM-023 | import ledger | 设备未连接 | 需 Ledger 硬件/设备状态,本轮按范围排除 | + +## 十、结论建议 + +- 本轮失败用例复测未通过:13 条首轮失败用例中 1 条复测通过,12 条仍失败。 +- 已恢复/转绿用例:MGT-TX-007,本轮 TRC10 dry-run 查询未再出现 RPC 429,判定通过。 +- 仍需修复或对齐的重点:错误码契约(wrong_password/auth_failed、invalid_option/invalid_value)、timeoutMs=0 边界定义、官方 Token 地址簿幂等规则、contract deploy constructor 参数兼容、1ms timeout 错误码规范。 +- 建议修复后继续按本报告 12 条失败用例做定向复测,再执行完整非 Ledger 回归确认无连带回归。 + +## 十一、报告附件 + +| 附件 | 路径 | +|---|---| +| 复测原始日志 | /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/qa/execution-results/wallet-cli-v1-failed-cases-retest-no-ledger-20260702-151024/raw-execution-log.md | +| 复测结构化结果 | /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/qa/execution-results/wallet-cli-v1-failed-cases-retest-no-ledger-20260702-151024/results.json | +| 复测执行器报告 | /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/qa/execution-results/wallet-cli-v1-failed-cases-retest-no-ledger-20260702-151024/test-report.md | +| 首轮完整报告 | /Users/admin/Documents/wallet_cil/wallet-cli-feat-ts-version/ts/qa/execution-results/wallet-cli-v1-command-modules-no-ledger-20260702-144259/test-report.md | diff --git a/ts/package-lock.json b/ts/package-lock.json new file mode 100644 index 000000000..43826727f --- /dev/null +++ b/ts/package-lock.json @@ -0,0 +1,4674 @@ +{ + "name": "wallet-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wallet-cli", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@ledgerhq/hw-app-trx": "^6.36.3", + "@ledgerhq/hw-transport-node-hid": "^6.33.4", + "@ledgerhq/hw-transport-node-speculos-http": "^6.36.4", + "@noble/ciphers": "^2.2.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0", + "axios": "^1.18.1", + "lossless-json": "^4.3.0", + "tronweb": "^6.4.0", + "yaml": "^2.9.0", + "yargs": "^18.0.0", + "zod": "^4.4.3" + }, + "bin": { + "wallet-cli": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/yargs": "^17.0.35", + "dependency-cruiser": "^17.4.3", + "tsup": "^8.5.1", + "tsx": "^4.22.4", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ledgerhq/devices": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.15.1.tgz", + "integrity": "sha512-o4XEHiwPytnZxYUnOC5JoHX5TPDJpeMXPfXjwhsXVJ9LAbahxQWzC37Iuzk8ZY/oC2yDptzqjs/qNP2y9D9vCA==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/logs": "^6.17.0", + "rxjs": "7.8.2", + "semver": "7.7.3" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.36.0.tgz", + "integrity": "sha512-o2Q5hNvf2TzAzlH8ORAozppRbzixRPYDfmSQrP7FOcM997OEH7qDleXgp/uMpvRdxR/t3CJCG+n0i+bU/oYMKA==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/hw-app-trx": { + "version": "6.36.3", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-trx/-/hw-app-trx-6.36.3.tgz", + "integrity": "sha512-MOVRjIqX/kwjFcwy7trk99OcKDCyB70sjmqynmGV5rgZKoG+J9pBsn3q8g2FS4DSsxO1o7piCw6KXjBi3fiyDQ==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/hw-transport": "6.35.4" + } + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.35.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.35.4.tgz", + "integrity": "sha512-FMVxiniQp0pgSIEBX9CjXYBuIAH9BTm3OcrN+/2LqkB8QBeFZdiwmO4BeN9nc2aWspe9z6kxN3SuUyouedNokA==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.15.1", + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/logs": "^6.17.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-node-hid": { + "version": "6.33.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-6.33.4.tgz", + "integrity": "sha512-/k0rH+wTpTmUifdxWuOER/dM3vPxW7RQIF0tByGC6noszVC3SGOZVcskBqJZ6xwIs1TvAHvZlEFvZWbnUJMBiw==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.15.1", + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/hw-transport": "6.35.4", + "@ledgerhq/hw-transport-node-hid-noevents": "^6.35.4", + "@ledgerhq/logs": "^6.17.0", + "lodash": "^4.17.21", + "node-hid": "2.1.2", + "usb": "2.9.0" + } + }, + "node_modules/@ledgerhq/hw-transport-node-hid-noevents": { + "version": "6.35.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-6.35.4.tgz", + "integrity": "sha512-187670Uuuek9ECcT92NQm1PnDfFHT6gBmaJinatnueMLV9lk3q4IrpFZqTotr+LhPNub+YdGQFjYJImcXvYWtw==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.15.1", + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/hw-transport": "6.35.4", + "@ledgerhq/logs": "^6.17.0", + "node-hid": "2.1.2" + } + }, + "node_modules/@ledgerhq/hw-transport-node-speculos-http": { + "version": "6.36.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-node-speculos-http/-/hw-transport-node-speculos-http-6.36.4.tgz", + "integrity": "sha512-Ae3Z5mCZcZ3hhOhSRB2us69pjlI0ghLuIydfGR6PdxxxscPqcGXqB7RbcnABAC5cKC/0Wj4NXllssH3hcI9GCA==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.36.0", + "@ledgerhq/hw-transport": "6.35.4", + "@ledgerhq/logs": "^6.17.0", + "axios": "1.13.5", + "rxjs": "7.8.2" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", + "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", + "license": "Apache-2.0" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.2.0.tgz", + "integrity": "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.14.tgz", + "integrity": "sha512-Qu3Nn6JFuF4+sHKYl+IcX9vYiI40ogleXzFFSxoE1W94rG98o/kXs8uJ0QSfFzuwBCZWlGfUGpPkgwuuX4PchA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dependency-cruiser": { + "version": "17.4.3", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.4.3.tgz", + "integrity": "sha512-L4GLuAvmXevWnPCIaFfOz6eD92c+yY+pDgVqgufrLDnW3xYA799CSZQlly2r2N13nhAlnZY6VzY7Rx5pHNvk2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "8.16.0", + "acorn-jsx": "5.3.2", + "acorn-jsx-walk": "2.0.0", + "acorn-loose": "8.5.2", + "acorn-walk": "8.3.5", + "commander": "14.0.3", + "enhanced-resolve": "5.22.1", + "ignore": "7.0.5", + "interpret": "3.1.1", + "is-installed-globally": "1.0.0", + "json5": "2.2.3", + "picomatch": "4.0.4", + "prompts": "2.4.2", + "rechoir": "0.8.0", + "safe-regex": "2.1.1", + "semver": "7.8.1", + "tsconfig-paths-webpack-plugin": "4.2.0", + "watskeburt": "5.0.3" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^20.12||^22||>=24" + } + }, + "node_modules/dependency-cruiser/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers": { + "version": "6.13.5", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz", + "integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lossless-json": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz", + "integrity": "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-hid": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-2.1.2.tgz", + "integrity": "sha512-qhCyQqrPpP93F/6Wc/xUR7L8mAJW0Z6R7HMQV8jCHHksAxNDe/4z4Un/H9CpLOT+5K39OPyt9tIQlavxWES3lg==", + "hasInstallScript": true, + "license": "(MIT OR X11)", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^3.0.2", + "prebuild-install": "^7.1.1" + }, + "bin": { + "hid-showdevices": "src/show-devices.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tronweb": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tronweb/-/tronweb-6.4.0.tgz", + "integrity": "sha512-WyrjHKN6YSnV/Dvc4cdULDyXVKJfj4bqQ/8dib6FlOaQ1T/EiOjLtOx24hDgcn8lY/iVMy4Axchzurk1wGcRJg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "7.26.10", + "axios": "1.18.0", + "bignumber.js": "9.1.2", + "ethereum-cryptography": "2.2.1", + "ethers": "6.13.5", + "eventemitter3": "5.0.1", + "google-protobuf": "3.21.4", + "semver": "7.7.1", + "validator": "13.15.23" + } + }, + "node_modules/tronweb/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/usb": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.9.0.tgz", + "integrity": "sha512-G0I/fPgfHUzWH8xo2KkDxTTFruUWfppgSFJ+bQxz/kVY2x15EQ/XDB7dqD1G432G4gBG4jYQuF3U7j/orSs5nw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^6.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=10.20.0 <11.x || >=12.17.0 <13.0 || >=14.0.0" + } + }, + "node_modules/usb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/watskeburt": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-5.0.3.tgz", + "integrity": "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA==", + "dev": true, + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^20.12||^22.13||>=24.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/ts/package.json b/ts/package.json new file mode 100644 index 000000000..96f516acb --- /dev/null +++ b/ts/package.json @@ -0,0 +1,81 @@ +{ + "name": "walletcli", + "version": "0.1.0", + "description": "Agent-first TypeScript CLI wallet for TRON — deterministic commands, JSON output, and discoverable schemas", + "type": "module", + "bin": { + "wallet-cli": "./dist/index.js" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "keywords": [ + "TRON", + "tron", + "wallet", + "cli", + "blockchain", + "trx", + "trc20" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/tronprotocol/wallet-cli.git", + "directory": "ts" + }, + "homepage": "https://github.com/tronprotocol/wallet-cli#readme", + "bugs": "https://github.com/tronprotocol/wallet-cli/issues", + "author": { + "name": "Tron Protocol", + "url": "https://github.com/tronprotocol" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsup", + "dev": "tsx src/index.ts", + "typecheck": "tsc --noEmit", + "depcruise": "depcruise src", + "test": "vitest run", + "test:watch": "vitest", + "prepublishOnly": "npm run build" + }, + "license": "LGPL-3.0-or-later", + "dependencies": { + "@ledgerhq/hw-app-trx": "^6.36.3", + "@ledgerhq/hw-transport-node-hid": "^6.33.4", + "@ledgerhq/hw-transport-node-speculos-http": "^6.36.4", + "@noble/ciphers": "^2.2.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0", + "axios": "^1.18.1", + "lossless-json": "^4.3.0", + "tronweb": "^6.4.0", + "yaml": "^2.9.0", + "yargs": "^18.0.0", + "zod": "^4.4.3" + }, + "overrides": { + "axios": "^1.18.1", + "ws": "^8.18.3", + "esbuild": "^0.28.1" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/yargs": "^17.0.35", + "dependency-cruiser": "^17.4.3", + "tsup": "^8.5.1", + "tsx": "^4.22.4", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + } +} diff --git a/ts/src/adapters/inbound/cli/arity/arity.test.ts b/ts/src/adapters/inbound/cli/arity/arity.test.ts new file mode 100644 index 000000000..a7c6ee69a --- /dev/null +++ b/ts/src/adapters/inbound/cli/arity/arity.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; +import { z } from "zod"; +import { ciEnum, enumOptions, introspectFields } from "./index.js"; + +describe("enumOptions", () => { + it("returns the literals of an enum field (through optional/default wrappers)", () => { + expect(enumOptions(z.enum(["tron", "ethereum"]))).toEqual(["tron", "ethereum"]); + expect(enumOptions(z.enum(["a", "b"]).optional())).toEqual(["a", "b"]); + }); + it("returns undefined for non-enum fields", () => { + expect(enumOptions(z.string())).toBeUndefined(); + }); + it("descends ciEnum's preprocess pipe to find the literals (through default/optional)", () => { + expect(enumOptions(ciEnum(["energy", "bandwidth"]))).toEqual(["energy", "bandwidth"]); + expect(enumOptions(ciEnum(["energy", "bandwidth"]).default("bandwidth"))).toEqual(["energy", "bandwidth"]); + expect(enumOptions(ciEnum(["native", "token"]).optional())).toEqual(["native", "token"]); + }); +}); + +describe("ciEnum", () => { + const schema = ciEnum(["energy", "bandwidth"]); + it("accepts the canonical lowercase literal", () => { + expect(schema.parse("energy")).toBe("energy"); + }); + it("accepts upper/mixed case and normalizes to the lowercase literal", () => { + expect(schema.parse("ENERGY")).toBe("energy"); + expect(schema.parse("BandWidth")).toBe("bandwidth"); + }); + it("still rejects values outside the literal set", () => { + expect(schema.safeParse("cpu").success).toBe(false); + }); +}); + +describe("introspectFields — defaults & choices", () => { + const fields = introspectFields( + z.object({ + to: z.string().describe("recipient"), + feeLimit: z.coerce.number().int().positive().default(100_000_000).describe("fee cap"), + resource: z.enum(["energy", "bandwidth"]).default("bandwidth").describe("resource type"), + only: z.enum(["native", "token"]).optional().describe("filter"), + flag: z.boolean().default(false).describe("a switch"), + }), + ); + const by = (name: string) => fields.find((f) => f.name === name)!; + + it("captures the default value of a defaulted field", () => { + expect(by("feeLimit").defaultValue).toBe(100_000_000); + expect(by("resource").defaultValue).toBe("bandwidth"); + expect(by("flag").defaultValue).toBe(false); + }); + + it("leaves defaultValue undefined for non-defaulted fields", () => { + expect(by("to").defaultValue).toBeUndefined(); + expect(by("only").defaultValue).toBeUndefined(); + }); + + it("captures enum choices (through default/optional wrappers)", () => { + expect(by("resource").choices).toEqual(["energy", "bandwidth"]); + expect(by("only").choices).toEqual(["native", "token"]); + expect(by("to").choices).toBeUndefined(); + }); +}); diff --git a/ts/src/adapters/inbound/cli/arity/index.ts b/ts/src/adapters/inbound/cli/arity/index.ts new file mode 100644 index 000000000..c9291e4b8 --- /dev/null +++ b/ts/src/adapters/inbound/cli/arity/index.ts @@ -0,0 +1,112 @@ +/** + * Arity adapter — derive the minimal arity hints yargs needs from a command's zod `fields` + * (boolean→switch, else takes a value). Validation/types/defaults/cross-field checks stay in + * zod only — single source of truth. [replaces FlagSpecRegistry] + */ +import type { Argv } from "yargs"; +import { z, type ZodObject, type ZodRawShape, type ZodType } from "zod"; + +// ── account-ref brand ───────────────────────────────────────────────────────── +// A `string` field that names an existing account (accountId/label/address). Branded so the +// TTY gap-fill can offer an arrow-select of existing accounts instead of free text. The brand +// lives on the FINAL schema instance (zod methods clone), so accountRef applies min+describe +// itself and must be the terminal call — no further chaining. +const ACCOUNT_REF = new WeakSet(); +export function accountRef(describe: string): ZodType { + const s = z.string().min(1).describe(describe); + ACCOUNT_REF.add(s); + return s; +} +export function isAccountRef(schema: unknown): boolean { + return typeof schema === "object" && schema !== null && ACCOUNT_REF.has(schema); +} + +// ── case-insensitive enum ────────────────────────────────────────────────────── +// TRON brands its resources/networks in canonical case (ENERGY/BANDWIDTH, Nile, TRON), so an +// exact-match z.enum rejects the casing users most naturally type. ciEnum lower-cases the input +// before matching; `values` must therefore be the lowercase literals. --help still shows those +// literals — enumOptions() descends the preprocess pipe to find them. +export function ciEnum(values: T) { + return z.preprocess((v) => (typeof v === "string" ? v.toLowerCase() : v), z.enum(values)); +} + +export interface FieldInfo { + name: string; + kebab: string; + baseType: string; + optional: boolean; + hasDefault: boolean; + /** the default value (when hasDefault) — surfaced verbatim in --help. */ + defaultValue?: unknown; + /** literal options when the field is an enum — surfaced as `` in --help. */ + choices?: string[]; + description?: string; +} + +export function camelToKebab(s: string): string { + return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); +} + +function unwrap(schema: ZodType): { base: ZodType; optional: boolean; hasDefault: boolean; defaultValue?: unknown; description?: string } { + let s: any = schema; + let optional = false; + let hasDefault = false; + let defaultValue: unknown; + let description: string | undefined = s?.description; + while (s?.def && (s.def.type === "optional" || s.def.type === "default" || s.def.type === "nullable")) { + if (s.def.type === "optional" || s.def.type === "nullable") optional = true; + if (s.def.type === "default") { + hasDefault = true; + defaultValue = s.def.defaultValue; // zod v4: plain value + } + description ??= s.description; + s = s.def.innerType; + } + description ??= s?.description; + return { base: s, optional, hasDefault, defaultValue, description }; +} + +export function introspectFields(fields: ZodObject): FieldInfo[] { + const shape = fields.shape; + return Object.entries(shape).map(([name, schema]) => { + const { base, optional, hasDefault, defaultValue, description } = unwrap(schema as ZodType); + return { + name, + kebab: camelToKebab(name), + baseType: (base as any)?.def?.type ?? "unknown", + optional, + hasDefault, + defaultValue, + choices: enumOptions(schema as ZodType), + description, + }; + }); +} + +/** literal options of an enum field (after unwrapping optional/default), else undefined. */ +export function enumOptions(schema: ZodType): string[] | undefined { + const { base } = unwrap(schema as ZodType); + let def = (base as unknown as { def?: { type?: string; entries?: Record; out?: { def?: any } } }).def; + // ciEnum() wraps the enum in a preprocess pipe; the literals live on the pipe's output side. + if (def?.type === "pipe") def = def.out?.def; + if (def?.type !== "enum" || !def.entries) return undefined; + return Object.values(def.entries); +} + +/** apply one command's zod fields as yargs options (arity only; requiredness stays in zod). */ +function applyArity(y: Argv, fields: ZodObject): Argv { + for (const f of introspectFields(fields)) { + y.option(f.kebab, { + type: f.baseType === "boolean" ? "boolean" : "string", + describe: f.description, + demandOption: false, // requiredness is enforced by zod, not yargs + }); + } + return y; +} + +/** union the arity hints of every command in a namespace group (single source = zod). */ +export function applyCommands(y: Argv, fields: ZodObject[]): Argv { + for (const f of fields) applyArity(y, f); + return y; +} diff --git a/ts/src/adapters/inbound/cli/command-id.ts b/ts/src/adapters/inbound/cli/command-id.ts new file mode 100644 index 000000000..22d1a9aa6 --- /dev/null +++ b/ts/src/adapters/inbound/cli/command-id.ts @@ -0,0 +1,12 @@ +/** + * Canonical command identifier — derived purely from `family` + `path`, never stored. + * This is the value surfaced as the `command` field in every result/error envelope, and the + * stable handle agents key on. Chain commands are family-qualified so the same logical path + * (e.g. tx send) yields a per-chain id (tron.tx.send); neutral commands + * are just their path (create, import.mnemonic, config.get, networks). + */ +import type { ChainFamily } from "../../../domain/types/index.js"; + +export function commandId(cmd: { family?: ChainFamily; path: string[] }): string { + return cmd.family ? [cmd.family, ...cmd.path].join(".") : cmd.path.join("."); +} diff --git a/ts/src/adapters/inbound/cli/commands/config.ts b/ts/src/adapters/inbound/cli/commands/config.ts new file mode 100644 index 000000000..e770aeefb --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/config.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../contracts/index.js"; +import { + CONFIG_KEYS, + type ConfigService, +} from "../../../../application/use-cases/config-service.js"; +import { CommandRegistry } from "../registry/index.js"; +import { TextFormatters } from "../render/index.js"; + +export function registerConfigCommands(registry: CommandRegistry, service: ConfigService): void { + const fields = z.object({ + key: z.enum(CONFIG_KEYS).optional() + .describe("config key to read or set; omit to show the whole effective config"), + value: z.string().min(1).optional().describe("new value; omit to read the key"), + }); + + registry.add({ + path: ["config"], + network: "none", + wallet: "none", + auth: "none", + summary: "Show / get / set configuration values", + positionals: [{ field: "key" }, { field: "value" }], + fields, + input: fields, + examples: [ + { cmd: "wallet-cli config" }, + { cmd: "wallet-cli config defaultNetwork" }, + { cmd: "wallet-cli config defaultNetwork tron:nile" }, + ], + formatText: TextFormatters.config, + run: async (ctx, _network, input) => service.execute(input, ctx.config, ctx.networkRegistry), + } satisfies CommandDefinition); +} diff --git a/ts/src/adapters/inbound/cli/commands/network.ts b/ts/src/adapters/inbound/cli/commands/network.ts new file mode 100644 index 000000000..14f7f2a59 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/network.ts @@ -0,0 +1,23 @@ +/** + * Network command — list known networks. Neutral and not bound to one family. + */ +import { z } from "zod"; +import type { CommandDefinition } from "../contracts/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { TextFormatters } from "../render/index.js"; + +export function registerNetworkCommands(reg: CommandRegistry): void { + const empty = z.object({}); + + // ── networks ──────────────────────────────────────────────────────────────── + reg.add({ + path: ["networks"], network: "none", wallet: "none", auth: "none", + summary: "List known networks", fields: empty, input: empty, + examples: [{ cmd: "wallet-cli networks" }], + formatText: TextFormatters.networks, + run: async (ctx) => + ctx.networkRegistry.all().map((n) => ({ + id: n.id, family: n.family, chainId: n.chainId, feeModel: n.feeModel, + })), + } satisfies CommandDefinition); +} diff --git a/ts/src/adapters/inbound/cli/commands/shared.ts b/ts/src/adapters/inbound/cli/commands/shared.ts new file mode 100644 index 000000000..cd6664f84 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/shared.ts @@ -0,0 +1,65 @@ +/** + * Shared chain-command factories — only for commands whose intent and input shape are + * identical across families. Divergent commands (for example send-native, + * with chain-specific amount units + build/estimate) live explicitly in each chain module. + */ +import { z } from "zod"; +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { Schemas } from "../schemas/index.js"; +import { TextFormatters } from "../render/index.js"; +import type { MessageService } from "../../../../application/use-cases/message-service.js"; + +// ── execution-mode flags shared by every signing command ───────────────────────── +/** dry-run / sign-only fields; default (no flag) = sign AND broadcast on-chain. */ +export const txModeFields = { + dryRun: z.boolean().default(false).describe("build and estimate only, with no signature and no broadcast; mutually exclusive with --sign-only"), + signOnly: z.boolean().default(false).describe("sign and output the transaction without broadcasting; mutually exclusive with --dry-run; broadcast later with tx broadcast"), +}; +// ── unified --amount / --raw-amount selector (shared by every chain's `tx send`) ──── +// 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. +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), + rawAmount: Schemas.positiveIntString().optional().describe(rawDesc), + }; +} + +/** superRefine: exactly one of --amount or --raw-amount must be present. */ +export function amountSelector(v: { amount?: string; rawAmount?: string }, ctx: z.RefinementCtx): void { + const n = [v.amount !== undefined, v.rawAmount !== undefined].filter(Boolean).length; + if (n !== 1) ctx.addIssue({ code: "custom", path: ["amount"], message: "provide exactly one of --amount or --raw-amount" }); +} + +/** message sign — direct SignerResolver path (no node, no TxPipeline). */ +export function messageSignCommand(family: ChainFamily, service: MessageService): CommandDefinition { + // --message OR --message-stdin (the latter is a global data channel via SecretResolver). + const fields = z.object({ + message: z.string().min(1).optional().describe("message text to sign; provide this OR --message-stdin; exactly one is required"), + }); + return { + path: ["message", "sign"], + stdin: "message", + family, + network: "optional", + wallet: "optional", + auth: "required", + capability: "message.sign", + summary: "Sign an arbitrary message (TIP-191/V2 · EIP-191)", + fields, + input: fields, + examples: [{ cmd: `wallet-cli message sign --message "hello"` }], + formatText: TextFormatters.messageSign, + run: async (ctx, _net, input) => { + const message = ctx.secrets.pick(input.message, "message", "message"); + return service.sign(ctx, family, ctx.activeAccount, message); + }, + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts b/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts new file mode 100644 index 000000000..2901e1e34 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/text-formatters.test.ts @@ -0,0 +1,225 @@ +import { describe, expect, it } from "vitest"; +import type { TronUseCases } from "./tron/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { registerWalletCommands } from "./wallet.js"; +import { registerConfigCommands } from "./config.js"; +import { registerNetworkCommands } from "./network.js"; +import { TronModule } from "./tron/index.js"; +import { commandId } from "../command-id.js"; +import { TextFormatters } from "../render/index.js"; +import type { TextRenderContext } from "../contracts/index.js"; +import type { ConfigService } from "../../../../application/use-cases/config-service.js"; + +const ctx = (over: Partial = {}): TextRenderContext => ({ command: "x", ...over }); + +describe("text formatters", () => { + it("every registered command has a command-owned text formatter", () => { + const services = {} as TronUseCases; + const registry = new CommandRegistry(); + registerWalletCommands(registry, {} as Parameters[1]); + registerConfigCommands(registry, {} as ConfigService); + registerNetworkCommands(registry); + new TronModule(services).registerCommands(registry); + + const missing = registry.all() + .filter((cmd) => typeof cmd.formatText !== "function") + .map(commandId) + .sort(); + + expect(missing).toEqual([]); + }); +}); + +describe("accountBalance formatter", () => { + it("converts native balance to the human coin amount using decimals + symbol", () => { + const out = TextFormatters.accountBalance({ address: "TXaddress", balance: "1983993000", decimals: 6, symbol: "TRX" }, ctx()); + expect(out).toContain("1983.993 TRX"); + expect(out).not.toContain("sun"); + }); + it("falls back to raw scalar balance when decimals are missing", () => { + const out = TextFormatters.accountBalance({ address: "TXaddress", balance: "1983993000" }, ctx()); + expect(out).toContain("1983993000"); + }); + it("prefers the account label over the address when present", () => { + const out = TextFormatters.accountBalance({ address: "TXaddress", balance: "1", decimals: 6, symbol: "TRX" }, ctx({ accountLabel: "main" })); + expect(out).toContain("main"); + expect(out).not.toContain("TXaddress"); + }); +}); + +describe("tokenBalance formatter", () => { + it("formats balance with decimals and symbol when metadata is present", () => { + const out = TextFormatters.tokenBalance({ address: "TXaddress", token: "TR7token", balance: "1204560000", symbol: "USDT", decimals: 6 }, ctx()); + expect(out).toContain("1204.56"); + expect(out).toContain("USDT"); + }); + it("falls back to raw scalar balance when metadata is missing", () => { + const out = TextFormatters.tokenBalance({ address: "TXaddress", token: "TR7token", balance: "1204560000" }, ctx()); + expect(out).toContain("1204560000"); + }); + it("prefers the account label over the address when present", () => { + const out = TextFormatters.tokenBalance({ address: "TXaddress", token: "t", balance: "1" }, ctx({ accountLabel: "main" })); + expect(out).toContain("main"); + expect(out).not.toContain("TXaddress"); + }); +}); + +describe("txReceipt formatter (typed kind, narrowed — no command-id matching)", () => { + it("tx send submitted (default): pending receipt with txid + track hint, no fee/energy", () => { + const out = TextFormatters.txReceipt( + { kind: "send", stage: "submitted", txId: "abc123", rawAmount: "5000000", token: "USDT", decimals: 6, to: "TrecipientAddress" }, + ctx({ net: { id: "tron:nile", family: "tron", chainId: "nile", feeModel: "tron-resource", aliases: [], capabilities: [] } }), + ); + expect(out).toContain("⏳"); + expect(out).toContain("Sent 5 USDT"); + expect(out).toContain("TrecipientAddress"); + expect(out).toContain("abc123"); + expect(out).toContain("pending — not yet on-chain"); + expect(out).toContain("Track it: wallet-cli tx info --network tron:nile --txid abc123"); + expect(out).not.toContain("Fee"); + }); + it("tx send TRC20 via --contract --raw-amount (no symbol): never mislabels as TRX", () => { + const out = TextFormatters.txReceipt({ kind: "send", stage: "submitted", txId: "t20", rawAmount: "10000", contract: "TXYZtokenContract", to: "Tdest" }); + expect(out).toContain("Sent 10000 TXYZtokenContract"); + expect(out).not.toContain("TRX"); + }); + it("tx send TRC10 via --asset-id --raw-amount (no symbol): labels by asset id, not TRX", () => { + const out = TextFormatters.txReceipt({ kind: "send", stage: "submitted", txId: "t10", rawAmount: "500000", assetId: "1005416", to: "Tdest" }); + expect(out).toContain("Sent 500000 asset 1005416"); + expect(out).not.toContain("TRX"); + }); + it("tx send confirmed (--wait): success receipt with real block + fee", () => { + const out = TextFormatters.txReceipt({ kind: "send", stage: "confirmed", txId: "abc", rawAmount: "1000000", to: "Tdest", blockNumber: 66000000, feeSun: "268000" }); + expect(out).toContain("✅"); + expect(out).toContain("Sent 1 TRX"); + expect(out).toContain("#66,000,000"); + expect(out).toContain("0.268 TRX"); + expect(out).toContain("success"); + }); + it("confirmed receipt preserves legitimate zero-valued chain fields", () => { + const out = TextFormatters.txReceipt({ + kind: "send", stage: "confirmed", txId: "zero", + rawAmount: "0", to: "Tdest", blockNumber: 0, energyUsed: 0, feeSun: 0, + }); + expect(out).toContain("#0"); + expect(out).toMatch(/Energy\s+0/); + expect(out).toContain("0 TRX"); + }); + it("contract send failed (--wait): failure receipt with reason", () => { + const out = TextFormatters.txReceipt({ kind: "contract-send", stage: "failed", txId: "abc", method: "transfer(address,uint256)", contract: "TR7contract", result: "OUT_OF_ENERGY", blockNumber: 1, failed: true }); + expect(out).toContain("❌"); + expect(out).toContain("Called transfer"); + expect(out).toContain("TR7contract"); + expect(out).toContain("OUT_OF_ENERGY"); + }); + it("contract deploy submitted: renders populated Address row", () => { + const out = TextFormatters.txReceipt( + { kind: "contract-deploy", stage: "submitted", txId: "dep1", contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" }, + ctx({ net: { id: "tron:nile", family: "tron", chainId: "nile", feeModel: "tron-resource", aliases: [], capabilities: [] } }), + ); + expect(out).toContain("Contract deployed"); + expect(out).toContain("Address"); + expect(out).toContain("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"); + }); + it("dry-run with an energy estimate (TRC20/contract): renders energy, never [object Object]", () => { + const out = TextFormatters.txReceipt({ + kind: "send", mode: "dry-run", + fee: { feeModel: "tron-resource", energy: 29650, availableEnergy: 133440569 } as any, + tx: { txID: "deadbeef" } as any, rawAmount: "10000", contract: "TXYZtoken", to: "Tdest", + } as any); + expect(out).toContain("Dry run"); + expect(out).not.toContain("[object Object]"); + expect(out).toContain("29,650 energy"); + expect(out).toContain("covered by staked energy"); // availableEnergy >= energy + }); + it("dry-run energy estimate with insufficient available energy: no 'covered' note", () => { + const out = TextFormatters.txReceipt({ + kind: "send", mode: "dry-run", + fee: { feeModel: "tron-resource", energy: 29650, availableEnergy: 100 } as any, + tx: { txID: "deadbeef" } as any, rawAmount: "10000", contract: "TXYZtoken", to: "Tdest", + } as any); + expect(out).toContain("29,650 energy"); + expect(out).not.toContain("covered by staked energy"); + }); + it("stake freeze submitted: renders staked amount and resource", () => { + const out = TextFormatters.txReceipt({ kind: "stake-freeze", stage: "submitted", txId: "abc", amountSun: "2000000", resource: "energy" }); + expect(out).toContain("Staked"); + expect(out).toContain("2 TRX"); + expect(out).toContain("energy"); + }); +}); + +describe("txStatus formatter (family-agnostic; command supplies `state`)", () => { + it("tron: confirmed when not failed", () => { + const out = TextFormatters.txStatus({ txid: "abc", state: "confirmed", confirmed: true, failed: false, blockNumber: 123 }); + expect(out).toContain("confirmed"); + expect(out).toContain("#123"); + }); + it("tron: failed when command flags it", () => { + const out = TextFormatters.txStatus({ txid: "abc", state: "failed", confirmed: true, failed: true, blockNumber: 1 }); + expect(out).toContain("failed"); + }); + it("pending when known but not yet confirmed", () => { + const out = TextFormatters.txStatus({ txid: "abc", state: "pending", confirmed: false, failed: false }); + expect(out).toContain("pending"); + }); + it("not found when the node has no record of the tx", () => { + const out = TextFormatters.txStatus({ txid: "abc", state: "not_found", confirmed: false, failed: false }); + expect(out).toContain("not found"); + }); +}); + +describe("txInfo formatter (per-family, narrowed on ctx.net.family)", () => { + it("tron: shows TRX amount, energy and fee in TRX", () => { + const out = TextFormatters.txInfo({ + txid: "abc", from: "Tfrom", to: "Tto", amount: "1.5", symbol: "TRX", + status: "SUCCESS", blockNumber: 66000000, energyUsed: 28000, feeSun: 268000, transaction: {}, info: {}, + }, ctx({ net: { id: "tron:nile", family: "tron", chainId: "nile", feeModel: "tron-resource", aliases: [], capabilities: [] } })); + expect(out).toContain("1.5 TRX"); + expect(out).toContain("#66,000,000"); + expect(out).toContain("28,000"); + expect(out).toContain("0.268 TRX"); + expect(out).toContain("SUCCESS"); + }); +}); + +describe("accountInfo staking summary", () => { + const accountInfo = (amount: unknown) => TextFormatters.accountInfo({ + address: "Towner", + account: { balance: 0, frozenV2: [{ type: "ENERGY", amount }] }, + resources: {}, + }, ctx()); + + it("preserves staking amounts above Number.MAX_SAFE_INTEGER when supplied as strings", () => { + expect(accountInfo("9007199254740993")).toContain("9007199254.740993 TRX"); + }); + + it("omits the staking summary for an already-unsafe numeric amount", () => { + expect(accountInfo(9007199254740992)).not.toContain("Staked"); + }); +}); + +describe("contractInfo formatter", () => { + it("uses normalized methods + count", () => { + const out = TextFormatters.contractInfo({ address: "TR7c", name: "Foo", methods: ["a", "b"], functionCount: 2 }); + expect(out).toContain("Foo"); + expect(out).toContain("Methods"); + expect(out).toContain("2 (a / b)"); + }); + it("falls back to raw contract/info ABI shape", () => { + const out = TextFormatters.contractInfo({ address: "TR7c", contract: { name: "Bar", abi: { entrys: [{ type: "Function", name: "x" }] } } }); + expect(out).toContain("Bar"); + expect(out).toContain("1 (x)"); + }); +}); + +describe("accountHistory formatter", () => { + it("renders normalized rows", () => { + const out = TextFormatters.accountHistory({ + address: "TXaddr", + records: [{ time: 1700000000000, type: "Transfer", amount: "1000000", symbol: "TRX", counterparty: "Tother", status: "ok" }], + }, ctx()); + expect(out).toContain("Transfer"); + expect(out).toContain("Tother"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/commands/tron/account.ts b/ts/src/adapters/inbound/cli/commands/tron/account.ts new file mode 100644 index 000000000..4949c307d --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/account.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import type { TronAccountService } from "../../../../../application/use-cases/tron/account-service.js"; +import { ciEnum } from "../../arity/index.js"; +import { TextFormatters } from "../../render/index.js"; + +export function accountCommands(service: TronAccountService): CommandDefinition[] { + return [balance(service), info(service), history(service), portfolio(service)]; +} + +function balance(service: TronAccountService): CommandDefinition { + const fields = z.object({}); + return { + path: ["account", "balance"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "account.balance.native", + summary: "Show native balance (TRX/SUN)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account balance" }], + formatText: TextFormatters.accountBalance, + run: async (ctx, network) => service.balance(ctx, network, "tron"), + }; +} + +function info(service: TronAccountService): CommandDefinition { + const fields = z.object({}); + return { + path: ["account", "info"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + summary: "Show raw account data (getAccount; TRON includes resources)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account info" }], + formatText: TextFormatters.accountInfo, + run: async (ctx, network) => service.info(ctx, network), + }; +} + +function history(service: TronAccountService): CommandDefinition { + const fields = z.object({ + limit: z.coerce.number().int().positive().max(200).default(20) + .describe("maximum records to return, in records; range: 1-200"), + only: ciEnum(["native", "token"]).optional() + .describe("filter history by transfer type; omit to show all transfer types"), + }); + return { + path: ["account", "history"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + summary: "Show transaction history (requires TronGrid)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account history --limit 10" }], + formatText: TextFormatters.accountHistory, + run: async (ctx, network, input) => service.historyFor(ctx, network, input), + }; +} + +function portfolio(service: TronAccountService): CommandDefinition { + const fields = z.object({}); + return { + path: ["account", "portfolio"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "account.portfolio", + summary: "Show native + token balances with best-effort USD value", + fields, + input: fields, + examples: [{ cmd: "wallet-cli account portfolio" }], + formatText: TextFormatters.accountPortfolio, + run: async (ctx, network) => service.portfolio(ctx, network), + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/block.ts b/ts/src/adapters/inbound/cli/commands/tron/block.ts new file mode 100644 index 000000000..14a5fa511 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/block.ts @@ -0,0 +1,28 @@ +/** + * TRON block group — block lookup. + */ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import type { TronBlockService } from "../../../../../application/use-cases/tron/block-service.js"; +import { Schemas } from "../../schemas/index.js"; +import { TextFormatters } from "../../render/index.js"; + +function blockGet(service: TronBlockService): CommandDefinition { + const fields = z.object({ number: Schemas.uintString().optional().describe("block number to fetch, in block height; omit to fetch the latest block") }); + return { + path: ["block"], family: "tron", + network: "optional", wallet: "none", auth: "none", + positionals: [{ field: "number" }], + summary: "Get a block (latest if omitted)", fields, input: fields, + examples: [ + { cmd: "wallet-cli block" }, + { cmd: "wallet-cli block 12345" }, + ], + formatText: TextFormatters.block, + run: async (_ctx, net, input) => service.get(net, input.number), + }; +} + +export function blockCommands(service: TronBlockService): CommandDefinition[] { + return [blockGet(service)]; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/contract.ts b/ts/src/adapters/inbound/cli/commands/tron/contract.ts new file mode 100644 index 000000000..36528d210 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/contract.ts @@ -0,0 +1,153 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import { UsageError } from "../../../../../domain/errors/index.js"; +import type { TronContractService } from "../../../../../application/use-cases/tron/contract-service.js"; +import type { TronContractParameter } from "../../../../../application/ports/chain/tron-gateway.js"; +import { Schemas } from "../../schemas/index.js"; +import { txModeFields } from "../shared.js"; +import { TextFormatters } from "../../render/index.js"; + +function jsonArray(raw: string | undefined, flag = "--params"): unknown[] { + if (!raw) return []; + try { + const value = JSON.parse(raw); + if (Array.isArray(value)) return value; + } catch { + // Fall through to the stable usage error. + } + throw new UsageError("invalid_value", `${flag} must be a JSON array`); +} + +// call/send 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 encoder/RPC +// error deep in TronWeb. (deploy params are raw positional values — they use jsonArray, not this.) +const typedParam = z + .object({ type: z.string().min(1), value: z.unknown() }) + .refine((e) => e.value !== undefined, { message: "value is required" }); + +function typedParams(raw: string | undefined): TronContractParameter[] { + const arr = jsonArray(raw); + if (!z.array(typedParam).safeParse(arr).success) { + throw new UsageError( + "invalid_value", + '--params entries must be {"type","value"} objects with a non-empty ABI type', + ); + } + return arr as TronContractParameter[]; +} + +export function contractCommands(service: TronContractService): CommandDefinition[] { + return [call(service), send(service), deploy(service), info(service)]; +} + +function call(service: TronContractService): CommandDefinition { + const fields = z.object({ + contract: Schemas.addressFor("tron").describe("TRON contract address"), + method: z.string().min(1).describe("function signature, e.g. balanceOf(address)"), + params: z.string().optional() + .describe("JSON array of ABI parameters as {type,value}; omit to pass no parameters"), + }); + return { + path: ["contract", "call"], family: "tron", + network: "optional", wallet: "none", auth: "none", + capability: "contract.call", + summary: "Read-only call (triggerConstantContract)", + fields, + input: fields, + examples: [{ + cmd: `wallet-cli contract call --contract TR7... --method "balanceOf(address)" --params '[{"type":"address","value":"T..."}]'`, + }], + formatText: TextFormatters.contractCall, + run: async (_ctx, network, input) => service.call( + network, input.contract, input.method, typedParams(input.params), + ), + }; +} + +function send(service: TronContractService): CommandDefinition { + const fields = z.object({ + contract: Schemas.addressFor("tron").describe("TRON contract address"), + method: z.string().min(1).describe("function signature, e.g. transfer(address,uint256)"), + params: z.string().optional() + .describe("JSON array of ABI parameters as {type,value}; omit to pass no parameters"), + callValueSun: Schemas.uintString().default("0") + .describe("native TRX attached to the call, in SUN"), + feeLimit: Schemas.positiveIntString().default("100000000") + .describe("maximum energy fee to burn, in SUN"), + ...txModeFields, + }); + return { + path: ["contract", "send"], family: "tron", + network: "required", wallet: "optional", auth: "required", + broadcasts: true, + capability: "contract.call", + summary: "State-changing call (triggerSmartContract)", + fields, + input: fields, + examples: [{ + cmd: `wallet-cli contract send --contract TR7... --method "transfer(address,uint256)" --params '[...]'`, + }], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => service.send(ctx, network, { + ...input, + parameters: typedParams(input.params), + }), + }; +} + +function deploy(service: TronContractService): CommandDefinition { + const fields = z.object({ + abi: z.string().min(1).describe("contract ABI as a JSON array string"), + bytecode: z.string().min(1).describe("compiled contract bytecode as hex, 0x-prefixed or bare"), + feeLimit: Schemas.positiveIntString().describe("maximum energy fee to burn, in SUN"), + params: z.string().optional() + .describe("constructor args as a JSON array of raw positional values, e.g. [100, \"T...\"]; types are taken from the ABI constructor; omit to pass no constructor args"), + ...txModeFields, + }); + return { + path: ["contract", "deploy"], family: "tron", + network: "required", wallet: "optional", auth: "required", + broadcasts: true, + capability: "contract.deploy", + summary: "Deploy a smart contract", + // The Ledger TRON app firmware rejects CreateSmartContract (APDU 0x6a80), even with + // blind-signing enabled; software accounts sign and deploy it fine. + requires: ["a software (non-Ledger) account — the Ledger TRON app cannot sign this transaction type"], + fields, + input: fields, + examples: [{ + cmd: "wallet-cli contract deploy --abi '[...]' --bytecode 60... --fee-limit 1000000000 --params '[100, \"T...\"]'", + }], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => { + let abi: unknown; + try { + abi = JSON.parse(input.abi); + } catch { + throw new UsageError("invalid_value", "--abi must be valid JSON"); + } + return service.deploy(ctx, network, { + ...input, + abi, + parameters: jsonArray(input.params), + }); + }, + }; +} + +function info(service: TronContractService): CommandDefinition { + const fields = z.object({ + contract: Schemas.addressFor("tron").describe("TRON contract address"), + }); + return { + path: ["contract", "info"], family: "tron", + network: "optional", wallet: "none", auth: "none", + capability: "contract.call", + summary: "Show contract ABI + metadata", + fields, + input: fields, + examples: [{ cmd: "wallet-cli contract info --contract TR7..." }], + formatText: TextFormatters.contractInfo, + run: async (_ctx, network, input) => service.info(network, input.contract), + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/index.ts b/ts/src/adapters/inbound/cli/commands/tron/index.ts new file mode 100644 index 000000000..6d7244b9b --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/index.ts @@ -0,0 +1,48 @@ +/** + * TronModule — TRON's own command surface. No universal + * provider: TRON-specific build/estimate/codecs live in the per-group files; only infra + * (TxPipeline, SignerResolver, RpcProvider) is shared. Implements the ChainModule contract. + */ +import type { ChainModule, CommandRegistryLike } from "../../contracts/index.js"; +import type { TronAccountService } from "../../../../../application/use-cases/tron/account-service.js"; +import type { TronTokenService } from "../../../../../application/use-cases/tron/token-service.js"; +import type { TronTransactionService } from "../../../../../application/use-cases/tron/transaction-service.js"; +import type { TronStakeService } from "../../../../../application/use-cases/tron/stake-service.js"; +import type { TronBlockService } from "../../../../../application/use-cases/tron/block-service.js"; +import type { TronContractService } from "../../../../../application/use-cases/tron/contract-service.js"; +import type { MessageService } from "../../../../../application/use-cases/message-service.js"; +import { accountCommands } from "./account.js"; +import { tokenCommands } from "./token.js"; +import { txCommands } from "./tx.js"; +import { stakeCommands } from "./stake.js"; +import { blockCommands } from "./block.js"; +import { contractCommands } from "./contract.js"; +import { messageCommands } from "./message.js"; + +export interface TronUseCases { + tronAccount: TronAccountService; + tronToken: TronTokenService; + tronTransaction: TronTransactionService; + tronStake: TronStakeService; + tronBlock: TronBlockService; + tronContract: TronContractService; + message: MessageService; +} + +export class TronModule implements ChainModule { + readonly family = "tron" as const; + constructor(private readonly services: TronUseCases) {} + + registerCommands(reg: CommandRegistryLike): void { + const groups = [ + accountCommands(this.services.tronAccount), + tokenCommands(this.services.tronToken), + txCommands(this.services.tronTransaction), + stakeCommands(this.services.tronStake), + blockCommands(this.services.tronBlock), + contractCommands(this.services.tronContract), + messageCommands(this.services.message), + ]; + for (const cmds of groups) for (const cmd of cmds) reg.add(cmd); + } +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/message.ts b/ts/src/adapters/inbound/cli/commands/tron/message.ts new file mode 100644 index 000000000..afffe2e6c --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/message.ts @@ -0,0 +1,11 @@ +/** + * TRON message group — message signing (TIP-191/V2). The command itself comes from the + * shared (family-agnostic) factory; this file just scopes it to TRON. + */ +import type { CommandDefinition } from "../../contracts/index.js"; +import type { MessageService } from "../../../../../application/use-cases/message-service.js"; +import { messageSignCommand } from "../shared.js"; + +export function messageCommands(service: MessageService): CommandDefinition[] { + return [messageSignCommand("tron", service)]; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/shared.ts b/ts/src/adapters/inbound/cli/commands/tron/shared.ts new file mode 100644 index 000000000..a95b44f7e --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/shared.ts @@ -0,0 +1,18 @@ +import type { z } from "zod"; + +/** TRC20 contract XOR TRC10 asset id; exactly one selector is required. */ +export function tokenSelector( + value: { contract?: string; assetId?: string }, + context: z.RefinementCtx, +): void { + const count = [value.contract, value.assetId] + .filter((candidate) => candidate !== undefined).length; + if (count !== 1) { + context.addIssue({ + code: "custom", + path: ["contract"], + message: "exactly one of --contract (TRC20) or --asset-id (TRC10) is required", + }); + } +} + diff --git a/ts/src/adapters/inbound/cli/commands/tron/stake.ts b/ts/src/adapters/inbound/cli/commands/tron/stake.ts new file mode 100644 index 000000000..4d73c36ec --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/stake.ts @@ -0,0 +1,130 @@ +import { z } from "zod"; +import type { NetworkDescriptor } from "../../../../../domain/types/index.js"; +import type { + CommandDefinition, + ExecutionContext, +} from "../../contracts/index.js"; +import type { TronStakeService } from "../../../../../application/use-cases/tron/stake-service.js"; +import { RESOURCES } from "../../../../../domain/resources/index.js"; +import { Schemas } from "../../schemas/index.js"; +import { ciEnum } from "../../arity/index.js"; +import { txModeFields } from "../shared.js"; +import { TextFormatters } from "../../render/index.js"; + +const resourceField = (description: string) => + ciEnum(RESOURCES).default("bandwidth").describe(description); + +interface StakeCommandOptions { + capability?: string; + refine?: (value: any, context: z.RefinementCtx) => void; + requires?: string[]; +} + +type StakeExecutor = ( + context: ExecutionContext, + network: NetworkDescriptor, + input: any, +) => Promise; + +function command( + action: string, + summary: string, + execute: StakeExecutor, + extra: z.ZodRawShape = {}, + options: StakeCommandOptions = {}, +): CommandDefinition { + const fields = z.object({ ...extra, ...txModeFields }); + return { + path: ["stake", action], family: "tron", + network: "required", wallet: "optional", auth: "required", + broadcasts: true, + capability: options.capability ?? "staking.freeze", + summary, + requires: options.requires, + fields, + input: options.refine ? fields.superRefine(options.refine) : fields, + examples: [{ cmd: `wallet-cli stake ${action}` }], + formatText: TextFormatters.txReceipt, + run: async (context, network, input) => execute(context, network, input), + }; +} + +export function stakeCommands(service: TronStakeService): CommandDefinition[] { + return [ + command( + "freeze", + "Stake TRX for energy/bandwidth (FreezeBalanceV2)", + (context, network, input) => service.freeze(context, network, input), + { + amountSun: Schemas.positiveIntString().describe("amount to freeze as staked TRX, in SUN"), + resource: resourceField("resource type to obtain"), + }, + ), + command( + "unfreeze", + "Unstake TRX (UnfreezeBalanceV2)", + (context, network, input) => service.unfreeze(context, network, input), + { + amountSun: Schemas.positiveIntString().describe("amount to unfreeze as staked TRX, in SUN"), + resource: resourceField("resource type to release"), + }, + ), + command( + "withdraw", + "Withdraw expired unfrozen TRX (WithdrawExpireUnfreeze)", + (context, network, input) => service.withdraw(context, network, input), + ), + command( + "cancel-unfreeze", + "Cancel all pending unstakes (roll back to frozen)", + (context, network, input) => service.cancelUnfreeze(context, network, input), + {}, + { + // The Ledger TRON app firmware rejects CancelAllUnfreezeV2Contract (APDU 0x6a80), + // even with blind-signing enabled; software accounts sign it fine. + requires: ["a software (non-Ledger) account — the Ledger TRON app cannot sign this transaction type"], + }, + ), + command( + "delegate", + "Delegate resource to another address (DelegateResourceV2)", + (context, network, input) => service.delegate(context, network, input), + { + amountSun: Schemas.positiveIntString() + .describe("staked-TRX amount backing the delegated resource, in SUN"), + receiver: Schemas.addressFor("tron") + .describe("TRON address receiving the delegated resource"), + resource: resourceField("resource type to delegate or reclaim"), + lock: z.boolean().default(false) + .describe("lock the delegation and prevent early undelegation"), + lockPeriod: Schemas.positiveIntString().optional() + .describe("lock duration in blocks, approximately 3 seconds per block; requires --lock"), + }, + { + capability: "staking.delegate", + refine: (value, context) => { + if (value.lockPeriod !== undefined && !value.lock) { + context.addIssue({ + code: "custom", + message: "--lock-period requires --lock", + path: ["lockPeriod"], + }); + } + }, + }, + ), + command( + "undelegate", + "Reclaim delegated resource (UnDelegateResourceV2)", + (context, network, input) => service.undelegate(context, network, input), + { + amountSun: Schemas.positiveIntString() + .describe("staked-TRX amount backing the resource to reclaim, in SUN"), + receiver: Schemas.addressFor("tron") + .describe("TRON address that previously received the delegated resource"), + resource: resourceField("resource type to delegate or reclaim"), + }, + { capability: "staking.delegate" }, + ), + ]; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/token.ts b/ts/src/adapters/inbound/cli/commands/tron/token.ts new file mode 100644 index 000000000..66efe85f9 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/token.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import type { TronTokenService } from "../../../../../application/use-cases/tron/token-service.js"; +import { Schemas } from "../../schemas/index.js"; +import { TextFormatters } from "../../render/index.js"; +import { tokenSelector } from "./shared.js"; + +const selectorFields = z.object({ + contract: Schemas.addressFor("tron").optional() + .describe("TRC20 contract address; provide exactly one of --contract or --asset-id"), + assetId: z.string().regex(/^\d+$/).optional() + .describe("TRC10 numeric asset id; provide exactly one of --asset-id or --contract"), +}); + +export function tokenCommands(service: TronTokenService): CommandDefinition[] { + return [balance(service), info(service), add(service), list(service), remove(service)]; +} + +function balance(service: TronTokenService): CommandDefinition { + return { + path: ["token", "balance"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "account.balance.token", + summary: "Show a single token balance (--contract / --asset-id)", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token balance --contract TR7..." }], + formatText: TextFormatters.tokenBalance, + run: async (ctx, network, input) => service.balance(ctx, network, input), + }; +} + +function info(service: TronTokenService): CommandDefinition { + return { + path: ["token", "info"], family: "tron", + network: "optional", wallet: "none", auth: "none", + capability: "account.balance.token", + summary: "Show token metadata (name/symbol/decimals/totalSupply)", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token info --contract TR7..." }], + formatText: TextFormatters.tokenInfo, + run: async (_ctx, network, input) => service.info(network, input), + }; +} + +function add(service: TronTokenService): CommandDefinition { + return { + path: ["token", "add"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "token.tokenbook", + summary: "Add a token to the address book (fetches symbol/decimals)", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token add --contract TR7..." }], + formatText: TextFormatters.tokenBookAdd, + run: async (ctx, network, input) => service.add(ctx, network, input), + }; +} + +function list(service: TronTokenService): CommandDefinition { + const fields = z.object({}); + return { + path: ["token", "list"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "token.tokenbook", + summary: "List the address book (official + user)", + fields, + input: fields, + examples: [{ cmd: "wallet-cli token list" }], + formatText: TextFormatters.tokenBookList, + run: async (ctx, network) => service.list(ctx, network), + }; +} + +function remove(service: TronTokenService): CommandDefinition { + return { + path: ["token", "remove"], family: "tron", + network: "optional", wallet: "optional", auth: "none", + capability: "token.tokenbook", + summary: "Remove a user-added token from the address book", + fields: selectorFields, + input: selectorFields.superRefine(tokenSelector), + examples: [{ cmd: "wallet-cli token remove --contract TR7..." }], + formatText: TextFormatters.tokenBookRemove, + run: async (ctx, network, input) => service.remove(ctx, network, input), + }; +} diff --git a/ts/src/adapters/inbound/cli/commands/tron/tx.ts b/ts/src/adapters/inbound/cli/commands/tron/tx.ts new file mode 100644 index 000000000..e68b46053 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/tron/tx.ts @@ -0,0 +1,123 @@ +import { z } from "zod"; +import type { CommandDefinition } from "../../contracts/index.js"; +import { UsageError } from "../../../../../domain/errors/index.js"; +import type { TronTransactionService } from "../../../../../application/use-cases/tron/transaction-service.js"; +import { Schemas } from "../../schemas/index.js"; +import { + amountSelector, + txModeFields, + unifiedAmountFields, +} from "../shared.js"; +import { TextFormatters } from "../../render/index.js"; + +export function txCommands(service: TronTransactionService): CommandDefinition[] { + return [send(service), broadcast(service), status(service), info(service)]; +} + +function send(service: TronTransactionService): CommandDefinition { + const fields = z.object({ + to: Schemas.addressFor("tron").describe("recipient TRON base58 address"), + token: z.string().min(1).optional() + .describe("token symbol from the address book; mutually exclusive with --contract and --asset-id"), + contract: Schemas.addressFor("tron").optional() + .describe("TRC20 contract address; omit with --asset-id for native TRX"), + assetId: z.string().regex(/^\d+$/).optional() + .describe("TRC10 numeric asset id; omit with --contract for native TRX"), + feeLimit: Schemas.positiveIntString().default("100000000") + .describe("maximum TRX energy fee to burn for TRC20 transfers, in SUN"), + ...unifiedAmountFields( + "human amount: TRX for native, token units for TRC20/TRC10; mutually exclusive with --raw-amount", + "raw integer amount in SUN or token base units; mutually exclusive with --amount", + ), + ...txModeFields, + }); + return { + path: ["tx", "send"], family: "tron", + network: "optional", wallet: "optional", auth: "required", + broadcasts: true, + capability: "tx.send", + summary: "Send native TRX or TRC20/TRC10 tokens with human --amount", + fields, + input: fields.superRefine(amountSelector).superRefine(tokenOptional), + examples: [ + { cmd: "wallet-cli tx send --to T... --amount 1" }, + { cmd: "wallet-cli tx send --to T... --token USDT --amount 5" }, + { cmd: "wallet-cli tx send --to T... --contract TR7... --amount 5" }, + { cmd: "wallet-cli tx send --to T... --asset-id 1002000 --raw-amount 1000000" }, + ], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => service.send(ctx, network, input), + }; +} + +function broadcast(service: TronTransactionService): CommandDefinition { + const fields = z.object({ + transaction: z.string().optional() + .describe("signed TRON transaction JSON; provide this OR --tx-stdin; exactly one is required"), + }); + return { + path: ["tx", "broadcast"], stdin: "tx", family: "tron", + network: "required", wallet: "none", auth: "none", + broadcasts: true, + capability: "tx.broadcast", + summary: "Broadcast a presigned transaction", + fields, + input: fields, + examples: [{ cmd: "wallet-cli tx broadcast --tx-stdin < signed.json" }], + formatText: TextFormatters.txReceipt, + run: async (ctx, network, input) => { + const raw = ctx.secrets.pick(input.transaction, "tx", "transaction"); + try { + return service.broadcast(ctx, network, JSON.parse(raw)); + } catch (error) { + if (error instanceof SyntaxError) { + throw new UsageError("invalid_value", "TRON presigned tx must be JSON"); + } + throw error; + } + }, + }; +} + +function status(service: TronTransactionService): CommandDefinition { + const fields = z.object({ txid: z.string().min(1).describe("TRON transaction id/hash") }); + return { + path: ["tx", "status"], family: "tron", + network: "optional", wallet: "none", auth: "none", + summary: "Show confirmation status of a transaction", + fields, + input: fields, + examples: [{ cmd: "wallet-cli tx status --txid abc123" }], + formatText: TextFormatters.txStatus, + run: async (_ctx, network, input) => service.status(network, input.txid), + }; +} + +function info(service: TronTransactionService): CommandDefinition { + const fields = z.object({ txid: z.string().min(1).describe("TRON transaction id/hash") }); + return { + path: ["tx", "info"], family: "tron", + network: "optional", wallet: "none", auth: "none", + summary: "Show full transaction detail + receipt", + fields, + input: fields, + examples: [{ cmd: "wallet-cli tx info --txid abc123" }], + formatText: TextFormatters.txInfo, + run: async (_ctx, network, input) => service.info(network, input.txid), + }; +} + +function tokenOptional( + value: { token?: string; contract?: string; assetId?: string }, + context: z.RefinementCtx, +): void { + const count = [value.token, value.contract, value.assetId] + .filter((candidate) => candidate !== undefined).length; + if (count > 1) { + context.addIssue({ + code: "custom", + path: ["token"], + message: "choose at most one of --token, --contract or --asset-id", + }); + } +} diff --git a/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts new file mode 100644 index 000000000..1ee465995 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/wallet.import-ledger.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { walletImportLedgerInput } from "./wallet.js"; + +const ok = (v: unknown) => walletImportLedgerInput.safeParse(v).success; + +describe("wallet import-ledger contract", () => { + it("requires --app", () => { + expect(ok({ index: 0 })).toBe(false); + }); + + it("accepts exactly one locator", () => { + expect(ok({ app: "tron", index: 0 })).toBe(true); + expect(ok({ app: "tron", path: "m/44'/195'/0'/0/0" })).toBe(true); + expect(ok({ app: "tron", address: "Tabc", scanLimit: 30 })).toBe(true); + }); + + it("accepts --app with no locator (defaults to index 0 downstream)", () => { + expect(ok({ app: "tron" })).toBe(true); + }); + + it("rejects more than one locator (mutually exclusive)", () => { + expect(ok({ app: "tron", index: 0, path: "m/44'/195'/0'/0/0" })).toBe(false); + expect(ok({ app: "tron", index: 0, address: "Tabc" })).toBe(false); + }); + + it("coerces --index and --scan-limit from strings", () => { + const r = walletImportLedgerInput.safeParse({ app: "tron", index: "2", scanLimit: "30" }); + expect(r.success && (r.data as { index?: number }).index).toBe(2); + }); + + it("rejects a hidden-family app (EVM is not currently exposed)", () => { + expect(ok({ app: "ethereum", index: 0 })).toBe(false); + }); +}); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.test.ts b/ts/src/adapters/inbound/cli/commands/wallet.test.ts new file mode 100644 index 000000000..f73a464ca --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/wallet.test.ts @@ -0,0 +1,356 @@ +import { describe, it, expect } from "vitest"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { z } from "zod"; +import { Keystore } from "../../../outbound/keystore/index.js"; +import { AtomicFileStore } from "../../../outbound/persistence/fs/index.js"; +import { SecretResolver } from "../input/secret/index.js"; +import { StreamManager } from "../stream/index.js"; +import { Prompter } from "../input/prompt/index.js"; +import { ConfigLoader, NetworkRegistry } from "../../../outbound/config/index.js"; +import { buildExecutionContext, RuntimeDeps } from "../context/index.js"; +import { createOutputFormatter } from "../output/index.js"; +import { registerWalletCommands, walletImportLedgerFields, walletImportLedgerInput } from "./wallet.js"; +import { CommandRegistry } from "../registry/index.js"; +import { commandId } from "../command-id.js"; +import type { Globals, NetworklessCommandDefinition } from "../contracts/index.js"; +import { Derivation } from "../../../../domain/derivation/index.js"; +import { WalletService } from "../../../../application/use-cases/wallet-service.js"; + +// ── test constants ───────────────────────────────────────────────────────────── +const VALID_MNEMONIC = "test test test test test test test test test test test junk"; +const VALID_PRIVATE_KEY = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; +const VALID_PASSWORD = "Abcdef1!"; + +// ── helpers ──────────────────────────────────────────────────────────────────── + +interface FakePromptOpts { + tty?: boolean; + hiddenAnswers?: string[]; + confirmResult?: boolean; + confirmAnswer?: string; +} + +function makeFakeBackend(opts: FakePromptOpts = {}): ConstructorParameters[0] { + const { tty = true, hiddenAnswers = [], confirmResult = true, confirmAnswer } = opts; + let hiddenIdx = 0; + + return { + isTTY: () => tty, + async question(_prompt: string, hidden: boolean) { + if (hidden) { + return hiddenAnswers[hiddenIdx++] ?? VALID_PASSWORD; + } + // confirm prompts are not hidden + return confirmAnswer ?? ""; + }, + async readKey() { return { name: "return" }; }, + write(_s: string) {}, + beginRaw() {}, + endRaw() {}, + }; +} + +function buildTestDeps(opts: FakePromptOpts & { root?: string } = {}): { + deps: RuntimeDeps; + ks: Keystore; + prompter: Prompter; + streams: StreamManager; + secrets: SecretResolver; +} { + const root = opts.root ?? mkdtempSync(join(tmpdir(), "wallet-test-")); + const store = new AtomicFileStore(); + const streams = new StreamManager("text", false); + const prompter = new Prompter(makeFakeBackend(opts)); + // password: "-" means "read from stdin"; we prime the stdin via streams + // Actually: use no paths for password, rely on primePassword via prompter + const secrets = new SecretResolver(streams, {}, prompter); + const ks = new Keystore(root, store, () => secrets.masterPassword()); + const config = ConfigLoader.load(); + const networkRegistry = new NetworkRegistry(config); + const formatter = createOutputFormatter("text", streams, Date.now()); + const deps: RuntimeDeps = { config, networkRegistry, streams, secrets, keystore: ks, prompter, formatter }; + return { deps, ks, prompter, streams, secrets }; +} + +function buildGlobals(): Globals { + return { output: "text", verbose: false }; +} + +function buildServices(ks: Keystore) { + const ledger = {} as any; + return { + walletService: new WalletService(ks, ledger, { + write: () => ({ out: "unused", fileMode: "0600", bytes: 0 }), + }), + ledger, + tokenBook: {} as any, + priceProvider: {} as any, + rpc: {} as any, + signerResolver: {} as any, + txPipeline: {} as any, + capabilityRegistry: {} as any, + }; +} + +/** Resolve a command by its derived canonical id (e.g. "create", "import.mnemonic"). */ +function getCmd(registry: CommandRegistry, id: string): NetworklessCommandDefinition | null { + const cmd = registry.all().find((c) => commandId(c) === id) ?? null; + if (!cmd) throw new Error(`command not found: ${id}`); + if (cmd.network !== "none") throw new Error(`wallet command unexpectedly requires a network: ${id}`); + return cmd; +} + +async function runCmd( + cmdId: string, + input: Record, + opts: FakePromptOpts & { root?: string } = {}, +) { + const { deps, ks } = buildTestDeps(opts); + // Prime the password before command runs (simulating what dispatch does for passwordMode) + await deps.secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + const cmd = getCmd(registry, cmdId)!; + const result = await cmd.run(ctx, undefined as any, input as any); + return { result, ks }; +} + +// ── wallet create ────────────────────────────────────────────────────────────── + +describe("wallet create", () => { + it("creates an account with a tron address", async () => { + const { result, ks } = await runCmd("create", {}, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD], + }); + expect(result).toBeDefined(); + expect(result.accountId).toMatch(/^wlt_/); + expect(result.addresses?.tron?.startsWith("T")).toBe(true); + const accounts = ks.list(); + expect(accounts).toHaveLength(1); + }); + + it("createFields schema does NOT have a words key", async () => { + // We verify the schema at runtime by checking the registered command's fields + const { deps, ks } = buildTestDeps({ tty: true, hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD] }); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + const cmd = getCmd(registry, "create")!; + const shape = (cmd.fields as any).shape as Record; + expect(Object.keys(shape)).not.toContain("words"); + expect(Object.keys(shape)).toContain("label"); + }); + + it("generates a 12-word mnemonic", async () => { + // create, then backup to read mnemonic word count + const root = mkdtempSync(join(tmpdir(), "wallet-create-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD], + }); + // prime password for create + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + const createCmd = getCmd(registry, "create")!; + const createResult = await createCmd.run(ctx, undefined as any, {} as any); + + // verify the created account has a seed source + const accountList = ks.list(); + expect(accountList).toHaveLength(1); + const wallet = ks.resolveAccount(createResult.accountId).wallet; + expect(wallet.source.type).toBe("seed"); + + // reveal the mnemonic to check word count + const vaultId = (wallet.source as any).vaultId as string; + const revealed = ks.revealMnemonic(vaultId); + const words = revealed.mnemonic.split(" ").filter(Boolean); + expect(words).toHaveLength(12); + }); +}); + +// ── wallet import-mnemonic ───────────────────────────────────────────────────── + +describe("wallet import-mnemonic", () => { + it("creates an account from a mnemonic provided via interactive prompt", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-import-mnemonic-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: true, + // order: set password, confirm password, then mnemonic + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + }); + + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + const cmd = getCmd(registry, "import.mnemonic")!; + const result = await cmd.run(ctx, undefined as any, {} as any); + + expect(result.accountId).toMatch(/^wlt_/); + expect(result.addresses?.tron?.startsWith("T")).toBe(true); + expect(ks.list()).toHaveLength(1); + }); +}); + +// ── wallet import-private-key ────────────────────────────────────────────────── + +describe("wallet import-private-key", () => { + it("creates an account from a private key provided via interactive prompt", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-import-pk-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: true, + // order: set password, confirm password, then private key + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_PRIVATE_KEY], + }); + + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + const cmd = getCmd(registry, "import.private-key")!; + const result = await cmd.run(ctx, undefined as any, {} as any); + + expect(result.accountId).toMatch(/^wlt_/); + expect(result.addresses?.tron?.startsWith("T")).toBe(true); + expect(ks.list()).toHaveLength(1); + }); +}); + +// ── wallet delete ───────────────────────────────────────────────────────────── + +describe("wallet delete", () => { + async function setupAccountForDelete(root: string, opts: FakePromptOpts) { + const { deps, ks, secrets } = buildTestDeps({ root, ...opts }); + await secrets.primePassword({ mode: "set" }); + const ctx = buildExecutionContext(buildGlobals(), deps); + const registry = new CommandRegistry(); + registerWalletCommands(registry, buildServices(ks)); + + // import an account first + const importCmd = getCmd(registry, "import.mnemonic")!; + const importResult = await importCmd.run(ctx, undefined as any, {} as any); + return { ctx, registry, ks, accountId: importResult.accountId }; + } + + it("deletes an account when --yes is true", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-test-")); + // hiddenAnswers for set+confirm password, then mnemonic for import + const { ctx, registry, ks, accountId } = await setupAccountForDelete(root, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + }); + + expect(ks.list()).toHaveLength(1); + + const deleteCmd = getCmd(registry, "delete")!; + await deleteCmd.run(ctx, undefined as any, { account: accountId, yes: true } as any); + + expect(ks.list()).toHaveLength(0); + }); + + it("confirms deletion by exact label when a label is available", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-label-test-")); + const { ctx, registry, ks, accountId } = await setupAccountForDelete(root, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + confirmAnswer: "wallet-1", + }); + + expect(ks.describe(accountId).label).toBe("wallet-1"); + + const deleteCmd = getCmd(registry, "delete")!; + await deleteCmd.run(ctx, undefined as any, { account: accountId } as any); + + expect(ks.list()).toHaveLength(0); + }); + + it("throws aborted when --yes is false and confirm returns wrong string", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-abort-test-")); + const { ctx, registry, ks, accountId } = await setupAccountForDelete(root, { + tty: true, + hiddenAnswers: [VALID_PASSWORD, VALID_PASSWORD, VALID_MNEMONIC], + confirmAnswer: "wrong-ref", + }); + + expect(ks.list()).toHaveLength(1); + + const deleteCmd = getCmd(registry, "delete")!; + await expect( + deleteCmd.run(ctx, undefined as any, { account: accountId } as any), + ).rejects.toMatchObject({ code: "aborted" }); + + // account should still exist + expect(ks.list()).toHaveLength(1); + }); + + it("throws tty_required when --yes is omitted and not a TTY", async () => { + const root = mkdtempSync(join(tmpdir(), "wallet-delete-notty-test-")); + const { deps, ks, secrets } = buildTestDeps({ + root, + tty: false, + }); + // for non-TTY, secrets need password via stdin path + const storeNonTty = new AtomicFileStore(); + // Use a separate TTY keystore just to create the account + const root2 = mkdtempSync(join(tmpdir(), "wallet-delete-notty2-test-")); + const streams2 = new StreamManager("text", false); + const fakeBackend2 = { + isTTY: () => true, + async question(_p: string, hidden: boolean) { return VALID_PASSWORD; }, + async readKey() { return { name: "return" }; }, + write(_s: string) {}, + beginRaw() {}, + endRaw() {}, + }; + const prompter2 = new Prompter(fakeBackend2); + const secrets2 = new SecretResolver(streams2, {}, prompter2); + const ks2 = new Keystore(root2, storeNonTty, () => secrets2.masterPassword()); + await secrets2.primePassword({ mode: "set" }); + const { accountId } = ks2.import({ secret: VALID_MNEMONIC, type: "seed" }); + + // Now set up a non-TTY context pointing to the same root + const storeNonTty2 = new AtomicFileStore(); + const streamsNT = new StreamManager("text", false); + const fakeBackendNT = { + isTTY: () => false, + async question(_p: string, _hidden: boolean) { return ""; }, + async readKey() { return { name: "return" }; }, + write(_s: string) {}, + beginRaw() {}, + endRaw() {}, + }; + const prompterNT = new Prompter(fakeBackendNT); + // prime password via stdin path by priming it directly + const secretsNT = new SecretResolver(streamsNT, {}, prompterNT); + // manually prime the password so ks.delete can proceed if needed + // Actually we just need the delete to fail at the TTY check, before touching ks + const ksNT = new Keystore(root2, storeNonTty2, () => secrets2.masterPassword()); + + const config = ConfigLoader.load(); + const networkRegistry = new NetworkRegistry(config); + const formatter = createOutputFormatter("text", streamsNT, Date.now()); + const depsNT: RuntimeDeps = { + config, networkRegistry, streams: streamsNT, secrets: secretsNT, + keystore: ksNT, prompter: prompterNT, formatter, + }; + const ctxNT = buildExecutionContext(buildGlobals(), depsNT); + const registryNT = new CommandRegistry(); + registerWalletCommands(registryNT, buildServices(ksNT)); + + const deleteCmd = getCmd(registryNT, "delete")!; + await expect( + deleteCmd.run(ctxNT, undefined as any, { account: accountId } as any), + ).rejects.toMatchObject({ code: "tty_required" }); + }); +}); diff --git a/ts/src/adapters/inbound/cli/commands/wallet.ts b/ts/src/adapters/inbound/cli/commands/wallet.ts new file mode 100644 index 000000000..855290771 --- /dev/null +++ b/ts/src/adapters/inbound/cli/commands/wallet.ts @@ -0,0 +1,316 @@ +/** + * Wallet root commands — create/import/list/current/use/backup. Not chain-bound; no --network. + * Calls WalletService rather than the transaction pipeline. + */ +import { z } from "zod" +import type { CommandDefinition } from "../contracts/index.js" +import { Schemas } from "../schemas/index.js" +import { CommandRegistry } from "../registry/index.js" +import { accountRef, ciEnum } from "../arity/index.js" +import type { LedgerDevice } from "../../../../application/ports/ledger-device.js" +import type { WalletService } from "../../../../application/use-cases/wallet-service.js" +import { resolveLedgerPath, selectLedgerPath } from "../../../../application/services/ledger-account.js" +import { ChainFamily, CHAIN_FAMILIES, FAMILIES } from "../../../../domain/family/index.js" +import { UsageError } from "../../../../domain/errors/index.js" +import { TextFormatters } from "../render/index.js" + +// ── wallet import-ledger contract (module scope so it can be unit-tested) ─────── +// The selectable Ledger apps are the families with a wired Ledger app (FAMILIES[f].ledger); +// the enum drives both --help and the interactive prompt. +const LEDGER_APP_BY_FAMILY: Partial> = Object.fromEntries(CHAIN_FAMILIES.flatMap((f) => (FAMILIES[f].ledger ? [[f, FAMILIES[f].ledger!.app]] : []))) +const FAMILY_BY_LEDGER_APP: Record = Object.fromEntries((Object.entries(LEDGER_APP_BY_FAMILY) as [ChainFamily, string][]).map(([f, app]) => [app, f])) +const LEDGER_APPS = CHAIN_FAMILIES.map((f) => LEDGER_APP_BY_FAMILY[f]).filter((a): a is string => a !== undefined) as [string, ...string[]] +export const walletImportLedgerFields = z.object({ + app: ciEnum(LEDGER_APPS).describe("Ledger app to open on the device, selecting the address-derivation scheme"), + index: z.coerce + .number() + .int() + .nonnegative() + .optional() + .describe("HD account index to import; omit with no --path/--address to use index 0; mutually exclusive with --path and --address"), + path: z.string().optional().describe("explicit BIP32 derivation path, e.g. m/44'/195'/0'/0/0 for TRON; mutually exclusive with --index and --address"), + address: z.string().optional().describe("known address to locate by bounded scan; mutually exclusive with --index and --path"), + scanLimit: z.coerce.number().int().positive().optional().describe("number of account indexes to scan when using --address, in indexes; omit to scan 20 indexes"), + label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate"), +}) +/** --index / --path / --address are mutually exclusive (at most one locator). */ +export const walletImportLedgerInput = walletImportLedgerFields.superRefine((v, c) => { + const locators = [v.index !== undefined, v.path !== undefined, v.address !== undefined].filter(Boolean).length + if (locators > 1) c.addIssue({ code: "custom", path: ["index"], message: "--index, --path and --address are mutually exclusive" }) +}) + +export function registerWalletCommands(reg: CommandRegistry, services: { walletService: WalletService; ledger: LedgerDevice }): void { + const wallets = services.walletService + const empty = z.object({}) + + // ── create ─────────────────────────────────────────────────────────────── + const createFields = z.object({ + label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate"), + }) + reg.add({ + path: ["create"], + network: "none", + wallet: "none", + auth: "required", + passwordMode: "establish", + interactive: true, + promptHints: { label: "default-label" }, + summary: "Create a new HD wallet (BIP39 seed)", + fields: createFields, + input: createFields, + examples: [{ cmd: "wallet-cli create --label main" }], + formatText: TextFormatters.walletCreated("Created", ["Recovery phrase is encrypted locally and was not printed.", "Run `backup` soon and store the file offline."]), + run: async (_ctx, _net, input) => { + return wallets.create(input.label) + }, + } satisfies CommandDefinition) + + // ── import mnemonic ─────────────────────────────────────────────────────── + // BIP39 passphrase intentionally NOT exposed in phase 1 ; plumbing stays. + const importMnemonicFields = z.object({ + label: Schemas.label() + .optional() + .describe("human-friendly unique account label, 1-64 chars; omit to auto-generate; mnemonic comes from --mnemonic-stdin or interactive prompt"), + }) + reg.add({ + path: ["import", "mnemonic"], + stdin: "mnemonic", + network: "none", + wallet: "none", + auth: "required", + passwordMode: "establish", + interactive: true, + promptHints: { label: "default-label" }, + summary: "Import a BIP39 mnemonic phrase", + fields: importMnemonicFields, + input: importMnemonicFields, + examples: [{ cmd: "wallet-cli import mnemonic --label main" }], + formatText: TextFormatters.walletCreated("Imported", ["Recovery phrase was read from hidden input and was not printed."]), + run: async (ctx, _net, input) => { + const secret = await ctx.secrets.resolveSecret("mnemonic") + return wallets.importMnemonic(secret, input.label) + }, + } satisfies CommandDefinition) + + // ── import private-key ──────────────────────────────────────────────────── + const importPrivateKeyFields = z.object({ + label: Schemas.label() + .optional() + .describe("human-friendly unique account label, 1-64 chars; omit to auto-generate; private key comes from --private-key-stdin or interactive prompt"), + }) + reg.add({ + path: ["import", "private-key"], + stdin: "privateKey", + network: "none", + wallet: "none", + auth: "required", + passwordMode: "establish", + interactive: true, + promptHints: { label: "default-label" }, + summary: "Import a raw private key", + fields: importPrivateKeyFields, + input: importPrivateKeyFields, + examples: [{ cmd: "wallet-cli import private-key --label hot" }], + formatText: TextFormatters.walletCreated("Imported", ["Private key was read from hidden input and was not printed."]), + run: async (ctx, _net, input) => { + const secret = await ctx.secrets.resolveSecret("privateKey") + return wallets.importPrivateKey(secret, input.label) + }, + } satisfies CommandDefinition) + + // ── import ledger ───────────────────────────────────────────────────────── + reg.add({ + path: ["import", "ledger"], + network: "none", + wallet: "none", + auth: "none", + interactive: true, + promptHints: { label: "default-label", index: "skip", path: "skip", address: "skip", scanLimit: "skip" }, + requires: ["a connected, unlocked Ledger with the selected app (--app) open"], + summary: "Register a Ledger account (watch-only; signs on device)", + fields: walletImportLedgerFields, + input: walletImportLedgerInput, + examples: [{ cmd: "wallet-cli import ledger --app tron --index 0 --label cold" }], + formatText: TextFormatters.walletLedger, + run: async (ctx, _net, input) => { + const family: ChainFamily = FAMILY_BY_LEDGER_APP[input.app]! + const hasLocator = input.index !== undefined || input.path !== undefined || input.address !== undefined + const path = hasLocator || !ctx.prompt.isTTY() ? await resolveLedgerPath(services.ledger, family, input) : await selectLedgerPath(services.ledger, family, ctx.prompt) + ctx.emit({ type: "deriving-address" }) + return wallets.importLedger(family, path, input.label) + }, + } satisfies CommandDefinition) + + // ── import watch ────────────────────────────────────────────────────────── + const importWatchFields = z.object({ + address: z.string().min(1).describe("watch-only address to track; format: TRON base58 T...; family is auto-detected"), + label: Schemas.label().optional().describe("human-friendly unique account label, 1-64 chars; omit to auto-generate"), + }) + reg.add({ + path: ["import", "watch"], + network: "none", + wallet: "none", + auth: "none", + interactive: true, + promptHints: { label: "default-label" }, + summary: "Register a watch-only address (no secret)", + fields: importWatchFields, + input: importWatchFields, + examples: [{ cmd: "wallet-cli import watch --address T... --label team-vault" }], + formatText: TextFormatters.walletWatch, + run: async (_ctx, _net, input) => { + return wallets.importWatch(input.address, input.label) + }, + } satisfies CommandDefinition) + + // ── list ───────────────────────────────────────────────────────────────── + reg.add({ + path: ["list"], + network: "none", + wallet: "none", + auth: "none", + summary: "List wallets/accounts (no unlock needed)", + fields: empty, + input: empty, + examples: [{ cmd: "wallet-cli list --output json" }], + formatText: TextFormatters.walletList, + run: async () => wallets.list(), + } satisfies CommandDefinition) + + // ── use ────────────────────────────────────────────────────────────────── + const setActiveFields = z.object({ account: z.string().min(1).describe("accountId, label, or address to make active for future commands") }) + reg.add({ + path: ["use"], + network: "none", + wallet: "none", + auth: "none", + positionals: [{ field: "account" }], + summary: "Set the active account", + fields: setActiveFields, + input: setActiveFields, + examples: [{ cmd: "wallet-cli use main" }], + formatText: TextFormatters.walletUse, + run: async (_ctx, _net, input) => { + return wallets.use(input.account) + }, + } satisfies CommandDefinition) + + // ── current ─────────────────────────────────────────────────────────────── + // Read-only: always reports the persisted active account; ignores --account + // (use `use` to change it). wallet:"none" keeps help from + // advertising an --account override here. + reg.add({ + path: ["current"], + network: "none", + wallet: "none", + auth: "none", + summary: "Show the current active account", + fields: empty, + input: empty, + examples: [{ cmd: "wallet-cli current" }], + formatText: TextFormatters.walletCurrent, + run: async () => wallets.current(), + } satisfies CommandDefinition) + + // ── rename ──────────────────────────────────────────────────────────────── + const renameFields = z.object({ + account: z.string().min(1).describe("accountId, current label, or address to rename"), + label: Schemas.label().describe("new unique label, 1-64 chars"), + }) + reg.add({ + path: ["rename"], + network: "none", + wallet: "none", + auth: "none", + positionals: [{ field: "account" }], + summary: "Rename an account label", + fields: renameFields, + input: renameFields, + examples: [{ cmd: "wallet-cli rename main --label primary" }], + formatText: TextFormatters.walletRename, + run: async (_ctx, _net, input) => { + return wallets.rename(input.account, input.label) + }, + } satisfies CommandDefinition) + + // ── derive ──────────────────────────────────────────────────────────────── + // Wallet-level op: --seed-id picks the HD wallet directly by its seed id. No --account/active. + const addAccountFields = z.object({ + seedId: z.string().min(1).describe("seed id (wlt_…) of the HD wallet to derive from — shown as the HD group header in `list`"), + index: z.coerce.number().int().nonnegative().optional().describe("explicit HD account index, in account index; omit to use the next free index"), + label: Schemas.label().optional().describe("label for the new derived account, 1-64 chars; omit to auto-generate -"), + }) + reg.add({ + path: ["derive"], + network: "none", + wallet: "none", + auth: "required", + summary: "Derive the next HD account from a seed wallet (by --seed-id)", + fields: addAccountFields, + input: addAccountFields, + examples: [{ cmd: "wallet-cli derive --seed-id wlt_ab12cd34" }], + formatText: TextFormatters.walletDerive, + run: async (_ctx, _net, input) => { + return wallets.derive(input.seedId, input.index, input.label) + }, + } satisfies CommandDefinition) + + // ── delete ──────────────────────────────────────────────────────────────── + const deleteFields = z.object({ + account: accountRef("account or wallet to delete, addressed by accountId, label, or address"), + yes: z.boolean().default(false).describe("skip the interactive confirmation; required for non-TTY deletion"), + }) + reg.add({ + path: ["delete"], + network: "none", + wallet: "none", + auth: "none", + interactive: true, + positionals: [{ field: "account" }], + summary: "Delete a wallet/account and clean orphan labels", + fields: deleteFields, + input: deleteFields, + examples: [{ cmd: "wallet-cli delete old --yes" }], + formatText: TextFormatters.walletDelete, + run: async (ctx, _net, input) => { + if (!input.yes) { + if (!ctx.prompt.isTTY()) { + throw new UsageError("tty_required", "deletion needs confirmation: pass --yes or run in a terminal") + } + const d = wallets.describe(input.account) + const expect = d.label ?? d.accountId + const kind = d.label ? "label" : "account" + const ok = await ctx.prompt.confirm({ label: `Delete ${expect}? Type the exact ${kind} "${expect}" to confirm`, expect }) + if (!ok) throw new UsageError("aborted", "deletion not confirmed") + } + return wallets.delete(input.account) + }, + } satisfies CommandDefinition) + + // ── backup ──────────────────────────────────────────────────────────────── + // Writes the secret + metadata to a 0600 FILE (never stdout/envelope): the secret stays off + // screen, logs and AI context. stdout returns only metadata + the written path. + // master password via dispatch prime (passwordMode: "verify"); --password-stdin is the non-interactive source. + const backupFields = z.object({ + account: accountRef("account or wallet to export, addressed by accountId, label, or address"), + out: z + .string() + .optional() + .describe("output file path; omit to write /backups/-.json; file is created with mode 0600 and never overwritten"), + }) + reg.add({ + path: ["backup"], + network: "none", + wallet: "none", + auth: "required", + passwordMode: "verify", + interactive: true, + positionals: [{ field: "account" }], + summary: "Export an account's secret + metadata to a 0600 file", + fields: backupFields, + input: backupFields, + examples: [{ cmd: "wallet-cli backup main --out ~/main-backup.json --password-stdin" }], + formatText: TextFormatters.walletBackup, + run: async (_ctx, _net, input) => wallets.backup(input.account, input.out), + } satisfies CommandDefinition) +} diff --git a/ts/src/adapters/inbound/cli/context/context.test.ts b/ts/src/adapters/inbound/cli/context/context.test.ts new file mode 100644 index 000000000..7e97044cb --- /dev/null +++ b/ts/src/adapters/inbound/cli/context/context.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { buildExecutionContext, RuntimeDeps } from "./index.js"; +import { StreamManager } from "../stream/index.js"; +import { createOutputFormatter } from "../output/index.js"; +import type { Globals } from "../contracts/index.js"; + +function ctxWith(output: "text" | "json") { + const out: string[] = []; + const err: string[] = []; + const sm = new StreamManager(output, false, (s) => out.push(s), (s) => err.push(s)); + const formatter = createOutputFormatter(output, sm, 0); + const globals = { output, verbose: false } as Globals; + // only streams + formatter are exercised by emit(); the rest is lazily used elsewhere. + const deps = { config: { timeoutMs: 1 }, streams: sm, formatter } as unknown as RuntimeDeps; + return { ctx: buildExecutionContext(globals, deps), out, err }; +} + +describe("ExecutionContext.emit (progress events)", () => { + it("routes a json event through formatter+streams to stderr, never stdout", () => { + const { ctx, out, err } = ctxWith("json"); + ctx.emit({ type: "awaiting_device", reason: "sign" }); + expect(out).toEqual([]); + expect(JSON.parse(err[0]!)).toEqual({ type: "awaiting_device", reason: "sign" }); + }); + + it("renders a human line in text mode", () => { + const { ctx, err } = ctxWith("text"); + ctx.emit({ type: "broadcasting" }); + expect(err[0]).toContain("broadcasting"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/context/index.ts b/ts/src/adapters/inbound/cli/context/index.ts new file mode 100644 index 000000000..2f548ff5f --- /dev/null +++ b/ts/src/adapters/inbound/cli/context/index.ts @@ -0,0 +1,98 @@ +/** + * ExecutionContext — assemble runtime context from config, environment, and flags. Selection is + * account-level: activeAccount is resolved lazily from --account/--wallet or wallets.json. + * Build is side-effect-free; secrets never enter the serializable surface. + */ +import type { AccountRef, ChainFamily, Config, OutputMode } from "../../../../domain/types/index.js"; +import type { ProgressEvent } from "../../../../application/contracts/index.js"; +import type { NetworkRegistry } from "../../../../application/ports/network-registry.js"; +import type { ExecutionContext, Globals, SecretResolver, StreamManager } from "../contracts/index.js"; +import type { OutputFormatter } from "../output/index.js"; +import type { Prompter } from "../input/prompt/index.js"; +import type { AccountStore } from "../../../../application/ports/account-store.js"; +import { accountRef, walletAddress } from "../../../../domain/wallet/index.js"; +import { WalletError } from "../../../../domain/errors/index.js"; +import { SOURCE_KINDS } from "../../../../domain/sources/index.js"; + +export interface RuntimeDeps { + config: Config; + networkRegistry: NetworkRegistry; + streams: StreamManager; + secrets: SecretResolver; + keystore: AccountStore; + prompter: Prompter; + formatter: OutputFormatter; +} + +class ExecutionContextImpl implements ExecutionContext { + output: OutputMode; + timeoutMs: number; + wait: boolean; + waitTimeoutMs: number; + #activeRef?: AccountRef; + + constructor( + private readonly globals: Globals, + private readonly deps: RuntimeDeps, + ) { + this.output = globals.output ?? deps.config.defaultOutput; + this.timeoutMs = globals.timeoutMs ?? deps.config.timeoutMs; + this.wait = globals.wait ?? false; + this.waitTimeoutMs = globals.waitTimeoutMs ?? 60_000; + } + + get config(): Config { + return this.deps.config; + } + get networkRegistry(): NetworkRegistry { + return this.deps.networkRegistry; + } + get streams(): StreamManager { + return this.deps.streams; + } + get secrets(): SecretResolver { + return this.deps.secrets; + } + get prompt(): Prompter { + return this.deps.prompter; + } + get network(): string | undefined { + return this.globals.network; + } + + get activeAccount(): AccountRef { + if (this.#activeRef) return this.#activeRef; + const ks = this.deps.keystore; + let ref: AccountRef | null; + if (this.globals.account) { + const { wallet, index } = ks.resolveAccount(this.globals.account); + ref = accountRef(wallet.id, SOURCE_KINDS[wallet.source.type].isHD ? index : null); + } else { + ref = ks.activeAccount(); + } + if (!ref) { + throw new WalletError("missing_wallet_address", "no active account; import one or pass --account"); + } + this.#activeRef = ref; + return ref; + } + + resolveAddress(family: ChainFamily): string { + const { wallet, index } = this.deps.keystore.resolveAccount(this.activeAccount); + const address = walletAddress(wallet, family, index); + if (!address) throw new WalletError("missing_wallet_address", `active account has no ${family} address`); + return address; + } + + emit(e: ProgressEvent): void { + this.deps.streams.event(this.deps.formatter.event(e)); + } + + warn(message: string): void { + this.deps.streams.diagnostic("warn", message); + } +} + +export function buildExecutionContext(globals: Globals, deps: RuntimeDeps): ExecutionContext { + return new ExecutionContextImpl(globals, deps); +} diff --git a/ts/src/adapters/inbound/cli/contracts/command.ts b/ts/src/adapters/inbound/cli/contracts/command.ts new file mode 100644 index 000000000..00216fe78 --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/command.ts @@ -0,0 +1,96 @@ +/** CLI command metadata, validation, rendering and registration contracts. */ +import type { ZodObject, ZodRawShape, ZodType } from "zod"; +import type { ChainFamily } from "../../../../domain/family/index.js"; +import type { NetworkDescriptor } from "../../../../domain/types/network.js"; +import type { NetworkRequirement, WalletRequirement } from "../../../../application/contracts/index.js"; +import type { ExecutionContext } from "./execution-context.js"; + +export interface Example { + cmd: string; + note?: string; +} + +// "optional" = the command operates on an account; --account is optional and falls back to the +// active account (errors only if no account exists at all). "none" = never touches an account. +// (No "required": no command forces --account — active is always a valid default. cf. network.) +// "required" = unlocks the master password (sign / read secrets / encrypt); +// "none" = never unlocks. (No middle state — a command either needs the password or it doesn't.) +export type AuthRequirement = "none" | "required"; + +/** secret/payload channel a command reads from stdin; documents the matching --*-stdin flag. */ +export type StdinChannel = "privateKey" | "mnemonic" | "tx" | "message"; + +export interface TextRenderContext { + command: string; + net?: NetworkDescriptor; + /** label of the resolved active account, injected centrally; absent for wallet:"none" commands. */ + accountLabel?: string; +} + +export type TextFormatter = (data: O, ctx: TextRenderContext) => string | null; + +interface CommandDefinitionBase { + /** full typed path. Neutral commands carry their complete path (e.g. ["import","mnemonic"], + * ["config","get"], ["create"]); chain commands carry the logical path (e.g. ["tx","send"]) + * shared across families. The only routing discriminator is `family` present/absent. + * The stable identity (envelope `command` field) is derived from command metadata, not stored. */ + path: string[]; + family?: ChainFamily; + /** declares the command reads from a *-stdin channel; drives help/catalog input-flag docs. */ + stdin?: StdinChannel; + wallet: WalletRequirement; + auth: AuthRequirement; + /** broadcasts a transaction on-chain (✍️); enables the --wait global flag in help projection. */ + broadcasts?: boolean; + /** opt-in interactive master-password handling: "establish" = set on first wallet else verify; "verify" = require existing. Commands without this keep the lazy hasMasterPassword guard. */ + passwordMode?: "establish" | "verify"; + /** expose one or more `fields` entries as leading positionals (`block []`, `use []`, + * `config [] []`) instead of --flags: binds the CLI positionals in order, and help + * documents them under Args + Usage and drops them from the Flags list. `placeholder` defaults to `field`. */ + positionals?: { field: string; placeholder?: string }[]; + /** allow interactive TTY prompts (master password, secret, gap-fill, confirm). Absent ⇒ fail fast — safer for scripts/agents. */ + interactive?: boolean; + /** gap-fill prompt hints, by field name: "skip" = never prompt this optional field; "default-label" = offer a generated default. */ + promptHints?: Record; + capability?: string; + summary?: string; + /** extra command-specific preconditions rendered in the help "Requires:" block, ahead of the + * auto-derived network/auth/account lines (e.g. a connected Ledger for `import ledger`). */ + requires?: string[]; + /** per-field zod object; feeds the arity adapter + HelpService. */ + fields: ZodObject; + /** full validation schema (often fields.superRefine), used in dispatch. */ + input: ZodType; + examples: Example[]; + /** Optional command-specific renderer for text mode. JSON mode always uses the envelope. */ + formatText?: TextFormatter; +} + +/** A networkless command never receives a chain target. */ +export interface NetworklessCommandDefinition + extends CommandDefinitionBase { + network: "none"; + run(ctx: ExecutionContext, net: undefined, input: I): Promise; +} + +/** Both policies resolve a concrete network; "optional" only means the CLI flag may be omitted. */ +export interface NetworkedCommandDefinition + extends CommandDefinitionBase { + network: Exclude; + run(ctx: ExecutionContext, net: NetworkDescriptor, input: I): Promise; +} + +/** Network policy discriminates the run signature, preventing unsafe `network!` assertions. */ +export type CommandDefinition = + | NetworklessCommandDefinition + | NetworkedCommandDefinition; + +export interface ChainModule { + family: ChainFamily; + registerCommands(reg: CommandRegistryLike): void; +} + +/** structural view of CommandRegistry needed by ChainModule.registerCommands. */ +export interface CommandRegistryLike { + add(cmd: CommandDefinition): void; +} diff --git a/ts/src/adapters/inbound/cli/contracts/envelope.ts b/ts/src/adapters/inbound/cli/contracts/envelope.ts new file mode 100644 index 000000000..99cef10ae --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/envelope.ts @@ -0,0 +1,44 @@ +/** + * SharedTypes — output contract (result/error envelopes + progress events) + * and the global runtime flags parsed off argv. + */ +import type { ChainFamily } from "../../../../domain/family/index.js"; +import type { OutputMode } from "../../../../domain/types/primitives.js"; + +export interface ChainView { + family: ChainFamily; + network: string; + chainId: string; +} +export interface Meta { + durationMs: number; + warnings: string[]; +} +export interface ResultEnvelope { + schema: "wallet-cli.result.v1"; + success: true; + command: string; + chain?: ChainView; + data: unknown; + meta: Meta; +} +export interface ErrorEnvelope { + schema: "wallet-cli.result.v1"; + success: false; + command: string; + chain?: ChainView; + error: { code: string; message: string; details?: object }; + meta: Meta; +} + +// ═══════════════ global runtime flags parsed off argv ═════════════════════ +export interface Globals { + /** absent until the config default is resolved (runner bootstrap / buildExecutionContext). */ + output?: OutputMode; + network?: string; + account?: string; + timeoutMs?: number; + verbose: boolean; + wait?: boolean; + waitTimeoutMs?: number; +} diff --git a/ts/src/adapters/inbound/cli/contracts/execution-context.ts b/ts/src/adapters/inbound/cli/contracts/execution-context.ts new file mode 100644 index 000000000..86c33c94f --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/execution-context.ts @@ -0,0 +1,16 @@ +import type { Config, OutputMode } from "../../../../domain/types/index.js"; +import type { TransactionScope } from "../../../../application/contracts/index.js"; +import type { NetworkRegistry } from "../../../../application/ports/network-registry.js"; +import type { PromptPort } from "../../../../application/ports/prompt.js"; +import type { SecretResolver, StreamManager } from "./runtime.js"; + +/** CLI command context; application workflows receive only narrower execution scopes. */ +export interface ExecutionContext extends TransactionScope { + readonly config: Config; + readonly networkRegistry: NetworkRegistry; + readonly streams: StreamManager; + readonly secrets: SecretResolver; + readonly prompt: PromptPort; + readonly output: OutputMode; + readonly network?: string; +} diff --git a/ts/src/adapters/inbound/cli/contracts/index.ts b/ts/src/adapters/inbound/cli/contracts/index.ts new file mode 100644 index 000000000..5b7ce27aa --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/index.ts @@ -0,0 +1,4 @@ +export * from "./command.js"; +export * from "./envelope.js"; +export * from "./execution-context.js"; +export * from "./runtime.js"; diff --git a/ts/src/adapters/inbound/cli/contracts/runtime.ts b/ts/src/adapters/inbound/cli/contracts/runtime.ts new file mode 100644 index 000000000..06c6788de --- /dev/null +++ b/ts/src/adapters/inbound/cli/contracts/runtime.ts @@ -0,0 +1,40 @@ +/** CLI runtime seams implemented by stream and secret input adapters. */ +import type { NetworkDescriptor } from "../../../../domain/types/network.js"; + +export type DiagnosticLevel = "info" | "debug" | "warn"; + +export interface StreamManager { + result(text: string): void; + diagnostic(level: DiagnosticLevel, msg: string): void; + /** always-on stderr line. */ + errorLine(msg: string): void; + /** intermediate progress frame → stderr plain line; null is skipped (StreamManager). */ + event(frame: string | null): void; + readStdinOnce(): string; + /** warnings accumulated for the JSON envelope's meta.warnings. */ + warnings(): string[]; +} + +export type SecretKind = "password" | "privateKey" | "mnemonic" | "tx" | "message"; +export interface SecretResolver { + masterPassword(): string; + /** whether a master-password source exists, WITHOUT consuming stdin. */ + hasMasterPassword(): boolean; + /** whether a source for `kind` is configured, WITHOUT consuming it. */ + has(kind: SecretKind): boolean; + read(kind: SecretKind): string; + /** read a required source; missing → missing_option (usage), not secret_source_error. */ + require(kind: SecretKind): string; + /** exactly-one selector: inline value XOR the file/stdin source for `kind`. */ + pick(inline: string | undefined, kind: SecretKind, inlineFlag: string): string; + /** resolve a non-password secret: stdin source → hidden prompt → missing_option. */ + resolveSecret(kind: "mnemonic" | "privateKey"): Promise; + /** establish/verify the master password before synchronous keystore use. */ + primePassword(plan: { mode: "set" | "verify"; verify?: (pw: string) => boolean }): Promise; +} + +/** Mutable dispatch contract: CliShell records the in-flight command (+ resolved network) here so the + * runner's single terminal catch can attach commandId/net to the error envelope across yargs. */ +export interface SessionRef { + current?: { commandId: string; net?: NetworkDescriptor }; +} diff --git a/ts/src/adapters/inbound/cli/globals/index.ts b/ts/src/adapters/inbound/cli/globals/index.ts new file mode 100644 index 000000000..ef4a4fc8c --- /dev/null +++ b/ts/src/adapters/inbound/cli/globals/index.ts @@ -0,0 +1,134 @@ +/** + * Single source of truth for GLOBAL (kubectl-style) flags — the flag list, arity (kinds, aliases, + * yargs scalar types, choices), per-flag COERCION, and the DOCUMENTATION facts. Everything that + * otherwise drifts across a flag's touch-points derives from GLOBAL_FLAG_SPECS: + * - bootstrap/runner's pre-yargs scan (token maps via globalTokenMaps + coerceGlobalValue), + * - cli/shell's yargs `.options()` declaration (globalYargsOptions), and + * - cli/help/catalog's documentation projection (globalFlagDoc over description/defaultValue). + * Add a global flag HERE only; every layer is a projection. + * + * Array order is the documented display order (it's what cli/help renders). Other projections are + * order-independent. + */ +import type { SecretKind } from "../contracts/index.js"; + +export type GlobalFlagKind = "value" | "boolean" | "secret-stdin"; + +export interface GlobalFlagSpec { + /** kebab flag name, no leading dashes (e.g. "wait-timeout", "password-stdin"). */ + name: string; + /** single-char alias, no dash (e.g. "o", "v"). */ + alias?: string; + kind: GlobalFlagKind; + /** value flags only: yargs scalar type. */ + valueType?: "string" | "number"; + /** value flags only: restrict accepted values (yargs `choices`). */ + choices?: readonly string[]; + /** number flags only: minimum accepted value (inclusive). Defaults to 0. */ + min?: number; + /** secret-stdin flags only: which secret kind this `--` binds. */ + secretKey?: SecretKind; + /** override the derived camelCase field name when the runtime Globals key differs from the flag + * (e.g. `--timeout` → `timeoutMs`); defaults to globalFlagField(name). */ + field?: string; + /** human-readable flag description (cli/help text + --json-schema catalog). */ + description: string; + /** documented default; absent → rendered as plain "[optional]". */ + defaultValue?: string | number | boolean; + /** secret-stdin only: documented under each owning command, not in the global flag list. */ + commandScoped?: boolean; +} + +export const GLOBAL_FLAG_SPECS: readonly GlobalFlagSpec[] = [ + { name: "output", alias: "o", kind: "value", valueType: "string", choices: ["text", "json"], + description: "result format", defaultValue: "config.defaultOutput (built-in: text)" }, + { name: "network", kind: "value", valueType: "string", + description: "canonical network id, e.g. tron:mainnet, tron:nile, tron:shasta; chain commands fall back to config.defaultNetwork when omitted" }, + { name: "account", kind: "value", valueType: "string", + description: "accountId, label, or address for wallet-bound commands; falls back to the active account set by use" }, + { name: "timeout", kind: "value", valueType: "number", field: "timeoutMs", min: 1, + description: "per RPC/device call timeout, in milliseconds", defaultValue: "config.timeoutMs (built-in: 60000)" }, + { name: "verbose", alias: "v", kind: "boolean", + description: "show extra diagnostic output", defaultValue: false }, + { name: "wait", kind: "boolean", + description: "after broadcast, poll until the tx is confirmed/failed before returning; default returns the submitted txid without blocking", defaultValue: false }, + { name: "wait-timeout", kind: "value", valueType: "number", field: "waitTimeoutMs", + description: "--wait polling cap, in milliseconds; on timeout return the submitted receipt", defaultValue: 60000 }, + { name: "password-stdin", kind: "secret-stdin", secretKey: "password", + description: "read the master password from stdin (fd 0); only one *-stdin flag can consume stdin per run" }, + { name: "private-key-stdin", kind: "secret-stdin", secretKey: "privateKey", commandScoped: true, + description: "read the private key from stdin (fd 0)" }, + { name: "mnemonic-stdin", kind: "secret-stdin", secretKey: "mnemonic", commandScoped: true, + description: "read the BIP39 mnemonic from stdin (fd 0)" }, + { name: "tx-stdin", kind: "secret-stdin", secretKey: "tx", commandScoped: true, + description: "read the signed transaction JSON from stdin (fd 0)" }, + { name: "message-stdin", kind: "secret-stdin", secretKey: "message", commandScoped: true, + description: "read the message bytes/text from stdin (fd 0)" }, +]; + +/** kebab → camel default; the `field` override wins when the runtime Globals key differs from the flag. */ +export const globalFlagField = (name: string): string => name.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase()); +const specField = (f: GlobalFlagSpec): string => f.field ?? globalFlagField(f.name); + +/** value-flag spec keyed by its runtime Globals field, for coercion. */ +const VALUE_SPEC_BY_FIELD: Record = Object.fromEntries( + GLOBAL_FLAG_SPECS.filter((f) => f.kind === "value").map((f) => [specField(f), f]), +); + +/** + * Coerce a raw value-flag string per its spec; `undefined` = invalid (caller falls back to default). + * Derives entirely from valueType/choices: number flags accept a finite value at or above the spec's + * `min` (default 0; --timeout sets min 1 since a 0ms bound aborts instantly), choice flags must match, + * everything else passes through as a string. + */ +export function coerceGlobalValue(field: string, raw: string): string | number | undefined { + const spec = VALUE_SPEC_BY_FIELD[field]; + if (!spec) return raw; + if (spec.valueType === "number") { + const n = Number(raw); + return Number.isFinite(n) && n >= (spec.min ?? 0) ? n : undefined; + } + if (spec.choices) return spec.choices.includes(raw) ? raw : undefined; + return raw; +} + +interface YargsOption { + type: "string" | "number" | "boolean"; + choices?: readonly string[]; + alias?: string; +} + +/** yargs `.options()` shape, keyed by kebab flag name. secret-stdin + boolean flags are presence flags. */ +export function globalYargsOptions(): Record { + const out: Record = {}; + for (const f of GLOBAL_FLAG_SPECS) { + const o: YargsOption = { type: f.kind === "value" ? f.valueType! : "boolean" }; + if (f.choices) o.choices = f.choices; + if (f.alias) o.alias = f.alias; + out[f.name] = o; + } + return out; +} + +/** Token-keyed lookup maps for the pre-yargs scan (bootstrap/runner). Sibling projection to + * globalYargsOptions — the other layer that derives from GLOBAL_FLAG_SPECS. */ +export interface GlobalTokenMaps { + /** flag token (`--long` or `-alias`) → runtime Globals field. */ + valueFlags: Record; + booleanFlags: Record; + /** `---stdin` → secret kind (the only stdin source is fd 0). */ + secretStdinFlags: Record; +} + +export function globalTokenMaps(): GlobalTokenMaps { + const valueFlags: Record = {}; + const booleanFlags: Record = {}; + const secretStdinFlags: Record = {}; + for (const f of GLOBAL_FLAG_SPECS) { + const tokens = f.alias ? [`--${f.name}`, `-${f.alias}`] : [`--${f.name}`]; + if (f.kind === "value") for (const t of tokens) valueFlags[t] = specField(f); + else if (f.kind === "boolean") for (const t of tokens) booleanFlags[t] = specField(f); + else secretStdinFlags[`--${f.name}`] = f.secretKey!; + } + return { valueFlags, booleanFlags, secretStdinFlags }; +} diff --git a/ts/src/adapters/inbound/cli/help/catalog.ts b/ts/src/adapters/inbound/cli/help/catalog.ts new file mode 100644 index 000000000..8438bd739 --- /dev/null +++ b/ts/src/adapters/inbound/cli/help/catalog.ts @@ -0,0 +1,86 @@ +/** + * Help catalog — the machine-readable half of HelpService: the structured global/stdin flag + * model and the `--json-schema` command catalog. The human text renderer lives in./index. + * Single structured source: the flag model is rendered as text in command --help AND emitted as + * `globalFlags` in the root catalog. + */ +import { z } from "zod"; +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { CommandRegistry } from "../registry/index.js"; +import { commandId } from "../command-id.js"; +import { GLOBAL_FLAG_SPECS, type GlobalFlagSpec } from "../globals/index.js"; + +// Flags accepted on every command (kubectl-style globals + secret channels). The flag model — arity, +// descriptions, defaults, and the global-vs-command-scoped split — is owned by domain metadata +// GLOBAL_FLAG_SPECS; this layer is the rendered shape (`--flag`/`-alias` tokens) and a pure +// projection over it. GLOBAL_FLAGS = the globally-listed flags; STDIN_FLAGS = the command-scoped ones. +export interface GlobalFlag { + flag: string; + alias?: string; + type: "string" | "number" | "boolean"; + values?: string[]; + description: string; + optional?: boolean; + defaultValue?: string | number | boolean; +} + +/** spec → rendered GlobalFlag (adds the `--`/`-` token prefixes the help/catalog surface use). */ +function globalFlagDoc(f: GlobalFlagSpec): GlobalFlag { + return { + flag: `--${f.name}`, + ...(f.alias ? { alias: `-${f.alias}` } : {}), + type: f.kind === "value" ? f.valueType! : "boolean", + ...(f.choices ? { values: [...f.choices] } : {}), + description: f.description, + ...(f.defaultValue !== undefined ? { defaultValue: f.defaultValue } : {}), + }; +} + +export const GLOBAL_FLAGS: readonly GlobalFlag[] = GLOBAL_FLAG_SPECS.filter((f) => !f.commandScoped).map(globalFlagDoc); + +// stdin channel → its documented --*-stdin flag, derived from the command-scoped specs (keyed by secretKey). +const STDIN_FLAGS = Object.fromEntries( + GLOBAL_FLAG_SPECS.filter((f) => f.commandScoped).map((f) => [f.secretKey, globalFlagDoc(f)]), +) as Record, GlobalFlag>; + +export function inputFlagsFor(cmd: CommandDefinition): readonly GlobalFlag[] { + return cmd.stdin ? [STDIN_FLAGS[cmd.stdin]] : []; +} + +/** usage line: the typed path is complete for both kinds (neutral = full, chain = logical). */ +export function commandUsage(cmd: CommandDefinition): string { + return `wallet-cli ${cmd.path.join(" ")} [options]`; +} + +/** never let one un-convertible schema break the whole catalog. */ +function commandInputSchema(input: CommandDefinition["input"]): unknown { + try { + return z.toJSONSchema(input as z.ZodType); + } catch { + return { type: "object" }; + } +} + +/** machine-readable catalog of the whole command surface — the agent's single discovery call. */ +export function buildCatalog(registry: CommandRegistry, version: string, familyFilter?: ChainFamily): string { + const commands = registry + .all() + .filter((c) => !familyFilter || c.family === familyFilter) + .map((cmd) => ({ cmd, id: commandId(cmd) })) + .sort((a, b) => a.id.localeCompare(b.id)) + .map(({ cmd, id }) => ({ + id, + kind: cmd.family ? "chain" : "neutral", + ...(cmd.family ? { family: cmd.family } : {}), + path: cmd.path, + usage: commandUsage(cmd), + summary: cmd.summary ?? "", + requires: { network: cmd.network, auth: cmd.auth, wallet: cmd.wallet }, + ...(cmd.capability ? { capability: cmd.capability } : {}), + examples: cmd.examples.map((e) => e.cmd), + ...(inputFlagsFor(cmd).length ? { inputFlags: inputFlagsFor(cmd) } : {}), + inputSchema: commandInputSchema(cmd.input), + })); + return JSON.stringify({ tool: "wallet-cli", version, globalFlags: GLOBAL_FLAGS, commands }); +} diff --git a/ts/src/adapters/inbound/cli/help/help.test.ts b/ts/src/adapters/inbound/cli/help/help.test.ts new file mode 100644 index 000000000..4e5e6cdc4 --- /dev/null +++ b/ts/src/adapters/inbound/cli/help/help.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest" +import { z } from "zod" +import { HelpService } from "./index.js" +import { CommandRegistry } from "../registry/index.js" +import type { CommandDefinition, StreamManager } from "../contracts/index.js" + +// ── minimal fakes ───────────────────────────────────────────────────────────── + +function makeStream(): StreamManager & { last: string | undefined } { + const s: any = { + last: undefined, + result(text: string) { s.last = text }, + diagnostic() {}, errorLine() {}, event() {}, readStdinOnce: () => "", warnings: () => [], + } + return s +} + +function chainCmd(path: string[], shape: z.ZodRawShape): CommandDefinition { + const fields = z.object(shape) + return { + path, family: "tron", network: "optional", wallet: "none", auth: "none", + fields, input: fields, examples: [], run: async () => ({}), + } as unknown as CommandDefinition +} + +function build(): { help: HelpService; stream: ReturnType } { + const reg = new CommandRegistry() + reg.add(chainCmd(["block"], { number: z.string().optional() })) // single-segment leaf + reg.add(chainCmd(["tx", "info"], { txid: z.string().min(1) })) // multi-segment leaf + reg.add(chainCmd(["tx", "send"], { to: z.string() })) // sibling under the tx group + const stream = makeStream() + const help = new HelpService(reg, stream, "9.9.9") + return { help, stream } +} + +// ── --json-schema resolution ────────────────────────────────────────────────── + +describe("HelpService --json-schema", () => { + it("emits a multi-segment chain leaf's own input schema (not the catalog)", () => { + const { help, stream } = build() + help.handleMeta(["tx", "info", "--json-schema"]) + const out = JSON.parse(stream.last!) + expect(out.properties).toHaveProperty("txid") + expect(out).not.toHaveProperty("commands") // catalog shape has `commands` + }) + + it("still emits a single-segment chain leaf's input schema", () => { + const { help, stream } = build() + help.handleMeta(["block", "--json-schema"]) + const out = JSON.parse(stream.last!) + expect(out.properties).toHaveProperty("number") + expect(out).not.toHaveProperty("commands") + }) + + it("emits the machine catalog for a group head with no bare command", () => { + const { help, stream } = build() + help.handleMeta(["tx", "--json-schema"]) + const out = JSON.parse(stream.last!) + expect(out).toHaveProperty("commands") // group head → catalog, not a phantom command schema + }) +}) diff --git a/ts/src/adapters/inbound/cli/help/index.ts b/ts/src/adapters/inbound/cli/help/index.ts new file mode 100644 index 000000000..de1759826 --- /dev/null +++ b/ts/src/adapters/inbound/cli/help/index.ts @@ -0,0 +1,430 @@ +/** + * HelpService — --help / --version / --json-schema. Zod-driven: every flag's help, + * required/optional/default, examples, and the agent JSON-schema come from the command's + * zod fields/input; one schema supplies validation, types, help, and agent schema. + * + * Two command kinds, discriminated by `family`: neutral (full path) and chain (logical path, + * per-family impls). A leading family token (e.g. tron) is an optional addressing prefix here. + */ +import { z } from "zod" +import type { ChainFamily, ExitCode } from "../../../../domain/types/index.js" +import type { CommandDefinition, StreamManager } from "../contracts/index.js" +import { CommandRegistry } from "../registry/index.js" +import { introspectFields, type FieldInfo } from "../arity/index.js" +import { GLOBAL_FLAGS, type GlobalFlag, inputFlagsFor, buildCatalog } from "./catalog.js" + +const META = new Set(["--help", "-h", "--version", "-V", "--json-schema"]) + +export function hasMeta(tokens: string[]): boolean { + return tokens.some((t) => META.has(t)) +} + +export class HelpService { + constructor( + private readonly registry: CommandRegistry, + private readonly streams: StreamManager, + private readonly version: string, + ) {} + + handleMeta(tokens: string[]): ExitCode { + if (tokens.includes("--version") || tokens.includes("-V")) { + this.streams.result(this.version) + return 0 + } + const positionals = tokens.filter((t) => !t.startsWith("-")) + const { family, path } = this.#split(positionals) + const concrete = this.#resolveConcrete(family, path) + + if (tokens.includes("--json-schema")) { + if (concrete) { + this.streams.result(JSON.stringify(z.toJSONSchema(concrete.input))) + return 0 + } + // no concrete command → machine catalog (every command + flags), optionally scoped to a + // chain family (`tron --json-schema`). Mirrors the help tree. + this.streams.result(this.#catalog(family)) + return 0 + } + + if (concrete) { + this.streams.result(this.#renderCommand(concrete)) + return 0 + } + if (!family && path.length === 1 && this.#isNeutralGroup(path[0]!)) { + this.streams.result(this.#renderNeutralGroup(path[0]!)) + return 0 + } + if (path.length > 1 && this.#isChainGroup(path[0]!)) { + let candidates = this.registry.resolveCandidates(path) + if (family) candidates = candidates.filter((c) => c.family === family) + if (candidates.length > 0) { + this.streams.result(this.#renderLogicalCommand(path, candidates)) + return 0 + } + } + this.streams.result(this.#renderTree(path[0])) + return 0 + } + + /** strip an optional leading family token (e.g. tron) — a help/catalog addressing prefix. */ + #split(positionals: string[]): { family?: ChainFamily; path: string[] } { + const head = positionals[0] + if (head && (this.registry.families() as string[]).includes(head)) { + return { family: head as ChainFamily, path: positionals.slice(1) } + } + return { path: positionals } + } + + /** resolve to a single command: a neutral command by full path, or a family-pinned chain command. */ + #resolveConcrete(family: ChainFamily | undefined, path: string[]): CommandDefinition | null { + if (path.length === 0) return null + if (family) return this.registry.resolveForFamily(path, family) + const neutral = this.registry.resolveNeutral(path) + if (neutral) return neutral + // unique chain leaf: if the full logical path has exactly one impl (single family), render/emit + // that command directly — so `block` and `tx info` behave alike without a family prefix. Once a + // path has multiple families (e.g. tron + evm), it's ambiguous → fall through to the family-scoped + // catalog instead. + const exact = this.registry.resolveCandidates(path) + if (exact.length === 1) return exact[0]! + // single-segment chain leaf (e.g. `block`): resolve by its HEAD so `block 123` and even + // `block ` still render the leaf help instead of a phantom `block COMMAND` group. Group heads + // like `account` have no command at the bare path, so they stay groups (headLeaf is undefined). + const headLeaf = this.registry.resolveCandidates([path[0]!])[0] + if (headLeaf && headLeaf.path.length === 1) return headLeaf + return null + } + + #renderTree(head?: string): string { + if (!head) return this.#renderRoot() + if (this.#isChainGroup(head)) return this.#renderLogicalNs(head) + if (this.#isNeutralGroup(head)) return this.#renderNeutralGroup(head) + return this.#renderRoot() + } + + /** top-level overview: first release presents TRON as the product surface. + * Docker-style three groups: Common (高频入口) / Management (链上资源名词) / Commands (本机治理). */ + #renderRoot(): string { + const common = [ + ["create", "Create a new HD wallet (BIP39 seed)", ""], + ["import", "Import a wallet", ""], + ["list", "List wallets / accounts", ""], + ] as const + const management = [ + ["account", "Query on-chain account state", ""], + ["token", "Manage the token address book and query tokens", ""], + ["tx", "Build, send, broadcast, and inspect transactions", ""], + ["contract", "Call, send, deploy, and inspect smart contracts", ""], + ["stake", "Stake / delegate resources", "tron"], + ["message", "Sign arbitrary messages", ""], + ["block", "Get a block (latest if omitted)", ""], + ] as const + const commands = [ + ["use", "Set the active account", ""], + ["current", "Show the current (active) account", ""], + ["rename", "Rename an account label", ""], + ["derive", "Derive the next HD account from a seed wallet", ""], + ["backup", "Export an account's secret + metadata (0600)", ""], + ["delete", "Delete a wallet / account", ""], + ["config", "Show / get / set configuration values", ""], + ["networks", "List known networks", ""], + ] as const + const sections = [common, management, commands] as const + const nameWidth = Math.max(...sections.flat().map(([name]) => name.length)) + 2 + // chain-only groups carry a right-hand (family) tag; align it past the widest description. + const tagCol = Math.max(...sections.flat().map(([, desc]) => desc.length)) + 2 + const commandRow = (name: string, desc: string, tag: string): string => { + const body = ` ${name.padEnd(nameWidth)}${dim(desc)}` + return tag ? `${body}${" ".repeat(Math.max(2, tagCol - desc.length))}(${tag})` : body.trimEnd() + } + const row = + (width: number) => + (name: string, desc: string): string => + ` ${name.padEnd(width)}${desc ? dim(desc) : ""}`.trimEnd() + const optionRows = [ + ["-o, --output string", 'Output format ("text", "json") (default from config)'], + ["--network string", 'Canonical network id, e.g. "tron:mainnet", "tron:nile", "tron:shasta"'], + ["--account string", "Account label or address to act as (overrides active)"], + ["--timeout int", "Request timeout in milliseconds"], + ["-v, --verbose", "Verbose / debug logging"], + ["-h, --help", "Show help"], + ["-V, --version", "Print version information and quit"], + ] as const + const optionRow = row(Math.max(...optionRows.map(([name]) => name.length)) + 2) + + // Usage first, description after (: 描述统一在 Usage 之后); root Usage is the inline form. + const lines = [ + `${bold("Usage:")} wallet-cli [OPTIONS] COMMAND`, + "", + `${bold("wallet-cli")} — CLI wallet for TRON.`, + "Agent-first: deterministic exit codes, JSON output.", + "", + bold("Common Commands:"), + ] + for (const [name, desc, tag] of common) lines.push(commandRow(name, desc, tag)) + + lines.push("", bold("Management Commands:")) + for (const [name, desc, tag] of management) lines.push(commandRow(name, desc, tag)) + + lines.push("", bold("Commands:")) + for (const [name, desc, tag] of commands) lines.push(commandRow(name, desc, tag)) + + lines.push("", bold("Global Options:")) + for (const [name, desc] of optionRows) lines.push(optionRow(name, desc)) + lines.push("", "Run 'wallet-cli COMMAND --help' for more information on a command.") + return lines.join("\n") + } + + /** neutral group (`import --help`): list the group's sub-commands. Derived from the registry. */ + #renderNeutralGroup(head: string): string { + const cmds = this.#neutralGroupCommands(head) + const rows = cmds.map((c) => [c.path[1] ?? "", c.summary ?? ""] as const) + return this.#renderGroup(head, rows, 1000) + } + + /** logical resource group (`account --help`): default surface, implementations chosen by --network/defaultNetwork. */ + #renderLogicalNs(group: string): string { + const commands = this.#chainGroupCommands(group) + const rows = commands.map((c) => [c.path[1] ?? "", c.summary ?? ""] as const) + return this.#renderGroup(group, rows, 18) + } + + /** shared group skeleton (群组层): inline Usage → description → verb list → footer. */ + #renderGroup(group: string, rows: ReadonlyArray, maxWidth: number): string { + const width = Math.min(maxWidth, Math.max(0, ...rows.map(([verb]) => verb.length)) + 2) + const lines = [`${bold("Usage:")} wallet-cli ${group} COMMAND`, ""] + const desc = GROUP_DESCRIPTIONS[group] + if (desc) lines.push(desc, "") + lines.push(bold("Commands:")) + for (const [verb, summary] of rows) lines.push(` ${verb.padEnd(width)} ${summary}`.trimEnd()) + lines.push("", `Run 'wallet-cli ${group} COMMAND --help' for more information on a command.`) + return lines.join("\n") + } + + /** logical leaf (`account balance --help`): merge per-family flags; addressing/auth taken from the first impl. */ + #renderLogicalCommand(path: string[], candidates: CommandDefinition[]): string { + const fields = new Map() + for (const cmd of candidates) { + for (const f of introspectFields(cmd.fields)) fields.set(f.name, f) + } + const base = candidates[0]! + return this.#renderLeaf({ + path, + summary: base.summary, + network: base.network, + auth: base.auth, + wallet: base.wallet, + broadcasts: base.broadcasts, + fields: [...fields.values()], + inputFlags: inputFlagsFor(base), + examples: base.examples, + requires: base.requires, + positionals: base.positionals, + }) + } + + #renderCommand(cmd: CommandDefinition): string { + return this.#renderLeaf({ + path: cmd.path, + summary: cmd.summary, + network: cmd.network, + auth: cmd.auth, + wallet: cmd.wallet, + broadcasts: cmd.broadcasts, + fields: introspectFields(cmd.fields), + inputFlags: inputFlagsFor(cmd), + examples: cmd.examples, + requires: cmd.requires, + positionals: cmd.positionals, + }) + } + + /** shared leaf skeleton (叶子层): Usage → description → Requires → Options (incl. stdin channel) → Global options → Examples. */ + #renderLeaf(c: { + path: string[] + summary?: string + network: CommandDefinition["network"] + auth: CommandDefinition["auth"] + wallet: CommandDefinition["wallet"] + broadcasts?: boolean + fields: FieldInfo[] + inputFlags: readonly GlobalFlag[] + examples: CommandDefinition["examples"] + requires?: string[] + positionals?: { field: string; placeholder?: string }[] + }): string { + const positionals = (c.positionals ?? []).map((p) => { + const field = c.fields.find((f) => f.name === p.field) + const name = p.placeholder ?? p.field + const required = field ? !field.optional && !field.hasDefault : false + return { name, required, description: field?.description ?? "" } + }) + const usagePositional = positionals.map((p) => (p.required ? ` <${p.name}>` : ` [<${p.name}>]`)).join("") + const lines = ["Usage:", ` wallet-cli ${c.path.join(" ")}${usagePositional} [options]`] + if (c.summary) lines.push("", c.summary) + + if (positionals.length) { + lines.push("", "Args:") + const width = Math.min(34, Math.max(...positionals.map((p) => p.name.length))) + for (const p of positionals) lines.push(` ${p.name.padEnd(width)} ${p.description}`.trimEnd()) + } + + const requires: string[] = [...(c.requires ?? [])] + if (c.network === "required") requires.push("--network ") + if (c.auth === "required") requires.push("master password — pass --password-stdin for non-interactive use, or enter it interactively in a TTY") + if (c.wallet !== "none") requires.push("an account — defaults to active; override with --account (or run `wallet-cli use ` to change the active account)") + if (requires.length) { + lines.push("", "Requires:") + for (const r of requires) lines.push(` ${r}`) + } + + // positional fields are documented under Args, not repeated as --flags. A command's stdin channel + // (--*-stdin) is a command-specific option too, so it renders inline under Options — not a section + // of its own. (The machine --json-schema catalog still keeps inputFlags as a distinct key.) + const posNames = new Set((c.positionals ?? []).map((p) => p.field)) + const flagFields = posNames.size ? c.fields.filter((f) => !posNames.has(f.name)) : c.fields + const optionRows: Array<{ head: string; desc: string; tag: string }> = [ + ...flagFields.map((f) => ({ head: flagHead(f), desc: f.description ?? "", tag: flagTag(f) })), + ...c.inputFlags.map((g) => ({ head: globalFlagHead(g), desc: g.description, tag: globalFlagTag(g) })), + ] + if (optionRows.length) { + const width = Math.min(34, Math.max(...optionRows.map((r) => r.head.length))) + lines.push("", "Options:") + for (const r of optionRows) { + lines.push(` ${r.head.padEnd(width)} ${r.desc}${r.desc && r.tag ? " " : ""}${r.tag}`.trimEnd()) + } + } + + lines.push("", "Global options:") + // curated per command: --network only when the command selects a network; --password-stdin + // only when it requires unlock; --account only when the command acts as an account. + for (const g of globalFlagsForText(c.network, c.auth, c.wallet, c.broadcasts ?? false)) lines.push(globalFlagLine(g)) + + if (c.examples.length) { + lines.push("", "Examples:") + for (const e of c.examples) lines.push(` ${e.cmd}${e.note ? ` # ${e.note}` : ""}`) + } + return lines.join("\n") + } + + /** chain groups = first path segment of every family-bound command. */ + #chainGroups(): string[] { + const seen = new Set() + const out: string[] = [] + for (const c of this.registry.all()) { + if (!c.family) continue + const group = c.path[0] + if (group && !seen.has(group)) (seen.add(group), out.push(group)) + } + return out + } + + #isChainGroup(group: string): boolean { + return this.#chainGroups().includes(group) + } + + /** chain group sub-commands, deduped across families by logical path. */ + #chainGroupCommands(group: string): Array<{ path: string[]; summary?: string }> { + const seen = new Set() + const out: Array<{ path: string[]; summary?: string }> = [] + for (const c of this.registry.all()) { + if (!c.family || c.path[0] !== group) continue + const key = c.path.join(".") + if (!seen.has(key)) (seen.add(key), out.push({ path: c.path, summary: c.summary })) + } + return out + } + + /** neutral groups = heads of neutral commands that have sub-verbs (e.g. import). */ + #neutralGroupCommands(head: string): CommandDefinition[] { + return this.registry.all().filter((c) => !c.family && c.path[0] === head && c.path.length > 1) + } + + #isNeutralGroup(head: string): boolean { + return this.#neutralGroupCommands(head).length > 0 + } + + /** machine-readable catalog of the whole command surface — the agent's single discovery call. */ + #catalog(familyFilter?: ChainFamily): string { + return buildCatalog(this.registry, this.version, familyFilter) + } +} + +/** "--flag " header for a command flag — enum fields list their choices instead of . */ +function flagHead(f: FieldInfo): string { + const typ = f.choices ? ` <${f.choices.join("|")}>` : f.baseType === "boolean" ? "" : ` <${f.baseType}>` + return `--${f.kebab}${typ}` +} + +/** "[required]" / "[optional, default: X]" / "[optional]" tag derived from the zod schema. */ +function flagTag(f: FieldInfo): string { + if (!f.optional && !f.hasDefault) return "[required]" + if (f.hasDefault) return `[optional, default: ${formatDefault(f.defaultValue)}]` + return "[optional]" +} + +function formatDefault(v: unknown): string { + if (typeof v === "string") return v === "" ? '""' : v + return String(v) +} + +// Per-command "Global options" projection: output/timeout/verbose always; --network only when the +// command selects a network; --password-stdin only when it requires unlock; --wait/--wait-timeout +// only for ✍️ broadcast commands; --account only when the command acts as an account (also surfaced, +// with fuller semantics, under Requires). The full GLOBAL_FLAGS array still backs the --json-schema catalog. +function globalFlagsForText( + network: CommandDefinition["network"], + auth: CommandDefinition["auth"], + wallet: CommandDefinition["wallet"], + broadcasts: boolean, +): GlobalFlag[] { + return GLOBAL_FLAGS.filter((g) => { + if (g.flag === "--account") return wallet !== "none" + if (g.flag === "--network") return network !== "none" + if (g.flag === "--password-stdin") return auth === "required" + if (g.flag === "--wait" || g.flag === "--wait-timeout") return broadcasts + return true + }) +} + +/** one rendered " --flag description [tag]" line, used by the Global options section. */ +function globalFlagLine(g: GlobalFlag): string { + const tag = globalFlagTag(g) + return ` ${globalFlagHead(g).padEnd(26)} ${g.description}${g.description && tag ? " " : ""}${tag}`.trimEnd() +} + +// Group (群组层) one-line descriptions, keyed by the registry group head. Only groups that surface a +// ` --help` page need an entry; absent → the description line is omitted. +const GROUP_DESCRIPTIONS: Record = { + import: "Import a wallet from an existing secret or device.", + account: "Query on-chain account state.", + token: "Manage the token address book and query tokens.", + tx: "Build, send, broadcast, and inspect transactions.", + contract: "Call, send, deploy, and inspect smart contracts.", + stake: "Stake / delegate resources (TRON Stake 2.0).", + message: "Sign arbitrary messages.", + block: "Get a block (latest if omitted).", +} + +/** "--output, -o " style header for text help. */ +function globalFlagHead(g: GlobalFlag): string { + const head = g.alias ? `${g.flag}, ${g.alias}` : g.flag + const typ = g.type === "boolean" ? "" : ` <${g.values ? g.values.join("|") : g.type}>` + return `${head}${typ}` +} + +function globalFlagTag(g: GlobalFlag): string { + if (g.defaultValue !== undefined) return `[optional, default: ${formatDefault(g.defaultValue)}]` + return "[optional]" +} + +/** color only when stdout is a TTY and NO_COLOR is unset — piped/redirected help stays plain. */ +function colorOn(): boolean { + return !!process.stdout.isTTY && !process.env.NO_COLOR +} +function bold(s: string): string { + return colorOn() ? `\x1b[1m${s}\x1b[0m` : s +} +function dim(s: string): string { + return colorOn() ? `\x1b[2m${s}\x1b[0m` : s +} diff --git a/ts/src/adapters/inbound/cli/input/prompt/index.ts b/ts/src/adapters/inbound/cli/input/prompt/index.ts new file mode 100644 index 000000000..6b0a02975 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/index.ts @@ -0,0 +1,249 @@ +/** + * Prompter — the single owner of interactive TTY I/O. Logic (validation loops, + * confirm match, select navigation) is backend-agnostic; the real backend talks to + * /dev/tty. Prompts/echo never touch stdout. + */ +import { openSync, closeSync } from "node:fs"; +import { ReadStream } from "node:tty"; +import * as readline from "node:readline"; +import { ExecutionError } from "../../../../../domain/errors/index.js"; + +export type KeyEvent = { name?: string; ctrl?: boolean; sequence?: string }; +export interface Choice { value: T; label: string } + +export interface PromptBackend { + isTTY(): boolean; + /** read one line; when hidden, keystrokes are not echoed. */ + question(prompt: string, hidden: boolean): Promise; + /** resolve on the next keypress (raw mode must be active). */ + readKey(): Promise; + write(s: string): void; + beginRaw(): void; + endRaw(): void; + /** release any held resources (e.g. the /dev/tty stream) so the process can exit. */ + close?(): void; +} + +export class Prompter { + #renderedSelectLines = 0; + #interactive = true; + + constructor(private readonly be: PromptBackend) {} + + /** + * Per-dispatch interaction policy. Dispatch sets this per command so non-interactive commands + * behave as if there were no TTY — every prompt site already gates on isTTY(), so flipping this + * off makes gap-fill / password / secret prompts fail-fast on missing input instead of blocking. + */ + setInteractive(allowed: boolean): void { this.#interactive = allowed; } + + isTTY(): boolean { return this.#interactive && this.be.isTTY(); } + + /** release backend resources at end of run (no-op for in-memory backends). */ + close(): void { this.be.close?.(); } + + async text(o: { label: string; validate?: (v: string) => string | null }): Promise { + for (;;) { + const v = (await this.be.question(`${color("cyan", "?")} ${o.label}: `, false)).trim(); + const err = o.validate?.(v); + if (err) { this.be.write(`${color("red", " x")} ${err}\n`); continue; } + return v; + } + } + + async hidden(o: { label: string; confirm?: boolean; confirmLabel?: string; validate?: (v: string) => string | null }): Promise { + for (;;) { + const v = await this.be.question(`${color("cyan", "?")} ${o.label}: `, true); + const err = o.validate?.(v); + if (err) { this.be.write(`${color("red", " x")} ${err}\n`); continue; } + if (o.confirm) { + const again = await this.be.question(`${color("cyan", "?")} ${o.confirmLabel ?? "Confirm"}: `, true); + if (v !== again) { this.be.write(`${color("red", " x")} entries do not match\n`); continue; } + } + return v; + } + } + + async confirm(o: { label: string; expect?: string }): Promise { + const suffix = o.expect === undefined ? " [y/N]" : ""; + const v = await this.be.question(`${color("yellow", "?")} ${o.label}${suffix}: `, false); + if (o.expect !== undefined) return v.trim() === o.expect; + return /^y(es)?$/i.test(v.trim()); + } + + async select(o: { label: string; choices: Choice[]; loadMore?: () => Promise[]> }): Promise { + let items = [...o.choices]; + let idx = 0; + this.be.beginRaw(); + try { + this.#renderedSelectLines = 0; + this.#render(o.label, items, idx); + for (;;) { + const k = await this.be.readKey(); + if (k.ctrl && k.name === "c") throw new ExecutionError("aborted", "cancelled"); + if (k.name === "up") idx = Math.max(0, idx - 1); + else if (k.name === "down") { + if (idx === items.length - 1 && o.loadMore) { + const more = await o.loadMore(); + if (more.length > items.length) { items = more; idx++; } + } else { + idx = Math.min(items.length - 1, idx + 1); + } + } else if (k.name === "return") return items[idx]!.value; + this.#render(o.label, items, idx); + } + } finally { + this.be.endRaw(); + this.#renderedSelectLines = 0; + } + } + + #render(label: string, items: Choice[], idx: number): void { + if (this.#renderedSelectLines > 0) { + this.be.write(`\x1b[${this.#renderedSelectLines}F\x1b[J`); + } + const lines = items.map((c, i) => `${i === idx ? color("cyan", ">") : " "} ${c.label}`); + const frame = [ + `${color("cyan", "?")} ${label} ${dim("(Up/Down, Enter)")}`, + ...lines, + ]; + this.#renderedSelectLines = frame.length; + this.be.write(`${frame.join("\n")}\n`); + } +} + +function color(kind: "cyan" | "red" | "yellow" | "green", s: string): string { + if (process.env.NO_COLOR) return s; + const code = { cyan: 36, red: 31, yellow: 33, green: 32 }[kind]; + return `\x1b[${code}m${s}\x1b[0m`; +} + +function dim(s: string): string { + return process.env.NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`; +} + +/** Real backend: reads /dev/tty, writes prompts to /dev/tty (never stdout). */ +export class TtyBackend implements PromptBackend { + #tty: boolean; + #fd?: number; + #input?: ReadStream; + #keyQueue: KeyEvent[] = []; + #pendingKey?: (key: KeyEvent) => void; + #keyListener?: (s: string, key: KeyEvent) => void; + constructor() { + // Probe for a controlling terminal without holding the fd; the real stream opens on first prompt. + try { + closeSync(openSync("/dev/tty", "r")); + this.#tty = true; + } catch { + this.#tty = false; + } + } + isTTY(): boolean { return this.#tty; } + write(s: string): void { process.stderr.write(s); } + + /** + * ONE persistent tty.ReadStream for the whole run (a real TTY stream, unlike fs.createReadStream). + * Per-prompt fds caused two failures: an undestroyed fs stream hung the event loop, and opening a + * fresh fd per prompt let the previous prompt's async teardown reset the terminal AFTER the next + * prompt set raw mode → the confirm prompt echoed the secret. A single reused stream avoids both; + * the runner calls close once at the end to release it so the process exits. + */ + #stream(): ReadStream { + if (!this.#input) { + this.#fd = openSync("/dev/tty", "r"); + this.#input = new ReadStream(this.#fd); + } + return this.#input; + } + + /** + * Visible input goes through readline (echoes typed text, manages the prompt). Hidden input is read + * MANUALLY in raw mode: readline's terminal-mode redraw emits `ESC[1G ESC[0J` which erases the + * just-written prompt on a real terminal (prompt vanished → looked hung). A raw manual read writes + * the prompt once and never redraws; raw mode means the OS never echoes the secret. + */ + question(prompt: string, hidden: boolean): Promise { + const input = this.#stream(); + if (!hidden) { + const rl = readline.createInterface({ input, output: process.stderr, terminal: true }); + return new Promise((resolve) => { + rl.question(prompt, (ans) => { rl.close(); input.pause(); resolve(ans); }); + }); + } + return new Promise((resolve) => { + process.stderr.write(prompt); + input.setRawMode(true); + input.resume(); + let buf = ""; + const finish = (val: string): void => { + input.setRawMode(false); + input.off("data", onData); + input.pause(); + process.stderr.write("\n"); + resolve(val); + }; + const onData = (d: Buffer): void => { + for (const ch of d.toString("utf8")) { + const code = ch.charCodeAt(0); + if (ch === "\r" || ch === "\n") return finish(buf); // Enter + if (code === 3) { process.stderr.write("\n"); process.exit(130); } // Ctrl-C + if (code === 4) return finish(buf); // Ctrl-D + if (code === 127 || code === 8) { buf = buf.slice(0, -1); continue; } // Backspace + if (code < 32) continue; // ignore other control chars + buf += ch; + } + }; + input.on("data", onData); + }); + } + + beginRaw(): void { + const s = this.#stream(); + readline.emitKeypressEvents(s); + if (!this.#keyListener) { + this.#keyListener = (_s: string, key: KeyEvent) => { + if (this.#pendingKey) { + const resolve = this.#pendingKey; + this.#pendingKey = undefined; + resolve(key ?? {}); + return; + } + this.#keyQueue.push(key ?? {}); + }; + s.on("keypress", this.#keyListener); + } + s.setRawMode(true); + s.resume(); + } + endRaw(): void { + if (this.#keyListener) { + this.#input?.off("keypress", this.#keyListener); + this.#keyListener = undefined; + } + this.#pendingKey = undefined; + this.#keyQueue = []; + this.#input?.setRawMode(false); + this.#input?.pause(); + } + readKey(): Promise { + const queued = this.#keyQueue.shift(); + if (queued) return Promise.resolve(queued); + return new Promise((resolve) => { + this.#pendingKey = resolve; + }); + } + /** Release the persistent /dev/tty stream so the event loop drains and the process exits. */ + close(): void { + if (this.#input) { + try { this.#input.setRawMode(false); } catch { /* may already be closed */ } + this.#input.destroy(); + this.#input = undefined; + } + this.#fd = undefined; + } +} + +export function createPrompter(): Prompter { + return new Prompter(new TtyBackend()); +} diff --git a/ts/src/adapters/inbound/cli/input/prompt/prompter.test.ts b/ts/src/adapters/inbound/cli/input/prompt/prompter.test.ts new file mode 100644 index 000000000..197da86d6 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/prompter.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from "vitest"; +import { Prompter, type PromptBackend, type KeyEvent } from "./index.js"; + +class FakeBackend implements PromptBackend { + out = ""; + #answers: string[]; + #keys: KeyEvent[]; + constructor(answers: string[] = [], keys: KeyEvent[] = []) { this.#answers = answers; this.#keys = keys; } + isTTY() { return true; } + async question(prompt: string, _hidden: boolean) { this.out += prompt; return this.#answers.shift() ?? ""; } + async readKey() { return this.#keys.shift() ?? { name: "return" }; } + write(s: string) { this.out += s; } + beginRaw() {} + endRaw() {} +} + +describe("Prompter.setInteractive", () => { + it("forces isTTY false when interaction is disabled, even on a real TTY", () => { + const p = new Prompter(new FakeBackend()); // FakeBackend.isTTY() === true + expect(p.isTTY()).toBe(true); + p.setInteractive(false); + expect(p.isTTY()).toBe(false); + p.setInteractive(true); + expect(p.isTTY()).toBe(true); + }); +}); + +describe("Prompter.text", () => { + it("re-prompts until validate passes", async () => { + const be = new FakeBackend(["", " ", "ok"]); + const p = new Prompter(be); + const v = await p.text({ label: "name", validate: (s) => (s.trim() ? null : "required") }); + expect(v).toBe("ok"); + }); +}); + +describe("Prompter.hidden", () => { + it("requires the confirm entry to match", async () => { + const be = new FakeBackend(["Abcdef1!", "nope", "Abcdef1!", "Abcdef1!"]); + const p = new Prompter(be); + const v = await p.hidden({ label: "pw", confirm: true }); + expect(v).toBe("Abcdef1!"); + }); + it("re-prompts on validate failure", async () => { + const be = new FakeBackend(["weak", "Abcdef1!"]); + const p = new Prompter(be); + const v = await p.hidden({ label: "pw", validate: (s) => (s.length >= 8 ? null : "too short") }); + expect(v).toBe("Abcdef1!"); + }); +}); + +describe("Prompter.confirm", () => { + it("expect-mode returns true only when the exact ref is typed", async () => { + const ok = new Prompter(new FakeBackend(["wlt_a.0"])); + expect(await ok.confirm({ label: "type ref", expect: "wlt_a.0" })).toBe(true); + const no = new Prompter(new FakeBackend(["wrong"])); + expect(await no.confirm({ label: "type ref", expect: "wlt_a.0" })).toBe(false); + }); +}); + +describe("Prompter.select", () => { + it("arrows to an item and returns its value on enter", async () => { + const be = new FakeBackend([], [{ name: "down" }, { name: "return" }]); + const p = new Prompter(be); + const v = await p.select({ label: "pick", choices: [{ value: "a", label: "A" }, { value: "b", label: "B" }] }); + expect(v).toBe("b"); + }); + it("loads more when arrowing past the last item", async () => { + const be = new FakeBackend([], [{ name: "down" }, { name: "down" }, { name: "return" }]); + const p = new Prompter(be); + let loaded = false; + const v = await p.select({ + label: "pick", + choices: [{ value: "x0", label: "0" }], + loadMore: async () => { loaded = true; return [{ value: "x0", label: "0" }, { value: "x1", label: "1" }]; }, + }); + expect(loaded).toBe(true); + expect(v).toBe("x1"); + }); + it("advances onto the newly loaded item after a single down past the end", async () => { + const be = new FakeBackend([], [{ name: "down" }, { name: "return" }]); + const p = new Prompter(be); + const v = await p.select({ + label: "pick", + choices: [{ value: "x0", label: "0" }], + loadMore: async () => [{ value: "x0", label: "0" }, { value: "x1", label: "1" }], + }); + expect(v).toBe("x1"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/input/prompt/validators.test.ts b/ts/src/adapters/inbound/cli/input/prompt/validators.test.ts new file mode 100644 index 000000000..fdcc46ab3 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/validators.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { passwordPolicyErrors, isValidPrivateKeyHex, isValidMnemonic, PASSWORD_SPECIALS } from "./validators.js"; + +describe("passwordPolicyErrors", () => { + it("accepts a strong password", () => { + expect(passwordPolicyErrors("Abcdef1!")).toEqual([]); + }); + it("rejects too short", () => { + expect(passwordPolicyErrors("Ab1!")).toContainEqual(expect.stringContaining("8")); + }); + it("flags each missing class", () => { + const errs = passwordPolicyErrors("abcdefgh"); // no upper, digit, special + expect(errs.length).toBe(3); + }); + it("uses the documented special set", () => { + expect(PASSWORD_SPECIALS).toContain("!"); + expect(passwordPolicyErrors("Abcdefg1#")).toEqual([]); // # is in the set + }); +}); + +describe("isValidPrivateKeyHex", () => { + it("accepts 64 hex with or without 0x", () => { + const k = "a".repeat(64); + expect(isValidPrivateKeyHex(k)).toBe(true); + expect(isValidPrivateKeyHex("0x" + k)).toBe(true); + }); + it("rejects wrong length or non-hex", () => { + expect(isValidPrivateKeyHex("a".repeat(63))).toBe(false); + expect(isValidPrivateKeyHex("z".repeat(64))).toBe(false); + }); +}); + +describe("isValidMnemonic", () => { + it("accepts a valid 12-word phrase", () => { + expect(isValidMnemonic("legal winner thank year wave sausage worth useful legal winner thank yellow")).toBe(true); + }); + it("rejects garbage", () => { + expect(isValidMnemonic("not a real mnemonic phrase at all nope nope nope nope nope")).toBe(false); + }); +}); diff --git a/ts/src/adapters/inbound/cli/input/prompt/validators.ts b/ts/src/adapters/inbound/cli/input/prompt/validators.ts new file mode 100644 index 000000000..480e56d3d --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/prompt/validators.ts @@ -0,0 +1,24 @@ +/** Pure input validators for the interactive prompt layer (no I/O). */ +import { Derivation } from "../../../../../domain/derivation/index.js"; + +export const PASSWORD_SPECIALS = "!@#$%^&*()-_=+[]{};:,.?"; + +/** First-time master-password policy. Returns unmet requirements ([] = acceptable). */ +export function passwordPolicyErrors(pw: string): string[] { + const errs: string[] = []; + if (pw.length < 8) errs.push("must be at least 8 characters"); + if (!/[A-Z]/.test(pw)) errs.push("must include an uppercase letter"); + if (!/[a-z]/.test(pw)) errs.push("must include a lowercase letter"); + if (!/[0-9]/.test(pw)) errs.push("must include a digit"); + const specials = new Set(PASSWORD_SPECIALS); + if (![...pw].some((c) => specials.has(c))) errs.push(`must include a special character (${PASSWORD_SPECIALS})`); + return errs; +} + +export function isValidPrivateKeyHex(s: string): boolean { + return /^(0x)?[0-9a-fA-F]{64}$/.test(s.trim()); +} + +export function isValidMnemonic(s: string): boolean { + return Derivation.validateMnemonic(s); +} diff --git a/ts/src/adapters/inbound/cli/input/secret/index.ts b/ts/src/adapters/inbound/cli/input/secret/index.ts new file mode 100644 index 000000000..c5f8d4650 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/secret/index.ts @@ -0,0 +1,165 @@ +/** + * SecretResolver — the single place that reads secrets, memoized per source. + * Every secret kind binds to its own source `---stdin`, which reads stdin (fd 0) — at most + * one secret may use it per run. The `---file`/`/dev/fd/N` multi-fd path was removed; commands + * needing a 2nd secret (import-mnemonic/import-private-key/backup) go interactive. + * There is NO env source (no MASTER_PASSWORD): secrets never sit in env/process-table/history. + * Handlers must never touch process.stdin directly. Secrets never enter logs/envelopes. + */ +import type { SecretKind, SecretResolver as ISecretResolver, StreamManager } from "../../contracts/index.js"; +import { ExecutionError, UsageError } from "../../../../../domain/errors/index.js"; +import type { Prompter } from "../prompt/index.js"; +import { passwordPolicyErrors, isValidMnemonic, isValidPrivateKeyHex } from "../prompt/validators.js"; + +/** path per secret kind; the only source is `---stdin`, so the value is always `-` (stdin). */ +export type SecretPaths = Partial>; + +/** the flag stem for a kind (e.g. privateKey → "private-key", so `--private-key-stdin`). */ +function flagOf(kind: SecretKind): string { + return kind.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); +} + +export class SecretResolver implements ISecretResolver { + #byPath = new Map(); + #stdinUsedBy?: SecretKind; + #primed = new Map(); + + constructor( + private readonly streams: StreamManager, + private readonly paths: SecretPaths = {}, + private readonly prompter?: Prompter, + ) {} + + /** whether a master-password source is configured, WITHOUT consuming it. */ + hasMasterPassword(): boolean { + return this.paths.password !== undefined || this.#primed.has("password"); + } + + masterPassword(): string { + const primed = this.#primed.get("password"); + if (primed !== undefined) return primed; + if (this.paths.password === undefined) { + throw new ExecutionError("auth_required", "master password required: pass --password-stdin"); + } + return this.read("password"); + } + + /** whether a source for `kind` is configured, WITHOUT consuming it. */ + has(kind: SecretKind): boolean { + return this.paths[kind] !== undefined; + } + + read(kind: SecretKind): string { + const primed = this.#primed.get(kind); + if (primed !== undefined) return primed; + const path = this.paths[kind]; + if (path === undefined) { + if (kind === "password") + throw new ExecutionError("auth_required", "master password required: pass --password-stdin"); + throw new ExecutionError("secret_source_error", `missing --${flagOf(kind)}-stdin`); + } + return this.#readPath(path, kind); + } + + /** + * Read a REQUIRED source. A missing source is a usage error (forgot a required flag → + * missing_option, exit 2); secret_source_error is reserved for present-but-unreadable. + */ + require(kind: SecretKind): string { + if (!this.has(kind)) { + throw new UsageError("missing_option", `--${flagOf(kind)}-stdin is required`); + } + return this.read(kind); + } + + /** + * Exactly-one selector for commands that accept an inline value OR a stdin source + * (e.g. --transaction|--tx-stdin, --message|--message-stdin). Both → invalid_option; + * neither → missing_option (both usage/exit 2). + */ + pick(inline: string | undefined, kind: SecretKind, inlineFlag: string): string { + const hasStdin = this.has(kind); + if (inline !== undefined && hasStdin) { + throw new UsageError("invalid_option", `--${inlineFlag} and --${flagOf(kind)}-stdin are mutually exclusive`); + } + if (inline !== undefined) return inline; + if (hasStdin) return this.read(kind); + throw new UsageError("missing_option", `--${inlineFlag} or --${flagOf(kind)}-stdin is required`); + } + + async resolveSecret(kind: "mnemonic" | "privateKey"): Promise { + const validate = kind === "mnemonic" ? isValidMnemonic : isValidPrivateKeyHex; + if (this.has(kind)) { + const v = this.read(kind).trim(); + if (!validate(v)) throw new UsageError("invalid_secret", `--${flagOf(kind)}-stdin is not a valid ${kind}`); + this.streams.diagnostic("info", `${flagOf(kind)} ✓ via pipe`); + return v; + } + if (this.prompter?.isTTY()) { + const label = kind === "mnemonic" + ? "Paste recovery phrase (hidden)" + : "Paste private key (hidden)"; + const v = await this.prompter.hidden({ + label, + validate: (s) => (validate(s.trim()) ? null : `invalid ${kind}`), + }); + const trimmed = v.trim(); + this.#primed.set(kind, trimmed); + return trimmed; + } + throw new UsageError("missing_option", `--${flagOf(kind)}-stdin is required (or run in a terminal)`); + } + + async primePassword(plan: { mode: "set" | "verify"; verify?: (pw: string) => boolean }): Promise { + if (this.has("password")) { + const pw = this.read("password"); + if (plan.mode === "set") { + const errs = passwordPolicyErrors(pw); + if (errs.length) throw new UsageError("weak_password", `password too weak: ${errs.join("; ")}`); + } + if (plan.mode === "verify" && plan.verify && !plan.verify(pw)) { + throw new ExecutionError("auth_failed", "incorrect master password"); + } + this.#primed.set("password", pw); + this.streams.diagnostic("info", "password ✓ via pipe"); + return; + } + if (this.prompter?.isTTY()) { + let pw: string; + if (plan.mode === "set") { + pw = await this.prompter.hidden({ + label: "Set master password (hidden)", + confirmLabel: "Confirm master password", + confirm: true, + validate: (s) => { const e = passwordPolicyErrors(s); return e.length ? e.join("; ") : null; }, + }); + } else { + pw = ""; + for (let attempt = 0; attempt < 3; attempt++) { + pw = await this.prompter.hidden({ label: "Master password (hidden)" }); + if (plan.verify?.(pw)) { this.#primed.set("password", pw); return; } + this.streams.diagnostic("warn", "incorrect master password"); + } + throw new ExecutionError("auth_failed", "incorrect master password"); + } + this.#primed.set("password", pw); + return; + } + throw new ExecutionError("auth_required", "master password required: pass --password-stdin"); + } + + /** The only source is stdin (fd 0), so `path` is always `-`; at most one secret may use it. */ + #readPath(_path: string, kind: SecretKind): string { + if (this.#stdinUsedBy && this.#stdinUsedBy !== kind) { + throw new ExecutionError( + "secret_source_error", + `stdin already consumed by --${flagOf(this.#stdinUsedBy)}-stdin; only one secret may use stdin per run`, + ); + } + this.#stdinUsedBy = kind; + if (!this.#byPath.has("-")) { + this.#byPath.set("-", this.streams.readStdinOnce().replace(/\r?\n$/, "")); + } + return this.#byPath.get("-")!; + } +} diff --git a/ts/src/adapters/inbound/cli/input/secret/secret.test.ts b/ts/src/adapters/inbound/cli/input/secret/secret.test.ts new file mode 100644 index 000000000..af1d9f438 --- /dev/null +++ b/ts/src/adapters/inbound/cli/input/secret/secret.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi } from "vitest"; +import { SecretResolver } from "./index.js"; +import { Prompter, type PromptBackend, type KeyEvent } from "../prompt/index.js"; +import { StreamManager } from "../../stream/index.js"; + +function streams(stdin = ""): StreamManager { + // out/err captured to no-op; readStdinOnce returns the provided value + const sm = new StreamManager("text", false, () => {}, () => {}); + vi.spyOn(sm, "readStdinOnce").mockReturnValue(stdin); + return sm; +} + +class Backend implements PromptBackend { + constructor(private answers: string[], private tty = true) {} + isTTY() { return this.tty; } + async question() { return this.answers.shift() ?? ""; } + async readKey(): Promise { return { name: "return" }; } + write() {} + beginRaw() {} + endRaw() {} +} + +const PW = "Abcdef1!"; + +describe("resolveSecret", () => { + it("prompts (hidden) when no stdin source and validates mnemonic", async () => { + const valid = "legal winner thank year wave sausage worth useful legal winner thank yellow"; + const r = new SecretResolver(streams(), {}, new Prompter(new Backend(["bad phrase", valid]))); + expect(await r.resolveSecret("mnemonic")).toBe(valid); + }); + it("uses the stdin source when present", async () => { + const k = "a".repeat(64); + const r = new SecretResolver(streams(k + "\n"), { privateKey: "-" }, new Prompter(new Backend([]))); + expect(await r.resolveSecret("privateKey")).toBe(k); + }); + it("rejects a malformed stdin secret with invalid_secret", async () => { + const r = new SecretResolver(streams("zzz\n"), { privateKey: "-" }, new Prompter(new Backend([]))); + await expect(r.resolveSecret("privateKey")).rejects.toMatchObject({ code: "invalid_secret" }); + }); + it("errors when missing and no TTY", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend([], false))); + await expect(r.resolveSecret("mnemonic")).rejects.toMatchObject({ code: "missing_option" }); + }); +}); + +describe("primePassword", () => { + it("set mode prompts with confirm + policy, then masterPassword() returns it", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend(["weak", PW, PW]))); + await r.primePassword({ mode: "set" }); + expect(r.masterPassword()).toBe(PW); + }); + it("verify mode re-prompts until verify() passes", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend(["nope", PW]))); + await r.primePassword({ mode: "verify", verify: (pw) => pw === PW }); + expect(r.masterPassword()).toBe(PW); + }); + it("set mode via --password-stdin enforces policy (weak_password)", async () => { + const r = new SecretResolver(streams("weak\n"), { password: "-" }, new Prompter(new Backend([]))); + await expect(r.primePassword({ mode: "set" })).rejects.toMatchObject({ code: "weak_password" }); + }); + it("verify mode via --password-stdin just caches", async () => { + const r = new SecretResolver(streams(PW + "\n"), { password: "-" }, new Prompter(new Backend([]))); + await r.primePassword({ mode: "verify", verify: () => true }); + expect(r.masterPassword()).toBe(PW); + }); + it("verify mode via --password-stdin rejects a wrong password", async () => { + const r = new SecretResolver(streams("wrong\n"), { password: "-" }, new Prompter(new Backend([]))); + await expect(r.primePassword({ mode: "verify", verify: (pw) => pw === PW })).rejects.toMatchObject({ code: "auth_failed" }); + }); + it("no source and no TTY → auth_required", async () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend([], false))); + await expect(r.primePassword({ mode: "verify", verify: () => true })).rejects.toMatchObject({ code: "auth_required" }); + }); +}); + +describe("hasMasterPassword", () => { + it("hasMasterPassword is false with no source/primed even under a TTY (lazy guard must fail fast)", () => { + const r = new SecretResolver(streams(), {}, new Prompter(new Backend([], /* tty */ true))); + expect(r.hasMasterPassword()).toBe(false); + }); +}); diff --git a/ts/src/adapters/inbound/cli/output/envelope.test.ts b/ts/src/adapters/inbound/cli/output/envelope.test.ts new file mode 100644 index 000000000..208d0cebc --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/envelope.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { OutputEnvelope } from "./envelope.js"; +import type { CommandDefinition } from "../contracts/index.js"; + +// Single shipping family (TRON); the envelope no longer redacts addresses — it passes the +// command's result payload through verbatim under the wallet-cli.result.v1 contract. +const cmd = { path: ["current"] } as CommandDefinition; +const m = { durationMs: 0, warnings: [] }; + +describe("OutputEnvelope.success — result payload passthrough", () => { + it("passes a single descriptor's addresses map through unchanged", () => { + const data = { accountId: "a.0", addresses: { tron: "Ttron" } }; + const env = OutputEnvelope.success(cmd, undefined, data, m); + expect((env.data as { addresses: Record }).addresses).toEqual({ tron: "Ttron" }); + }); + + it("passes a list of descriptors through unchanged", () => { + const data = [ + { accountId: "a.0", addresses: { tron: "Ttron0" } }, + { accountId: "a.1", addresses: { tron: "Ttron1" } }, + ]; + const env = OutputEnvelope.success(cmd, undefined, data, m); + expect(env.data).toEqual(data); + }); + + it("leaves data without an addresses field untouched", () => { + const data = { accountId: "a.0", scope: "wallet", secretRemoved: true }; + const env = OutputEnvelope.success(cmd, undefined, data, m); + expect(env.data).toEqual(data); + }); +}); diff --git a/ts/src/adapters/inbound/cli/output/envelope.ts b/ts/src/adapters/inbound/cli/output/envelope.ts new file mode 100644 index 000000000..7f79718a5 --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/envelope.ts @@ -0,0 +1,69 @@ +/** + * OutputEnvelope — the result/error envelope builder for the OutputFormatter. Shapes + * the user-facing `wallet-cli.result.v1` contract: schema version, chain view, and meta. + * Pure (no I/O); the formatter turns the envelope into strings. + */ +import type { NetworkDescriptor } from "../../../../domain/types/index.js"; +import type { ChainView, CommandDefinition, ErrorEnvelope, Meta, ResultEnvelope } from "../contracts/index.js"; +import { commandId } from "../command-id.js"; + +type CliErrorEnvelopeShape = { code: string; message: string; details?: object }; + +const SCHEMA_VERSION = "wallet-cli.result.v1" as const; + +/** JSON serialization that keeps big numbers as strings. */ +export function toJson(value: unknown): string { + return JSON.stringify(value, (_k, v) => { + if (typeof v === "bigint") return v.toString(); + if (v instanceof Uint8Array) return Buffer.from(v).toString("hex"); + return v; + }); +} + +function chainView(net: NetworkDescriptor): ChainView { + return { + family: net.family, + network: net.id, + chainId: net.chainId, + }; +} + +function meta(durationMs: number, warnings: string[]): Meta { + return { durationMs, warnings }; +} + +export const OutputEnvelope = { + success( + cmd: CommandDefinition, + net: NetworkDescriptor | undefined, + data: unknown, + m: { durationMs: number; warnings: string[] }, + ): ResultEnvelope { + const env: ResultEnvelope = { + schema: SCHEMA_VERSION, + success: true, + command: commandId(cmd), + data: data ?? {}, + meta: meta(m.durationMs, m.warnings), + }; + if (net) env.chain = chainView(net); // neutral commands omit chain + return env; + }, + + error( + commandId: string, + net: NetworkDescriptor | undefined, + err: CliErrorEnvelopeShape, + m: { durationMs: number; warnings: string[] }, + ): ErrorEnvelope { + const env: ErrorEnvelope = { + schema: SCHEMA_VERSION, + success: false, + command: commandId, + error: err, + meta: meta(m.durationMs, m.warnings), + }; + if (net) env.chain = chainView(net); + return env; + }, +}; diff --git a/ts/src/adapters/inbound/cli/output/index.ts b/ts/src/adapters/inbound/cli/output/index.ts new file mode 100644 index 000000000..d529d8eb3 --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/index.ts @@ -0,0 +1,100 @@ +/** + * OutputFormatter — turn outcomes into result/event frame strings without changing + * behavior. Instead of one class branching on `if (output === "json")`, this is an interface + * with two implementations chosen by createOutputFormatter (borrowed from ledger wallet-cli + * output.ts). The formatter only computes strings; StreamManager owns writing & stream choice. + * - success → single terminal frame (caller hands to streams.result) + * - error → terminal error (json envelope to stdout, or short stderr line) + * - event → intermediate progress frame (caller hands to streams.event); null = not shown + * JSON mode emits exactly one terminal envelope; empty data is {}; big numbers stay strings. + * Neutral commands omit `chain`. + */ +import type { NetworkDescriptor, OutputMode } from "../../../../domain/types/index.js"; +import type { ProgressEvent } from "../../../../application/contracts/index.js"; +import type { CommandDefinition, StreamManager } from "../contracts/index.js"; +import type { CliError } from "../../../../domain/errors/index.js"; +import { OutputEnvelope, toJson } from "./envelope.js"; +import { renderGenericText } from "../render/index.js"; +import { sanitizeText } from "../render/scalars.js"; + +export interface OutputFormatter { + /** the single result frame for the caller to hand to streams.result. */ + success(cmd: CommandDefinition, net: NetworkDescriptor | undefined, data: unknown, accountLabel?: string): string; + /** terminal error output (JSON envelope to stdout, or short line to stderr). */ + error(err: CliError, ctx?: { commandId?: string; net?: NetworkDescriptor }): void; + /** intermediate progress frame for streams.event; null = this mode does not show it. */ + event(e: ProgressEvent): string | null; +} + +abstract class BaseOutputFormatter { + constructor( + protected readonly streams: StreamManager, + protected readonly startedAt: number, + ) {} + + protected meta() { + return { durationMs: Date.now() - this.startedAt, warnings: this.streams.warnings() }; + } +} + +class JsonOutputFormatter extends BaseOutputFormatter implements OutputFormatter { + success(cmd: CommandDefinition, net: NetworkDescriptor | undefined, data: unknown): string { + // JSON mode always uses the envelope; the account label is a text-mode display nicety. + return toJson(OutputEnvelope.success(cmd, net, data, this.meta())); + } + + error(err: CliError, ctx?: { commandId?: string; net?: NetworkDescriptor }): void { + const env = OutputEnvelope.error(ctx?.commandId ?? "", ctx?.net, err.toEnvelope(), this.meta()); + this.streams.result(toJson(env)); + } + + event(e: ProgressEvent): string { + return JSON.stringify(e); + } +} + +class HumanOutputFormatter extends BaseOutputFormatter implements OutputFormatter { + // Text mode: strip terminal control bytes from every frame so a hostile wallet label or remote + // token/RPC metadata value cannot inject ANSI/OSC sequences (CLI-OUT-001). JSON mode stays raw. + success(cmd: CommandDefinition, net: NetworkDescriptor | undefined, data: unknown, accountLabel?: string): string { + const env = OutputEnvelope.success(cmd, net, data, this.meta()); + const custom = cmd.formatText?.(env.data, { command: env.command, net, accountLabel }); + return sanitizeText(custom ?? renderGenericText(env.command, net, env.data)); + } + + error(err: CliError): void { + this.streams.errorLine(sanitizeText(`error [${err.code}]: ${err.message}`)); + } + + event(e: ProgressEvent): string { + return sanitizeText(renderEvent(e)); + } +} + +export function createOutputFormatter( + output: OutputMode, + streams: StreamManager, + startedAt: number, +): OutputFormatter { + return output === "json" + ? new JsonOutputFormatter(streams, startedAt) + : new HumanOutputFormatter(streams, startedAt); +} + +// plain human progress line (no spinner / no TTY detection — Standard CLI / agent-first). +function renderEvent(e: ProgressEvent): string { + switch (e.type) { + case "awaiting_device": + switch (e.reason) { + case "sign": return "⧖ review and approve the transaction on your device"; + case "open_app": return "⧖ confirm on your device to open the app"; + case "unlock": return "⧖ unlock your device with your PIN"; + } + // eslint-disable-next-line no-fallthrough + case "deriving-address": return "deriving address from your device…"; + case "pre-verify-address": return `compare with your device: ${e.address}`; + case "signed": return "✓ signed; broadcasting…"; + case "broadcasting": return "broadcasting…"; + case "dry-run": return "dry run (transaction not broadcast)"; + } +} diff --git a/ts/src/adapters/inbound/cli/output/output.test.ts b/ts/src/adapters/inbound/cli/output/output.test.ts new file mode 100644 index 000000000..dc62abfa4 --- /dev/null +++ b/ts/src/adapters/inbound/cli/output/output.test.ts @@ -0,0 +1,181 @@ +import { describe, it, expect } from "vitest"; +import { createOutputFormatter } from "./index.js"; +import { StreamManager } from "../stream/index.js"; +import { UsageError } from "../../../../domain/errors/index.js"; +import { commandId } from "../command-id.js"; +import type { NetworkDescriptor } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { renderGenericText, TextFormatters } from "../render/index.js"; + +function capture(output: "text" | "json") { + const out: string[] = []; + const err: string[] = []; + const sm = new StreamManager(output, false, (s) => out.push(s), (s) => err.push(s)); + return { sm, out, err }; +} + +const cmd = { family: "tron", path: ["account", "balance"] } as unknown as CommandDefinition; +const net: NetworkDescriptor = { + id: "tron:nile", family: "tron", chainId: "nile", aliases: ["nile"], capabilities: [], +}; + +describe("createOutputFormatter (json)", () => { + it("success returns a single parseable envelope", () => { + const { sm } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + const env = JSON.parse(f.success(cmd, net, { balance: "1" })); + expect(env.success).toBe(true); + expect(env.command).toBe("tron.account.balance"); + expect(env.chain).toMatchObject({ network: "tron:nile", chainId: "nile" }); + expect(env.data).toEqual({ balance: "1" }); + expect(env.meta).toMatchObject({ warnings: [] }); + }); + + it("error writes an error envelope to stdout via streams.result", () => { + const { sm, out, err } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + f.error(new UsageError("missing_option", "need --to"), { commandId: commandId(cmd), net }); + expect(err).toEqual([]); + const env = JSON.parse(out[0]!); + expect(env.success).toBe(false); + expect(env.error).toMatchObject({ code: "missing_option" }); + }); + + it("event renders an NDJSON line that parses back to the event", () => { + const { sm } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + const frame = f.event({ type: "awaiting_device", reason: "sign" }); + expect(JSON.parse(frame!)).toEqual({ type: "awaiting_device", reason: "sign" }); + }); +}); + +describe("createOutputFormatter (text)", () => { + it("generic output identifies the network by canonical id", () => { + const text = renderGenericText("tron.test", net, {}); + expect(text).toContain("network: tron:nile"); + expect(text).not.toContain("network: nile"); + }); + + it("success returns human lines naming the command", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const text = f.success(cmd, net, { balance: "1" }); + expect(text).toContain("tron.account.balance"); + expect(text).toContain("balance"); + }); + + it("error writes a short line to stderr, not stdout", () => { + const { sm, out, err } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + f.error(new UsageError("missing_option", "need --to")); + expect(out).toEqual([]); + expect(err[0]).toContain("missing_option"); + }); + + it("event renders a non-null human progress line (no spinner)", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const frame = f.event({ type: "awaiting_device", reason: "sign" }); + expect(frame).not.toBeNull(); + expect(frame).not.toContain("{"); // human text, not NDJSON + }); + + it("renders wallet create as a focused human receipt", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const walletCmd = { + path: ["create"], + formatText: TextFormatters.walletCreated("Created", [ + "Recovery phrase is encrypted locally and was not printed.", + "Run `backup` soon and store the file offline.", + ]), + } as unknown as CommandDefinition; + const text = f.success(walletCmd, undefined, { + status: "created", + accountId: "wlt_abc.0", + label: "main", + type: "seed", + active: true, + addresses: { tron: "T1234567890abcdef", evm: "0x1234567890abcdef" }, + }); + expect(text).toContain("Created wallet"); + expect(text).toContain("main"); + expect(text).toContain("Run `backup`"); + }); + + it("renders existing wallet receipts with a warning marker", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const walletCmd = { + path: ["import", "private-key"], + formatText: TextFormatters.walletCreated("Imported", [ + "Private key was read from hidden input and was not printed.", + ]), + } as unknown as CommandDefinition; + const text = f.success(walletCmd, undefined, { + status: "existing", + accountId: "wlt_abc.0", + label: "main", + type: "seed", + addresses: { tron: "T1234567890abcdef", evm: "0x1234567890abcdef" }, + }); + // icon and label live in separate ANSI spans, so assert on the pieces (not a fused substring). + expect(text).toContain("⚠"); + expect(text).toContain("Existing wallet"); + expect(text).not.toContain("✅"); // existing wallets must not show the success check + }); + + it("strips terminal control sequences from rendered data values", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + // a hostile label / remote metadata value carrying ANSI CSI, OSC, and a bare C1 CSI byte. + const text = f.success(cmd, net, { balance: "1\x1b[31mHACKED\x1b]0;pwn\x07\x9bK" }); + expect(text).not.toContain("\x1b"); + expect(text).not.toContain("\x9b"); + expect(text).not.toContain("\x07"); + expect(text).toContain("1"); // the real value survives, only control bytes are removed + }); + + it("preserves newlines while stripping control bytes", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const text = f.success(cmd, net, { balance: "1" }); + expect(text).toContain("\n"); // layout line breaks must remain intact + }); + + it("sanitizes human error and event lines", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const frame = f.event({ type: "pre-verify-address", address: "T1\x1b[2Jabc" }); + expect(frame).not.toContain("\x1b"); + const { sm: sm2, err } = capture("text"); + const f2 = createOutputFormatter("text", sm2, 0); + f2.error(new UsageError("invalid_value", "bad \x1b[31mvalue\x1b[0m from node")); + expect(err[0]).not.toContain("\x1b"); + }); + + it("json mode keeps data raw (no sanitization)", () => { + const { sm } = capture("json"); + const f = createOutputFormatter("json", sm, 0); + const env = JSON.parse(f.success(cmd, net, { balance: "1\x1b[31m" })); + expect(env.data.balance).toBe("1\x1b[31m"); // machine-parseable output stays byte-exact + }); + + it("renders backup metadata without secret material", () => { + const { sm } = capture("text"); + const f = createOutputFormatter("text", sm, 0); + const backupCmd = { path: ["backup"], formatText: TextFormatters.walletBackup } as unknown as CommandDefinition; + const text = f.success(backupCmd, undefined, { + accountId: "wlt_abc.0", + secretType: "mnemonic", + out: "/tmp/main-backup.json", + fileMode: "0600", + bytes: 512, + mnemonic: "test test test test test test test test test test test junk", + privateKey: "00".repeat(32), + }); + expect(text).toContain("/tmp/main-backup.json"); + expect(text).not.toContain("test test"); + expect(text).not.toContain("000000"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/registry/index.ts b/ts/src/adapters/inbound/cli/registry/index.ts new file mode 100644 index 000000000..7fda1186d --- /dev/null +++ b/ts/src/adapters/inbound/cli/registry/index.ts @@ -0,0 +1,81 @@ +/** + * CommandRegistry — holds all CommandDefinitions; resolves a command from a positional path + * (+ family for chain commands); exposes metadata for CliShell (yargs tree) and HelpService. + * Thin: tokenizing, flag collection, and help layout are yargs' job. + * + * Two command kinds, discriminated solely by `family`: + * - neutral (no family): addressed by its full path (create, import mnemonic, config get…). + * - chain (family set): same logical path may have per-family impls, chosen by --network. + */ +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition, CommandRegistryLike } from "../contracts/index.js"; +import { commandId } from "../command-id.js"; + +/** flat command-tree metadata for CliShell (yargs tree) + HelpService. */ +export interface CommandTreeMeta { + commands: Array<{ path: string[]; id: string; family?: ChainFamily; summary?: string }>; +} + +/** storage/lookup key: family scopes chain commands; neutral commands share the empty scope. */ +function keyOf(cmd: CommandDefinition): string { + return `${cmd.family ?? ""}:${cmd.path.join(".")}`; +} + +export class CommandRegistry implements CommandRegistryLike { + #byKey = new Map(); + + add(cmd: CommandDefinition): void { + // Family commands are selected through a resolved network, so this combination cannot be + // dispatched. Keep this routing invariant here instead of coupling it to the run signature. + if (cmd.family && cmd.network === "none") { + throw new Error(`family command must resolve a network: ${commandId(cmd)}`); + } + const key = keyOf(cmd); + if (this.#byKey.has(key)) throw new Error(`duplicate command ${key}`); + this.#byKey.set(key, cmd); + } + + families(): ChainFamily[] { + const set = new Set(); + for (const cmd of this.#byKey.values()) if (cmd.family) set.add(cmd.family); + return [...set]; + } + + /** command-backed capability keys per family (deduped). Summaries are resolved by the caller + * (runner) against CAP_SUMMARIES — the registry stays free of presentation/infra concerns. */ + capabilityKeysByFamily(): Map { + const out = new Map>(); + for (const cmd of this.#byKey.values()) { + if (!cmd.family || !cmd.capability) continue; + const set = out.get(cmd.family) ?? new Set(); + set.add(cmd.capability); + out.set(cmd.family, set); + } + return new Map([...out].map(([f, s]) => [f, [...s]])); + } + + /** Resolve a neutral command (no family) by its full path. */ + resolveNeutral(path: string[]): CommandDefinition | null { + return this.#byKey.get(`:${path.join(".")}`) ?? null; + } + + /** All commands matching a logical path, regardless of family (used to pick by --network). */ + resolveCandidates(path: string[]): CommandDefinition[] { + const key = path.join("."); + return this.all().filter((c) => c.path.join(".") === key); + } + + /** Family-specific implementation of a logical path. */ + resolveForFamily(path: string[], family: ChainFamily): CommandDefinition | null { + return this.resolveCandidates(path).find((c) => c.family === family) ?? null; + } + + all(): CommandDefinition[] { + return [...this.#byKey.values()]; + } + + tree(): CommandTreeMeta { + const commands = this.all().map((c) => ({ path: c.path, id: commandId(c), family: c.family, summary: c.summary })); + return { commands }; + } +} diff --git a/ts/src/adapters/inbound/cli/registry/registry.test.ts b/ts/src/adapters/inbound/cli/registry/registry.test.ts new file mode 100644 index 000000000..4651f2434 --- /dev/null +++ b/ts/src/adapters/inbound/cli/registry/registry.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import type { ChainFamily } from "../../../../domain/types/index.js"; +import type { CommandDefinition } from "../contracts/index.js"; +import { CommandRegistry } from "./index.js"; +import { commandId } from "../command-id.js"; + +function command(family: ChainFamily, path: string[]): CommandDefinition { + const fields = z.object({}); + return { + family, + path, + network: "optional", + wallet: "optional", + auth: "none", + fields, + input: fields, + examples: [], + run: async () => ({}), + }; +} + +describe("CommandRegistry logical resolution", () => { + it("rejects a family command that cannot resolve a network", () => { + const reg = new CommandRegistry(); + const fields = z.object({}); + expect(() => reg.add({ + family: "tron", + path: ["invalid"], + network: "none", + wallet: "none", + auth: "none", + fields, + input: fields, + examples: [], + run: async () => ({}), + })).toThrow("family command must resolve a network: tron.invalid"); + }); + + it("returns every implementation for a logical path", () => { + const reg = new CommandRegistry(); + // synthetic second family via cast: only tron ships, but the registry keys on the family + // string, so this still exercises multi-implementation logical resolution. + reg.add(command("tron", ["account", "balance"])); + reg.add(command("evm" as any, ["account", "balance"])); + + expect(reg.resolveCandidates(["account", "balance"]).map((c) => commandId(c))).toEqual([ + "tron.account.balance", + "evm.account.balance", + ]); + }); + + it("selects one implementation by family", () => { + const reg = new CommandRegistry(); + reg.add(command("tron", ["account", "balance"])); + reg.add(command("evm" as any, ["account", "balance"])); + + expect(commandId(reg.resolveForFamily(["account", "balance"], "evm" as any)!)).toBe("evm.account.balance"); + expect(reg.resolveForFamily(["account", "missing"], "evm" as any)).toBeNull(); + }); +}); diff --git a/ts/src/adapters/inbound/cli/render/family-render.test.ts b/ts/src/adapters/inbound/cli/render/family-render.test.ts new file mode 100644 index 000000000..e0fb9be0f --- /dev/null +++ b/ts/src/adapters/inbound/cli/render/family-render.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { FAMILY_RENDER } from "./index.js"; + +describe("FAMILY_RENDER parity", () => { + it("nativeAmount units", () => { + expect(FAMILY_RENDER.tron.nativeAmount("1000000")).toBe("1 TRX"); + }); + it("feeFallback: tron formats sun→TRX", () => { + expect(FAMILY_RENDER.tron.feeFallback("1000000")).toBe("1 TRX"); + }); + it("addressLabel", () => { + expect(FAMILY_RENDER.tron.addressLabel).toBe("TRON address"); + }); + it("tron txInfoRows include Energy + Fee in TRX", () => { + const rows = FAMILY_RENDER.tron.txInfoRows({ txid: "t", status: "SUCCESS", feeSun: "1000000", energyUsed: 5 } as any); + expect(rows).toContainEqual(["Fee", "1 TRX"]); + expect(rows.map((r) => r[0])).toContain("Energy"); + }); +}); diff --git a/ts/src/adapters/inbound/cli/render/index.ts b/ts/src/adapters/inbound/cli/render/index.ts new file mode 100644 index 000000000..a54b90aa1 --- /dev/null +++ b/ts/src/adapters/inbound/cli/render/index.ts @@ -0,0 +1,634 @@ +import type { NetworkDescriptor, TxInfoView, TxReceiptKind, TxReceiptView, TxStatusView } from "../../../../domain/types/index.js" +import type { TextFormatter, TextRenderContext } from "../contracts/index.js" +import { ChainFamily } from "../../../../domain/family/index.js" +import { RESOURCES, resourceOfRpcCode, type Resource } from "../../../../domain/resources/index.js" +import { sourceLabel } from "../../../../domain/sources/index.js" +import { fromBaseUnits } from "../../../../domain/amounts/index.js" +import { formatScalar, formatInt, formatUsd, formatSun, formatTime, formatUtc, num, shorten, quote } from "./scalars.js" +import { type Obj, type Pair, asObj, kv, query, receipt, titled, table, ok, fail, pending, warn, unknown } from "./layout.js" + +/** + * Per-family render hooks — the one table that folds the scattered `r.family === tron ? … : …` + * branches. Adding a chain = one entry here (alongside its FAMILIES + FamilyDef entries). + */ +interface FamilyRenderHooks { + /** the full TxInfo detail rows (family-shaped: Energy/TRX vs Gas/wei). Reads the flat + * TxInfoView and picks its own family's fields — no narrowing cast (no closed union). */ + txInfoRows(r: TxInfoView): Pair[] + /** native smallest-unit amount → display string (sun→TRX / wei). */ + nativeAmount(raw: string): string + /** fee fallback when no structured fee object is present. */ + feeFallback(fee: unknown): string + /** address-type label for the per-family address rows. */ + addressLabel: string +} + +const txInfoAmount = (v: string | undefined, suffix: string): string => (v === undefined || v === "" ? "" : `${formatScalar(v)}${suffix}`) + +export const FAMILY_RENDER: Record = { + tron: { + nativeAmount: (raw) => `${formatSun(raw)} TRX`, + feeFallback: (fee) => `${formatSun(fee)} TRX`, + addressLabel: "TRON address", + txInfoRows: (r) => [ + ["TxID", r.txid], + ["From", r.from ?? ""], + ["To", r.to ?? ""], + ["Amount", txInfoAmount(r.amount, r.symbol ? ` ${r.symbol}` : "")], + ["Status", r.status ?? "unknown"], + ["Block", r.blockNumber === undefined ? "" : `#${formatInt(r.blockNumber)}`], + ["Energy", r.energyUsed === undefined ? "" : formatInt(r.energyUsed)], + ["Fee", r.feeSun === undefined ? "" : `${formatSun(r.feeSun)} TRX`], + ], + }, +} + +/** humanize a raw base-unit balance: scale by `decimals` when known, else show the raw integer. */ +function humanBalance(d: Obj): string { + return d.decimals !== undefined ? fromBaseUnits(String(d.balance ?? "0"), num(d.decimals, 0)) : formatScalar(d.balance) +} + +export const TextFormatters = { + walletCreated: + (verb: "Created" | "Imported", notes: string[]): TextFormatter => + (data) => + renderWalletCreated(verb, asObj(data), notes), + walletWatch: ((data) => { + const d = asObj(data) + return receipt(ok(), `Added watch-only account ${quote(displayName(d))}`, [ + ["Address", firstAddress(d)], + ["Note", "read-only; signing operations will be rejected"], + ]) + }) satisfies TextFormatter, + walletLedger: ((data) => renderLedgerImported(asObj(data))) satisfies TextFormatter, + walletList: ((data) => renderWalletList(Array.isArray(data) ? data.map(asObj) : [])) satisfies TextFormatter, + walletUse: ((data) => { + const d = asObj(data) + return receipt(ok(), `Active account: ${displayName(d)}`, addressPairs(d)) + }) satisfies TextFormatter, + walletCurrent: ((data) => { + const d = asObj(data) + return titled(`Active account: ${displayName(d)}`, addressPairs(d)) + }) satisfies TextFormatter, + walletRename: ((data) => { + const d = asObj(data) + return receipt(ok(), "Renamed account", [ + ["Old label", String(d.previousLabel ?? "")], + ["New label", displayName(d)], + ]) + }) satisfies TextFormatter, + walletDerive: ((data) => { + const d = asObj(data) + return receipt(ok(), `Derived sub-account ${quote(displayName(d))}`, [ + ["Address", firstAddress(d)], + ["Active", d.active === true ? "yes" : ""], + ["Note", "shares master mnemonic; no separate backup needed"], + ]) + }) satisfies TextFormatter, + walletDelete: ((data) => { + const d = asObj(data) + const scope = d.scope === "account" ? "account" : "wallet" + return receipt(ok(), `Deleted ${scope} ${String(d.accountId ?? "")}`, [ + ["Secret removed", d.secretRemoved === false ? "no" : "yes"], + ["New active", d.newActive ? String(d.newActive) : ""], + ]) + }) satisfies TextFormatter, + walletBackup: ((data) => { + const d = asObj(data) + return [ + receipt(warn(), `Backup written ${String(d.out ?? "")}`, [ + ["Account ID", String(d.accountId ?? "")], + ["Secret", secretLabel(d.secretType)], + ["File mode", String(d.fileMode ?? "0600")], + ["Bytes", String(d.bytes ?? "?")], + ]), + "", + `${warn()} Secret material was written only to the backup file, never to stdout.`, + ].join("\n") + }) satisfies TextFormatter, + + config: ((data) => renderConfig(asObj(data))) satisfies TextFormatter, + networks: ((data) => + table( + ["Network", "Family", "Chain", "Fee model"], + (Array.isArray(data) ? data : []).map(asObj).map((n) => [String(n.id ?? ""), String(n.family ?? ""), String(n.chainId ?? ""), String(n.feeModel ?? "")]), + )) satisfies TextFormatter, + + accountBalance: ((data, ctx) => { + const d = asObj(data) + const symbol = d.symbol ? ` ${String(d.symbol)}` : "" + return query([identity(ctx, d.address), ["Balance", `${humanBalance(d)}${symbol}`]]) + }) satisfies TextFormatter, + accountInfo: ((data, ctx) => renderAccountInfo(asObj(data), ctx)) satisfies TextFormatter, + accountHistory: ((data, ctx) => { + const d = asObj(data) + const rows = (Array.isArray(d.records) ? d.records : []).map(asObj).map(historyRow) + return [`${quote(acct(ctx, d.address))} recent transactions`, table(["Time", "Type", "Amount", "From / To", "Status"], rows)].join("\n") + }) satisfies TextFormatter, + tokenBookAdd: ((data) => { + const d = asObj(data) + const token = asObj(d.token) + const verb = d.action === "updated" ? "Updated token book" : "Added to token book" + return receipt(ok(), verb, [ + ["Name", String(token.name ?? "")], + ["Symbol", String(token.symbol ?? token.id ?? "")], + ["Decimals", token.decimals === undefined ? "" : String(token.decimals)], + ]) + }) satisfies TextFormatter, + tokenBookList: ((data) => { + const d = asObj(data) + const rows = (Array.isArray(d.tokens) ? d.tokens : []).map(asObj).map((t) => [String(t.symbol ?? ""), String(t.name ?? ""), String(t.source ?? ""), String(t.id ?? "")]) + return table(["Symbol", "Name", "Source", "Contract / ID"], rows) + }) satisfies TextFormatter, + tokenBookRemove: ((data) => { + const removed = asObj(asObj(data).removed) + return receipt(ok(), "Removed from token book", [ + ["Name", String(removed.name ?? "")], + ["Symbol", String(removed.symbol ?? "")], + ]) + }) satisfies TextFormatter, + accountPortfolio: ((data, ctx) => { + const d = asObj(data) + const holdings = (Array.isArray(d.holdings) ? d.holdings : []).map(asObj) + const rows = holdings.map((h) => [ + String(h.symbol ?? ""), + h.balanceUnavailable ? "unavailable" : formatScalar(h.balance), + h.priceUsd === null || h.priceUsd === undefined ? "-" : `$${formatUsd(h.priceUsd)}`, + h.valueUsd === null || h.valueUsd === undefined ? "-" : `$${formatUsd(h.valueUsd)}`, + ]) + const total = d.totalValueUsd === null || d.totalValueUsd === undefined ? "-" : `$${formatUsd(d.totalValueUsd)}` + const lines = [`${quote(acct(ctx, d.address ?? d.account))} Portfolio`, table(["Token", "Balance", "Price (USD)", "Value (USD)"], rows), `Total ≈ ${total}`] + for (const h of holdings) { + if (h.balanceUnavailable) lines.push(`${warn()} ${String(h.symbol ?? "")} balance unavailable (${String(h.reason ?? "")})`) + } + if (d.priceUnavailable) lines.push(`${warn()} price warning (${String(d.priceReason ?? "")})`) + return lines.join("\n") + }) satisfies TextFormatter, + + tokenBalance: ((data, ctx) => { + const d = asObj(data) + return query([identity(ctx, d.address), ["Name", String(d.name ?? "")], ["Symbol", String(d.symbol ?? "")], ["Balance", humanBalance(d)]]) + }) satisfies TextFormatter, + tokenInfo: ((data) => { + const d = asObj(data) + return query([ + ["Name", String(d.name ?? d.token_name ?? d.id ?? "")], + ["Symbol", String(d.symbol ?? d.abbr ?? "")], + ["Decimals", String(d.decimals ?? d.precision ?? "")], + ]) + }) satisfies TextFormatter, + + txReceipt: ((r, ctx?: TextRenderContext) => renderTxReceipt(r, ctx)) satisfies TextFormatter, + txStatus: ((r) => { + // `state` is computed by the command (tron: getTransactionById + receipt result) — no family branch. + const status = { + confirmed: `confirmed ${ok()}`, + failed: `failed ${fail()}`, + pending: `pending ${pending()}`, + not_found: `not found ${unknown()}`, + }[r.state] + return query([ + ["TxID", r.txid], + ["Status", status], + ["Block", r.blockNumber === undefined ? "" : `#${formatInt(r.blockNumber)}`], + ]) + }) satisfies TextFormatter, + txInfo: ((r, ctx) => { + return query(FAMILY_RENDER[renderFamily(ctx)].txInfoRows(r)) + }) satisfies TextFormatter, + + contractCall: ((data) => { + const d = asObj(data) + return query([ + ["Method", methodName(String(d.method ?? ""))], + ["Result", `${formatResult(d.result)} (raw)`], + ]) + }) satisfies TextFormatter, + contractInfo: ((data) => renderContractInfo(asObj(data))) satisfies TextFormatter, + + messageSign: ((data) => { + const d = asObj(data) + return receipt(ok(), "Signed", [ + ["Address", String(d.address ?? "")], + ["Signature", String(d.signature ?? "")], + ]) + }) satisfies TextFormatter, + block: ((data) => { + const block = asObj(asObj(data).block) + const header = asObj(asObj(block.block_header).raw_data) + const n = block.number ?? header.number + const ts = block.timestamp ?? header.timestamp + const txs = Array.isArray(block.transactions) ? block.transactions.length : 0 + return query([ + ["Number", n === undefined ? "" : `#${formatInt(n)}`], + ["Time", ts ? formatUtc(ts) : "unknown"], + ["Transactions", String(txs)], + ]) + }) satisfies TextFormatter, +} + +export function renderGenericText(command: string, net: NetworkDescriptor | undefined, data: unknown): string { + const lines: string[] = [`${ok()} ${command}`] + if (net) lines.push(` network: ${net.id}`) + if (data && typeof data === "object" && !Array.isArray(data)) { + for (const [k, v] of Object.entries(data as Obj)) { + if (Array.isArray(v) && v.length > 0) { + lines.push(` ${k}:`) + for (const item of v) lines.push(` - ${formatScalar(item)}`) + } else { + lines.push(` ${k}: ${formatScalar(v)}`) + } + } + } else if (Array.isArray(data)) { + for (const item of data) lines.push(` - ${formatScalar(item)}`) + } else if (data !== undefined && data !== null) { + lines.push(` ${String(data)}`) + } + return lines.join("\n") +} + +function renderWalletCreated(verb: "Created" | "Imported", d: Obj, notes: string[]): string { + const existing = d.status === "existing" + const title = existing ? "Existing wallet" : `${verb} wallet` + const lines = [ + receipt(existing ? warn() : ok(), `${title} ${quote(displayName(d))}`, [ + ["Account ID", String(d.accountId ?? "")], + ["Type", typeLabel(d.type)], + ...addressPairs(d), + ["Active", d.active === true ? "yes" : ""], + ]), + ] + if (notes.length) lines.push("", ...notes.map((n) => `${warn()} ${n}`)) + return lines.join("\n") +} + +function renderLedgerImported(d: Obj): string { + const existing = d.status === "existing" + return [ + receipt(existing ? warn() : ok(), `${existing ? "Existing Ledger account" : "Registered Ledger account"} ${quote(displayName(d))}`, [ + ["Account ID", String(d.accountId ?? "")], + ["App", String(d.family ?? "")], + ["Path", String(d.path ?? "")], + ...addressPairs(d), + ]), + "", + `${warn()} No private key is stored locally. Signing requires device confirmation.`, + ].join("\n") +} + +/** Tree view: each seed wallet is a group headed by its seed id (the `--seed` handle for `derive`), + * its accounts listed under `├─/└─` connectors as `[index] label`. Non-HD accounts group by type. + * Plain text only — the text-mode frame is control-byte-stripped (CLI-OUT-001) so ANSI colour + * can't survive here anyway; the active account is marked with a trailing `(active)`. */ +function renderWalletList(items: Obj[]): string { + if (items.length === 0) return "No wallets found." + // group seeds by their seed id (wlt_x); non-HD accounts by type. Insertion order preserved. + const groups = new Map() + for (const d of items) { + const isSeed = d.type === "seed" + const seedId = String(d.accountId ?? "").split(".")[0] ?? "" + const key = isSeed ? `hd:${seedId}` : `type:${String(d.type)}` + const header = isSeed ? `HD ${seedId}` : typeLabel(d.type) + ;(groups.get(key) ?? groups.set(key, { hd: isSeed, header, rows: [] }).get(key)!).rows.push(d) + } + const leftOf = (d: Obj): string => (d.type === "seed" ? `[${d.index ?? "?"}] ${displayName(d)}` : displayName(d)) + const leftW = Math.max(...items.map((d) => leftOf(d).length)) + const addrW = Math.max(...items.map((d) => firstAddress(d).length)) + const row = (d: Obj, last: boolean): string => + `${last ? "└─ " : "├─ "}${leftOf(d).padEnd(leftW)} ${firstAddress(d).padEnd(addrW)} ${d.active ? "(active)" : ""}`.replace(/\s+$/, "") + const blocks: string[] = [] + for (const g of groups.values()) { + const rows = g.hd ? [...g.rows].sort((a, b) => Number(a.index) - Number(b.index)) : g.rows + blocks.push([g.header, ...rows.map((d, i) => row(d, i === rows.length - 1))].join("\n")) + } + return blocks.join("\n\n") +} + +function renderAccountInfo(d: Obj, ctx: TextRenderContext): string { + const account = asObj(d.account) + const owner = asObj(account.owner_permission) + const active = Array.isArray(account.active_permission) ? account.active_permission.length : 0 + const created = account.create_time ? new Date(Number(account.create_time)).toISOString().slice(0, 10) : "" + const ownerKeys = Array.isArray(owner.keys) ? owner.keys.length : "?" + const resources = asObj(d.resources) + const bandwidth = asObj(resources.bandwidth) + const energy = asObj(resources.energy) + const pairs: Pair[] = [] + if (ctx.accountLabel) pairs.push(["Label", ctx.accountLabel]) + pairs.push(["Address", String(d.address ?? "")]) + pairs.push(["Balance", `${formatSun(account.balance)} TRX`]) + const staked = stakedSummary(account) + if (staked) pairs.push(["Staked", staked]) + if (resources.energy) pairs.push(["Energy", `used ${formatInt(energy.used)} / ${formatInt(energy.limit)}`]) + if (resources.bandwidth) pairs.push(["Bandwidth", `used ${formatInt(bandwidth.used)} / ${formatInt(bandwidth.limit)}`]) + pairs.push(["Created", created]) + pairs.push(["Permissions", `owner ${String(owner.threshold ?? "?")}-of-${ownerKeys}, ${active} active group${active === 1 ? "" : "s"}`]) + return query(pairs) +} + +/** Sum FreezeBalanceV2 stakes into a " TRX (energy + bandwidth )" summary. */ +function stakedSummary(account: Obj): string { + const frozen = Array.isArray(account.frozenV2) ? account.frozenV2.map(asObj) : [] + // frozenV2's bandwidth entries carry no `type`, so an unrecognized code folds into bandwidth. + const sums = new Map(RESOURCES.map((r) => [r, 0n])) + for (const f of frozen) { + const r = resourceOfRpcCode(String(f.type ?? "")) ?? "bandwidth" + const amount = safeUnsignedBigInt(f.amount ?? 0) + // An unsafe JS number has already lost precision. Omit the summary instead of presenting a + // plausible but incorrect total; the raw account payload remains available in JSON mode. + if (amount === null) return "" + sums.set(r, (sums.get(r) ?? 0n) + amount) + } + const total = RESOURCES.reduce((t, r) => t + (sums.get(r) ?? 0n), 0n) + if (total === 0n) return "" + const parts = RESOURCES.map((r) => `${r} ${formatSun(sums.get(r) ?? 0n)}`).join(" + ") + return `${formatSun(total)} TRX (${parts})` +} + +function safeUnsignedBigInt(value: unknown): bigint | null { + if (typeof value === "bigint") return value >= 0n ? value : null + if (typeof value === "number") { + return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null + } + if (typeof value === "string" && /^\d+$/.test(value)) return BigInt(value) + return null +} + +function renderContractInfo(d: Obj): string { + let names: string[] + let count: number + if (Array.isArray(d.methods)) { + names = d.methods.map(String) + count = num(d.functionCount, names.length) + } else { + const contract = asObj(d.contract) + const info = asObj(d.info) + const abi = contract.abi ?? info.abi ?? contract.ABI ?? info.ABI + const nestedEntries = asObj(abi).entrys + const entries: unknown[] = Array.isArray(abi) ? abi : Array.isArray(nestedEntries) ? nestedEntries : [] + const methods = entries.map(asObj).filter((e) => e.type === "Function" || e.type === "function") + names = methods + .map((e) => e.name) + .filter(Boolean) + .map(String) + count = methods.length + } + const name = String(d.name ?? asObj(d.contract).name ?? asObj(d.info).name ?? "") + const preview = names.slice(0, 3).join(" / ") + return query([ + ["Contract", String(d.address ?? "")], + ["Name", name], + ["Methods", `${count}${preview ? ` (${preview}${count > 3 ? " …" : ""})` : ""}`], + ]) +} + +function renderConfig(d: Obj): string { + if ("input" in d) { + return receipt(ok(), "Set config", [ + ["Key", String(d.key)], + ["Value", configValue(d.value)], + ]) + } + if ("key" in d) return kv([[String(d.key), configValue(d.value)]], "") + return kv( + Object.entries(d).map(([k, v]) => [k, configValue(v)] as Pair), + "", + ) +} + +/** config values keep their literal form (no thousands grouping, raw key names). */ +function configValue(v: unknown): string { + if (Array.isArray(v)) return v.map(String).join(", ") + return v === null || v === undefined ? "" : String(v) +} + +/** Default-mode broadcast/dry-run/sign-only receipt for tx/stake/contract signing commands. + * Narrows on the typed `kind`; the active family comes from `ctx.net` (see renderFamily) — no + * `family` in the payload, no stringly command-id matching, no alias probing. */ +function renderTxReceipt(r: TxReceiptView, ctx?: TextRenderContext): string { + const family = renderFamily(ctx) + if (r.mode === "dry-run") { + return receipt(pending(), `Dry run ${actionLabel(r.kind)}`, [ + ["Fee", formatFee(r.fee, family)], + ["Tx", summarizeTx(r.tx)], + ]) + } + if (r.mode === "sign-only") { + return receipt(ok(), `Signed ${actionLabel(r.kind)}`, [ + ["Fee", formatFee(r.fee, family)], + ["Signed", summarizeTx(r.signed)], + ]) + } + const txid = String(r.txId ?? r.hash ?? "") + const stage = r.stage ?? "submitted" + const summary = receiptSummary(r, family) + const pairs: Pair[] = [...receiptRows(r)] + if (txid) pairs.push(["TxID", txid]) + + // submitted (default, non-blocking): txid only, no fee/energy yet — those need confirmation. + if (stage === "submitted") { + pairs.push(["Status", "pending — not yet on-chain"]) + const body = receipt(pending(), summary, pairs) + const networkFlag = ctx?.net ? ` --network ${ctx.net.id}` : "" + return txid ? `${body}\n! Track it: wallet-cli tx info${networkFlag} --txid ${txid}` : body + } + + // confirmed / failed (after --wait): real on-chain block / fee / energy / result. + if (r.blockNumber !== undefined && r.blockNumber !== null) pairs.push(["Block", `#${formatInt(r.blockNumber)}`]) + if (r.energyUsed !== undefined && r.energyUsed !== null) pairs.push(["Energy", formatInt(r.energyUsed)]) + if (r.feeSun !== undefined && r.feeSun !== null) pairs.push(["Fee", `${formatSun(r.feeSun)} TRX`]) + if (r.kind === "stake-unfreeze") pairs.push(["Withdrawable", "after the unlock period — then run `stake withdraw`"]) + if (stage === "failed") { + pairs.push(["Status", "failed"]) + if (r.result) pairs.push(["Reason", String(r.result)]) + return receipt(fail(), summary, pairs) + } + pairs.push(["Status", "success"]) + return receipt(ok(), summary, pairs) +} + +/** the verb-phrase summary for a broadcast receipt, by action kind. */ +function receiptSummary(r: TxReceiptView, family: ChainFamily): string { + const stakeAmt = r.amountSun !== undefined ? `${formatSun(r.amountSun)} TRX` : "TRX" + const resource = r.resource ? String(r.resource) : "" + switch (r.kind) { + case "stake-freeze": + return `Staked ${stakeAmt}${resource ? ` for ${resource}` : ""}` + case "stake-unfreeze": + return `Unstaked ${stakeAmt}` + case "stake-delegate": + return `Delegated ${stakeAmt}${resource ? ` of ${resource}` : ""}` + case "stake-undelegate": + return `Reclaimed ${stakeAmt}${resource ? ` of ${resource}` : ""}` + case "stake-withdraw": + return r.withdrawnSun ? `Withdrew ${formatSun(r.withdrawnSun)} TRX to balance` : "Withdrew expired TRX to balance" + case "stake-cancel": + return "Cancelled pending unstakes" + case "contract-send": + return `Called ${methodName(String(r.method ?? ""))}` + case "contract-deploy": + return "Contract deployed" + case "send": { + const amount = receiptAmount(r, family) + return amount ? `Sent ${amount}` : "Sent" + } + case "broadcast": + return "Broadcast" + } +} + +/** action-specific extra rows (To/From/Address/Contract), by kind. */ +function receiptRows(r: TxReceiptView): Pair[] { + const rows: Pair[] = [] + if (r.kind === "stake-delegate") rows.push(["To", String(r.receiver ?? "")]) + else if (r.kind === "stake-undelegate") rows.push(["From", String(r.receiver ?? "")]) + else if (r.kind === "contract-deploy") rows.push(["Address", String(r.contractAddress ?? "")]) + else if (r.to ?? r.receiver) rows.push(["To", String(r.to ?? r.receiver)]) + if (r.kind === "contract-send") rows.push(["Contract", String(r.contract ?? "")]) + return rows +} + +/** broadcast-receipt amount: token-aware (symbol/decimals when known, else the contract/asset-id + * identifier for raw-amount sends), native smallest-unit → coin only when no token is involved. */ +function receiptAmount(r: TxReceiptView, family: ChainFamily): string { + if (r.rawAmount !== undefined && r.rawAmount !== null && r.rawAmount !== "") { + const raw = String(r.rawAmount) + const isToken = r.token !== undefined || r.contract !== undefined || r.assetId !== undefined + if (isToken) { + const human = r.decimals !== undefined && r.decimals !== null ? fromBaseUnits(raw, num(r.decimals, 0)) : formatScalar(raw) + const label = r.token ?? r.contract ?? (r.assetId !== undefined ? `asset ${String(r.assetId)}` : "") + return label ? `${human} ${String(label)}` : human + } + return FAMILY_RENDER[family].nativeAmount(raw) + } + if (r.amountSun) return `${formatSun(r.amountSun)} TRX` + return "" +} + +/** human label for an action kind, e.g. "send" → "tx send" (for dry-run/sign-only headers). */ +function actionLabel(kind: TxReceiptKind): string { + switch (kind) { + case "send": + return "tx send" + case "broadcast": + return "tx broadcast" + case "stake-freeze": + return "stake freeze" + case "stake-unfreeze": + return "stake unfreeze" + case "stake-delegate": + return "stake delegate" + case "stake-undelegate": + return "stake undelegate" + case "stake-withdraw": + return "stake withdraw" + case "stake-cancel": + return "stake cancel-unfreeze" + case "contract-send": + return "contract send" + case "contract-deploy": + return "contract deploy" + } +} + +function historyRow(r: Obj): string[] { + const ts = r.time ?? r.block_timestamp ?? r.timestamp + const type = r.type ?? r.transfer_type ?? r.direction ?? "" + const amount = r.amount ?? r.value ?? r.quant ?? "" + const symbol = r.symbol ?? (r.token_info && typeof r.token_info === "object" ? asObj(r.token_info).symbol : undefined) + const counterparty = r.counterparty ?? r.to ?? r.from ?? "" + const status = r.status === "failed" || r.confirmed === false ? "failed" : "ok" + return [formatTime(ts), String(type), `${formatScalar(amount)}${symbol ? ` ${String(symbol)}` : ""}`, String(counterparty), status === "ok" ? ok() : fail()] +} + +/** account display id for receipts: the centrally-injected --account label when present, + * else the full on-chain address. Callers add their own quoting where wanted. */ +function acct(ctx: TextRenderContext, address: unknown): string { + return ctx.accountLabel ?? String(address ?? "") +} + +/** identity field pair: prefer the account label, else show the full address — the field + * name tracks the value's real meaning (§0.4). */ +function identity(ctx: TextRenderContext, address: unknown): Pair { + return ctx.accountLabel ? ["Label", ctx.accountLabel] : ["Address", String(address ?? "")] +} + +function displayName(d: Obj): string { + return String(d.label ?? d.accountId ?? d.id ?? "unnamed") +} + +/** non-empty address entries — drops families whose address is blank/absent. */ +function nonEmptyAddressEntries(d: Obj): Pair[] { + return Object.entries(asObj(d.addresses)) + .filter(([, address]) => typeof address === "string" && address.length > 0) + .map(([family, address]) => [family, String(address)] as Pair) +} + +function firstAddress(d: Obj): string { + const first = nonEmptyAddressEntries(d)[0] + return first ? first[1] : "" +} + +/** per-family address field pairs, addresses shown in full (§0.4 ②). */ +function addressPairs(d: Obj): Pair[] { + return nonEmptyAddressEntries(d).map(([family, address]) => [familyAddressLabel(family), address] as Pair) +} + +function familyAddressLabel(family: string): string { + return FAMILY_RENDER[family as ChainFamily]?.addressLabel ?? `${family} address` +} + +/** the active chain family for a chain-command renderer. Chain commands always resolve a network + * before running, so `ctx.net` is present; the tron fallback only guards a shape that can't occur. */ +function renderFamily(ctx?: TextRenderContext): ChainFamily { + return ctx?.net?.family ?? "tron" +} + +function typeLabel(v: unknown): string { + return sourceLabel(v) +} + +function secretLabel(v: unknown): string { + switch (v) { + case "mnemonic": + return "recovery phrase" + case "privateKey": + return "private key" + default: + return String(v ?? "secret") + } +} + +function methodName(sig: string): string { + return sig.replace(/\(.*/, "") || sig +} + +function formatResult(v: unknown): string { + if (Array.isArray(v)) return v.map((x) => formatScalar(x)).join(", ") + return formatScalar(v) +} + +function formatFee(fee: unknown, family: ChainFamily): string { + if (!fee) return "unknown" + if (typeof fee === "object") { + const f = asObj(fee) + if (f.feeSun) return `${formatSun(f.feeSun)} TRX` + if (f.bandwidthBurnSunIfNoFreeze) return `${formatSun(f.bandwidthBurnSunIfNoFreeze)} TRX` + // energy estimate (TRC20/contract via estimateResources): no sun figure — staked energy may + // cover it. Report the estimated energy + whether the account's available energy covers it. + if (f.energy !== undefined) { + const energy = Number(f.energy) + const avail = f.availableEnergy === undefined ? undefined : Number(f.availableEnergy) + const covered = avail !== undefined && avail >= energy ? " (covered by staked energy)" : "" + return `~${energy.toLocaleString()} energy${covered}` + } + if (f.note) return String(f.note) + } + return FAMILY_RENDER[family].feeFallback(fee) +} + +function summarizeTx(tx: unknown): string { + if (!tx || typeof tx !== "object") return formatScalar(tx) + const o = asObj(tx) + return shorten(String(o.txid ?? o.txID ?? o.hash ?? JSON.stringify(o))) +} diff --git a/ts/src/adapters/inbound/cli/render/layout.ts b/ts/src/adapters/inbound/cli/render/layout.ts new file mode 100644 index 000000000..02443a9f1 --- /dev/null +++ b/ts/src/adapters/inbound/cli/render/layout.ts @@ -0,0 +1,47 @@ +/** + * Layout primitives — structural composition of label/value blocks and tables, + * plus status glyphs. The "§0.4 字段独占一行" vocabulary; no scalar or domain knowledge. + */ +export type Obj = Record; +export type Pair = [string, string]; + +/** aligned `