UNPKG

sui-svelte-wallet-kit

Version:

Svelte 5 wallet kit for Sui: connect wallets, manage accounts, SuiNS, balance, sign transactions/messages

321 lines (320 loc) 13.2 kB
import { ConnectionStatus } from '@suiet/wallet-sdk'; import { getWallets } from '@wallet-standard/core'; import { getConnectionData, updateConnectionData, saveConnectionData, getDefaultChain, isBrowser } from './core.js'; // Type helpers export const isSuiAccount = (acc) => typeof acc === 'object' && acc !== null && 'chains' in acc && Array.isArray(acc.chains) && acc.chains.some((c) => typeof c === 'string' && c.startsWith('sui:')); export const setAccountChainsInPlace = (account, chains, zkLoginGoogle) => { if (!account.value) return; try { const desc = Object.getOwnPropertyDescriptor(account.value, 'chains'); if (desc && desc.writable) { account.value.chains = chains || account.value.chains; } else { Object.defineProperty(account.value, 'chains', { value: chains || account.value.chains, writable: true, configurable: true, enumerable: true }); } } catch (_) { account.setAccount({ ...account.value, address: account.value.address, chains: chains || account.value.chains }); return; } account.setAccount(account.value); }; export const attachWalletNetworkListener = (state, zkLoginGoogle) => { if (!isBrowser) return; try { if (typeof state.walletEventsOff.get() === 'function') { state.walletEventsOff.get()(); state.walletEventsOff.set(undefined); } const wallets = getWallets().get(); const activeAddr = state.account.value?.address; if (!activeAddr) return; const byName = wallets.find((w) => w.name === state.wallet.get()?.name && w.accounts?.some?.((a) => a.address === activeAddr)); const byAddr = wallets.find((w) => w.accounts?.some?.((a) => a.address === activeAddr)); const active = byName || byAddr; if (!active) return; const eventsFeature = active.features?.['standard:events']; if (!eventsFeature || typeof eventsFeature.on !== 'function') return; state.walletEventsOff.set(eventsFeature.on('change', async (payload) => { const { accounts, chains } = payload || {}; if (Array.isArray(chains) && chains.length > 0) { setAccountChainsInPlace(state.account, chains, zkLoginGoogle); updateConnectionData({ selectedChain: chains[0] }); return; } if (!Array.isArray(accounts) || accounts.length === 0) return; const suiAccounts = accounts.filter(isSuiAccount); if (suiAccounts.length === 0) return; state.accountsSnapshot.set(suiAccounts); const lower = (s) => s.toLowerCase(); const currentAddr = lower(activeAddr); const nextAccount = suiAccounts.find((a) => lower(a.address) === currentAddr) || suiAccounts[0]; if (!nextAccount) return; state.account.setAccount(nextAccount); const effChains = Array.isArray(nextAccount.chains) ? nextAccount.chains : state.account.value?.chains; if (Array.isArray(effChains) && effChains.length > 0) { setAccountChainsInPlace(state.account, effChains, zkLoginGoogle); } updateConnectionData({ selectedAccountAddress: nextAccount.address, selectedChain: Array.isArray(nextAccount?.chains) ? nextAccount.chains[0] : undefined }); })); } catch (_) { // ignore } }; export const connect = async (wallet, state, zkLoginGoogle, onConnect, passkeyAdapter) => { const adapter = wallet?.adapter; state.walletAdapter.set(adapter); if (adapter) { state.status.set(ConnectionStatus.CONNECTING); try { if (typeof adapter.connect === 'function') { await adapter.connect(); } else if (adapter?.features?.['standard:connect'] && typeof adapter.features['standard:connect'].connect === 'function') { await adapter.features['standard:connect'].connect(); } else { throw new Error('No compatible connect method on wallet'); } const rawAccounts = Array.isArray(adapter.accounts) ? adapter.accounts : []; const allAccounts = rawAccounts.filter(isSuiAccount); state.accountsSnapshot.set(allAccounts); const connectionData = getConnectionData(); const preferredAddress = connectionData?.selectedAccountAddress?.toLowerCase(); let selectedAccount = allAccounts[0]; if (preferredAddress) { const found = allAccounts.find((acc) => acc.address.toLowerCase() === preferredAddress); if (found) selectedAccount = found; } state.account.setAccount(selectedAccount); if (!Array.isArray(selectedAccount?.chains) || selectedAccount.chains.length === 0) { const defaultChain = getDefaultChain(zkLoginGoogle); setAccountChainsInPlace(state.account, [defaultChain], zkLoginGoogle); } state.wallet.set(wallet); state.status.set(ConnectionStatus.CONNECTED); saveConnectionData(wallet.name, true); updateConnectionData({ selectedAccountAddress: selectedAccount?.address, selectedChain: Array.isArray(selectedAccount?.chains) && selectedAccount.chains[0] ? selectedAccount.chains[0] : getDefaultChain(zkLoginGoogle) }); onConnect(); attachWalletNetworkListener(state, zkLoginGoogle); } catch { state.status.set(ConnectionStatus.DISCONNECTED); } } }; export const disconnect = (state) => { const adapter = state.walletAdapter.get(); try { const std = adapter?.features?.['standard:disconnect']; if (std && typeof std.disconnect === 'function') { try { std.disconnect(); } catch { } } else if (adapter && typeof adapter.disconnect === 'function') { try { adapter.disconnect(); } catch { } } } catch { } state.account.removeAccount(); state.status.set(ConnectionStatus.DISCONNECTED); state.walletAdapter.set(undefined); state.wallet.set(undefined); if (typeof state.walletEventsOff.get() === 'function') { state.walletEventsOff.get()(); state.walletEventsOff.set(undefined); } }; export const connectWithModal = async (connectModal, onSelection, state, zkLoginGoogle, onConnect, passkeyAdapter) => { if (state.account.value) return { connected: true, alreadyConnected: true }; while (true) { const result = await connectModal?.openAndWaitForResponse(); if (!result) return { wallet: undefined, installed: false, connected: false, cancelled: true }; const selectedWallet = result?.wallet ?? result; const installed = typeof result === 'object' ? !!result?.installed : !!selectedWallet?.installed; state.lastWalletSelection.set({ wallet: selectedWallet, installed: !!installed }); try { onSelection?.({ wallet: selectedWallet, installed, connected: false }); } catch { } if (!installed) continue; if (!result?.started) { await connect(selectedWallet, state, zkLoginGoogle, onConnect, passkeyAdapter); } return { wallet: selectedWallet, installed: true, connected: true }; } }; export const switchWallet = async (getConnectModal, options, state, zkLoginGoogle, onConnect, passkeyAdapter) => { try { const modal = typeof getConnectModal === 'function' ? getConnectModal() : undefined; if (!modal) return { connected: false, cancelled: true }; while (true) { const result = await modal.openAndWaitForResponse(); if (!result) { try { options?.onCancel?.(); } catch { } return { connected: false, cancelled: true }; } const selectedWallet = result?.wallet ?? result; const installed = typeof result === 'object' ? !!result?.installed : !!selectedWallet?.installed; state.lastWalletSelection.set({ wallet: selectedWallet, installed: !!installed }); try { options?.onSelection?.({ wallet: selectedWallet, installed, connected: false }); } catch { } if (!installed) { continue; } const proceed = typeof options?.shouldConnect === 'function' ? !!options.shouldConnect({ selectedWallet, currentWallet: state.wallet.get() }) : true; if (!proceed) { return { wallet: selectedWallet, installed: true, connected: false, skipped: true }; } try { options?.onBeforeDisconnect?.(state.wallet.get(), selectedWallet); } catch { } if (result?.started) { try { options?.onConnected?.(selectedWallet); } catch { } return { wallet: selectedWallet, installed: true, connected: true }; } try { disconnect(state); } catch { } await connect(selectedWallet, state, zkLoginGoogle, onConnect, passkeyAdapter); try { options?.onConnected?.(state.wallet.get()); } catch { } return { wallet: selectedWallet, installed: true, connected: true }; } } catch (_) { return { connected: false, error: 'switch-failed' }; } }; export const autoConnectWallet = async (autoConnect, availableWallets, state, zkLoginGoogle, onConnect, passkeyAdapter) => { if (!autoConnect) return; const connectionData = getConnectionData(); if (!connectionData?.autoConnect) return; const wallet = availableWallets.find((w) => w.name === connectionData.walletName); if (wallet && wallet.installed) { if (wallet.name === 'Passkey' && passkeyAdapter && 'silentConnect' in passkeyAdapter) { const success = await passkeyAdapter.silentConnect(); if (success) { state.wallet.set(wallet); state.walletAdapter.set(passkeyAdapter); state.accountsSnapshot.set([...passkeyAdapter.accounts]); state.account.setAccount(state.accountsSnapshot.get()[0]); state.status.set(ConnectionStatus.CONNECTED); return; } return; } await connect(wallet, state, zkLoginGoogle, onConnect, passkeyAdapter); } }; export const switchAccount = (selector, state, zkLoginGoogle) => { if (state.status.get() !== ConnectionStatus.CONNECTED) { throw Error('wallet is not connected'); } const list = Array.isArray(state.accountsSnapshot.get()) ? state.accountsSnapshot.get() : []; let nextAccount = undefined; if (typeof selector === 'number') { nextAccount = list[selector]; } else if (typeof selector === 'string') { const target = selector.toLowerCase(); nextAccount = list.find((acc) => acc.address.toLowerCase() === target); } else if (selector && typeof selector === 'object' && 'address' in selector) { const target = selector.address?.toLowerCase(); nextAccount = list.find((acc) => acc.address.toLowerCase() === target) || selector; } if (!nextAccount) return false; state.account.setAccount(nextAccount); if (!Array.isArray(nextAccount?.chains) || nextAccount.chains.length === 0) { setAccountChainsInPlace(state.account, [getDefaultChain(zkLoginGoogle)], zkLoginGoogle); } updateConnectionData({ selectedAccountAddress: nextAccount.address, selectedChain: Array.isArray(nextAccount?.chains) && nextAccount.chains[0] ? nextAccount.chains[0] : getDefaultChain(zkLoginGoogle) }); return true; }; export const activeAccountIndex = (state) => { return { get value() { if (!state.account.value) return -1; const list = Array.isArray(state.accountsSnapshot.get()) ? state.accountsSnapshot.get() : []; return list.findIndex((acc) => acc.address === state.account.value.address); } }; }; export const ensureCallable = (state) => { if (state.status.get() !== ConnectionStatus.CONNECTED) { throw Error('wallet is not connected'); } };