"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

var _memoizee = _interopRequireDefault(require("memoizee"));

var _rxjs = require("rxjs");

var _operators = require("rxjs/operators");

var _jsonrpc = _interopRequireDefault(require("@polkadot/jsonrpc"));

var _method = _interopRequireDefault(require("@polkadot/jsonrpc/create/method"));

var _param = _interopRequireDefault(require("@polkadot/jsonrpc/create/param"));

var _types = require("@polkadot/types");

var _codec = require("@polkadot/types/codec");

var _util = require("@polkadot/util");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

const l = (0, _util.logger)('rpc-core');
const EMPTY_META = {
  fallback: undefined,
  modifier: {
    isOptional: true
  },
  type: {
    asMap: {
      linked: {
        isTrue: false
      }
    },
    isMap: false
  }
};
/**
 * @name Rpc
 * @summary The API may use a HTTP or WebSockets provider.
 * @description It allows for querying a Polkadot Client Node.
 * WebSockets provider is recommended since HTTP provider only supports basic querying.
 *
 * ```mermaid
 * graph LR;
 *   A[Api] --> |WebSockets| B[WsProvider];
 *   B --> |endpoint| C[ws://127.0.0.1:9944]
 * ```
 *
 * @example
 * <BR>
 *
 * ```javascript
 * import Rpc from '@polkadot/rpc-core';
 * import WsProvider from '@polkadot/rpc-provider/ws';
 *
 * const provider = new WsProvider('ws://127.0.0.1:9944');
 * const rpc = new Rpc(provider);
 * ```
 */

class Rpc {
  // Ok, this is quite horrible - we really should not be using the ! here, but we are actually assigning
  // these via the createInterfaces inside the constructor. However... this is not quite visible. The reason
  // why we don't do for individual assignments is to allow user-defined RPCs to also be defined

  /**
   * @constructor
   * Default constructor for the Api Object
   * @param  {ProviderInterface} provider An API provider using HTTP or WebSocket
   */
  constructor(provider) {
    let userRpc = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    this._storageCache = new Map();
    this.provider = void 0;
    this.mapping = new Map();
    this.sections = [];
    this.account = void 0;
    this.author = void 0;
    this.chain = void 0;
    this.contracts = void 0;
    this.rpc = void 0;
    this.state = void 0;
    this.system = void 0;
    // eslint-disable-next-line @typescript-eslint/unbound-method
    (0, _util.assert)(provider && (0, _util.isFunction)(provider.send), 'Expected Provider to API create');
    this.provider = provider;
    this.createInterfaces(_jsonrpc.default, userRpc);
  }
  /**
   * @name signature
   * @summary Returns a string representation of the method with inputs and outputs.
   * @description
   * Formats the name, inputs and outputs into a human-readable string. This contains the input parameter names input types and output type.
   *
   * @example
   * <BR>
   *
   * ```javascript
   * import Api from '@polkadot/rpc-core';
   *
   * Api.signature({ name: 'test_method', params: [ { name: 'dest', type: 'Address' } ], type: 'Address' }); // => test_method (dest: Address): Address
   * ```
   */


  static signature(_ref) {
    let {
      method,
      params,
      type
    } = _ref;
    const inputs = params.map((_ref2) => {
      let {
        name,
        type
      } = _ref2;
      return "".concat(name, ": ").concat(type);
    }).join(', ');
    return "".concat(method, " (").concat(inputs, "): ").concat(type);
  }
  /**
   * @description Manually disconnect from the attached provider
   */


  disconnect() {
    this.provider.disconnect();
  }

  createErrorMessage(method, error) {
    return "".concat(Rpc.signature(method), ":: ").concat(error.message);
  }

  createInterfaces(interfaces, userBare) {
    // these are the base keys (i.e. part of jsonrpc)
    this.sections.push(...Object.keys(interfaces)); // add any extra user-defined sections

    this.sections.push(...Object.keys(userBare).filter(key => !this.sections.includes(key))); // convert the user inputs into the same format as used in jsonrpc

    const user = Object.entries(userBare).reduce((user, _ref3) => {
      let [sectionName, methods] = _ref3;
      user[sectionName] = methods.reduce((section, def) => {
        const {
          description = 'User defined',
          name,
          params,
          type
        } = def;
        section[name] = (0, _method.default)(sectionName, name, {
          description,
          params: params.map((_ref4) => {
            let {
              isOptional,
              name,
              type
            } = _ref4;
            return (0, _param.default)(name, type, {
              isOptional
            });
          }),
          type: type
        });
        return section;
      }, {});
      return user;
    }, {}); // decorate the sections with base and user methods

    this.sections.forEach(sectionName => {
      this[sectionName] = _objectSpread({}, this.createInterface(sectionName, interfaces[sectionName] ? interfaces[sectionName].methods : {}), {}, this.createInterface(sectionName, user[sectionName] || {}));
    });
  }

  createInterface(section, methods) {
    return Object.keys(methods).reduce((exposed, method) => {
      const def = methods[method];
      this.mapping.set("".concat(section, "_").concat(method), def); // FIXME Remove any here
      // To do so, remove `RpcInterfaceMethod` from './types.ts', and refactor
      // every method inside this class to take:
      // `<S extends keyof RpcInterface, M extends keyof RpcInterface[S]>`
      // Not doing so, because it makes this class a little bit less readable,
      // and leaving it as-is doesn't harm much

      exposed[method] = def.isSubscription ? this.createMethodSubscribe(def) : this.createMethodSend(def);
      return exposed;
    }, {});
  }

  createMethodSend(method) {
    var _this = this;

    const rpcName = "".concat(method.section, "_").concat(method.method);

    const call = function call() {
      for (var _len = arguments.length, values = new Array(_len), _key = 0; _key < _len; _key++) {
        values[_key] = arguments[_key];
      }

      // TODO Warn on deprecated methods
      // Here, logically, it should be `of(this.formatInputs(method, values))`.
      // However, formatInputs can throw, and when it does, the above way
      // doesn't throw in the "Observable loop" (which is internally wrapped in
      // a try/catch block). So we:
      // - first do `of(1)` - won't throw
      // - then do `map(()=>this.formatInputs)` - might throw, but inside Observable.
      return (0, _rxjs.of)(1).pipe((0, _operators.map)(() => _this.formatInputs(method, values)), (0, _operators.switchMap)(params => (0, _rxjs.combineLatest)([(0, _rxjs.of)(params), (0, _rxjs.from)(_this.provider.send(rpcName, params.map(param => param.toJSON())))])), (0, _operators.map)((_ref5) => {
        let [params, result] = _ref5;
        return _this.formatOutput(method, params, result);
      }), (0, _operators.catchError)(error => {
        const message = _this.createErrorMessage(method, error); // don't scare with old nodes, this is handled transparently


        rpcName !== 'rpc_methods' && l.error(message);
        return (0, _rxjs.throwError)(new Error(message));
      }), (0, _operators.publishReplay)(1), // create a Replay(1)
      (0, _operators.refCount)() // Unsubscribe WS when there are no more subscribers
      );
    }; // We voluntarily don't cache the "one-shot" RPC calls. For example,
    // `getStorage('123')` returns the current value, but this value can change
    // over time, so we wouldn't want to cache the Observable.


    return call;
  }

  createMethodSubscribe(method) {
    var _this2 = this;

    const [updateType, subMethod, unsubMethod] = method.pubsub;
    const subName = "".concat(method.section, "_").concat(subMethod);
    const unsubName = "".concat(method.section, "_").concat(unsubMethod);
    const subType = "".concat(method.section, "_").concat(updateType);

    const call = function call() {
      for (var _len2 = arguments.length, values = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        values[_key2] = arguments[_key2];
      }

      return new _rxjs.Observable(observer => {
        // Have at least an empty promise, as used in the unsubscribe
        let subscriptionPromise = Promise.resolve();

        const errorHandler = error => {
          const message = _this2.createErrorMessage(method, error);

          l.error(message);
          observer.error(new Error(message));
        };

        try {
          const params = _this2.formatInputs(method, values);

          const paramsJson = params.map(param => param.toJSON());

          const update = (error, result) => {
            if (error) {
              l.error(_this2.createErrorMessage(method, error));
              return;
            }

            observer.next(_this2.formatOutput(method, params, result));
          };

          subscriptionPromise = _this2.provider.subscribe(subType, subName, paramsJson, update).catch(errorHandler);
        } catch (error) {
          errorHandler(error);
        } // Teardown logic


        return () => {
          // Delete from cache
          // Reason:
          // ```
          //    const s = api.query.system.accountNonce(addr1).subscribe(); // let's say it's 6
          //    s.unsubscribe();
          //    // wait a bit, for the nonce to increase to 7
          //    api.query.system.accountNonce(addr1).subscribe(); // will output 6 instead of 7 if we don't clear cache
          //    // that's because all our observables are replay(1)
          // ```
          // eslint-disable-next-line @typescript-eslint/no-use-before-define
          memoized.delete(...values); // Unsubscribe from provider

          subscriptionPromise.then(subscriptionId => (0, _util.isNumber)(subscriptionId) ? _this2.provider.unsubscribe(subType, unsubName, subscriptionId) : Promise.resolve(false)).catch(error => l.error(_this2.createErrorMessage(method, error)));
        };
      }).pipe( // Duplicated in api-derive/util/drr
      (0, _operators.distinctUntilChanged)((a, b) => JSON.stringify({
        value: a
      }) === JSON.stringify({
        value: b
      })), (0, _operators.publishReplay)(1), (0, _operators.refCount)());
    };

    const memoized = (0, _memoizee.default)(call, {
      // Dynamic length for argument
      length: false,
      // Normalize args so that different args that should be cached
      // together are cached together.
      // E.g.: `query.my.method('abc') === query.my.method(createType('AccountId', 'abc'));`
      // eslint-disable-next-line @typescript-eslint/unbound-method
      normalizer: JSON.stringify
    });
    return memoized;
  }

  formatInputs(method, inputs) {
    const reqArgCount = method.params.filter((_ref6) => {
      let {
        isOptional
      } = _ref6;
      return !isOptional;
    }).length;
    const optText = reqArgCount === method.params.length ? '' : " (".concat(method.params.length - reqArgCount, " optional)");
    (0, _util.assert)(inputs.length >= reqArgCount && inputs.length <= method.params.length, "Expected ".concat(method.params.length, " parameters").concat(optText, ", ").concat(inputs.length, " found instead"));
    return inputs.map((input, index) => (0, _codec.createTypeUnsafe)(method.params[index].type, [input]));
  }

  treatAsHex(key) {
    // :code is problematic - it does not have the length attached, which is
    // unlike all other storage entries where it is indeed properly encoded
    return ['0x3a636f6465'].includes(key.toHex());
  }

  formatOutput(method, params, result) {
    if (method.type === 'StorageData') {
      const key = params[0];

      try {
        return this.formatStorageData(key, result);
      } catch (error) {
        console.error("Unable to decode storage ".concat(key.section, ".").concat(key.method, ":"), error.message);
        throw error;
      }
    } else if (method.type === 'StorageChangeSet') {
      // For StorageChangeSet, the changes has the [key, value] mappings
      const changes = result.changes;
      const keys = params[0];
      const withCache = keys.length !== 1; // multiple return values (via state.storage subscription), decode the values
      // one at a time, all based on the query types. Three values can be returned -
      //   - Codec - There is a valid value, non-empty
      //   - null - The storage key is empty

      return keys.reduce((results, key) => {
        try {
          results.push(this.formatStorageSet(key, changes, withCache));
        } catch (error) {
          console.error("Unable to decode storage ".concat(key.section, ".").concat(key.method, ":"), error.message);
          throw error;
        }

        return results;
      }, []);
    }

    return (0, _codec.createTypeUnsafe)(method.type, [result]);
  }

  formatStorageData(key, value) {
    // single return value (via state.getStorage), decode the value based on the
    // outputType that we have specified. Fallback to Data on nothing
    const type = key.outputType || 'Data';
    const meta = key.meta || EMPTY_META;
    const isEmpty = (0, _util.isNull)(value); // we convert to Uint8Array since it maps to the raw encoding, all
    // data will be correctly encoded (incl. numbers, excl. :code)

    const input = isEmpty ? null : this.treatAsHex(key) ? value : (0, _util.u8aToU8a)(value);

    if (meta.modifier.isOptional) {
      return new _types.Option((0, _types.createClass)(type), isEmpty ? null : (0, _codec.createTypeUnsafe)(type, [input], true));
    }

    return (0, _codec.createTypeUnsafe)(type, [isEmpty ? meta.fallback : input], true);
  }

  formatStorageSet(key, changes, witCache) {
    // Fallback to Data (i.e. just the encoding) if we don't have a specific type
    const type = key.outputType || 'Data';
    const hexKey = key.toHex();
    const meta = key.meta || EMPTY_META; // if we don't find the value, this is our fallback
    //   - in the case of an array of values, fill the hole from the cache
    //   - if a single result value, don't fill - it is not an update hole
    //   - fallback to an empty option in all cases

    const emptyVal = witCache && this._storageCache.get(hexKey) || null; // see if we have a result value for this specific key, fallback to the cache value
    // when the value in the set is not available, or is null/empty.

    const [, value] = changes.find((_ref7) => {
      let [key, value] = _ref7;
      return !(0, _util.isNull)(value) && key === hexKey;
    }) || [null, emptyVal];
    const isEmpty = (0, _util.isNull)(value);
    const input = isEmpty ? null : this.treatAsHex(key) ? value : (0, _util.u8aToU8a)(value); // store the retrieved result - the only issue with this cache is that there is no
    // clearing of it, so very long running processes (not just a couple of hours, longer)
    // will increase memory beyond what is allowed.

    this._storageCache.set(hexKey, value);

    if (meta.modifier.isOptional) {
      return new _types.Option((0, _types.createClass)(type), isEmpty ? null : (0, _codec.createTypeUnsafe)(type, [input], true));
    }

    return (0, _codec.createTypeUnsafe)(type, [isEmpty ? meta.fallback : input], true);
  }

}

exports.default = Rpc;