diff --git a/src/model/Api/indexi.ts b/src/model/Api/indexi.ts new file mode 100644 index 00000000..6248d866 --- /dev/null +++ b/src/model/Api/indexi.ts @@ -0,0 +1,141 @@ +// Copyright 2024 @rossbulat/console authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ApiPromise } from '@polkadot/api'; +import type { VoidFn } from '@polkadot/api/types'; +import { WsProvider } from '@polkadot/rpc-provider'; +import type { ChainId } from 'config/networks'; +import type { EventDetail, EventStatus } from './types'; + +export class Api { + // ------------------------------------------------------ + // Class members. + // ------------------------------------------------------ + + // The supplied chain id. + #chainId: ChainId; + + // API provider. + #provider: WsProvider; + + // API provider unsubs. + #providerUnsubs: VoidFn[] = []; + + // API instance. + #api: ApiPromise; + + // The current RPC endpoint. + #rpcEndpoint: string; + + // ------------------------------------------------------ + // Getters. + // ------------------------------------------------------ + + get chainId() { + return this.#chainId; + } + + get provider() { + return this.#provider; + } + + get api() { + return this.#api; + } + + get rpcEndpoint() { + return this.#rpcEndpoint; + } + + // ------------------------------------------------------ + // Constructor + // ------------------------------------------------------ + + constructor(chainId: ChainId, endpoint: string) { + this.#rpcEndpoint = endpoint; + this.#chainId = chainId; + this.initialize(); + } + + // ------------------------------------------------------ + // Initialization + // ------------------------------------------------------ + + // Initialize the API. + async initialize() { + // Initialize provider. + this.#provider = new WsProvider(this.#rpcEndpoint); + + // Tell UI api is connecting. + this.dispatchEvent(this.ensureEventStatus('connecting')); + + // Tell UI api is connecting. + this.dispatchEvent(this.ensureEventStatus('connecting')); + + // Initialise provider events. + this.initProviderEvents(); + + // Initialise api. + this.#api = await ApiPromise.create({ provider: this.provider }); + + // Tell UI api is ready. + this.dispatchEvent(this.ensureEventStatus('ready')); + } + + // ------------------------------------------------------ + // Event handling. + // ------------------------------------------------------ + + // Set up API event listeners. Relays information to `document` for the UI to handle. + async initProviderEvents() { + this.#providerUnsubs.push( + this.#provider.on('connected', () => { + this.dispatchEvent(this.ensureEventStatus('connected')); + }) + ); + + this.#providerUnsubs.push( + this.#provider.on('disconnected', () => { + this.dispatchEvent(this.ensureEventStatus('disconnected')); + }) + ); + this.#providerUnsubs.push( + this.#provider.on('error', (err: string) => { + this.dispatchEvent(this.ensureEventStatus('error'), { err }); + }) + ); + } + + // Handler for dispatching events. + dispatchEvent( + event: EventStatus, + options?: { + err?: string; + } + ) { + const detail: EventDetail = { event }; + if (options?.err) { + detail['err'] = options.err; + } + document.dispatchEvent(new CustomEvent('api-status', { detail })); + } + + // ------------------------------------------------------ + // Class helpers. + // ------------------------------------------------------ + + // Ensures the provided status is a valid `EventStatus` being passed, or falls back to `error`. + ensureEventStatus = (status: string | EventStatus): EventStatus => { + const eventStatus: string[] = [ + 'connecting', + 'connected', + 'disconnected', + 'ready', + 'error', + ]; + if (eventStatus.includes(status)) { + return status as EventStatus; + } + return 'error' as EventStatus; + }; +} diff --git a/src/model/Api/types.ts b/src/model/Api/types.ts new file mode 100644 index 00000000..5df048cd --- /dev/null +++ b/src/model/Api/types.ts @@ -0,0 +1,11 @@ +// Copyright 2024 @rossbulat/console authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export type ApiStatus = 'connecting' | 'connected' | 'disconnected' | 'ready'; + +export type EventStatus = ApiStatus | 'error'; + +export interface EventDetail { + event: EventStatus; + err?: string; +} diff --git a/src/types.ts b/src/types.ts index de7c5d15..a3810c84 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,12 @@ +import type { EventDetail } from 'model/Api/types'; import type { CSSProperties, ReactNode } from 'react'; +declare global { + interface DocumentEventMap { + 'api-status': CustomEvent; + } +} + export interface ComponentBase { children?: ReactNode; style?: CSSProperties; diff --git a/tsconfig.json b/tsconfig.json index 691d1471..2d9924c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, + "strictPropertyInitialization": false, "noUnusedLocals": true, }, "include": [ diff --git a/vite.config.ts b/vite.config.ts index fca58755..16f3b05a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,6 @@ +// Copyright 2024 @rossbulat/console authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import checker from 'vite-plugin-checker';