- 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>
194 lines
6.9 KiB
TypeScript
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;
|
|
}
|