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
JavaScript
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');
}
};