"use strict";
// Copyright 2017-2020 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const memoizee_1 = __importDefault(require("memoizee"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const jsonrpc_1 = __importDefault(require("../type-jsonrpc"));
const method_1 = __importDefault(require("../type-jsonrpc/create/method"));
const param_1 = __importDefault(require("../type-jsonrpc/create/param"));
const types_1 = require("../types");
const codec_1 = require("../types/codec");
const util_1 = require("@polkadot/util");
const rxjs_2 = require("./rxjs");
const l = util_1.logger('rpc-core');
const EMPTY_META = {
    fallback: undefined,
    modifier: { isOptional: true },
    type: {
        asMap: { linked: { isTrue: false } },
        isMap: false
    }
};
// utility method to create a nicely-formatted error
/** @internal */
function createErrorMessage({ method, params, type }, error) {
    const inputs = params.map(({ isOptional, name, type }) => `${name}${isOptional ? '?' : ''}: ${type}`).join(', ');
    return `${method}(${inputs}): ${type}:: ${error.message}`;
}
/**
 * @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 {
    /**
     * @constructor
     * Default constructor for the Api Object
     * @param  {ProviderInterface} provider An API provider using HTTP or WebSocket
     */
    constructor(registry, provider, userRpc = {}) {
        this._storageCache = new Map();
        this.mapping = new Map();
        this.sections = [];
        // eslint-disable-next-line @typescript-eslint/unbound-method
        util_1.assert(provider && util_1.isFunction(provider.send), 'Expected Provider to API create');
        this.registry = registry;
        this.provider = provider;
        this.createInterfaces(jsonrpc_1.default, userRpc);
    }
    /**
     * @description Manually disconnect from the attached provider
     */
    disconnect() {
        this.provider.disconnect();
    }
    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, [sectionName, methods]) => {
            user[sectionName] = methods.reduce((section, def) => {
                const { description = 'User defined', name, params, type } = def;
                section[name] = method_1.default(sectionName, name, {
                    description,
                    params: params.map(({ isOptional, name, type }) => param_1.default(name, type, { isOptional })),
                    type: type
                });
                return section;
            }, {});
            return user;
        }, {});
        // decorate the sections with base and user methods
        this.sections.forEach((sectionName) => {
            this[sectionName] = Object.assign(Object.assign({}, 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(`${section}_${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) {
        const rpcName = `${method.section}_${method.method}`;
        const call = (...values) => {
            // 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 rxjs_1.of(1).pipe(operators_1.map(() => this.formatInputs(method, values)), operators_1.switchMap((params) => rxjs_1.combineLatest([
                rxjs_1.of(params),
                rxjs_1.from(this.provider.send(rpcName, params.map((param) => param.toJSON())))
            ])), operators_1.map(([params, result]) => this.formatOutput(method, params, result)), operators_1.catchError((error) => {
                const message = createErrorMessage(method, error);
                // don't scare with old nodes, this is handled transparently
                rpcName !== 'rpc_methods' && l.error(message);
                return rxjs_1.throwError(new Error(message));
            }), operators_1.publishReplay(1), // create a Replay(1)
            operators_1.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;
    }
    // create a subscriptor, it subscribes once and resolves with the id as subscribe
    createSubscriber({ subType, subName, paramsJson, update }, errorHandler) {
        return new Promise((resolve, reject) => {
            this.provider
                .subscribe(subType, subName, paramsJson, update)
                .then(resolve)
                .catch((error) => {
                errorHandler(error);
                reject(error);
            });
        });
    }
    createMethodSubscribe(method) {
        const [updateType, subMethod, unsubMethod] = method.pubsub;
        const subName = `${method.section}_${subMethod}`;
        const unsubName = `${method.section}_${unsubMethod}`;
        const subType = `${method.section}_${updateType}`;
        const call = (...values) => {
            return new rxjs_1.Observable((observer) => {
                // Have at least an empty promise, as used in the unsubscribe
                let subscriptionPromise = Promise.resolve();
                const errorHandler = (error) => {
                    const message = createErrorMessage(method, error);
                    l.error(message);
                    observer.error(new Error(message));
                };
                try {
                    const params = this.formatInputs(method, values);
                    const paramsJson = params.map((param) => param.toJSON());
                    const update = (error, result) => {
                        if (error) {
                            l.error(createErrorMessage(method, error));
                            return;
                        }
                        observer.next(this.formatOutput(method, params, result));
                    };
                    subscriptionPromise = this.createSubscriber({ subType, subName, paramsJson, update }, 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) => util_1.isNumber(subscriptionId)
                        ? this.provider.unsubscribe(subType, unsubName, subscriptionId)
                        : Promise.resolve(false))
                        .catch((error) => l.error(createErrorMessage(method, error)));
                };
            }).pipe(rxjs_2.drr());
        };
        const memoized = memoizee_1.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(({ isOptional }) => !isOptional).length;
        const optText = reqArgCount === method.params.length
            ? ''
            : ` (${method.params.length - reqArgCount} optional)`;
        util_1.assert(inputs.length >= reqArgCount && inputs.length <= method.params.length, `Expected ${method.params.length} parameters${optText}, ${inputs.length} found instead`);
        return inputs.map((input, index) => codec_1.createTypeUnsafe(this.registry, 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 ${key.section}.${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 ${key.section}.${key.method}:`, error.message);
                    throw error;
                }
                return results;
            }, []);
        }
        return codec_1.createTypeUnsafe(this.registry, 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 Raw on nothing
        const type = key.outputType || 'Raw';
        const meta = key.meta || EMPTY_META;
        const isEmpty = util_1.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
                : util_1.u8aToU8a(value);
        if (meta.modifier.isOptional) {
            return new types_1.Option(this.registry, types_1.createClass(this.registry, type), isEmpty
                ? null
                : codec_1.createTypeUnsafe(this.registry, type, [input], true));
        }
        return codec_1.createTypeUnsafe(this.registry, type, [isEmpty ? meta.fallback : input], true);
    }
    formatStorageSet(key, changes, witCache) {
        // Fallback to Raw (i.e. just the encoding) if we don't have a specific type
        const type = key.outputType || 'Raw';
        const hexKey = key.toHex();
        const meta = key.meta || EMPTY_META;
        const found = changes.find(([key]) => key === hexKey);
        // 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 value = util_1.isUndefined(found)
            ? (witCache && this._storageCache.get(hexKey)) || null
            : found[1];
        const isEmpty = util_1.isNull(value);
        const input = isEmpty || this.treatAsHex(key)
            ? value
            : util_1.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_1.Option(this.registry, types_1.createClass(this.registry, type), isEmpty
                ? null
                : codec_1.createTypeUnsafe(this.registry, type, [input], true));
        }
        return codec_1.createTypeUnsafe(this.registry, type, [isEmpty ? meta.fallback : input], true);
    }
}
exports.default = Rpc;
