feat: Implement AlbedoWallet adapter - #674
Open
OG-wura wants to merge 1 commit into
Open
Conversation
|
@OG-wura 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! 🚀 |
|
@OG-wura is attempting to deploy a commit to the Collins' projects Team on Vercel. A member of the Team first needs to authorize it. |
OG-wura
force-pushed
the
Implement_AlbedoWallet
branch
from
August 31, 2026 10:09
2a776fd to
40c6703
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Close #612
PR: feat(web): implement AlbedoWallet adapter
Summary
Implements
AlbedoWallet(apps/web/src/lib/wallet.ts:256-303) as the fourthWalletAdapterimplementation alongsideFreighterWallet(apps/web/src/lib/wallet.ts:106-151) andLobstrWallet(apps/web/src/lib/wallet.ts:180-228). Albedo is a web-based Stellar signer atalbedo.linkthat authorizes via a popup/intent flow — no browser extension or app install required. This satisfies the same interface used by all call sites, so no wallet-selection UI or caller changes are needed in this PR.Motivation
From
README.mdand roadmap:Albedo's intent flow (
@albedo-link/intent→postMessagepopup atalbedo.link) needs no install at all — usable from any mobile browser. It is categorically different from extension wallets, not just another installable option. Combined with the existing adapters it gives a genuinely frictionless fallback for the target user.Scope / Non-Goals
In scope
AlbedoWalletimplementingWalletAdapter(apps/web/src/lib/wallet.ts:97-104):E2E-mock short-circuit parity (
withMockWallet,apps/web/src/lib/wallet.ts:83-90) so Playwright fixtures (apps/web/e2e/fixtures.ts) can exercise the adapter without a real popup.Unit tests mirroring
wallet.test.ts/lobstr-wallet.test.ts.Dependency addition
@albedo-link/intent@0.13.0.Out of scope
walletexport remainsFreighterWallet(apps/web/src/lib/wallet.ts:306).isImplicitSessionAllowed— not needed forconnect/sign.Changes
1. Dependency
apps/web/package.json:18pnpm-lock.yamlupdated (integritysha512-A8CBXqG...). Verified package surfacespublicKey/txintents andsrc/index.d.tstypes.2. Core implementation —
apps/web/src/lib/wallet.tsa) Dynamic Albedo loader (
apps/web/src/lib/wallet.ts:13-53)Why dynamic import?
@albedo-link/intentshipslib/albedo.intent.js(UMD) asmainandsrc/index.js(ESM) asmodule. In Vitestjsdomthe UMD wrapper executesthisasundefinedin ESM strict mode:A static
import albedo from "@albedo-link/intent"evaluates at module load and breaks even tests that never touch Albedo (e.g.,lobstr-wallet.test.ts,VaultPanel.test.tsx). Moving toawait importdefers evaluation toconnect()/sign()and allowsvi.mock("@albedo-link/intent")to intercept. The loader catches load failure and returns a stub that throwsAlbedo not available, so non-browser/SSR contexts fail gracefully.The implementation intentionally does not cache the resolved module across Vitest runs — caching would pin the first mocked
vi.fn()identity and leak state between suites thatmockResolvedValuedifferently.__resetAlbedoForTestsis retained as a no-op for backward compatibility with tests that call it explicitly.b) Session-scoped public-key storage (
apps/web/src/lib/wallet.ts:237-247)Mirrors LOBSTR pattern (
apps/web/src/lib/wallet.ts:158-168, keymeridian-lobstr-public-key).sessionStorage(notlocalStorage) means a returning tab re-validates via a freshconnect()popup instead of trusting a stale key.c) Network mapping (
apps/web/src/lib/wallet.ts:249-254)Callers (
useSignAndSubmit.ts:18) pass the full passphrase (STELLAR_NETWORKS[network].passphrase). AlbedoTxIntentParams.network(@albedo-link/intent/src/index.d.ts:60-68) accepts eitherpublic/testnetidentifiers or a private passphrase. Mapping the two known passphrases to Albedo's short identifiers matches Albedo playground/docs examples (albedo.tx({xdr, network: "testnet", submit:false})) and ensures the popup displays the correct network label. Unknown/custom passphrases are forwarded verbatim (supports private networks). Generic test literal"passphrase"is also forwarded verbatim, so existing-style tests (wallet.test.ts:65uses"passphrase") remain green.d)
AlbedoWalletclass (apps/web/src/lib/wallet.ts:256-303)Details per method:
isInstalled()(wallet.ts:257-259): Alwaystruein a browser (no extension to detect). Short-circuits tomock.installedwhenwindow.__E2E_MOCK_WALLET__is present (wallet.ts:73-75, injected byapps/web/e2e/fixtures.ts:48-60). Does not call any Albedo API — the popup is the install check.isAuthorized()(wallet.ts:266-275): No passive Albedo permission query exists without opening the popup (publicKey()prompts). Same rationale as LOBSTR (wallet.ts:185-201comment: would pop grant-access dialog on every tab focus viastore/wallet.ts:revalidate()). Treatsinstalled && readStoredAlbedoPublicKey() !== nullas authorized. Never callsalbedo.publicKey.connect()(wallet.ts:277-288): Delegates toalbedo.publicKey({})(@albedo-link/intent/src/index.js:publicKey). Internally generates a randomtokenif none is supplied. ThrowsAlbedo wallet returned no public keyifpubkeyis falsy (matches LOBSTR error shape,wallet.ts:209). On success stores the key viastoreAlbedoPublicKeyfor subsequentisAuthorized()checks.sign()(wallet.ts:290-302): Callsalbedo.tx({xdr, network: albedoNetworkFromPassphrase(passphrase), submit:false})(@albedo-link/intent/src/index.js:tx).submit:falseis explicit — without it Albedo would POST the envelope to Horizon itself; Meridian must return the signed XDR forapi.submitTx(useSignAndSubmit.ts:19). ThrowsSigning cancelledifsigned_envelope_xdris falsy, matching Freighter (wallet.ts:146) and LOBSTR (wallet.ts:223) semantics. Propagates Albedo rejection (user closes popup) as-is.3. Tests —
apps/web/src/__tests__/lib/albedo-wallet.test.ts(new, 187 lines)Created from scratch, structurally identical to
lobstr-wallet.test.ts:1-187to satisfy the acceptance criterion "Unit tests covering the real Albedo path and the e2e-mock path, mirroring wallet.test.ts's existing Freighter/LOBSTR coverage".Setup:
Real-path suite (13 tests):
isInstalledalways true, never calls Albedo APIisAuthorizedtrue only whenmeridian-albedo-public-keypresent, never callspublicKeyconnectreturnspubkey, persists to sessionStorage, revalidated by nextisAuthorized; throws on""/ propagates rejection; assertspublicKeycalled with{}signreturnssigned_envelope_xdr; asserts exact forwarding:testnetpassphrase →network:"testnet",public→"public", unknown"passphrase"→ passthrough; asserts{xdr, network, submit:false}shape; throws on""/ propagates rejectionE2E-mock suite (6 tests):
isInstalled/isAuthorized/connect/signall short-circuit towindow.__E2E_MOCK_WALLET__and never callalbedo.publicKey/albedo.txmock.signreceives(xdr, networkPassphrase)and rejection propagation (decline)The vi mock shape (
default: {publicKey, tx}) matches both the dynamicawait importpath (mod.default ?? mod) and the staticimport albedo fromused in the test's own assertions. BecausegetAlbedonow re-imports on each call, eachmockResolvedValueupdate is observed without stale caching.How to verify
Manual:
window.__E2E_MOCK_WALLET__ = {installed:true, authorized:true, address:"G...", sign: async xdr => "MOCK_SIGNED:"+xdr}before load (ase2e/fixtures.ts:42-60does) and instantiatenew AlbedoWallet()—isInstalled/isAuthorized/connect/signall resolve from the mock without opening a popup.new AlbedoWallet().isInstalled()→true;isAuthorized()→falseuntilconnect()succeeds (which opensalbedo.linkpopup);sign("XDR","Test SDF Network ; September 2015")opens the Albedo signing popup withnetwork:"testnet"inspected in the iframepostMessage.Design decisions & trade-offs
isInstalledalwaystruewindow.openreachabletypeof window !== "undefined"guard would add SSRfalsebut is not required for tests (jsdom haswindow) and would diverge from the spec's "likely always true".isAuthorized=storedKey !== nullalbedo.publicKeyorisImplicitSessionAllowedpublicKeyprompts;isImplicitSessionAllowed(intent, pubkey)requires a knownpubkey+implicit_flowgrant that Meridian does not request. LOBSTR's proven sessionStorage pattern avoids prompts on everyrevalidate()(store/wallet.ts runs on mount + window focus).publicKey({})with empty objectpublicKey({token})explicitgenerateRandomToken()iftokenmissing (intent/src/index.js:publicKey). Passing{}keeps the call minimal and test-assertable.tx({xdr, network, submit:false}){xdr}only or{xdr, network: passphrase}networkis required so Albedo knows which passphrase to bind the signature to; mapping ensures the UI shows "testnet"/"public" rather than a raw passphrase.submit:falseprevents Albedo from submitting to Horizon — Meridian submits viaPOST /api/v1/tx/submit(useSignAndSubmit.ts:19) after simulation + footprint.importimport albedo from+vitest.configaliasRisks / Follow-ups
walletsingleton staysFreighterWallet(wallet.ts:306). A futurewallet-pickerwill instantiate the correct adapter based onnavigator.userAgent/ stored preference.isAuthorizedsignal,AlbedoWallet.isAuthorizedcould delegate toisImplicitSessionAllowed("tx", storedKey).getAlbedofallback throwsAlbedo not availableifimportfails (e.g., SSR withoutwindow.fetch). Acceptable; wallet code only runs client-side.await import("@albedo-link/intent")creates a separate Vite chunk in production; verified viavite build(2065 modules, singleindex-*.js@ 2.3MB includes the chunk — Albedo is small, not worth manual chunking yet).Checklist
AlbedoWalletimplementsWalletAdapter(isInstalled,isAuthorized,connect,sign) —apps/web/src/lib/wallet.ts:256-303withMockWallet) — same helper asFreighterWallet/LobstrWalletlobstr-wallet.test.ts—apps/web/src/__tests__/lib/albedo-wallet.test.ts(13+6 tests)pnpm --filter @meridian/web lint— passpnpm --filter @meridian/web typecheck— passpnpm --filter @meridian/web test— 17/17 files, 110/110 tests