Summary
The current signing flow calls the wallet extension and handles all errors as a single generic failure. This issue builds a full signing pipeline that supports both software wallets (Freighter) and hardware wallets (Ledger via WebHID), aggregates multi-signature payloads where required, classifies every possible failure into a typed error with a user-actionable recovery hint, and implements retry logic with exponential backoff for transient hardware faults.
Scope
1. Signer abstraction
- Define a
Signer interface: { sign(xdr: string): Promise<string>, getPublicKey(): Promise<string>, type: 'software' | 'hardware' }
- Implement
FreighterSigner wrapping the Freighter browser extension API
- Implement
LedgerSigner wrapping the Stellar Ledger app via @ledgerhq/hw-transport-webhid
- Expose a
useSigner() hook that detects which signers are available and returns the active signer based on the user's wallet preference stored in localStorage
2. Ledger transport layer
- Open a WebHID transport on first use and keep it alive for the session (re-open on disconnect)
- Implement a connection health check: send a no-op APDU and verify the response before each signing request
- If the Ledger is locked, return typed error
LedgerLocked with hint 'Unlock your Ledger and open the Stellar app'
- If the Stellar app is not open, return typed error
LedgerAppNotOpen with hint 'Open the Stellar app on your Ledger'
3. Typed error classification
- Define a discriminated union
SigningError:
UserRejected — user dismissed the signing prompt
NetworkMismatch — wallet is on a different network than the app
LedgerLocked — Ledger device is locked
LedgerAppNotOpen — Stellar app not open on Ledger
LedgerTimeout — no response from Ledger within 30 seconds
TransactionTooLarge — XDR exceeds the signer's maximum size
SignerUnavailable — wallet extension not installed or WebHID not supported
- Map every raw extension and WebHID error to one of the above types in a central error classifier
4. Retry pipeline with backoff
- On
LedgerTimeout: retry up to 3 times with 2s, 4s, 8s delays; show a 'Retrying…' indicator between attempts
- On
LedgerLocked or LedgerAppNotOpen: do not retry — immediately show the typed error hint and a 'Check Ledger' button that re-runs the health check
- On
UserRejected: do not retry — reset the UI immediately
5. Signing progress UI
- Show a multi-step progress indicator during signing: 'Preparing transaction' → 'Waiting for signature' → 'Submitting to Stellar'
- Each step transitions only when the underlying async operation completes
- On error, the failed step turns red and displays the typed error hint inline
6. Unit tests
- Unit tests: error classifier correctly maps every known raw error to its typed
SigningError
- Unit tests: retry logic fires correct delays and stops after 3 attempts
- Unit tests:
useSigner returns the correct signer based on localStorage preference and availability
Acceptance Criteria
ETA: 24 hours
Coordinate on Telegram
Summary
The current signing flow calls the wallet extension and handles all errors as a single generic failure. This issue builds a full signing pipeline that supports both software wallets (Freighter) and hardware wallets (Ledger via WebHID), aggregates multi-signature payloads where required, classifies every possible failure into a typed error with a user-actionable recovery hint, and implements retry logic with exponential backoff for transient hardware faults.
Scope
1. Signer abstraction
Signerinterface:{ sign(xdr: string): Promise<string>, getPublicKey(): Promise<string>, type: 'software' | 'hardware' }FreighterSignerwrapping the Freighter browser extension APILedgerSignerwrapping the Stellar Ledger app via@ledgerhq/hw-transport-webhiduseSigner()hook that detects which signers are available and returns the active signer based on the user's wallet preference stored in localStorage2. Ledger transport layer
LedgerLockedwith hint 'Unlock your Ledger and open the Stellar app'LedgerAppNotOpenwith hint 'Open the Stellar app on your Ledger'3. Typed error classification
SigningError:UserRejected— user dismissed the signing promptNetworkMismatch— wallet is on a different network than the appLedgerLocked— Ledger device is lockedLedgerAppNotOpen— Stellar app not open on LedgerLedgerTimeout— no response from Ledger within 30 secondsTransactionTooLarge— XDR exceeds the signer's maximum sizeSignerUnavailable— wallet extension not installed or WebHID not supported4. Retry pipeline with backoff
LedgerTimeout: retry up to 3 times with 2s, 4s, 8s delays; show a 'Retrying…' indicator between attemptsLedgerLockedorLedgerAppNotOpen: do not retry — immediately show the typed error hint and a 'Check Ledger' button that re-runs the health checkUserRejected: do not retry — reset the UI immediately5. Signing progress UI
6. Unit tests
SigningErroruseSignerreturns the correct signer based on localStorage preference and availabilityAcceptance Criteria
FreighterSignerandLedgerSignerboth implement theSignerinterfaceSigningErrorwith a user-actionable hintLedgerLockedandLedgerAppNotOpennot retried — hint shown immediatelyuseSignerdetects available signers and respects localStorage preferenceETA: 24 hours
Coordinate on Telegram