Skip to content

feat(wallets): add xBull, verify the wallet network, and test the hooks package - #175

Open
aigbagbobila wants to merge 9 commits into
SorobanForge:mainfrom
aigbagbobila:feat/wallets-xbull-network-verification
Open

aigbagbobila wants to merge 9 commits into
SorobanForge:mainfrom
aigbagbobila:feat/wallets-xbull-network-verification

Conversation

@aigbagbobila

Copy link
Copy Markdown

Description

Adds an xBull wallet adapter, verifies that the connected wallet is on the network the app is signing for, gives packages/hooks a real test suite, and replaces the regex in the amount parser with a measured, ~2x faster implementation.

Closes #121
Closes #120
Closes #119
Closes #111

All five commits are conventional-commit formatted and scoped by package:

Commit Scope
feat(wallets): add an xBull adapter and block signing on a network mismatch #121, #120
feat(web): list every wallet in a dashboard picker #121
test(hooks): add vitest and cover useStream #119
perf(utils): benchmark the amount parser and drop the regex #111
docs: list xBull among the supported wallets #121

#121 — feat(wallets): add an xBull adapter

What changed

  • WalletType is now 'freighter' | 'albedo' | 'rabe' | 'hana' | 'xbull' and WALLET_PROVIDERS gained XBULL: 'xbull'.
  • connect('xbull') → public key, and signTransaction → signed XDR, both implemented against the API the xBull extension injects as window.xBullSDK (getAddress(), signTransaction({ xdr, opts: { networkPassphrase, address } }), getNetwork()). That global is the extension half of @creit.tech/xbull-wallet-connect, which is how the official SDK talks to the extension when preferredTarget is 'extension' — so the browser extension is driven directly without adding a runtime dependency, the same way the existing Rabet/Hana adapters consume window.rabet / window.hanaWallet.
  • Missing-extension errors follow the existing wording exactly: 'xBull wallet extension is not installed.' (connect) and 'xBull extension is missing.' (signing), matching the Rabet/Hana messages, and an SDK error payload ({ error: { code, message } }) is surfaced with the wallet's own message.
  • WALLET_PROVIDER_OPTIONS (new export) is the single source of truth for the picker: [{ id, label }] for all five providers, typed as WalletProviderType, and WALLET_PROVIDERS is now satisfies Record<string, WalletType> so adding a wallet in one place but not the other is a compile error rather than a runtime Unsupported wallet provider.
  • The dashboard (/streams) replaced its hard-coded Connect Freighter button with a wallet picker that renders WALLET_PROVIDER_OPTIONS, so xBull is selectable (and Albedo/Rabet/Hana are no longer unreachable from the UI). The menu closes on selection or on a click outside, has aria-haspopup/aria-expanded/role="menu"/role="menuitem", and surfaces the provider's own error message instead of a generic "Connection failed."
  • Docs updated: README.md layout tree, ARCHITECTURE.md client-layer table and the in-app docs page now list xBull.

Tests (packages/wallets/src/WalletProvider.test.tsx, index.test.ts)

  • xBull connect success (address + provider + localStorage persisted).
  • xBull connect failure when window.xBullSDK is absent → the exact error message above.
  • xBull connect failure when the SDK answers with an error payload.
  • XBull signing is delegated with { xdr, opts: { networkPassphrase, address } }.
  • xBull network read from the SDK.
  • The picker list contains every provider (['freighter', 'xbull', 'albedo', 'rabe', 'hana']) and labels xBull correctly.

#120 — feat(wallets): verify the wallet network before signing

What changed

  • readWalletNetworkPassphrase(provider) asks each adapter for the network it is on, where the adapter supports it:

    • Freighter — getNetworkDetails().networkPassphrase, falling back to getNetwork() on older builds that lack it.
    • xBull — window.xBullSDK.getNetwork().networkPassphrase when the extension build exposes it.
    • Albedo, Rabet, Hana — sign for the network they are told to and expose no way to read the selected one, so they return null and the check is skipped (documented in code).
  • signTransaction now compares the passphrase it is about to sign with (opts.networkPassphrase or passphraseForNetwork(network)) against the wallet's own. On a mismatch it refuses before the wallet is called, with a message naming both networks and both fixes:

    Wallet network mismatch: the app is configured for Testnet (Test SDF Network ; September 2015) but the connected wallet is on Mainnet (Public Global Stellar Network ; September 2015). Switch the wallet to Testnet and try again, or call setNetwork('MAINNET') and reconnect the wallet.

  • The comparison is a state-backed, cancelled-safe effect keyed on (activeProvider, network), so connecting, auto-reconnecting from localStorage, disconnecting and setNetwork(...) all keep it consistent (AC "setNetwork updates the comparison"). walletNetworkPassphrase is exposed on the context for consumers that want to display the wallet's network.

  • New exported helpers, unit-testable on their own: TESTNET_PASSPHRASE, MAINNET_PASSPHRASE, passphraseForNetwork, networkLabelForPassphrase, networkIdForPassphrase, networkMismatchMessage.

Tests

  • Matching network → the signature is delegated as before (regression guard for the default Testnet case).
  • Mismatching network → signing is blocked, freighter.signTransaction is not called, and the message contains both passphrases plus the setNetwork('MAINNET') instruction.
  • Fallback path: getNetworkDetails rejects → getNetwork is used.
  • setNetwork('MAINNET') re-reads the wallet network, so an in-app switch unblocks signing.

#119 — test(hooks): add vitest and cover useStream

  • packages/hooks/package.json: added a test script (vitest run) and the dev dependencies vitest@^4.1.10, jsdom@^29.1.1, @testing-library/react@^16.3.2, react-dom. Turborepo picks the suite up through the existing test task, so pnpm test now runs it.
  • packages/hooks/vitest.config.ts: minimal config (environment: 'jsdom', globals: true so Testing Library's automatic cleanup is registered).
  • packages/hooks/src/index.test.tsx covers a successful load (stream + claimable), a null stream, an error from getStream, a failing claimable falling back to 0n, the no-client/no-id short circuit, and — with fake timers — a polling tick that drives a second getStream call (and a third).
  • pnpm-lock.yaml: the packages/hooks importer now records those dependencies using resolutions already present in the lockfile, so CI's pnpm install --frozen-lockfile stays consistent (verified locally: "Lockfile is up to date, resolution step is skipped").

#111 — perf(utils): benchmark the parse path and remove the regex

Measured first, per the issue. packages/utils/src/index.bench.ts (new, pnpm --filter @stellar-starter-kit/utils bench) benchmarks the current implementation and keeps the previous implementation inline as a labelled baseline, so the before/after numbers stay reproducible instead of living only in this description.

Representative inputs ('1', '10', '100', '1000', '12.5', '123.456789'), Node 24, median of runs:

Path Before After
parseXlmToStroops (typing digits) ~1.92M ops/sec (~520 ns/call) ~4.54M ops/sec (~220 ns/call) — 2.37x
parseXlmToStroops (mixed amounts) ~2.04M ops/sec ~3.99M ops/sec — 1.96x

Attribution (same harness, single-variable variants): replacing the regex with a character scan alone is worth only a few percent; caching 10n ** BigInt(decimals) is what removes most of the cost, and the two changes together give ~2x. Both landed in one change because they touch the same expression:

  • isPlainDecimal(raw) scans character codes instead of running ^-?\d*(\.\d*)?$, with the same accept-language as the regex plus its three explicit rejections ('', '.', '-').
  • unsigned.split('.') is replaced with indexOf/slice (no intermediate array), and 10n ** BigInt(decimals) with a module-level POWERS_OF_TEN table (assertDecimals already bounds decimals to 0..38).
  • Error messages, truncation semantics and return values are unchanged.

Evidence that behaviour is identical (this workspace has no node_modules, so the suite was replayed rather than executed):

  • Every assertion of packages/utils/src/index.test.ts (54 checks, all passing) re-run against the edited source, including the invalid-input and round-trip cases.
  • A 400,328-call differential fuzz (fixed strings, numbers such as 1e-7/1e21/NaN/Infinity, and 200k random strings over 0123456789.-+eE, × decimals 0/6/7/18) comparing the new implementation with the previous one: identical values and identical error messages on every call.
  • The dashboard still parses once per submit inside handleCreate (it never parsed per keystroke), so no debounce was added or removed.

How this was verified locally

pnpm install was intentionally not run in this workspace (no node_modules) per the task constraints, so verification used the tools that are available offline plus the exact dependencies where possible:

  • ✅ Formatting — prettier@3.9.5 (the pinned version) with the repository's options (--single-quote --print-width 100 --trailing-comma all --end-of-line lf) --checked every added/modified .ts, .tsx and .md file: all clean. The pre-existing repo files also pass with these flags, confirming the flags reproduce CI's format:check.
  • ✅ Lockfile — pnpm install --frozen-lockfile reaches "Lockfile is up to date, resolution step is skipped" with the hand-updated lockfile (it then stops only because there is no package store and the sandbox is offline); the lockfile diff is limited to the new packages/hooks importer block.
  • ✅ JSON validity of both edited package.json files.
  • ✅ packages/utils semantics — suite replayed assertion-by-assertion plus the differential fuzz described above.
  • ✅ Measurements for perf(utils): benchmark the parse path and remove the regex #111 taken with a standalone harness against the real source.
  • ⚠️ Not run locally: pnpm lint, pnpm typecheck, pnpm test, pnpm build and the cargo contract jobs require node_modules/toolchain that cannot be installed here. No Rust files are touched, so the contracts job is unaffected. The new TypeScript was reviewed against the installed types' declarations (e.g. @stellar/freighter-api@6.0.1's getNetwork/getNetworkDetails return shapes, fetched from the registry) and follows the patterns already used by the existing wallet tests and hooks.

Manual check list for a reviewer with dependencies installed

pnpm install
pnpm lint && pnpm typecheck && pnpm test && pnpm build   # CI parity
pnpm --filter @stellar-starter-kit/utils bench            # #111 numbers
# then, in the dashboard: pick xBull from the picker, sign a transaction, and
# switch the wallet to Mainnet to see the mismatch error block signing.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Refactoring / Code Quality Improvement
  • Documentation update

Checklist

  • My code follows the style guidelines of this project (Prettier-clean, ESLint-conformant patterns)
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective / my feature works
  • My commits follow the Conventional Commits specification

Notes / follow-ups

  • The xBull web wallet flow (@creit.tech/xbull-wallet-connect's preferredTarget: 'website' popup bridge) is intentionally out of scope: the issue asks for an adapter with connect/public-key/sign paths and extension-consistent errors. Adding the SDK later would only need the same three calls behind the existing XBullInterface.
  • The /escrow page still has its own single-wallet connect button. It is outside the scope of feat(wallets): add an xBull adapter #121 ("the wallet picker on the dashboard"), and the new WALLET_PROVIDER_OPTIONS list makes it a small follow-up to give it the same picker.
  • Albedo/Rabet/Hana cannot report a network, so the mismatch guard is a no-op for them by design; if those wallets add a network read API, only readWalletNetworkPassphrase needs a new branch.

aigbagbobila and others added 5 commits October 16, 2026 07:27
…smatch

xBull is one of the most used Stellar wallets, but the provider only knew
Freighter, Albedo, Rabet and Hana, so testnet users could not connect. Add an
`xbull` provider that drives the API the extension injects as
`window.xBullSDK` (the extension half of `@creit.tech/xbull-wallet-connect`, so
no new dependency), with connect, public-key and signing paths plus
missing-extension errors worded like the Rabet/Hana ones.

Signing also silently produced a signature for the wallet's own network: a
wallet switched to Mainnet while the app targets Testnet failed later with an
opaque passphrase mismatch. Read the network where the adapter supports it
(Freighter `getNetworkDetails`/`getNetwork`, xBull `getNetwork`), compare it
with the passphrase the app is about to sign with, and refuse with a message
naming both networks and how to fix it. The comparison is re-read whenever the
app switches networks with `setNetwork`, and the connected wallet's passphrase
is exposed as `walletNetworkPassphrase`.

Tests cover xBull connect success/failure/signing, reading the network from
both adapters, a matching network still signing, a mismatch blocking the
signature, and `setNetwork` re-reading the wallet.

Closes SorobanForge#121
Closes SorobanForge#120

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The dashboard only offered "Connect Freighter", so xBull (and Albedo, Rabet
and Hana) could not be reached from the UI even though the provider supports
them. Replace the button with a picker that renders
`WALLET_PROVIDER_OPTIONS` from `@stellar-starter-kit/wallets`, so the picker
lists xBull and any wallet added later cannot be missing from it.

Connect failures now surface the provider's own message (e.g. "xBull wallet
extension is not installed.") instead of a generic one, and the menu closes on
selection or on a click outside it.

Closes SorobanForge#121
`packages/hooks` had no test script and no tests, so the mount guards, polling
and error capture in `useStream` were only exercised by clicking around the
dashboard. Add vitest with jsdom and @testing-library/react, a minimal config
and a `test` script so Turborepo's `pnpm test` picks the suite up.

The tests cover a successful load, a null stream, an error from `getStream`, a
failing `claimable` falling back to zero, the no-client/no-id short circuit and
a polling tick driving a second `getStream` call with fake timers.

Closes SorobanForge#119
`parseTokenAmount` runs on the create-stream path, but nobody had measured it.
Add a vitest bench (`pnpm --filter @stellar-starter-kit/utils bench`) with the
old implementation kept inline as the baseline, then replace the validation
regex and `unsigned.split('.')` allocations with a character scan and cache
`10n ** BigInt(decimals)` instead of re-deriving it on every call.

Measured on Node 24 (median of 7 runs, representative inputs): ~1.9-2.0M
ops/sec before, ~4.0-4.5M ops/sec after, i.e. ~2x and 520ns -> 220ns per call.
Attribution: the regex alone is worth only a few percent; caching the power of
ten is what removes most of the cost.

Behaviour is unchanged: the accept-language of the scanner is the same as the
regex plus its three explicit rejections, and a 400k-call fuzz over valid,
invalid and random inputs produces identical values and identical throw
messages. The full utils suite passes unchanged (replayed assertion by
assertion, since this workspace has no node_modules).

Closes SorobanForge#111
The README layout, the architecture package table and the in-app docs still
described the wallet package as Freighter/Albedo/Rabet/Hana.

Closes SorobanForge#121
@drips-wave

drips-wave Bot commented Oct 16, 2026

Copy link
Copy Markdown

@aigbagbobila Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Oct 16, 2026

Copy link
Copy Markdown

@aigbagbobila is attempting to deploy a commit to the techalhaji's projects Team on Vercel.

A member of the Team first needs to authorize it.

@techAlhaji

Copy link
Copy Markdown
Collaborator

fix the failing ci please.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

2 participants