UNPKG

@ponziland/account

Version:

Account management library for Starknet with wallet modal and connection state

311 lines (310 loc) 11.3 kB
// Get the wanted system from the environment const browser = typeof window !== 'undefined'; import { accountConfig, configureAccount } from '../consts.js'; import { ArgentXAccount } from '../wallets/argentx.js'; import { setupController, SvelteController } from '../wallets/controller.js'; import { Provider as StarknetProvider } from 'starknet'; import getStarknet from '@starknet-io/get-starknet-core'; import { WALLET_API } from '@starknet-io/types-js'; import { Account, cairo, WalletAccount, constants as SNconstants, shortString, constants, } from 'starknet'; import { getContext, setContext } from 'svelte'; export const WalletWeights = { // Controller is preferred. controller: 99, // Session-supported wallets (ordering alphabetically) argentX: 21, braavos: 20, // Other wallets (sorted alphabetically) fordefi: 12, keplr: 11, okxwallet: 10, // We block metamask due to issues metamask: -1, }; const stubLocalStorage = { getItem(id) { return null; }, setItem(id, value) { }, removeItem(id) { }, }; const localStorage = browser ? window.localStorage : stubLocalStorage; // TODO: // Store in a sesion the last used wallet. // On setup, find all available wallets, and create an instance of the selected one if it exists. // If not available, not set the inner AccountProvider. // Add a function to request login for a specific wallet, that calls the .login() for the selected account (by id) // Then, delegate the rest to the current account. const accountManager = Symbol('accountManager'); const previousWalletSymbol = Symbol('previousWallet'); const previousWalletSession = Symbol('walletSession'); let controller; export async function Provider(wallet) { switch (wallet.id) { case 'controller': return controller ?? null; case 'argentX': return new ArgentXAccount(wallet, accountConfig); // NOTE: To add new providers, this is here. default: return null; } } let availableWallets = []; async function scanObjectForWalletsCustom() { if (!browser || availableWallets.length > 0) { return; } const wallets = await getStarknet.getAvailableWallets({}); console.log('List of starknet wallets', wallets); availableWallets = await Promise.all(wallets.map(async (wallet) => { let isValid = await checkCompatibility(wallet); // If not valid still check maybe its a virtual wallet ? if (!isValid) { try { wallet = await wallet.loadWallet(window); } catch (e) { console.log('Not a virtual wallet', e); } isValid = await checkCompatibility(wallet); } return { wallet: wallet, isValid: isValid }; })); console.log(availableWallets); } const checkCompatibility = async (myWalletSWO) => { let isCompatible = false; try { const permissions = (await myWalletSWO.request({ type: 'wallet_getPermissions', })); isCompatible = true; } catch { (err) => { console.log('Wallet compatibility failed.\n', err); }; } return isCompatible; }; export class AccountManager { _provider; _walletObject; _setup = false; _setupPromise; _listeners = []; constructor() { this._setupPromise = this.setup(); } listen(listener) { this._listeners.push(listener); return () => { const index = this._listeners.findIndex((e) => e == listener); if (index != -1) { this._listeners.splice(index, 1); } }; } async wait() { return await this._setupPromise; } async setup() { const previousWallet = localStorage.getItem(previousWalletSymbol.toString()); // Setup cartridge before anything else controller = await setupController(accountConfig); // Get all available wallets await scanObjectForWalletsCustom(); if (previousWallet != null) { console.info('Attempting auto-login with provider', previousWallet); try { await this.selectAndLogin(previousWallet); this.getSessionFromStorage(); } catch (e) { console.error('An error occurred while auto-logging the provider ', previousWallet, e); this.disconnect(); } return this; } console.info('The user did not have a previous wallet selected.'); // NOTE: If session is supported, extract the public & private session from local storage. return this; } getStarknetProvider() { // This method requires the config to be loaded first // We'll throw an error with a helpful message if not loaded return new StarknetProvider({ nodeUrl: accountConfig.rpcUrl, // We won't be using argent / braavos on slot deployments any time soon chainId: accountConfig.chainId == 'mainnet' ? SNconstants.StarknetChainId.SN_MAIN : SNconstants.StarknetChainId.SN_SEPOLIA, }); } async selectAndLogin(providerId) { const walletObject = availableWallets.find((e) => e.wallet.id == providerId); if (walletObject == null) { throw 'Unknown provider!'; } const provider = await Provider(walletObject.wallet); if (provider == null) { throw 'Could not setup provider (not registered in account.ts)'; } try { // Handle user cancelled action this._provider = provider; this._walletObject = walletObject.wallet; // First, ask for a login await provider.connect(); console.info('User logged-in successfully'); this._listeners.forEach((listener) => listener({ type: 'connected', provider, })); const walletAccount = provider.getWalletAccount(); if (walletAccount instanceof WalletAccount) { console.log('Wallet account!'); // Unregister the bugged accountsChanged from starknetjs console.log(this._walletObject); walletAccount.onNetworkChanged(this.onNetworkChanged.bind(this)); walletAccount.onAccountChange(this.onWalletChanged.bind(this)); } localStorage.setItem(previousWalletSymbol.toString(), providerId); } catch (error) { console.warn('The user did not log in successfully!', error); } } async switchChain(chainId) { const walletAccount = this._provider?.getWalletAccount(); if (walletAccount instanceof WalletAccount) { await walletAccount.switchStarknetChain(shortString.encodeShortString(chainId)); } else { console.error('The switch chain operation is not supported!'); } } getProviderName() { return this._walletObject?.id; } async getChainId() { console.log(this._walletObject); const chainId = await this._walletObject?.request({ type: 'wallet_requestChainId', }); console.log('Response:', chainId); console.log('chainId:', shortString.decodeShortString(chainId ?? '0x0')); return shortString.decodeShortString(chainId ?? '0x0'); } disconnect() { // Remove all associated strings from local storage localStorage.removeItem(previousWalletSymbol.toString()); localStorage.removeItem(previousWalletSession.toString()); if (this._provider) { this._provider.disconnect(); this._walletObject?.off('networkChanged', this.onNetworkChanged.bind(this)); this._walletObject?.off('accountsChanged', this.onWalletChanged.bind(this)); // Announce that you are disconnected. this._listeners.forEach((listener) => listener({ type: 'disconnected', })); this._provider = undefined; this._walletObject = undefined; } } getProvider() { return this._provider; } async setupSession() { if (this._provider == null) { throw 'No provider is setup!'; } if (!this._provider.supportsSession()) { throw 'The provider does not support session setup!'; } const result = await this._provider.setupSession(); if (result != undefined) { localStorage.setItem(previousWalletSession.toString(), JSON.stringify(result)); } } getAvailableWallets() { return availableWallets .map((e) => e.wallet) .filter((wallet) => // Allow unknown and non-ignored wallets !(wallet.id in WalletWeights) || WalletWeights[wallet.id] > 0) .sort((a, b) => (WalletWeights[b.id] ?? 0) - (WalletWeights[a.id] ?? 0)); } promptForLogin() { window.dispatchEvent(new Event('wallet_prompt')); return new Promise((resolve) => { const listener = () => { window.removeEventListener('wallet_login_success', listener); resolve(); }; window.addEventListener('wallet_login_success', listener); }); } getSessionFromStorage() { let parsed; try { const json = localStorage.getItem(previousWalletSession.toString()); if (json == null) { return; } parsed = JSON.parse(json); } catch (e) { console.log('Could not fetch session data'); return; } if (parsed.expiry < new Date()) { console.warn('The session has expired.'); localStorage.removeItem(previousWalletSession.toString()); } this._provider?.loadSession(parsed); } onWalletChanged(accounts) { console.log('This:', accounts); if ((accounts?.length ?? 0) == 0) { console.log('Disconnect due to lock!'); this.disconnect(); return; } this._listeners.forEach((listener) => { listener({ type: 'connected', provider: this._provider, }); }); } onNetworkChanged(chainId, accounts) { // Notify that the network has changed this._listeners.forEach((listener) => { listener({ type: 'chain_change', chainId: shortString.decodeShortString(chainId ?? '0x0') ?? 'unknown', }); }); } } let state = $state(null); export function setupAccount(config) { if (config) { configureAccount(config); } if (state != null) { return state.wait(); } const manager = new AccountManager(); state = manager; return manager.wait(); } export function useAccount() { const manager = state; if (manager == null) { console.error('You are using useAccount(), but the setupAccount() function has not been called.'); } return manager; }