@@ -153,6 +153,30 @@ async function openTronApp() {
153153 return { app, transport, appVersion } ;
154154}
155155
156+ /**
157+ * HID handles are exclusive — two concurrent attempts to open the Ledger USB
158+ * transport race and the loser sees "cannot open device". That's only a DoS
159+ * today (never a wrong-tx-signed), but it's a noisy failure the moment two
160+ * MCP tools run in parallel (e.g. `get_ledger_status` refreshes while a sign
161+ * is in flight). Serialize all transport-using calls through a module-local
162+ * queue so a second caller waits for the first to finish closing instead of
163+ * failing.
164+ */
165+ let usbLock : Promise < void > = Promise . resolve ( ) ;
166+ async function withUsbLock < T > ( fn : ( ) => Promise < T > ) : Promise < T > {
167+ const prev = usbLock ;
168+ let release ! : ( ) => void ;
169+ usbLock = new Promise < void > ( ( resolve ) => {
170+ release = resolve ;
171+ } ) ;
172+ try {
173+ await prev ;
174+ return await fn ( ) ;
175+ } finally {
176+ release ( ) ;
177+ }
178+ }
179+
156180/**
157181 * Query the device for its TRON address at `path`. Used by `pair_ledger_tron`
158182 * to cache the address for subsequent sign calls, and as the identity check
@@ -161,21 +185,23 @@ async function openTronApp() {
161185export async function getTronLedgerAddress (
162186 path : string = DEFAULT_TRON_PATH
163187) : Promise < { address : string ; publicKey : string ; path : string ; appVersion : string } > {
164- const { app, transport, appVersion } = await openTronApp ( ) ;
165- try {
166- const { address, publicKey } = await app . getAddress ( path , false ) ;
167- if ( ! isTronAddress ( address ) ) {
168- throw new Error (
169- `Ledger returned an address that doesn't look like a TRON mainnet address: "${ address } ". ` +
170- `Is the TRON (not Tron-classic / testnet) app open on the device?`
171- ) ;
188+ return withUsbLock ( async ( ) => {
189+ const { app, transport, appVersion } = await openTronApp ( ) ;
190+ try {
191+ const { address, publicKey } = await app . getAddress ( path , false ) ;
192+ if ( ! isTronAddress ( address ) ) {
193+ throw new Error (
194+ `Ledger returned an address that doesn't look like a TRON mainnet address: "${ address } ". ` +
195+ `Is the TRON (not Tron-classic / testnet) app open on the device?`
196+ ) ;
197+ }
198+ return { address, publicKey, path, appVersion } ;
199+ } catch ( e ) {
200+ throw mapLedgerError ( e , "getAddress" ) ;
201+ } finally {
202+ await transport . close ( ) . catch ( ( ) => { } ) ;
172203 }
173- return { address, publicKey, path, appVersion } ;
174- } catch ( e ) {
175- throw mapLedgerError ( e , "getAddress" ) ;
176- } finally {
177- await transport . close ( ) . catch ( ( ) => { } ) ;
178- }
204+ } ) ;
179205}
180206
181207export interface TronSignRequest {
@@ -206,31 +232,33 @@ export async function signTronTxOnLedger(
206232 req : TronSignRequest
207233) : Promise < { signature : string ; signerAddress : string } > {
208234 const path = req . path ?? DEFAULT_TRON_PATH ;
209- const { app, transport } = await openTronApp ( ) ;
210- try {
211- const { address } = await app . getAddress ( path , false ) ;
212- if ( address !== req . expectedFrom ) {
213- throw new Error (
214- `Ledger device address (${ address } ) does not match the prepared tx's \`from\` ` +
215- `(${ req . expectedFrom } ). Either connect the Ledger that holds keys for \`from\`, ` +
216- `or re-prepare the tx for the Ledger-derived address (\`pair_ledger_tron\`).`
217- ) ;
218- }
219- const signature = await app . signTransaction (
220- path ,
221- req . rawDataHex ,
222- req . tokenSignatures ?? [ ]
223- ) ;
224- // Ledger returns the signature as a hex string (65 bytes: r || s || v).
225- if ( ! / ^ [ 0 - 9 a - f A - F ] { 130 } $ / . test ( signature ) ) {
226- throw new Error (
227- `Ledger returned an unexpected signature shape (length ${ signature . length } ). Expected 130 hex chars.`
235+ return withUsbLock ( async ( ) => {
236+ const { app, transport } = await openTronApp ( ) ;
237+ try {
238+ const { address } = await app . getAddress ( path , false ) ;
239+ if ( address !== req . expectedFrom ) {
240+ throw new Error (
241+ `Ledger device address (${ address } ) does not match the prepared tx's \`from\` ` +
242+ `(${ req . expectedFrom } ). Either connect the Ledger that holds keys for \`from\`, ` +
243+ `or re-prepare the tx for the Ledger-derived address (\`pair_ledger_tron\`).`
244+ ) ;
245+ }
246+ const signature = await app . signTransaction (
247+ path ,
248+ req . rawDataHex ,
249+ req . tokenSignatures ?? [ ]
228250 ) ;
251+ // Ledger returns the signature as a hex string (65 bytes: r || s || v).
252+ if ( ! / ^ [ 0 - 9 a - f A - F ] { 130 } $ / . test ( signature ) ) {
253+ throw new Error (
254+ `Ledger returned an unexpected signature shape (length ${ signature . length } ). Expected 130 hex chars.`
255+ ) ;
256+ }
257+ return { signature, signerAddress : address } ;
258+ } catch ( e ) {
259+ throw mapLedgerError ( e , "signTransaction" ) ;
260+ } finally {
261+ await transport . close ( ) . catch ( ( ) => { } ) ;
229262 }
230- return { signature, signerAddress : address } ;
231- } catch ( e ) {
232- throw mapLedgerError ( e , "signTransaction" ) ;
233- } finally {
234- await transport . close ( ) . catch ( ( ) => { } ) ;
235- }
263+ } ) ;
236264}
0 commit comments