@web3modal/base
Version:
#### 🔗 [Website](https://web3modal.com)
490 lines • 21.9 kB
JavaScript
import { Connection } from '@solana/web3.js';
import { ApiController, AssetController, CoreHelperUtil, EventsController, NetworkController, OptionsController } from '@web3modal/core';
import { ConstantsUtil, PresetsUtil } from '@web3modal/scaffold-utils';
import { ConstantsUtil as CommonConstantsUtil } from '@web3modal/common';
import { SolConstantsUtil, SolHelpersUtil, SolStoreUtil } from '@web3modal/scaffold-utils/solana';
import { PublicKey } from '@solana/web3.js';
import UniversalProvider, {} from '@walletconnect/universal-provider';
import { watchStandard } from './utils/watchStandard.js';
import { WalletConnectProvider } from './providers/WalletConnectProvider.js';
import { AuthProvider } from './providers/AuthProvider.js';
import { W3mFrameHelpers, W3mFrameProvider, W3mFrameRpcConstants } from '@web3modal/wallet';
import { withSolanaNamespace } from './utils/withSolanaNamespace.js';
import { SafeLocalStorage } from '../../../utils/SafeLocalStorage.js';
import { createSendTransaction } from './utils/createSendTransaction.js';
import { CoinbaseWalletProvider } from './providers/CoinbaseWalletProvider.js';
export class SolanaWeb3JsClient {
constructor(options) {
this.appKit = undefined;
this.instanceOptions = undefined;
this.options = undefined;
this.hasSyncedConnectedAccount = false;
this.chain = CommonConstantsUtil.CHAIN.SOLANA;
this.availableProviders = [];
const { solanaConfig, chains, defaultChain, connectionSettings = 'confirmed' } = options;
if (!solanaConfig) {
throw new Error('web3modal:constructor - solanaConfig is undefined');
}
this.instanceOptions = options;
this.chains = chains;
this.connectionSettings = connectionSettings;
this.defaultChain = SolHelpersUtil.getChainFromCaip(this.chains, SafeLocalStorage.getItem(SolConstantsUtil.CAIP_CHAIN_ID) ||
withSolanaNamespace(defaultChain?.chainId) ||
withSolanaNamespace(chains[0]?.chainId));
this.networkControllerClient = {
switchCaipNetwork: async (caipNetwork) => {
if (caipNetwork) {
try {
await this.switchNetwork(caipNetwork);
}
catch (error) {
SolStoreUtil.setError(error);
}
}
},
getApprovedCaipNetworksData: async () => {
if (SolStoreUtil.state.provider) {
const approvedCaipNetworkIds = SolStoreUtil.state.provider.chains.map(chain => `solana:${chain.chainId}`);
return Promise.resolve({
approvedCaipNetworkIds,
supportsAllNetworks: false
});
}
return Promise.resolve({
approvedCaipNetworkIds: undefined,
supportsAllNetworks: false
});
}
};
this.connectionControllerClient = {
connectWalletConnect: async (onUri) => {
const wcProvider = this.availableProviders.find(provider => provider.type === 'WALLET_CONNECT');
if (!wcProvider || !(wcProvider instanceof WalletConnectProvider)) {
throw new Error('connectionControllerClient:getWalletConnectUri - provider is undefined');
}
wcProvider.onUri = onUri;
return this.setProvider(wcProvider);
},
connectExternal: async ({ id }) => {
const externalProvider = this.availableProviders.find(provider => provider.name.toLocaleLowerCase() === id.toLocaleLowerCase());
if (!externalProvider) {
throw Error('connectionControllerClient:connectExternal - adapter was undefined');
}
return this.setProvider(externalProvider);
},
disconnect: async () => {
await SolStoreUtil.state.provider?.disconnect();
},
signMessage: async (message) => {
const provider = SolStoreUtil.state.provider;
if (!provider) {
throw new Error('connectionControllerClient:signMessage - provider is undefined');
}
const signature = await provider.signMessage(new TextEncoder().encode(message));
return new TextDecoder().decode(signature);
},
estimateGas: async (params) => {
if (params.chainNamespace !== 'solana') {
throw new Error('Chain namespace is not supported');
}
const connection = SolStoreUtil.state.connection;
if (!connection) {
throw new Error('Connection is not set');
}
const provider = this.getProvider();
const transaction = await createSendTransaction({
provider,
connection,
to: '11111111111111111111111111111111',
value: 1
});
const fee = await transaction.getEstimatedFee(connection);
return BigInt(fee || 0);
},
getEnsAvatar: async (value) => await Promise.resolve(value),
getEnsAddress: async (value) => await Promise.resolve(value),
writeContract: async () => await Promise.resolve('0x'),
sendTransaction: async (params) => {
if (params.chainNamespace !== 'solana') {
throw new Error('Chain namespace is not supported');
}
const connection = SolStoreUtil.state.connection;
const address = SolStoreUtil.state.address;
if (!connection || !address) {
throw new Error('Connection is not set');
}
const provider = this.getProvider();
const transaction = await createSendTransaction({
provider,
connection,
to: params.to,
value: params.value
});
const result = await provider.sendTransaction(transaction, connection);
await new Promise(resolve => {
const interval = setInterval(async () => {
const status = await connection.getSignatureStatus(result);
if (status?.value) {
clearInterval(interval);
resolve();
}
}, 1000);
});
await this.syncBalance(address);
return result;
},
parseUnits: () => BigInt(0),
formatUnits: () => '',
checkInstalled: (ids = []) => {
const availableIds = new Set(this.availableProviders.map(provider => provider.name));
return ids.some(id => availableIds.has(id));
}
};
}
construct(appKit, options) {
const { projectId } = options;
const clientOptions = this.instanceOptions;
if (!clientOptions) {
throw new Error('Solana:construct - clientOptions is undefined');
}
this.appKit = appKit;
this.options = options;
const { chains } = clientOptions;
if (!projectId) {
throw new Error('Solana:construct - projectId is undefined');
}
this.initializeProviders({
relayUrl: SolConstantsUtil.UNIVERSAL_PROVIDER_RELAY_URL,
metadata: clientOptions.metadata,
projectId: options.projectId,
...clientOptions.solanaConfig.auth
});
if (this.defaultChain) {
this.appKit?.setCaipNetwork(this.defaultChain);
SolStoreUtil.setCaipChainId(this.defaultChain.id);
const chain = this.chains.find(c => withSolanaNamespace(c.chainId) === this.defaultChain.id);
if (chain) {
SolStoreUtil.setCurrentChain(chain);
}
}
this.syncNetwork();
this.syncRequestedNetworks(chains, this.options?.chainImages);
SolStoreUtil.subscribeKey('address', () => {
this.syncAccount();
});
SolStoreUtil.subscribeKey('caipChainId', () => {
this.syncNetwork();
});
SolStoreUtil.subscribeKey('isConnected', isConnected => {
this.appKit?.setIsConnected(isConnected, 'solana');
});
AssetController.subscribeNetworkImages(() => {
this.syncNetwork();
});
NetworkController.subscribeKey('caipNetwork', (newCaipNetwork) => {
const newChain = chains.find(_chain => _chain.chainId === newCaipNetwork?.id.split(':')[1]);
if (!newChain) {
return;
}
if (NetworkController.state.caipNetwork && !SolStoreUtil.state.isConnected) {
SolStoreUtil.setCaipChainId(`solana:${newChain.chainId}`);
SolStoreUtil.setCurrentChain(newChain);
SafeLocalStorage.setItem(SolConstantsUtil.CAIP_CHAIN_ID, `solana:${newChain.chainId}`);
ApiController.reFetchWallets();
}
});
EventsController.subscribe(state => {
if (state.data.event === 'SELECT_WALLET') {
const isMobile = CoreHelperUtil.isMobile();
const isClient = CoreHelperUtil.isClient();
if (isMobile && isClient) {
if (state.data.properties?.name === 'Phantom' && !('phantom' in window)) {
const href = window.location.href;
const protocol = href.startsWith('https') ? 'https' : 'http';
const host = href.split('/')[2];
const ref = `${protocol}://${host}`;
window.location.href = `https://phantom.app/ul/browse/${href}?ref=${ref}`;
}
if (state.data.properties?.name === 'Coinbase Wallet' && !('coinbaseSolana' in window)) {
const href = window.location.href;
window.location.href = `https://go.cb-w.com/dapp?cb_url=${href}`;
}
}
}
});
}
disconnect() {
return this.getProvider().disconnect();
}
getAddress() {
return SolStoreUtil.state.address;
}
getWalletProvider() {
return SolStoreUtil.state.provider;
}
getWalletProviderType() {
return SolStoreUtil.state.provider?.type;
}
getWalletConnection() {
return SolStoreUtil.state.connection;
}
syncConnectedWalletInfo() {
const currentActiveWallet = SafeLocalStorage.getItem(SolConstantsUtil.WALLET_ID);
const provider = SolStoreUtil.state.provider;
if (provider?.type === 'WALLET_CONNECT') {
const wcProvider = provider;
if (wcProvider.session) {
this.appKit?.setConnectedWalletInfo({
...wcProvider.session.peer.metadata,
name: wcProvider.session.peer.metadata.name,
icon: wcProvider.session.peer.metadata.icons?.[0]
}, this.chain);
}
}
else if (currentActiveWallet) {
this.appKit?.setConnectedWalletInfo({ name: currentActiveWallet }, this.chain);
}
}
async syncAccount() {
const address = SolStoreUtil.state.address;
const chainId = SolStoreUtil.state.currentChain?.chainId;
if (address && chainId) {
const caipAddress = `${ConstantsUtil.INJECTED_CONNECTOR_ID}:${chainId}:${address}`;
this.appKit?.setCaipAddress(caipAddress, this.chain);
await this.syncBalance(address);
this.syncConnectedWalletInfo();
this.hasSyncedConnectedAccount = true;
}
else if (this.hasSyncedConnectedAccount) {
this.appKit?.resetWcConnection();
this.appKit?.resetNetwork();
this.appKit?.resetAccount('solana');
}
}
async syncBalance(address) {
if (!SolStoreUtil.state.connection) {
throw new Error('Connection is not set');
}
if (!SolStoreUtil.state.currentChain) {
throw new Error('Chain is not set');
}
const balance = (await SolStoreUtil.state.connection.getBalance(new PublicKey(address))) /
SolConstantsUtil.LAMPORTS_PER_SOL;
this.appKit?.setBalance(balance.toString(), SolStoreUtil.state.currentChain.currency, this.chain);
}
syncRequestedNetworks(chains, chainImages) {
const requestedCaipNetworks = chains?.map(chain => ({
id: `solana:${chain.chainId}`,
name: chain.name,
imageId: PresetsUtil.EIP155NetworkImageIds[chain.chainId],
imageUrl: chainImages?.[chain.chainId],
chain: this.chain
}));
this.appKit?.setRequestedCaipNetworks(requestedCaipNetworks ?? [], this.chain);
}
async switchNetwork(caipNetwork) {
const caipChainId = caipNetwork.id;
if (this.provider instanceof AuthProvider) {
await this.provider.switchNetwork(caipChainId);
}
const chain = SolHelpersUtil.getChainFromCaip(this.chains, caipChainId);
SolStoreUtil.setCaipChainId(chain.id);
SolStoreUtil.setCurrentChain(chain);
SafeLocalStorage.setItem(SolConstantsUtil.CAIP_CHAIN_ID, chain.id);
await this.syncNetwork();
await this.syncAccount();
}
async syncNetwork() {
const chainImages = this.options?.chainImages;
const address = SolStoreUtil.state.address;
const storeChainId = SolStoreUtil.state.caipChainId;
if (this.chains) {
const chain = SolHelpersUtil.getChainFromCaip(this.chains, storeChainId);
if (chain) {
const caipChainId = `solana:${chain.chainId}`;
SolStoreUtil.setConnection(new Connection(SolHelpersUtil.detectRpcUrl(chain, OptionsController.state.projectId), this.connectionSettings));
this.appKit?.setCaipNetwork({
id: caipChainId,
name: chain.name,
imageId: PresetsUtil.EIP155NetworkImageIds[chain.chainId],
imageUrl: chainImages?.[chain.chainId],
chain: this.chain
});
if (address) {
if (chain.explorerUrl) {
const url = `${chain.explorerUrl}/account/${address}`;
this.appKit?.setAddressExplorerUrl(url, this.chain);
}
else {
this.appKit?.setAddressExplorerUrl(undefined, this.chain);
}
if (this.hasSyncedConnectedAccount) {
await this.syncBalance(address);
}
}
}
}
}
subscribeProvider(callback) {
return SolStoreUtil.subscribe(callback);
}
async setProvider(provider) {
try {
if (provider.type === 'AUTH') {
this.appKit?.setLoading(true);
}
const address = await provider.connect();
const connectionChain = provider.chains.find(chain => chain.chainId === SolStoreUtil.state.currentChain?.chainId) ||
provider.chains[0];
if (!connectionChain) {
provider.disconnect();
throw new Error('The wallet does not support any of the required chains');
}
SolStoreUtil.setAddress(address);
await this.switchNetwork(SolHelpersUtil.getChainFromCaip(this.chains, `solana:${connectionChain.chainId}`));
SolStoreUtil.setProvider(provider);
this.provider = provider;
SafeLocalStorage.setItem(SolConstantsUtil.WALLET_ID, provider.name);
await this.appKit?.setApprovedCaipNetworksData(this.chain);
this.watchProvider(provider);
SolStoreUtil.setIsConnected(true);
}
finally {
this.appKit?.setLoading(false);
}
}
watchProvider(provider) {
const rpcRequestHandler = (request) => {
if (!this.appKit) {
return;
}
if (W3mFrameHelpers.checkIfRequestExists(request)) {
if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) {
this.appKit.handleUnsafeRPCRequest();
}
}
else {
this.appKit.open();
console.error(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_MESSAGE, {
method: request.method
});
setTimeout(() => {
this.appKit?.showErrorMessage(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_UI_MESSAGE);
}, 300);
}
};
const rpcSuccessHandler = (_response) => {
if (!this.appKit) {
return;
}
if (this.appKit.isTransactionStackEmpty()) {
this.appKit.close();
}
else {
this.appKit.popTransactionStack();
}
};
const rpcErrorHandler = (_error) => {
if (!this.appKit) {
return;
}
if (this.appKit.isOpen()) {
if (this.appKit.isTransactionStackEmpty()) {
this.appKit.close();
}
else {
this.appKit.popTransactionStack(true);
}
}
};
function disconnectHandler() {
SafeLocalStorage.removeItem(SolConstantsUtil.WALLET_ID);
SolStoreUtil.reset();
provider.removeListener('disconnect', disconnectHandler);
provider.removeListener('accountsChanged', accountsChangedHandler);
provider.removeListener('connect', accountsChangedHandler);
provider.removeListener('auth_rpcRequest', rpcRequestHandler);
provider.removeListener('auth_rpcSuccess', rpcSuccessHandler);
provider.removeListener('auth_rpcError', rpcErrorHandler);
}
function accountsChangedHandler(publicKey) {
const currentAccount = publicKey.toBase58();
if (currentAccount) {
SolStoreUtil.setAddress(currentAccount);
}
else {
SafeLocalStorage.removeItem(SolConstantsUtil.WALLET_ID);
SolStoreUtil.reset();
}
}
provider.on('disconnect', disconnectHandler);
provider.on('accountsChanged', accountsChangedHandler);
provider.on('connect', accountsChangedHandler);
provider.on('auth_rpcRequest', rpcRequestHandler);
provider.on('auth_rpcSuccess', rpcSuccessHandler);
provider.on('auth_rpcError', rpcErrorHandler);
}
getProvider() {
if (!this.provider) {
throw new Error('Provider is not set');
}
return this.provider;
}
async initializeProviders(opts) {
if (CoreHelperUtil.isClient()) {
this.addProvider(new WalletConnectProvider({
provider: await UniversalProvider.init(opts),
chains: this.chains,
getActiveChain: () => SolStoreUtil.state.currentChain
}));
if (opts.email || opts.socials?.length) {
if (!opts.projectId) {
throw new Error('projectId is required for AuthProvider');
}
this.addProvider(new AuthProvider({
provider: new W3mFrameProvider(opts.projectId, withSolanaNamespace(SolStoreUtil.state.currentChain?.chainId)),
getActiveChain: () => SolStoreUtil.state.currentChain,
auth: {
email: opts.email,
socials: opts.socials,
showWallets: opts.showWallets,
walletFeatures: opts.walletFeatures
},
chains: this.chains
}));
}
if ('coinbaseSolana' in window) {
this.addProvider(new CoinbaseWalletProvider({
provider: window.coinbaseSolana,
chains: this.chains,
getActiveChain: () => SolStoreUtil.state.currentChain
}));
}
watchStandard(standardAdapters => this.addProvider.bind(this)(...standardAdapters));
}
}
addProvider(...providers) {
const activeProviderName = SafeLocalStorage.getItem(SolConstantsUtil.WALLET_ID);
for (const provider of providers) {
this.availableProviders = this.availableProviders.filter(p => p.name !== provider.name);
this.availableProviders.push(provider);
if (provider.name === activeProviderName) {
this.setProvider(provider);
}
}
this.syncConnectors();
}
syncConnectors() {
const connectors = this.availableProviders.map(provider => ({
id: provider.name,
type: provider.type,
imageUrl: provider.icon,
name: provider.name,
provider: provider.type === 'AUTH' ? provider : undefined,
chain: CommonConstantsUtil.CHAIN.SOLANA,
...provider.auth
}));
this.appKit?.setConnectors(connectors);
}
}
//# sourceMappingURL=client.js.map