Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a496006
fix(permission): exclude disabled ContractType 51 from default active…
Federico2014 Jun 12, 2026
8a1be30
Merge pull request #935 from Federico2014/fix/update-permission-shiel…
gummy789j Jun 18, 2026
610f241
test(permission): cover default active operation bitmap
gummy789j Jun 18, 2026
9d13be4
Merge pull request #936 from tronprotocol/followup/update-permission-…
gummy789j Jul 3, 2026
035ee4d
fix(staking): network-aware, fail-open resource-code validation (#939)
gummy789j Jul 2, 2026
8d117e4
fix(staking): reject TRON_POWER for v1 delegated freeze/unfreeze (#939)
gummy789j Jul 2, 2026
fd6e857
Merge pull request #943 from tronprotocol/fix/network-aware-resource-…
gummy789j Jul 3, 2026
011ac13
feat(ts): implement TypeScript wallet CLI
gummy789j Jun 30, 2026
6508832
refactor(cli): align canonical network IDs and docs
gummy789j Jun 30, 2026
829cfa4
refactor(ts): render balances via family native symbol/decimals
gummy789j Jul 1, 2026
9902e1e
fix(cli): strip terminal control sequences from text output
gummy789j Jul 1, 2026
8c07363
fix(tron): build contract calls locally so the RPC cannot substitute …
gummy789j Jul 1, 2026
8d062ab
fix(deps): remediate audited dependency vulnerabilities
gummy789j Jul 1, 2026
a7632b8
fix: harden CLI network and TRON account handling
gummy789j Jul 1, 2026
485da8f
refactor: normalize TRON responses at adapter boundary
gummy789j Jul 1, 2026
3e31a38
fix(cli): harden command-boundary handling from audit findings
gummy789j Jul 1, 2026
a14749a
feat(cli): bound every RPC/device call by --timeout
gummy789j Jul 1, 2026
e24af24
fix(cli): give tx status a four-state model (confirmed/failed/pending…
gummy789j Jul 1, 2026
ba7958f
refactor(cli): multi-positional support + unify help option terminology
gummy789j Jul 2, 2026
62eb08e
refactor(cli): drop redundant family/raw from tx JSON payloads
gummy789j Jul 2, 2026
9b2a396
fix(cli): bound Ledger device calls by --timeout at the adapter
gummy789j Jul 2, 2026
a300bf5
fix(cli): populate contractAddress on contract-deploy receipts
gummy789j Jul 2, 2026
b6a9180
feat(cli): redact sensitive fragments from internal error output
gummy789j Jul 2, 2026
d591b1f
feat(cli): make derive --seed-id explicit and HD delete cascade from …
gummy789j Jul 2, 2026
ace63ad
fix(cli): externalize axios in ESM bundle; note Ledger-unsignable sta…
gummy789j Jul 3, 2026
4605fb0
fix(cli): close Ledger transport on device-call timeout
gummy789j Jul 3, 2026
5823c09
docs: add agent-first wallet CLI guide
gummy789j Jul 3, 2026
afbea65
ops: update package.json & doc to production ready
gummy789j Jul 3, 2026
6b7927c
Merge pull request #941 from tronprotocol/feat/ts-version
gummy789j Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -38,3 +38,6 @@ qa/.verify.lock/
graphify-out/
.vscode/
docs/superpowers


/ts-deprecated/
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/standard-cli-contract-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions docs/standard-cli-user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> operationsMap = new HashMap<>();

static {
Expand Down Expand Up @@ -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));
Expand All @@ -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);
}
Expand Down Expand Up @@ -443,13 +461,12 @@ private void editActivePermissions() {
Collections.sort(currentOps);

while (true) {
List<Integer> allowedOps = currentOps.stream()
.filter(i -> operationsMap.get(String.valueOf(i)) != null).sorted().collect(Collectors.toList());
List<Integer> 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):");
Expand All @@ -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: ");
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -922,4 +933,4 @@ public long getWeight() {
}
}

}
}
46 changes: 46 additions & 0 deletions src/main/java/org/tron/walletcli/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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() + " !!!");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/tron/walletcli/WalletApiWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/org/tron/walletcli/cli/commands/CommandSupport.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading