polkadot-js/api

View on GitHub
packages/rpc-provider/src/mock/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors
// SPDX-License-Identifier: Apache-2.0

/* eslint-disable camelcase */

import type { Header } from '@polkadot/types/interfaces';
import type { Codec, Registry } from '@polkadot/types/types';
import type { ProviderInterface, ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '../types.js';
import type { MockStateDb, MockStateSubscriptionCallback, MockStateSubscriptions } from './types.js';

import { EventEmitter } from 'eventemitter3';

import { createTestKeyring } from '@polkadot/keyring/testing';
import { decorateStorage, Metadata } from '@polkadot/types';
import jsonrpc from '@polkadot/types/interfaces/jsonrpc';
import rpcHeader from '@polkadot/types-support/json/Header.004.json' assert { type: 'json' };
import rpcSignedBlock from '@polkadot/types-support/json/SignedBlock.004.immortal.json' assert { type: 'json' };
import rpcMetadata from '@polkadot/types-support/metadata/static-substrate';
import { BN, bnToU8a, logger, u8aToHex } from '@polkadot/util';
import { randomAsU8a } from '@polkadot/util-crypto';

const INTERVAL = 1000;
const SUBSCRIPTIONS: string[] = Array.prototype.concat.apply(
  [],
  Object.values(jsonrpc).map((section): string[] =>
    Object
      .values(section)
      .filter(({ isSubscription }) => isSubscription)
      .map(({ jsonrpc }) => jsonrpc)
      .concat('chain_subscribeNewHead')
  )
) as string[];

const keyring = createTestKeyring({ type: 'ed25519' });
const l = logger('api-mock');

/**
 * A mock provider mainly used for testing.
 * @return {ProviderInterface} The mock provider
 * @internal
 */
export class MockProvider implements ProviderInterface {
  private db: MockStateDb = {};

  private emitter = new EventEmitter();

  private intervalId?: ReturnType<typeof setInterval> | null;

  public isUpdating = true;

  private registry: Registry;

  private prevNumber = new BN(-1);

  private requests: Record<string, (...params: any[]) => unknown> = {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    chain_getBlock: () => this.registry.createType('SignedBlock', rpcSignedBlock.result).toJSON(),
    chain_getBlockHash: () => '0x1234000000000000000000000000000000000000000000000000000000000000',
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    chain_getFinalizedHead: () => this.registry.createType('Header', rpcHeader.result).hash,
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    chain_getHeader: () => this.registry.createType('Header', rpcHeader.result).toJSON(),
    rpc_methods: () => this.registry.createType('RpcMethods').toJSON(),
    state_getKeys: () => [],
    state_getKeysPaged: () => [],
    state_getMetadata: () => rpcMetadata,
    state_getRuntimeVersion: () => this.registry.createType('RuntimeVersion').toHex(),
    state_getStorage: (storage: MockStateDb, [key]: string[]) => u8aToHex(storage[key]),
    system_chain: () => 'mockChain',
    system_health: () => ({}),
    system_name: () => 'mockClient',
    system_properties: () => ({ ss58Format: 42 }),
    system_upgradedToTripleRefCount: () => this.registry.createType('bool', true),
    system_version: () => '9.8.7',
    // eslint-disable-next-line @typescript-eslint/no-unsafe-return, sort-keys
    dev_echo: (_, params: any) => params
  };

  public subscriptions: MockStateSubscriptions = SUBSCRIPTIONS.reduce((subs, name): MockStateSubscriptions => {
    subs[name] = {
      callbacks: {},
      lastValue: null
    };

    return subs;
  }, ({} as MockStateSubscriptions));

  private subscriptionId = 0;

  private subscriptionMap: Record<number, string> = {};

  constructor (registry: Registry) {
    this.registry = registry;

    this.init();
  }

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

  public clone (): MockProvider {
    throw new Error('Unimplemented');
  }

  public async connect (): Promise<void> {
    // noop
  }

  // eslint-disable-next-line @typescript-eslint/require-await
  public async disconnect (): Promise<void> {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

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

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

  public on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void {
    this.emitter.on(type, sub);

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

  // eslint-disable-next-line @typescript-eslint/require-await
  public async send <T = any> (method: string, params: unknown[]): Promise<T> {
    l.debug(() => ['send', method, params]);

    if (!this.requests[method]) {
      throw new Error(`provider.send: Invalid method '${method}'`);
    }

    return this.requests[method](this.db, params) as T;
  }

  // eslint-disable-next-line @typescript-eslint/require-await
  public async subscribe (_type: string, method: string, ...params: unknown[]): Promise<number> {
    l.debug(() => ['subscribe', method, params]);

    if (!this.subscriptions[method]) {
      throw new Error(`provider.subscribe: Invalid method '${method}'`);
    }

    const callback = params.pop() as MockStateSubscriptionCallback;
    const id = ++this.subscriptionId;

    this.subscriptions[method].callbacks[id] = callback;
    this.subscriptionMap[id] = method;

    if (this.subscriptions[method].lastValue !== null) {
      callback(null, this.subscriptions[method].lastValue);
    }

    return id;
  }

  // eslint-disable-next-line @typescript-eslint/require-await
  public async unsubscribe (_type: string, _method: string, id: number): Promise<boolean> {
    const sub = this.subscriptionMap[id];

    l.debug(() => ['unsubscribe', id, sub]);

    if (!sub) {
      throw new Error(`Unable to find subscription for ${id}`);
    }

    delete this.subscriptionMap[id];
    delete this.subscriptions[sub].callbacks[id];

    return true;
  }

  private init (): void {
    const emitEvents: ProviderInterfaceEmitted[] = ['connected', 'disconnected'];
    let emitIndex = 0;
    let newHead = this.makeBlockHeader();
    let counter = -1;

    const metadata = new Metadata(this.registry, rpcMetadata);

    this.registry.setMetadata(metadata);

    const query = decorateStorage(this.registry, metadata.asLatest, metadata.version);

    // Do something every 1 seconds
    this.intervalId = setInterval((): void => {
      if (!this.isUpdating) {
        return;
      }

      // create a new header (next block)
      newHead = this.makeBlockHeader();

      // increment the balances and nonce for each account
      keyring.getPairs().forEach(({ publicKey }, index): void => {
        this.setStateBn(query['system']['account'](publicKey), newHead.number.toBn().addn(index));
      });

      // set the timestamp for the current block
      this.setStateBn(query['timestamp']['now'](), Math.floor(Date.now() / 1000));
      this.updateSubs('chain_subscribeNewHead', newHead);

      // We emit connected/disconnected at intervals
      if (++counter % 2 === 1) {
        if (++emitIndex === emitEvents.length) {
          emitIndex = 0;
        }

        this.emitter.emit(emitEvents[emitIndex]);
      }
    }, INTERVAL);
  }

  private makeBlockHeader (): Header {
    const blockNumber = this.prevNumber.addn(1);
    const header = this.registry.createType('Header', {
      digest: {
        logs: []
      },
      extrinsicsRoot: randomAsU8a(),
      number: blockNumber,
      parentHash: blockNumber.isZero()
        ? new Uint8Array(32)
        : bnToU8a(this.prevNumber, { bitLength: 256, isLe: false }),
      stateRoot: bnToU8a(blockNumber, { bitLength: 256, isLe: false })
    });

    this.prevNumber = blockNumber;

    return header as unknown as Header;
  }

  private setStateBn (key: Uint8Array, value: BN | number): void {
    this.db[u8aToHex(key)] = bnToU8a(value, { bitLength: 64, isLe: true });
  }

  private updateSubs (method: string, value: Codec): void {
    this.subscriptions[method].lastValue = value;

    Object
      .values(this.subscriptions[method].callbacks)
      .forEach((cb): void => {
        try {
          cb(null, value.toJSON());
        } catch (error) {
          l.error(`Error on '${method}' subscription`, error);
        }
      });
  }
}