polkadot-js/extension

View on GitHub
packages/extension-base/src/page/PostMessageProvider.ts

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2019-2024 @polkadot/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { InjectedProvider, ProviderList, ProviderMeta } from '@polkadot/extension-inject/types';
import type { ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '@polkadot/rpc-provider/types';
import type { AnyFunction } from '@polkadot/types/types';
import type { SendRequest } from './types.js';

import { EventEmitter } from 'eventemitter3';

import { isUndefined, logger } from '@polkadot/util';

const l = logger('PostMessageProvider');

type CallbackHandler = (error?: null | Error, value?: unknown) => void;

// Same as https://github.com/polkadot-js/api/blob/57ca9a9c3204339e1e1f693fcacc33039868dc27/packages/rpc-provider/src/ws/Provider.ts#L17
interface SubscriptionHandler {
  callback: CallbackHandler;
  type: string;
}

// External to class, this.# is not private enough (yet)
let sendRequest: SendRequest;

/**
 * @name PostMessageProvider
 *
 * @description Extension provider to be used by dapps
 */
export default class PostMessageProvider implements InjectedProvider {
  readonly #eventemitter: EventEmitter;

  // Whether or not the actual extension background provider is connected
  #isConnected = false;

  // Subscription IDs are (historically) not guaranteed to be globally unique;
  // only unique for a given subscription method; which is why we identify
  // the subscriptions based on subscription id + type
  readonly #subscriptions: Record<string, AnyFunction> = {}; // {[(type,subscriptionId)]: callback}

  /**
   * @param {function}  sendRequest  The function to be called to send requests to the node
   * @param {function}  subscriptionNotificationHandler  Channel for receiving subscription messages
   */
  public constructor (_sendRequest: SendRequest) {
    this.#eventemitter = new EventEmitter();

    sendRequest = _sendRequest;
  }

  public get isClonable (): boolean {
    return !!true;
  }

  /**
   * @description Returns a clone of the object
   */
  public clone (): PostMessageProvider {
    return new PostMessageProvider(sendRequest);
  }

  /**
   * @description Manually disconnect from the connection, clearing autoconnect logic
   */
  // eslint-disable-next-line @typescript-eslint/require-await
  public async connect (): Promise<void> {
    // FIXME This should see if the extension's state's provider can disconnect
    console.error('PostMessageProvider.disconnect() is not implemented.');
  }

  /**
   * @description Manually disconnect from the connection, clearing autoconnect logic
   */
  // eslint-disable-next-line @typescript-eslint/require-await
  public async disconnect (): Promise<void> {
    // FIXME This should see if the extension's state's provider can disconnect
    console.error('PostMessageProvider.disconnect() is not implemented.');
  }

  /**
   * @summary `true` when this provider supports subscriptions
   */
  public get hasSubscriptions (): boolean {
    // FIXME This should see if the extension's state's provider has subscriptions
    return !!true;
  }

  /**
   * @summary Whether the node is connected or not.
   * @return {boolean} true if connected
   */
  public get isConnected (): boolean {
    return this.#isConnected;
  }

  public listProviders (): Promise<ProviderList> {
    return sendRequest('pub(rpc.listProviders)', undefined);
  }

  /**
   * @summary Listens on events after having subscribed using the [[subscribe]] function.
   * @param  {ProviderInterfaceEmitted} type Event
   * @param  {ProviderInterfaceEmitCb}  sub  Callback
   * @return unsubscribe function
   */
  public on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void {
    this.#eventemitter.on(type, sub);

    return (): void => {
      this.#eventemitter.removeListener(type, sub);
    };
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  public async send (method: string, params: unknown[], _?: boolean, subscription?: SubscriptionHandler): Promise<any> {
    if (subscription) {
      const { callback, type } = subscription;

      const id = await sendRequest('pub(rpc.subscribe)', { method, params, type }, (res): void => {
        subscription.callback(null, res);
      });

      this.#subscriptions[`${type}::${id}`] = callback;

      return id;
    }

    return sendRequest('pub(rpc.send)', { method, params });
  }

  /**
   * @summary Spawn a provider on the extension background.
   */
  public async startProvider (key: string): Promise<ProviderMeta> {
    // Disconnect from the previous provider
    this.#isConnected = false;
    this.#eventemitter.emit('disconnected');

    const meta = await sendRequest('pub(rpc.startProvider)', key);

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    sendRequest('pub(rpc.subscribeConnected)', null, (connected) => {
      this.#isConnected = connected;

      if (connected) {
        this.#eventemitter.emit('connected');
      } else {
        this.#eventemitter.emit('disconnected');
      }

      return true;
    });

    return meta;
  }

  public subscribe (type: string, method: string, params: unknown[], callback: AnyFunction): Promise<number> {
    return this.send(method, params, false, { callback, type }) as Promise<number>;
  }

  /**
   * @summary Allows unsubscribing to subscriptions made with [[subscribe]].
   */
  public async unsubscribe (type: string, method: string, id: number): Promise<boolean> {
    const subscription = `${type}::${id}`;

    // FIXME This now could happen with re-subscriptions. The issue is that with a re-sub
    // the assigned id now does not match what the API user originally received. It has
    // a slight complication in solving - since we cannot rely on the send id, but rather
    // need to find the actual subscription id to map it
    if (isUndefined(this.#subscriptions[subscription])) {
      l.debug((): string => `Unable to find active subscription=${subscription}`);

      return false;
    }

    delete this.#subscriptions[subscription];

    return this.send(method, [id]) as Promise<boolean>;
  }
}