Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/extension-polkagate/src/class/endpointManager2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ENDPOINTS_NAME_IN_STORAGE = 'endpoints2';
type SavedEndpoints = Record<string, EndpointType>;
type Listener = (genesisHash: string, endpoint: EndpointType) => void;

export default class endpointManager2 {
export default class EndpointManager2 {
// Store endpoints and listeners
private endpoints: SavedEndpoints = {};
private listeners = new Set<Listener>();
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-polkagate/src/components/contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { noop } from '@polkadot/util';
const AccountContext = React.createContext<AccountsContext>({ accounts: [], hierarchy: [], master: undefined });
const AccountsAssetsContext = React.createContext<AccountsAssetsContextType>({ accountsAssets: undefined, setAccountsAssets: noop });
const ActionContext = React.createContext<(to?: string) => void>(noop);
const APIContext = React.createContext<APIsContext>({ apis: {}, setIt: noop });
const APIContext = React.createContext<APIsContext>({ apis: {}, getApi: () => Promise.resolve(undefined) });
const AlertContext = React.createContext<AlertContextType>({ alerts: [], setAlerts: noop });
const AuthorizeReqContext = React.createContext<AuthorizeRequest[]>([]);
const CurrencyContext = React.createContext<CurrencyContextType>({ currency: undefined, setCurrency: noop });
Expand Down
272 changes: 10 additions & 262 deletions packages/extension-polkagate/src/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,278 +1,26 @@
// Copyright 2019-2025 @polkadot/extension-polkagate authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { useCallback, useContext, useEffect, useReducer } from 'react';
import type { ApiPromise } from '@polkadot/api';

import { ApiPromise, WsProvider } from '@polkadot/api';
import { useContext, useEffect, useState } from 'react';

import EndpointManager from '../class/endpointManager2';
import { APIContext } from '../components';
import { fastestConnection } from '../util';
import LCConnector from '../util/api/lightClient-connect';
import { AUTO_MODE } from '../util/constants';
import useEndpoint from './useEndpoint';
import { useEndpoints } from './useEndpoints';
import { useEndpoints } from '.';

// Define types for API state and actions
interface ApiState {
api: ApiPromise | undefined; // The API object, initially undefined
isLoading: boolean; // Whether the API connection is in the loading state
error: Error | null; // Any error that occurs during the API connection process
}

// Reducer function to manage API state
type ApiAction =
| { type: 'SET_API'; payload: ApiPromise }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: Error };

const apiReducer = (state: ApiState, action: ApiAction): ApiState => {
const { payload, type } = action;

switch (type) {
case 'SET_API':
return { ...state, api: payload, error: null, isLoading: false };
case 'SET_LOADING':
return { ...state, isLoading: payload };
case 'SET_ERROR':
return { ...state, error: payload, isLoading: false };
default:
return state;
}
};

const endpointManager = new EndpointManager();
const isAutoMode = (e: string) => e === AUTO_MODE.value;

export default function useApi (genesisHash: string | null | undefined, stateApi?: ApiPromise, _endpoint?: string): ApiPromise | undefined {
const { checkForNewOne, endpoint } = useEndpoint(genesisHash, _endpoint);
const apisContext = useContext(APIContext);
export default function useApi (genesisHash: string | null | undefined): ApiPromise | undefined {
const { getApi } = useContext(APIContext);
const endpoints = useEndpoints(genesisHash);

const [state, dispatch] = useReducer(apiReducer, {
api: stateApi,
error: null,
isLoading: false
});

// This function is called exclusively in auto mode to update the account's "auto mode" endpoint
// with the fastest endpoint available.
const updateEndpoint = useCallback((chainKey: string, selectedEndpoint: string, cbFunction?: () => void) => {
try {
const newEndpoint = {
checkForNewOne: false,
endpoint: selectedEndpoint,
isAuto: true,
timestamp: Date.now()
};

endpointManager.set(chainKey, newEndpoint);

cbFunction?.();
} catch (error) {
console.error(error);
}
}, []);

// Checks if there is an available API connection, then will change the address endpoint to the available API's endpoint
// Returns false if it was not successful to find an available API and true vice versa
const connectToExisted = useCallback((genesisHash: string): boolean => {
const apiList = apisContext.apis[genesisHash];

if (!apiList) {
return false;
}

const availableApi = apiList.find(({ api }) => api?.isConnected);

if (!availableApi?.api) {
return false;
}

dispatch({ payload: availableApi.api, type: 'SET_API' });
updateEndpoint(genesisHash, availableApi.endpoint);

console.log('Successfully connected to existing API for genesis hash:', genesisHash);

return true;
}, [apisContext.apis, updateEndpoint]);

// Handles a new API connection and updates the context with the new API
const handleNewApi = useCallback((api: ApiPromise, endpoint: string, onAutoMode?: boolean) => {
dispatch({ payload: api, type: 'SET_API' });

const genesisHash = String(api.genesisHash.toHex());
let toSaveApi = apisContext.apis[genesisHash] ?? [];

// Remove any existing API with the same endpoint
// it happens when the API is requested and not connected yet
toSaveApi = toSaveApi.filter((sApi) => sApi.endpoint !== endpoint);

// If in auto mode, remove any auto mode endpoint
if (onAutoMode) {
toSaveApi = toSaveApi.filter((sApi) => !isAutoMode(sApi.endpoint));
}

// Add the new API entry
toSaveApi.push({
api,
endpoint,
isRequested: false
});

apisContext.apis[genesisHash] = toSaveApi;
apisContext.setIt({ ...apisContext.apis });
}, [apisContext]);

// Connects to a specific WebSocket endpoint and creates a new API instance
// when it is not on Auto Mode
const connectToEndpoint = useCallback(async (endpointToConnect: string) => {
try {
dispatch({ payload: true, type: 'SET_LOADING' });
const wsProvider = new WsProvider(endpointToConnect);
const newApi = await ApiPromise.create({ provider: wsProvider });

handleNewApi(newApi, endpointToConnect);
} catch (error) {
dispatch({ payload: error as Error, type: 'SET_ERROR' });
} finally {
dispatch({ payload: false, type: 'SET_LOADING' });
}
}, [handleNewApi]);

// Handles auto mode by finding the fastest endpoint and connecting to it
const handleAutoMode = useCallback(async (genesisHash: string, findNewEndpoint: boolean) => {
const apisForGenesis = apisContext.apis[genesisHash];

const autoModeExists = apisForGenesis?.some(({ endpoint }) => isAutoMode(endpoint));

if (autoModeExists) {
return;
}

const result = !findNewEndpoint && connectToExisted(genesisHash);

if (result) {
return;
}

const wssEndpoints = endpoints.filter(({ value }) => String(value).startsWith('wss')); // to filter possible light client

dispatch({ payload: true, type: 'SET_LOADING' });

// Finds the fastest available endpoint and connects to it
const { api, selectedEndpoint } = await fastestConnection(wssEndpoints);
const [api, setApi] = useState<ApiPromise | undefined>(undefined);

if (!api || !selectedEndpoint) {
return;
}

updateEndpoint(genesisHash, selectedEndpoint, () => handleNewApi(api, selectedEndpoint, true));
}, [apisContext.apis, connectToExisted, endpoints, handleNewApi, updateEndpoint]);

const addApiRequest = useCallback((endpointToRequest: string, genesisHash: string) => {
const toSaveApi = apisContext.apis[genesisHash] ?? [];

toSaveApi.push({ endpoint: endpointToRequest, isRequested: true });

apisContext.apis[genesisHash] = toSaveApi;
apisContext.setIt({ ...apisContext.apis });
}, [apisContext]);

// check api in the context
const isInContext = useCallback((endpoint: string, genesisHash: string) => {
// Check if there is a saved API that is already connected
const savedApi = apisContext?.apis[genesisHash]?.find((sApi) => sApi.endpoint === endpoint);

// If the API is already being requested, skip the connection process
if (savedApi?.isRequested) {
return true;
}

if (savedApi?.api?.isConnected) {
dispatch({ payload: savedApi.api, type: 'SET_API' });

return true;
}

return false;
}, [apisContext?.apis]);

// Handles connection request to a manual endpoint
const handleApiWithChain = useCallback((manualEndpoint: string, genesisHash: string) => {
if (isInContext(manualEndpoint, genesisHash)) {
return;
}

addApiRequest(manualEndpoint, genesisHash);

connectToEndpoint(manualEndpoint).catch(console.error);
}, [addApiRequest, connectToEndpoint, isInContext]);

useEffect(() => {
// if _endpoint & genesisHash are available means useApiWithChain2 is trying to create a new connection!
if (_endpoint && genesisHash) {
handleApiWithChain(_endpoint, genesisHash);
}
}, [_endpoint, genesisHash, handleApiWithChain]);

// Manages the API connection when the endpoint, or genesis hash changes
useEffect(() => {
// @ts-expect-error to bypass access to private prop
if (!genesisHash || !endpoint || state.isLoading || state?.api?._options?.provider?.endpoint === endpoint) {
return;
}

// Validate the endpoint format (it should start with 'wss', 'light', or be in auto mode)
if (!endpoint.startsWith('wss') && !endpoint.startsWith('light') && !isAutoMode(endpoint)) {
console.log('📌 📌 Unsupported endpoint detected 📌 📌 ', endpoint);

return;
}

// To address the delay issue when setting the endpoint in this hook,
// we manually compare the endpoint obtained from `useEndpoint` (local state)
// and the endpoint stored in `EndpointManager`.
// If they are not equal, it means the state has not been updated yet,
// so we log a message and return early to prevent further processing.
const endpointFromTheManager = endpointManager.get(genesisHash)?.endpoint; // Endpoint stored in the manager

// Check if the two endpoints are not synchronized
if (endpoint !== endpointFromTheManager) {
// console.log('📌 📌 Not updated yet! The endpoint in the manager is still different from the local one.');

// Exit early to avoid further execution until the endpoints are in sync
if (!genesisHash || !endpoints) {
return;
}

// To provide api from context
if (isInContext(endpoint, genesisHash)) {
return;
}

// If in auto mode, check existing connections or find a new one
if (isAutoMode(endpoint)) {
handleAutoMode(genesisHash, !!checkForNewOne).catch(console.error);
}

// Connect to a WebSocket endpoint if provided
if (endpoint.startsWith('wss')) {
connectToEndpoint(endpoint).catch(console.error);
}

// Connect to a light client endpoint if provided
if (endpoint.startsWith('light')) {
LCConnector(endpoint).then((LCapi) => {
handleNewApi(LCapi, endpoint);
console.log('🖌️ light client connected', String(LCapi.genesisHash.toHex()));
}).catch((err) => {
console.error('📌 light client failed:', err);
});
}

addApiRequest(endpoint, genesisHash);
// @ts-expect-error to bypass access to private prop
}, [addApiRequest, genesisHash, checkForNewOne, connectToEndpoint, endpoint, handleAutoMode, handleNewApi, isInContext, state?.api?._options?.provider?.endpoint, state.isLoading]);
getApi(genesisHash, endpoints)?.then(setApi).catch(console.error);
}, [endpoints, genesisHash, getApi]);

return state.api;
return api;
}
9 changes: 2 additions & 7 deletions packages/extension-polkagate/src/hooks/useEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { EndpointType } from '../util/types';
import { useCallback, useEffect, useState } from 'react';

import EndpointManager2 from '../class/endpointManager2';
import { AUTO_MODE } from '../util/constants';
import { AUTO_MODE_DEFAULT_ENDPOINT } from '../util/constants';

// Create a singleton EndpointManager
const endpointManager = new EndpointManager2();
Expand Down Expand Up @@ -41,12 +41,7 @@ export default function useEndpoint (genesisHash: string | null | undefined, _en

// If an endpoint already saved or it should be on auto mode, then save the Auto Mode endpoint in the storage
if (!savedEndpoint || endpointManager.shouldBeOnAutoMode(savedEndpoint)) {
endpointManager.set(genesisHash, {
checkForNewOne: false,
endpoint: AUTO_MODE.value,
isAuto: true,
timestamp: Date.now()
});
endpointManager.set(genesisHash, AUTO_MODE_DEFAULT_ENDPOINT);
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/extension-polkagate/src/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ export const AUTO_MODE = {
value: 'AutoMode'
};

export const AUTO_MODE_DEFAULT_ENDPOINT = {
checkForNewOne: false,
endpoint: AUTO_MODE.value,
isAuto: true,
timestamp: Date.now()
};

export const KODADOT_URL = 'https://kodadot.xyz';

export const DEMO_ACCOUNT = '1ChFWeNRLarAPRCTM3bfJmncJbSAbSS9yqjueWz7jX7iTVZ';
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-polkagate/src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ export type APIs = Record<string, ApiPropsNew[] | undefined>;

export interface APIsContext {
apis: APIs;
setIt: (apis: APIs) => void;
getApi: (genesisHash: string | null | undefined, endpoints: DropdownOption[]) => Promise<ApiPromise | undefined>;
}

export interface LatestRefs {
Expand Down
Loading
Loading