UNPKG

@svelte-on-solana/wallet-adapter-core

Version:

The core of the wallet adapter is a Svelte Store which exposes methods and properties to run the wallet in your application. This allows to share this data among all components in your application.

362 lines (304 loc) 11.9 kB
import type { Adapter, MessageSignerWalletAdapter, MessageSignerWalletAdapterProps, SendTransactionOptions, SignerWalletAdapter, SignerWalletAdapterProps, WalletError, WalletName, } from '@solana/wallet-adapter-base'; import { WalletNotConnectedError, WalletNotReadyError, WalletReadyState } from '@solana/wallet-adapter-base'; import type { Connection, PublicKey, Transaction, TransactionSignature, VersionedTransaction } from '@solana/web3.js'; import { get, writable } from 'svelte/store'; import { WalletNotSelectedError } from './errors'; import { getLocalStorage, setLocalStorage } from './localStorage'; interface Wallet { adapter: Adapter; readyState: WalletReadyState; } type ErrorHandler = (error: WalletError) => void; type WalletPropsConfig = Pick<WalletStore, 'autoConnect' | 'localStorageKey' | 'onError'> & { wallets: Adapter[]}; type WalletReturnConfig = Pick<WalletStore, 'wallets' | 'autoConnect' | 'localStorageKey' | 'onError'>; type WalletStatus = Pick<WalletStore, 'connected' | 'publicKey'>; export interface WalletStore { // props autoConnect: boolean; wallets: Wallet[]; // wallet state adapter: Adapter | null; connected: boolean; connecting: boolean; disconnecting: boolean; localStorageKey: string; onError: ErrorHandler; publicKey: PublicKey | null; ready: WalletReadyState; wallet: Adapter | null; walletsByName: Record<WalletName, Adapter>; name: WalletName | null; // wallet methods connect(): Promise<void>; disconnect(): Promise<void>; select(walletName: WalletName): void; sendTransaction( transaction: Transaction | VersionedTransaction, connection: Connection, options?: SendTransactionOptions ): Promise<TransactionSignature>; signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined; signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined; signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined; } export const walletStore = createWalletStore(); function addAdapterEventListeners(adapter: Adapter) { const { onError, wallets } = get(walletStore); wallets.forEach(({adapter}) => { adapter.on('readyStateChange', onReadyStateChange, adapter); }) adapter.on('connect', onConnect); adapter.on('disconnect', onDisconnect); adapter.on('error', onError); } async function autoConnect() { const { adapter } = get(walletStore); try { walletStore.setConnecting(true); await adapter?.connect(); } catch (error: unknown) { // Clear the selected wallet walletStore.resetWallet(); // Don't throw error, but onError will still be called } finally { walletStore.setConnecting(false); } } async function connect(): Promise<void> { const { connected, connecting, disconnecting, ready, adapter } = get(walletStore); if (connected || connecting || disconnecting) return; if (!adapter) throw newError(new WalletNotSelectedError()); if (!(ready === WalletReadyState.Installed || ready === WalletReadyState.Loadable)) { walletStore.resetWallet(); if (typeof window !== 'undefined') { window.open(adapter.url, '_blank'); } throw newError(new WalletNotReadyError()); } try { walletStore.setConnecting(true); await adapter.connect(); } catch (error: unknown) { walletStore.resetWallet(); throw error; } finally { walletStore.setConnecting(false); } } function createWalletStore() { const { subscribe, update } = writable<WalletStore>({ autoConnect: false, wallets: [], adapter: null, connected: false, connecting: false, disconnecting: false, localStorageKey: 'walletAdapter', onError: (error: WalletError) => console.error(error), publicKey: null, ready: 'Unsupported' as WalletReadyState, wallet: null, name: null, walletsByName: {}, connect, disconnect, select, sendTransaction, signTransaction: undefined, signAllTransactions: undefined, signMessage: undefined, }); function updateWalletState(adapter: Adapter | null) { updateAdapter(adapter); update((store: WalletStore) => ({ ...store, name: adapter?.name || null, wallet: adapter, ready: adapter?.readyState || 'Unsupported' as WalletReadyState, publicKey: adapter?.publicKey || null, connected: adapter?.connected || false, })); if (!adapter) return; if (shouldAutoConnect()) { autoConnect(); } } function updateWalletName(name: WalletName | null) { const { localStorageKey, walletsByName } = get(walletStore); const adapter = walletsByName?.[name as WalletName] ?? null; setLocalStorage(localStorageKey, name); updateWalletState(adapter); } function updateAdapter(adapter: Adapter | null) { removeAdapterEventListeners(); let signTransaction: SignerWalletAdapter['signTransaction'] | undefined = undefined; let signAllTransactions: SignerWalletAdapter['signAllTransactions'] | undefined = undefined; let signMessage: MessageSignerWalletAdapter['signMessage'] | undefined = undefined; if (adapter) { // Sign a transaction if the wallet supports it if ('signTransaction' in adapter) { signTransaction = async function <T extends Transaction | VersionedTransaction>(transaction: T) { const { connected } = get(walletStore); if (!connected) throw newError(new WalletNotConnectedError()); return await adapter.signTransaction(transaction); }; } // Sign multiple transactions if the wallet supports it if ('signAllTransactions' in adapter) { signAllTransactions = async function <T extends Transaction | VersionedTransaction>(transactions: T[]) { const { connected } = get(walletStore); if (!connected) throw newError(new WalletNotConnectedError()); return await adapter.signAllTransactions(transactions); }; } // Sign an arbitrary message if the wallet supports it if ('signMessage' in adapter) { signMessage = async function (message: Uint8Array) { const { connected } = get(walletStore); if (!connected) throw newError(new WalletNotConnectedError()); return await adapter.signMessage(message); }; } addAdapterEventListeners(adapter); } update((store: WalletStore) => ({ ...store, adapter, signTransaction, signAllTransactions, signMessage })); } return { resetWallet: () => updateWalletName(null), setConnecting: (connecting: boolean) => update((store: WalletStore) => ({ ...store, connecting })), setDisconnecting: (disconnecting: boolean) => update((store: WalletStore) => ({ ...store, disconnecting })), setReady: (ready: WalletReadyState) => update((store: WalletStore) => ({ ...store, ready })), subscribe, updateConfig: (walletConfig: WalletReturnConfig & { walletsByName: Record<WalletName, Adapter> }) => update((store: WalletStore) => ({ ...store, ...walletConfig, })), updateWallets: (wallets: Wallet[]) => update((store: WalletStore) => ({ ...store, ...wallets })), updateStatus: (walletStatus: WalletStatus) => update((store: WalletStore) => ({ ...store, ...walletStatus })), updateWallet: (walletName: WalletName) => updateWalletName(walletName), }; } async function disconnect(): Promise<void> { const { disconnecting, adapter } = get(walletStore); if (disconnecting) return; if (!adapter) return walletStore.resetWallet(); try { walletStore.setDisconnecting(true); await adapter.disconnect(); } finally { walletStore.resetWallet(); walletStore.setDisconnecting(false); } } export async function initialize({ wallets, autoConnect = false, localStorageKey = 'walletAdapter', onError = (error: WalletError) => console.error(error), }: WalletPropsConfig): Promise<void> { const walletsByName = wallets.reduce<Record<WalletName, Adapter>>((walletsByName, wallet) => { walletsByName[wallet.name] = wallet; return walletsByName; }, {}); // Wrap adapters to conform to the `Wallet` interface const mapWallets = wallets.map((adapter) => ({ adapter, readyState: adapter.readyState, })) walletStore.updateConfig({ wallets: mapWallets, walletsByName, autoConnect, localStorageKey, onError, }); const walletName = getLocalStorage<WalletName>(localStorageKey); if (walletName) { walletStore.updateWallet(walletName); } } function newError(error: WalletError): WalletError { const { onError } = get(walletStore); onError(error); return error; } function onConnect() { const { adapter } = get(walletStore); if (!adapter) return; walletStore.updateStatus({ publicKey: adapter.publicKey, connected: adapter.connected, }); } function onDisconnect() { walletStore.resetWallet(); } function onReadyStateChange(this: Adapter, readyState: WalletReadyState) { const { adapter, wallets } = get(walletStore); if (!adapter) return; walletStore.setReady(adapter.readyState); // When the wallets change, start to listen for changes to their `readyState` const walletIndex = wallets.findIndex(({ adapter }) => adapter.name === this.name); if (walletIndex === -1) { return; } else { walletStore.updateWallets([ ...wallets.slice(0, walletIndex), { ...wallets[walletIndex], readyState }, ...wallets.slice(walletIndex + 1), ]) } } function removeAdapterEventListeners(): void { const { adapter, onError, wallets } = get(walletStore); if (!adapter) return; wallets.forEach(({adapter}) => { adapter.off('readyStateChange', onReadyStateChange, adapter); }) adapter.off('connect', onConnect); adapter.off('disconnect', onDisconnect); adapter.off('error', onError); } async function select(walletName: WalletName): Promise<void> { const { name, adapter } = get(walletStore); if (name === walletName) return; if (adapter) await disconnect(); walletStore.updateWallet(walletName); } async function sendTransaction( transaction: Transaction | VersionedTransaction, connection: Connection, options?: SendTransactionOptions ): Promise<TransactionSignature> { const { connected, adapter } = get(walletStore); if (!connected) throw newError(new WalletNotConnectedError()); if (!adapter) throw newError(new WalletNotSelectedError()); return await adapter.sendTransaction(transaction, connection, options); } function shouldAutoConnect(): boolean { const { adapter, autoConnect, ready, connected, connecting } = get(walletStore); return !( !autoConnect || !adapter || !( ready === WalletReadyState.Installed || ready === WalletReadyState.Loadable ) || connected || connecting ); } if (typeof window !== 'undefined') { // Ensure the adapter listeners are invalidated before refreshing the page. window.addEventListener('beforeunload', removeAdapterEventListeners); }