Skip to content

[Architecture] Lobstr module uses module-level singleton for pending callbacks — concurrent signing requests corrupt each other #130

Description

@cybermax4200

Why this matters now

With useProofStatus (Issue 4), automatic re-authentication on 401 (via refreshAuthToken), and Lobstr wallet connect all potentially triggering openLobstrForSigning from different code paths, the chance of concurrent calls is real. A second openLobstrForSigning call silently clobbers _pendingResolve / _pendingReject, causing the first caller to never settle and the second to receive the first caller's signed XDR if Lobstr routes the callback to the wrong context.

Problem / What

src/services/lobstr.ts uses module-level mutable singletons:

let _pendingResolve: CallbackResolve | null = null;
let _pendingReject:  CallbackReject  | null = null;

cancelLobstrCallback is called at the top of openLobstrForSigning, which means a second signing request silently cancels the first. The cancel rejection is swallowed by the first caller with no signal.

The correct design is a keyed pending-call registry (a Map<string, {resolve, reject, timer}>) where each call gets a unique correlation ID, and resolveLobstrCallback routes the callback to the correct entry by matching a correlation_id query param that was embedded in the SEP-7 URI's callback URL.

Key Challenges

  • The SEP-7 callback URL must embed a correlation ID that Lobstr echoes back in the deep-link. The ecotask://lobstr/callback?xdr=<XDR>&id=<correlationId> format is the minimal change to buildSep7TxUri and parseLobstrCallbackUrl.
  • The resolveLobstrCallback function in RootNavigator parses the URL — it must extract the correlation ID and route to the correct pending entry.
  • All existing callers pass the Lobstr callback URL opaquely and don't need to know about correlation IDs.
  • The timeout from Issue 3 should be stored per-entry in the registry.
  • lobstr.test.ts must test the concurrent-call scenario.

Acceptance Criteria

  • Two simultaneous openLobstrForSigning calls each receive their own {resolve, reject, timer} entry keyed by a unique correlation ID.
  • The second call does NOT cancel the first.
  • resolveLobstrCallback routes to the correct entry using the id query param.
  • Calling cancelLobstrCallback without an ID cancels all pending entries (for clean teardown on logout/disconnect).
  • Test: two concurrent signing promises each settle independently with the correct XDR.

Relevant files / functions

  • src/services/lobstr.tsopenLobstrForSigning, resolveLobstrCallback, cancelLobstrCallback, buildSep7TxUri, parseLobstrCallbackUrl
  • src/navigation/RootNavigator.tsxhandleUrl
  • src/__tests__/lobstr.test.ts

Out of scope

  • Changing the Lobstr deep-link scheme registration (AndroidManifest / Info.plist).
  • UI for managing multiple pending signing requests.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions