Hey everyone! While building Apogee, Resolvr’s self-custodial Liquid browser wallet, I’ve been experimenting with how web apps should discover and communicate with Liquid wallets.
The draft Liquid Wallet RPC Profile defines common operations such as requesting balances, retrieving wallet descriptors, and signing PSETs. However, it deliberately leaves the transport unspecified. A web app still needs answers to questions such as:
-
How does it discover an installed wallet?
-
What happens when several wallets are installed?
-
How does it request permission to access an account?
-
How are RPC requests, errors, and wallet events exposed to JavaScript?
Apogee now implements one possible design, which I’ll describe below. I’d like to get feedback from folks here, and eventually work toward consensus on a browser wallet transport spec/ELIP.
The singleton-provider problem
The simplest browser wallet integration is for an extension to inject a provider at a well-known location:
window.liquid = provider;
This is simple and convenient when only one wallet exists. It becomes problematic when multiple extensions attempt to use the same property. One wallet may overwrite another based entirely on extension initialization order. Apps can’t enumerate the available wallets or let the user choose between them.
Ethereum encountered this problem as its browser wallet ecosystem grew around window.ethereum. EIP-6963 even points out how this played out over time: “Some browser extensions attempt to counteract [the window.ethereum stomping] problem by delaying their injection to overwrite the same window.ethereum object”. That EIP introduced an event-based discovery mechanism so multiple wallet providers could coexist without competing for a global singleton. Liquid has an opportunity to learn from that experience before a particular global becomes the de facto discovery standard.
Discovering wallets through events
Apogee currently uses two window events:
-
liquid:requestProvider, dispatched by the app -
liquid:announceProvider, dispatched by each available wallet
A minimal app can discover wallets like this:
const providers = new Map();
window.addEventListener("liquid:announceProvider", (event) => {
const { info, provider } = event.detail;
providers.set(info.uuid, { info, provider });
});
window.dispatchEvent(new Event("liquid:requestProvider"));
Each announcement contains some display information such as a name, icon, and reverse-DNS identifier alongside the provider object. The web app can display all announced wallets and let the user select one. Wallets can also announce themselves when they become ready. The request event handles the opposite initialization order: when the app loads after the wallet, it can ask already-running wallets to announce themselves again. No wallet needs to claim window.liquid, and the order in which extensions load doesn’t select a winner.
A small provider interface
Once an app selects a provider, Apogee exposes two main operations:
provider.request({ method, params });
provider.on({ event, listener });
For example:
const connection = await provider.request({
method: "wallet_connect",
params: {
methods: ["getBalance", "signPset"],
},
});
const balance = await provider.request({
method: "getBalance",
params: {},
});
const unsubscribe = provider.on({
event: "wallet_connectionChanged",
listener: (connection) => {
console.log("Connection changed:", connection);
},
});
This is influenced by the small provider interface in EIP-1193, but it invokes the methods from the Liquid Wallet RPC Profile rather than Ethereum RPC methods.
A successful request resolves directly to the method’s result. A failed request rejects with a structured error. The app does not create JSON-RPC identifiers or process JSON-RPC response envelopes. A wallet may use JSON-RPC internally, but that’s below the app-facing interface.
Discovery is not authorization
Provider discovery reveals only wallet/provider metadata, not account, chain, balance, or connection state.
Before invoking account-scoped methods, an app calls wallet_connect and requests permission for the methods and events it intends to use. The wallet and user can then select an account and chain and decide which requested capabilities to grant.
A permission grant means that the app may request an operation. It does not necessarily mean that every future operation happens without review.
The interface does not prescribe exactly when a wallet displays a prompt. A wallet might require explicit approval for every signature, apply an existing user policy, interact with a hardware signer, or complete some read requests automatically. It must still honor any authorization or review requirements imposed by the underlying RPC method.
From the app’s perspective, the request Promise remains pending until the wallet returns a result, structured error, or timeout. My current inclination is not to expose whether the wallet is waiting for the user, a hardware device, network synchronization, or something else. The app can display a generic “Waiting for wallet…” state, while the wallet provides the authoritative interaction UI. I’m interested in whether people think that’s sufficient or whether a small, privacy-conscious progress mechanism would be useful.
Experimental TX Manifest support
Apogee is also using this provider interface for experimental TX Manifest support.
TX Manifest exposes a portable way to describe interactions with Simplicity apps without requiring every wallet to contain bespoke integration code for every contract. In Apogee, support detection and manifest execution are exposed as experimental provider methods.
This is separate from the proposed browser interface: wallets should not need to implement TX Manifest in order to participate in provider discovery. It is nevertheless a useful example of why a generic, capability-negotiated provider is valuable. New wallet capabilities can be introduced without creating another global object or an entirely separate app-to-wallet bridge.
Where I’d like feedback
This is currently an Apogee experiment, not a finished standard. In particular, I’d love to hear any thoughts on:
-
Is a request/announce event pair the right foundation for multi-wallet discovery on Liquid?
-
Are
liquid:requestProviderandliquid:announceProviderappropriate names, or is there an existing browser wallet convention we should align with more closely? -
Is the minimal
requestandonprovider surface sufficient for apps and other wallet implementations? -
Does an origin-bound connection with explicitly requested method and event permissions provide the right boundary between discovery and account access?
-
Should request progress remain an implementation detail, or do apps need a standardized way to distinguish user approval, hardware interaction, and background processing?
-
Are there browser environments or wallet architectures that would be difficult to support through this interface?
My hope is to use any feedback to move toward an eventual Liquid web wallet transport spec/ELIP.