@ponziland/account
Version:
Account management library for Starknet with wallet modal and connection state
311 lines (310 loc) • 11.3 kB
JavaScript
// 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;
}