Skip to content

feat: add XBullWallet adapter implementation (Closes #488) - #579

Open
waterWang wants to merge 1 commit into
drydocs:mainfrom
waterWang:feat/xbull-wallet-adapter
Open

feat: add XBullWallet adapter implementation (Closes #488)#579
waterWang wants to merge 1 commit into
drydocs:mainfrom
waterWang:feat/xbull-wallet-adapter

Conversation

@waterWang

Copy link
Copy Markdown

Summary

Implements XBullWallet in apps/web/src/lib/wallet.ts, a third WalletAdapter implementation alongside FreighterWallet and LobstrWallet, targeting the xBull Stellar wallet.

xBull is mobile-first (unlike Freighter, which is desktop-only), so it is a real path to reaching the low-end Android users the roadmap targets — the issue explicitly scopes this to the adapter implementation only, with a wallet-picker to follow later.

Implementation

  • XBullWallet implements WalletAdapter with all four methods:
    • isInstalled() — checks for window.xBullSDK (injected by the xBull browser extension)
    • connect() — calls sdk.connect({ canRequestPublicKey: true, canRequestSign: true }) then sdk.getPublicKey()
    • isAuthorized() — xBull has no passive site-permission query (no isAllowed equivalent; the only grant flow is connect(), which prompts). Mirrors the LobstrWallet stored-key pattern: a successful connect() writes the public key to sessionStorage (meridian-xbull-public-key), and isAuthorized() treats installed + stored key as authorized. This keeps revalidate() (which runs on mount and focus) non-prompting.
    • sign() — passes networkPassphrase through to sdk.signXDR(xdr, { network }), throws "Signing cancelled" on falsy return
  • Every method short-circuits through the existing withMockWallet() helper, so Playwright e2e tests can exercise the connect/sign paths without a real extension — same pattern as FreighterWallet/LobstrWallet.

Tests

New file apps/web/src/__tests__/lib/xbull-wallet.test.ts (207 lines), mirroring the structure of lobstr-wallet.test.ts:

  • Real xBull path (stubbed window.xBullSDK): isInstalled true/false, isAuthorized stored-key semantics (incl. a "never calls connect/getPublicKey passively" guard), connect success + key storage + permissions payload, connect error paths (SDK missing, empty key), sign success, signXDR receives networkPassphrase, sign error paths (SDK missing, cancel, propagated rejection)
  • E2E mock wallet path: short-circuiting for all four methods and rejection propagation

Verification

  • pnpm --filter @meridian/web lint — no new lint errors
  • pnpm --filter @meridian/web test — all wallet tests pass (existing Freighter/LOBSTR + new xBull suite)

Closes #488

Add XBullWallet to wallet.ts, alongside FreighterWallet and LobstrWallet,
implementing the full WalletAdapter interface (isInstalled, isAuthorized,
connect, sign) for the xBull mobile wallet via window.xBullSDK.

xBull has no passive site-permission query, so isAuthorized mirrors the
LobstrWallet stored-key pattern (sessionStorage signal written on connect).
sign passes the networkPassphrase through to signXDR.

Closes: drydocs#488

Signed-off-by: waterWang <water.wang@users.noreply.github.com>
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Collins' projects Team on Vercel.

A member of the Team first needs to authorize it.

@collinsezedike collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI has a real failure: Lint & Typecheck is failing on pnpm format:check, apps/web/src/__tests__/lib/xbull-wallet.test.ts and apps/web/src/lib/wallet.ts both have Prettier formatting issues. Run pnpm --filter @meridian/web format (or prettier --write) and push.

async () => {
const sdk = xbullSdk();
if (!sdk) throw new Error("xBull wallet not found");
const signedXdr = await sdk.signXDR(xdr, { network: networkPassphrase });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XBullSdk.signXDR's own type (a few lines up) declares an optional publicKey in its options, but this call only passes network. If a user connects account A (stored, and what Meridian's UI shows as the signer), then switches the active account to B inside the xBull extension without reconnecting, and xBull signs with whichever account is currently active rather than erroring, the returned signature comes from B while the app still believes and displays A as the signer, a silent account mismatch. Passing publicKey: readStoredXbullPublicKey() ?? undefined closes this the same way the stored key already backs isAuthorized().

return window.sessionStorage.getItem(XBULL_PUBLIC_KEY_STORAGE_KEY);
}

function storeXbullPublicKey(publicKey: string): void {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This session-stored-public-key pattern, readStoredXbullPublicKey/storeXbullPublicKey plus the isAuthorized/connect bodies that use them, is a byte-for-byte structural copy of LobstrWallet's equivalent (readStoredLobstrPublicKey/storeLobstrPublicKey, lines 116-171), differing only in the storage-key string. A future third wallet needing the same no-passive-permission workaround copies this a third time, and a fix to the pattern itself (key validation, clearing stale keys on disconnect, etc.) has to land in every copy and will silently miss whichever one gets forgotten. Worth factoring into a small shared helper, e.g. createSessionKeyAuth(storageKey) returning { read, store }, callable from both LobstrWallet and XBullWallet.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add xBull wallet support via the WalletAdapter interface

2 participants