"use strict";
// Copyright 2017-2020 @polkadot/api 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Decorated_1 = __importDefault(require("../../metadata/Decorated"));
const types_1 = require("../../types");
const known_1 = require("../../types/known");
const Extrinsic_1 = require("../../types/primitive/Extrinsic/Extrinsic");
const util_1 = require("@polkadot/util");
const util_crypto_1 = require("@polkadot/util-crypto");
const defaults_1 = __importDefault(require("@polkadot/util-crypto/address/defaults"));
const Decorate_1 = __importDefault(require("./Decorate"));
const KEEPALIVE_INTERVAL = 15000;
const l = util_1.logger('api/decorator');
class Init extends Decorate_1.default {
    constructor(options, type, decorateMethod) {
        super(options, type, decorateMethod);
        this._healthTimer = null;
        this._onProviderConnect = () => __awaiter(this, void 0, void 0, function* () {
            this.emit('connected');
            this._isConnected.next(true);
            try {
                const [hasMeta, cryptoReady] = yield Promise.all([
                    this.loadMeta(),
                    util_crypto_1.cryptoWaitReady()
                ]);
                if (hasMeta && !this._isReady && cryptoReady) {
                    this._isReady = true;
                    this.emit('ready', this);
                }
                this._healthTimer = setInterval(() => {
                    this._rpcCore.system.health().toPromise().catch(() => {
                        // ignore
                    });
                }, KEEPALIVE_INTERVAL);
            }
            catch (_error) {
                const error = new Error(`FATAL: Unable to initialize the API: ${_error.message}`);
                l.error(error);
                this.emit('error', error);
            }
        });
        this._onProviderDisconnect = () => {
            this.emit('disconnected');
            this._isConnected.next(false);
            if (this._healthTimer) {
                clearInterval(this._healthTimer);
                this._healthTimer = null;
            }
        };
        this._onProviderError = (error) => {
            this.emit('error', error);
        };
        if (!this.hasSubscriptions) {
            console.warn('Api will be available in a limited mode since the provider does not support subscriptions');
        }
        // We only register the types (global) if this is not a cloned instance.
        // Do right up-front, so we get in the user types before we are actually
        // doing anything on-chain, this ensures we have the overrides in-place
        if (!options.source && options.types) {
            this.registerTypes(options.types);
        }
        this._rpc = this.decorateRpc(this._rpcCore, this.decorateMethod);
        this._rx.rpc = this.decorateRpc(this._rpcCore, this.rxDecorateMethod);
        this._queryMulti = this.decorateMulti(this.decorateMethod);
        this._rx.queryMulti = this.decorateMulti(this.rxDecorateMethod);
        this._rx.signer = options.signer;
        this._rpcCore.provider.on('disconnected', this._onProviderDisconnect);
        this._rpcCore.provider.on('error', this._onProviderError);
        this._rpcCore.provider.on('connected', this._onProviderConnect);
        // If the provider was instantiated earlier, and has already emitted a
        // 'connected' event, then the `on('connected')` won't fire anymore. To
        // cater for this case, we call manually `this._onProviderConnect`.
        if (this._rpcCore.provider.isConnected()) {
            this._onProviderConnect();
        }
    }
    loadMeta() {
        return __awaiter(this, void 0, void 0, function* () {
            const { metadata = {} } = this._options;
            // only load from on-chain if we are not a clone (default path), alternatively
            // just use the values from the source instance provided
            if (!this._options.source || !this._options.source._isReady) {
                this._runtimeMetadata = yield this.metaFromChain(metadata);
            }
            else {
                this._runtimeMetadata = yield this.metaFromSource(this._options.source);
            }
            return this.initFromMeta(this._runtimeMetadata);
        });
    }
    // eslint-disable-next-line @typescript-eslint/require-await
    metaFromSource(source) {
        return __awaiter(this, void 0, void 0, function* () {
            this._extrinsicType = source.extrinsicVersion;
            this._runtimeVersion = source.runtimeVersion;
            this._genesisHash = source.genesisHash;
            const methods = [];
            // manually build a list of all available methods in this RPC, we are
            // going to filter on it to align the cloned RPC without making a call
            Object.keys(source.rpc).forEach((section) => {
                Object.keys(source.rpc[section]).forEach((method) => {
                    methods.push(`${section}_${method}`);
                });
            });
            this.filterRpcMethods(methods);
            return source.runtimeMetadata;
        });
    }
    metaFromChain(optMetadata) {
        return __awaiter(this, void 0, void 0, function* () {
            const { typesChain, typesSpec } = this._options;
            const [genesisHash, runtimeVersion, chain, chainProps] = yield Promise.all([
                this._rpcCore.chain.getBlockHash(0).toPromise(),
                this._rpcCore.state.getRuntimeVersion().toPromise(),
                this._rpcCore.system.chain().toPromise(),
                this._rpcCore.system.properties().toPromise()
            ]);
            // based on the node spec & chain, inject specific type overrides
            this.registerTypes(known_1.getChainTypes(chain, runtimeVersion, typesChain, typesSpec));
            // filter the RPC methods (this does an rpc-methods call)
            yield this.filterRpc();
            // retrieve metadata, either from chain  or as pass-in via options
            const metadataKey = `${genesisHash}-${runtimeVersion.specVersion}`;
            const metadata = metadataKey in optMetadata
                ? new types_1.Metadata(this.registry, optMetadata[metadataKey])
                : yield this._rpcCore.state.getMetadata().toPromise();
            // set our chain version & genesisHash as returned
            this._genesisHash = genesisHash;
            this._runtimeVersion = runtimeVersion;
            // set the global ss58Format as detected by the chain
            util_crypto_1.setSS58Format(chainProps.ss58Format.unwrapOr(new types_1.u32(this.registry, defaults_1.default.prefix)).toNumber());
            // get unique types & validate
            metadata.getUniqTypes(false);
            return metadata;
        });
    }
    initFromMeta(metadata) {
        return __awaiter(this, void 0, void 0, function* () {
            // inject types based on metadata, if applicable
            this.registerTypes(known_1.getMetadataTypes(metadata.version));
            const decoratedMeta = new Decorated_1.default(this.registry, metadata);
            const metaExtrinsic = metadata.asLatest.extrinsic;
            // only inject if we are not a clone (global init)
            if (metaExtrinsic.version.gtn(0)) {
                this._extrinsicType = metaExtrinsic.version.toNumber();
            }
            else if (!this._options.source) {
                // detect the extrinsic version in-use based on the last block
                const { block: { extrinsics: [firstTx] } } = yield this._rpcCore.chain.getBlock().toPromise();
                // If we haven't sync-ed to 1 yes, this won't have any values
                this._extrinsicType = firstTx ? firstTx.type : Extrinsic_1.LATEST_EXTRINSIC_VERSION;
            }
            this._extrinsics = this.decorateExtrinsics(decoratedMeta.tx, this.decorateMethod);
            this._query = this.decorateStorage(decoratedMeta.query, this.decorateMethod);
            this._consts = decoratedMeta.consts;
            this._rx.extrinsicType = this._extrinsicType;
            this._rx.genesisHash = this._genesisHash;
            this._rx.runtimeVersion = this._runtimeVersion;
            this._rx.tx = this.decorateExtrinsics(decoratedMeta.tx, this.rxDecorateMethod);
            this._rx.query = this.decorateStorage(decoratedMeta.query, this.rxDecorateMethod);
            this._rx.consts = decoratedMeta.consts;
            // derive is last, since it uses the decorated rx
            this._rx.derive = this.decorateDeriveRx(this.rxDecorateMethod);
            this._derive = this.decorateDerive(this.decorateMethod);
            return true;
        });
    }
}
exports.default = Init;
