wallet-adapter
wallet-adapter copied to clipboard
[rfc] Work with wallet provider ecosystem to improve reliability and performance of wallet detection
Sooner or later, the ability to determine who the user is and what they own finds itself on the critical path for any dApp to be able to deliver its core experience. For that reason, the performance and reliability of wallet detection and autoconnection is super important. In this RFC I would like to propose a different wallet readiness API than the one we have now, with the goal of being able to make wallet-bound decisions in dApps with 100% reliability and a minimum of delay.
Background
Today, most wallets announce themselves by adding variables to the global scope. For instance, when the Phantom extension boots, it adds the object window.phantom.solana to the global scope.
The situation today
The addition of an object to the global scope is not something that you can subscribe to. If dApps want to know whether Phantom is installed, they have to poll the global object looking for it.
Today, we do something like this in the Solana wallet adapter.
async function phantomIsInstalled(): Promise<boolean> {
// Is this even a browser?
if (typeof window === 'undefined' || typeof document === 'undefined') return false;
function check() {
return !!window.phantom?.solana;
}
// Is the page already loaded? Check now.
if (document.readyState === 'complete') return check();
// Wait until the page is loaded, then check.
return new Promise(resolve => {
function listener() {
window.removeEventListener('load', listener);
resolve(check());
}
window.addEventListener('load', listener);
});
}
Problem
Reliability vulnerabilities
- If a wallet doesn't install its hooks by the time the window's
loadevent has fired, we will miss it. Despite this being extremely difficult to reproduce, I have observed this happen in the wild, with Phantom.
Performance liabilities
First, take a look at this diagram by Kudzanayi (link):

- The
loadevent of a window fires after all image resources have loaded. In practice, this can delay our wallet readiness check by seconds, depending on what's on the page and how fast your network is; just imagine that a site has a big 25MB banner image, and you're on a slow connection. Simulate this by throttling your connection using the Chrome dev tools then running this playground.

Antiproposal
One way to narrow the gap between a wallet becoming available and a dApp being able to make use of it is to poll more aggressively (eg. by listening to DOMContentLoaded or by firing a check using requestIdleCallback. Aggressive polling like this will add competition for the JS work queue, which can regress startup performance overall. We also have to continue polling forever, or risk missing wallets that choose to install themselves after the window's load event.
Proposal
The proposal is to work with wallet makers to settle on a Google Analytics style readiness API. The idea is this.
Create an array-compatible API that you can use to register a readiness callback.
type WalletReadinessCallback<Wallet> = (wallet: Wallet) => void;
interface PhantomAPI {
push: (readinessCallback: WalletReadinessCallback<PhantomWallet>) => void;
/* ... */
}
Consumers of this API must take care to create an empty array if it doesn't already exist in place of the expected wallet API. After that, they can call push() on it.
window.phantom: PhantomAPI = (window.phantom || []).push(
(wallet) => {
// Code to run the moment the wallet becomes available.
},
);
- If the wallet loads after you do this then it will find an array of readiness callbacks waiting to be called. It saves the list of readiness callbacks, replaces the global with whatever it wants, then calls the readiness callbacks.
- If the wallet has loaded before you do this, then
pushwill be a wallet-supplied implementation that calls the readiness callback right away!
This API makes it impossible to miss the installation of a wallet and reduces the delay between a wallet becoming ready and your app being able to use it to near-zero.
Example implementation
Using this API, the Solana wallet adapter's readiness callback (and wallet detection system) for Phantom would simply become this:
async function isPhantomInstalled(): Promise<boolean> {
return new Promise((resolve) => {
(window.phantom || []).push(() => resolve(true));
});
}
This implementation would be unbreakable and would reduce the time between Phantom startup and wallet detection to near-zero.
Way forward
Thanks for reading! In order to achieve all of this, we'd need to reach out to wallet makers one by one, and then come up with a deprecation plan for the old polling-based implementation. I don't think there's a better time than the present to do this than now, since there are fewer Solana wallets at this moment than there will ever be :)
Please do poke at this RFC and let me know your thoughts.
- If a wallet doesn't install its hooks by the time the window's
loadevent has fired, we will miss it. Despite this being extremely difficult to reproduce, I have observed this happen in the wild, with Phantom.
Can you clarify "in the wild"? Until quite recently (https://github.com/solana-labs/wallet-adapter/pull/195), adapters were generally doing this by polling for 3 seconds which isn't great. Basically I'm wondering if this is an issue with the current code running in production or the old adapter code, which the overwhelming majority of apps are using.
- The
loadevent of a window fires after all image resources have loaded. In practice, this can delay our wallet readiness check by seconds
This definitely isn't great.
we'd need to reach out to wallet makers one by one, and then come up with a deprecation plan for the old polling-based implementation
This is doable for the most popular wallets that are actively maintained (Phantom, Slope) or open source (Sollet) even though it's not maintained. This may not be doable at all for some (Solong) that aren't maintained at all, but we could cover most users. We can also make it a requirement for new wallets to have their adapters added to this repo.
We also have to continue polling forever, or risk missing wallets that choose to install themselves after the window's
loadevent.
How likely is this and what might cause it?
Side question: if a browser extension is installed, does the window need to be reloaded for it to be discovered?
One way to narrow the gap between a wallet becoming available and a dApp being able to make use of it is to poll more aggressively (eg. by listening to DOMContentLoaded or by firing a check using setIdleTimeout. Aggressive polling like this will add competition for the JS work queue, which can regress startup performance overall.
Offhand the performance of this doesn't seem too bad but I could see it getting out of hand with a bunch of adapters loaded. The design we're working toward with #206 / #214 is asking apps to just use all the adapters and not think about it. It makes sense for us to consider how many wallets this scales to.
Can you clarify "in the wild"?
Yeah, for sure. I ran this a bunch of times, and once I saw it hit the ‘Phantom was not detected after all’ console log. I've never been able to reproduce that since.
In general, I like this proposal. Some thoughts:
- Pro: It's simpler on the adapter's side than what we have today.
- Pro: It allows for real-time detection at any point the wallet extension is ready, eliminating the need for polling.
- Con: It requires the extension wallet to write code specific for the adapter (checking if there's something in
window.phantombefore writing to it). This is not too big of an issue, but it's a disadvantage in the sense that today the adapters take care of "adapting" whatever the wallet's API is to a common interface, and the wallet remains agnostic of the adapter.
@steveluscher I believe there's a couple of bugs in the examples provided:
window.phantom: PhantomAPI = (window.phantom || []).push(/* ... */)
Should be:
window.phantom: PhantomAPI = window.phantom || []
window.phantom.push(/* ... */)
As Array.push() will return the new length of the array, and window.phantom would no longer reference the array or the PhantomAPI.
For the same reason, I think the example implementation should be:
async function isPhantomInstalled(): Promise<boolean> {
return new Promise((resolve) => {
window.phantom = window.phantom || []
window.phantom.push(() => resolve(true))
});
}
Closing here since this has been implemented in https://github.com/wallet-standard/wallet-standard