diff --git a/src/main/java/org/tron/ledger/LedgerAddressUtil.java b/src/main/java/org/tron/ledger/LedgerAddressUtil.java index 4676eed2..8a7db7a6 100644 --- a/src/main/java/org/tron/ledger/LedgerAddressUtil.java +++ b/src/main/java/org/tron/ledger/LedgerAddressUtil.java @@ -73,7 +73,12 @@ public static Map getMultiImportAddress(List paths, HidD return addressMap; } - public static String getTronAddress(String path, HidDevice hidDevice) { + /** + * Sends the "get address" APDU and returns the raw response bytes without parsing. + * Returns {@code null} on transport failure. Callers can inspect error status words + * (e.g. {@code 0x6511} = Tron app not open) before falling through to address parsing. + */ + public static byte[] getRawAddressResponse(String path, HidDevice hidDevice) { try { byte[] apdu = ApduMessageBuilder.buildTronAddressApduMessage(path); if (DebugConfig.isDebugEnabled()) { @@ -83,11 +88,25 @@ public static String getTronAddress(String path, HidDevice hidDevice) { if (DebugConfig.isDebugEnabled()) { System.out.println("Get Address Response: " + CommonUtil.bytesToHex(result)); } - if (LedgerConstant.LEDGER_LOCK.equalsIgnoreCase(CommonUtil.bytesToHex(result))) { - System.out.println(ANSI_RED + "Ledger is locked, please unlock it first"+ ANSI_RESET); - return EMPTY; + return result; + } catch (Exception e) { + if (DebugConfig.isDebugEnabled()) { + e.printStackTrace(); } + return null; + } + } + /** Parses a Tron Base58 address from a raw "get address" APDU response. Returns {@code ""} on any parse failure. */ + public static String parseTronAddress(byte[] result) { + if (result == null || result.length < 2) { + return EMPTY; + } + if (LedgerConstant.LEDGER_LOCK.equalsIgnoreCase(CommonUtil.bytesToHex(result))) { + System.out.println(ANSI_RED + "Ledger is locked, please unlock it first" + ANSI_RESET); + return EMPTY; + } + try { int offset = 0; int publicKeyLength = result[offset++] & 0xFF; byte[] publicKey = new byte[publicKeyLength]; @@ -98,6 +117,18 @@ public static String getTronAddress(String path, HidDevice hidDevice) { byte[] addressBytes = new byte[addressLength]; System.arraycopy(result, offset, addressBytes, 0, addressLength); return new String(addressBytes); + } catch (Exception e) { + if (DebugConfig.isDebugEnabled()) { + e.printStackTrace(); + } + return EMPTY; + } + } + + public static String getTronAddress(String path, HidDevice hidDevice) { + try { + byte[] result = getRawAddressResponse(path, hidDevice); + return parseTronAddress(result); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); if (DebugConfig.isDebugEnabled()) { diff --git a/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java b/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java index dccce021..aae497e9 100644 --- a/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java +++ b/src/main/java/org/tron/ledger/wrapper/HidServicesWrapper.java @@ -62,6 +62,11 @@ public HidServices initHidServices() { return hs; } + public boolean hasAnyLedgerAttached() { + return getHidServices().getAttachedHidDevices().stream() + .anyMatch(d -> d.getVendorId() == LEDGER_VENDOR_ID); + } + public static HidDevice getLedgerHidDevice(HidServices hidServices, String address, String path) { List hidDeviceList = new ArrayList<>(); HidDevice fidoDevice = null; diff --git a/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java b/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java index d1a30ed9..2126f387 100644 --- a/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java +++ b/src/main/java/org/tron/walletcli/cli/StdinPasswordReader.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; /** * Reads MASTER_PASSWORD from an {@link InputStream} (typically {@code System.in}) once and caches @@ -37,28 +38,34 @@ public synchronized String get() { private String readAll() { ByteArrayOutputStream buf = new ByteArrayOutputStream(); byte[] chunk = new byte[256]; + byte[] bytes = null; try { int n; while ((n = in.read(chunk)) != -1) { buf.write(chunk, 0, n); } + if (buf.size() == 0) { + return null; + } + bytes = buf.toByteArray(); + int len = bytes.length; + if (len > 0 && bytes[len - 1] == '\n') { + len--; + if (len > 0 && bytes[len - 1] == '\r') { + len--; + } + } + if (len == 0) { + return null; + } + return new String(bytes, 0, len, StandardCharsets.UTF_8); } catch (IOException e) { throw new IllegalStateException("Failed to read password from stdin: " + e.getMessage(), e); - } - if (buf.size() == 0) { - return null; - } - byte[] bytes = buf.toByteArray(); - int len = bytes.length; - if (len > 0 && bytes[len - 1] == '\n') { - len--; - if (len > 0 && bytes[len - 1] == '\r') { - len--; + } finally { + Arrays.fill(chunk, (byte) 0); + if (bytes != null) { + Arrays.fill(bytes, (byte) 0); } } - if (len == 0) { - return null; - } - return new String(bytes, 0, len, StandardCharsets.UTF_8); } } diff --git a/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java b/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java index 536e6763..9b75314e 100644 --- a/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java +++ b/src/main/java/org/tron/walletcli/cli/aliases/AliasResolutionException.java @@ -4,4 +4,9 @@ public class AliasResolutionException extends IllegalArgumentException { public AliasResolutionException(String message) { super(message); } + + @Override + public Throwable fillInStackTrace() { + return this; + } } diff --git a/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java b/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java index 26458827..04775128 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/LedgerPorts.java @@ -93,4 +93,15 @@ public interface SignResultReader { public interface ContractSupport { boolean canSign(Chain.Transaction transaction); } + + /** + * Thrown by {@link HidDeviceFinder#find} when the Ledger device is physically connected + * but the Tron app is not open (APDU status word {@code 0x6511}). + * Distinct from a plain {@code null} return (device not found / address mismatch). + */ + public static final class AppNotOpenException extends RuntimeException { + public AppNotOpenException(String message) { + super(message); + } + } } diff --git a/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java b/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java index 871dcdf0..959b8827 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSigner.java @@ -29,7 +29,7 @@ public final class NonInteractiveLedgerSigner implements LedgerSigner { static final String STATE_TIMEOUT = "timeout"; // SIGN_RESULT_TIMEOUT — timed out /** APDU status word: Tron app is not open on the device. */ - private static final byte[] APDU_APP_IS_OPEN = new byte[] { 0x65, 0x11 }; + private static final byte[] APDU_APP_NOT_OPEN = new byte[] { 0x65, 0x11 }; /** APDU status word: "Sign By Hash" setting is not enabled. */ private static final byte[] APDU_SIGN_BY_HASH = new byte[] { 0x6a, (byte) 0x8c }; @@ -68,6 +68,9 @@ public LedgerSignOutcome sign(Chain.Transaction transaction, try (SystemOutSuppressor ignored = SystemOutSuppressor.capture()) { try { device = finder.find(address, bip44Path); + } catch (LedgerPorts.AppNotOpenException e) { + return LedgerSignOutcome.failure(LedgerSignOutcome.Status.APP_NOT_OPEN, + e.getMessage()); } catch (RuntimeException e) { return LedgerSignOutcome.failure(LedgerSignOutcome.Status.NOT_CONNECTED, "HID transport failure: " + e.getMessage()); @@ -102,7 +105,7 @@ public LedgerSignOutcome sign(Chain.Transaction transaction, byte[] apdu = executor.lastSendResultBytes(); if (apdu != null && apdu.length > 0) { - if (matches(apdu, APDU_APP_IS_OPEN)) { + if (matches(apdu, APDU_APP_NOT_OPEN)) { stateReader.markCanceled(device.path(), txid); return LedgerSignOutcome.failure(LedgerSignOutcome.Status.APP_NOT_OPEN, "Open the Tron app on your Ledger device and try again"); diff --git a/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java b/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java index 2d1b417a..7746e7fe 100644 --- a/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java +++ b/src/main/java/org/tron/walletcli/cli/ledger/ProductionLedgerPorts.java @@ -24,15 +24,28 @@ public static NonInteractiveLedgerSigner buildSigner(OutputFormatter formatter) LedgerPorts.HidDeviceFinder finder = (address, path) -> { HidDevice device = HidServicesWrapper.getInstance().getHidDevice(address, path); if (device == null) { + if (HidServicesWrapper.getInstance().hasAnyLedgerAttached()) { + throw new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); + } return null; } boolean matched = false; try { - if (device.isClosed()) { - device.open(); + if (device.isClosed() && !device.open()) { + throw new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); + } + byte[] rawResponse = LedgerAddressUtil.getRawAddressResponse(path, device); + // 0x6511: Tron app is not open on the device (ISO 7816-4 "conditions not satisfied"). + // Distinguish from a genuine address mismatch so the caller can surface the right error. + if (rawResponse != null && rawResponse.length == 2 + && (rawResponse[0] & 0xFF) == 0x65 && (rawResponse[1] & 0xFF) == 0x11) { + throw new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); } - String deviceAddress = LedgerAddressUtil.getTronAddress(path, device); - matched = address.equals(deviceAddress); + String deviceAddress = LedgerAddressUtil.parseTronAddress(rawResponse); + matched = address.equals(deviceAddress) && !deviceAddress.isEmpty(); if (!matched) { return null; } @@ -87,14 +100,12 @@ public boolean executeSignListen(LedgerPorts.DeviceHandle device, Chain.Transact if (raw.isClosed()) { raw.open(); } - boolean accepted = listener.executeSignListen(raw, tx, path, gasfree); - if (listener.getLastSendResultBytes() != null || !accepted) { - listener.setStandardCliQuiet(false); - } - return accepted; - } catch (RuntimeException e) { + return listener.executeSignListen(raw, tx, path, gasfree); + } finally { + // Always reset: executeSignListen blocks until the 60-second wait completes, + // so by the time we return, the HID callback has either already reset this flag + // or it never will (silent timeout / device disconnect). listener.setStandardCliQuiet(false); - throw e; } } diff --git a/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java b/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java index c566bb5f..7262f0f7 100644 --- a/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java +++ b/src/test/java/org/tron/walletcli/cli/ledger/NonInteractiveLedgerSignerTest.java @@ -103,6 +103,17 @@ public void returnsNotConnectedWhenFinderThrows() { Assert.assertTrue(r.getMessage().contains("transport boom")); } + @Test + public void returnsAppNotOpenWhenFinderThrowsAppNotOpenException() { + // Simulates ProductionLedgerPorts detecting that a Ledger is physically attached + // (hasAnyLedgerAttached = true) but HID open() failed because the Tron app is not running. + finder.toThrow = new LedgerPorts.AppNotOpenException( + "Open the Tron app on your Ledger device and try again"); + LedgerSignOutcome r = signNonGasfree(); + Assert.assertEquals(LedgerSignOutcome.Status.APP_NOT_OPEN, r.getStatus()); + Assert.assertTrue(r.getMessage().contains("Tron app")); + } + @Test public void returnsUnsupportedContractBeforeDeviceLookup() { contractSupport.canSign = false;