Files
FrenoCorp/node_modules/@solana/wallet-adapter-base/src/adapter.ts
Michael Freno 7c684a42cc FRE-600: Fix code review blockers
- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 00:08:01 -04:00

194 lines
6.9 KiB
TypeScript

import type { Connection, PublicKey, SendOptions, Signer, Transaction, TransactionSignature } from '@solana/web3.js';
import EventEmitter from 'eventemitter3';
import { type WalletError, WalletNotConnectedError } from './errors.js';
import type { SupportedTransactionVersions, TransactionOrVersionedTransaction } from './transaction.js';
export { EventEmitter };
export interface WalletAdapterEvents {
connect(publicKey: PublicKey): void;
disconnect(): void;
error(error: WalletError): void;
readyStateChange(readyState: WalletReadyState): void;
}
export interface SendTransactionOptions extends SendOptions {
signers?: Signer[];
}
// WalletName is a nominal type that wallet adapters should use, e.g. `'MyCryptoWallet' as WalletName<'MyCryptoWallet'>`
// https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d
export type WalletName<T extends string = string> = T & { __brand__: 'WalletName' };
export interface WalletAdapterProps<Name extends string = string> {
name: WalletName<Name>;
url: string;
icon: string;
readyState: WalletReadyState;
publicKey: PublicKey | null;
connecting: boolean;
connected: boolean;
supportedTransactionVersions?: SupportedTransactionVersions;
autoConnect(): Promise<void>;
connect(): Promise<void>;
disconnect(): Promise<void>;
sendTransaction(
transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
connection: Connection,
options?: SendTransactionOptions
): Promise<TransactionSignature>;
}
export type WalletAdapter<Name extends string = string> = WalletAdapterProps<Name> & EventEmitter<WalletAdapterEvents>;
/**
* A wallet's readiness describes a series of states that the wallet can be in,
* depending on what kind of wallet it is. An installable wallet (eg. a browser
* extension like Phantom) might be `Installed` if we've found the Phantom API
* in the global scope, or `NotDetected` otherwise. A loadable, zero-install
* runtime (eg. Torus Wallet) might simply signal that it's `Loadable`. Use this
* metadata to personalize the wallet list for each user (eg. to show their
* installed wallets first).
*/
export enum WalletReadyState {
/**
* User-installable wallets can typically be detected by scanning for an API
* that they've injected into the global context. If such an API is present,
* we consider the wallet to have been installed.
*/
Installed = 'Installed',
NotDetected = 'NotDetected',
/**
* Loadable wallets are always available to you. Since you can load them at
* any time, it's meaningless to say that they have been detected.
*/
Loadable = 'Loadable',
/**
* If a wallet is not supported on a given platform (eg. server-rendering, or
* mobile) then it will stay in the `Unsupported` state.
*/
Unsupported = 'Unsupported',
}
export abstract class BaseWalletAdapter<Name extends string = string>
extends EventEmitter<WalletAdapterEvents>
implements WalletAdapter<Name>
{
abstract name: WalletName<Name>;
abstract url: string;
abstract icon: string;
abstract readyState: WalletReadyState;
abstract publicKey: PublicKey | null;
abstract connecting: boolean;
abstract supportedTransactionVersions?: SupportedTransactionVersions;
get connected() {
return !!this.publicKey;
}
async autoConnect() {
await this.connect();
}
abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;
abstract sendTransaction(
transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
connection: Connection,
options?: SendTransactionOptions
): Promise<TransactionSignature>;
protected async prepareTransaction(
transaction: Transaction,
connection: Connection,
options: SendOptions = {}
): Promise<Transaction> {
const publicKey = this.publicKey;
if (!publicKey) throw new WalletNotConnectedError();
transaction.feePayer = transaction.feePayer || publicKey;
transaction.recentBlockhash =
transaction.recentBlockhash ||
(
await connection.getLatestBlockhash({
commitment: options.preflightCommitment,
minContextSlot: options.minContextSlot,
})
).blockhash;
return transaction;
}
}
export function scopePollingDetectionStrategy(detect: () => boolean): void {
// Early return when server-side rendering
if (typeof window === 'undefined' || typeof document === 'undefined') return;
const disposers: (() => void)[] = [];
function detectAndDispose() {
const detected = detect();
if (detected) {
for (const dispose of disposers) {
dispose();
}
}
}
// Strategy #1: Try detecting every second.
const interval =
// TODO: #334 Replace with idle callback strategy.
setInterval(detectAndDispose, 1000);
disposers.push(() => clearInterval(interval));
// Strategy #2: Detect as soon as the DOM becomes 'ready'/'interactive'.
if (
// Implies that `DOMContentLoaded` has not yet fired.
document.readyState === 'loading'
) {
document.addEventListener('DOMContentLoaded', detectAndDispose, { once: true });
disposers.push(() => document.removeEventListener('DOMContentLoaded', detectAndDispose));
}
// Strategy #3: Detect after the `window` has fully loaded.
if (
// If the `complete` state has been reached, we're too late.
document.readyState !== 'complete'
) {
window.addEventListener('load', detectAndDispose, { once: true });
disposers.push(() => window.removeEventListener('load', detectAndDispose));
}
// Strategy #4: Detect synchronously, now.
detectAndDispose();
}
/**
* Users on iOS can be redirected into a wallet's in-app browser automatically,
* if that wallet has a universal link configured to do so
* But should not be redirected from within a webview, eg. if they're already
* inside a wallet's browser
* This function can be used to identify users who are on iOS and can be redirected
*
* @returns true if the user can be redirected
*/
export function isIosAndRedirectable() {
// SSR: return false
if (!navigator) return false;
const userAgent = navigator.userAgent.toLowerCase();
// if on iOS the user agent will contain either iPhone or iPad
// caveat: if requesting desktop site then this won't work
const isIos = userAgent.includes('iphone') || userAgent.includes('ipad');
// if in a webview then it will not include Safari
// note that other iOS browsers also include Safari
// so we will redirect only if Safari is also included
const isSafari = userAgent.includes('safari');
return isIos && isSafari;
}