"use strict";
/* eslint-disable no-dupe-class-members */
// 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 rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const types_1 = require("../../types");
const util_1 = require("@polkadot/util");
const util_2 = require("../util");
const Result_1 = __importDefault(require("./Result"));
// The default for 6s allowing for 5min eras. When translating this to faster blocks -
//   - 4s = (10 / 15) * 5 = 3.33m
//   - 2s = (10 / 30) * 5 = 1.66m
const BLOCKTIME = 6;
const ONE_MINUTE = 60 / BLOCKTIME;
const DEFAULT_MORTAL_LENGTH = 5 * ONE_MINUTE;
function createClass({ api, apiType, decorateMethod }) {
    // an instance of the base extrinsic for us to extend
    const Extrinsic = types_1.ClassOf(api.registry, 'Extrinsic');
    return class Submittable extends Extrinsic {
        constructor(registry, extrinsic) {
            super(registry, extrinsic, { version: api.extrinsicType });
            this._sendObservable = (updateId = -1) => {
                return this._api.rpc.author
                    .submitExtrinsic(this)
                    .pipe(operators_1.tap((hash) => {
                    this._updateSigner(updateId, hash);
                }));
            };
            this._subscribeObservable = (updateId = -1) => {
                return this._api.rpc.author
                    .submitAndWatchExtrinsic(this)
                    .pipe(operators_1.switchMap((status) => this._statusObservable(status)), operators_1.tap((status) => {
                    this._updateSigner(updateId, status);
                }));
            };
            // NOTE here we actually override nonce if it was specified (backwards compat for
            // the previous signature - don't let userspace break, but allow then time to upgrade)
            this._optionsOrNonce = (optionsOrNonce) => {
                return util_1.isBn(optionsOrNonce) || util_1.isNumber(optionsOrNonce)
                    ? { nonce: optionsOrNonce }
                    : optionsOrNonce;
            };
            this._api = api;
            this._decorateMethod = decorateMethod;
            this._ignoreStatusCb = apiType === 'rxjs';
        }
        // calculate the payment info for this transaction (if signed and submitted)
        paymentInfo(account, options) {
            const [allOptions] = this._makeSignAndSendOptions(options);
            const address = util_2.isKeyringPair(account) ? account.address : account.toString();
            return this._decorateMethod(() => this._getPrelimState(address, allOptions).pipe(operators_1.first(), operators_1.switchMap(([nonce, header]) => {
                // setup our options (same way as in signAndSend)
                const eraOptions = this._makeEraOptions(allOptions, { header, nonce });
                const signOptions = this._makeSignOptions(eraOptions, {});
                // add a fake signature to the extrinsic
                this.signFake(address, signOptions);
                return this._api.rpc.payment.queryInfo(this.toHex());
            })))();
        }
        // sign a transaction, returning the this to allow chaining, i.e. .sign(...).send()
        sign(account, optionsOrNonce) {
            super.sign(account, this._makeSignOptions(this._optionsOrNonce(optionsOrNonce), {}));
            return this;
        }
        // signs a transaction, returning `this` to allow chaining. E.g.: `sign(...).send()`
        //
        // also supports signing through external signers
        signAsync(account, optionsOrNonce) {
            return this._decorateMethod(() => this._signObservable(account, optionsOrNonce).pipe(operators_1.mapTo(this)))();
        }
        // signAndSend implementation for all 3 cases above
        signAndSend(account, optionsOrStatus, optionalStatusCb) {
            const [options, statusCb] = this._makeSignAndSendOptions(optionsOrStatus, optionalStatusCb);
            const isSubscription = this._api.hasSubscriptions && (this._ignoreStatusCb || !!statusCb);
            return this._decorateMethod(() => this._signObservable(account, options).pipe(operators_1.switchMap((updateId) => isSubscription
                ? this._subscribeObservable(updateId)
                : this._sendObservable(updateId))) // FIXME This is wrong, SubmittableResult is _not_ a codec
            )(statusCb);
        }
        // send implementation for both immediate Hash and statusCb variants
        send(statusCb) {
            const isSubscription = this._api.hasSubscriptions && (this._ignoreStatusCb || !!statusCb);
            return this._decorateMethod(isSubscription
                ? this._subscribeObservable
                : this._sendObservable)(statusCb);
        }
        _makeSignAndSendOptions(optionsOrStatus, statusCb) {
            let options = {};
            if (util_1.isFunction(optionsOrStatus)) {
                statusCb = optionsOrStatus;
            }
            else {
                options = Object.assign({}, optionsOrStatus);
            }
            return [options, statusCb];
        }
        _signObservable(account, optionsOrNonce) {
            const address = util_2.isKeyringPair(account) ? account.address : account.toString();
            const options = this._optionsOrNonce(optionsOrNonce);
            let updateId;
            return this._getPrelimState(address, options).pipe(operators_1.first(), operators_1.mergeMap(([nonce, header]) => __awaiter(this, void 0, void 0, function* () {
                const eraOptions = this._makeEraOptions(options, { header, nonce });
                if (util_2.isKeyringPair(account)) {
                    this.sign(account, eraOptions);
                }
                else {
                    updateId = yield this._signViaSigner(address, eraOptions, header);
                }
            })), operators_1.mapTo(updateId));
        }
        _signViaSigner(address, options, header) {
            const _super = Object.create(null, {
                addSignature: { get: () => super.addSignature }
            });
            return __awaiter(this, void 0, void 0, function* () {
                const signer = options.signer || this._api.signer;
                util_1.assert(signer, 'No signer specified, either via api.setSigner or via sign options');
                const payload = types_1.createType(this.registry, 'SignerPayload', Object.assign(Object.assign({}, options), { address, method: this.method, blockNumber: header ? header.number : 0 }));
                let result;
                if (signer.signPayload) {
                    result = yield signer.signPayload(payload.toPayload());
                }
                else if (signer.signRaw) {
                    result = yield signer.signRaw(payload.toRaw());
                }
                else {
                    throw new Error('Invalid signer interface, it should implement either signPayload or signRaw (or both)');
                }
                // Here we explicitly call `toPayload()` again instead of working with an object
                // (reference) as passed to the signer. This means that we are sure that the
                // payload data is not modified from our inputs, but the signer
                _super.addSignature.call(this, address, result.signature, payload.toPayload());
                return result.id;
            });
        }
        _makeSignOptions(options, extras) {
            return Object.assign(Object.assign(Object.assign({ blockHash: this._api.genesisHash }, options), extras), { genesisHash: this._api.genesisHash, runtimeVersion: this._api.runtimeVersion, version: this._api.extrinsicType });
        }
        _makeEraOptions(options, { header, nonce }) {
            if (!header) {
                if (util_1.isNumber(options.era)) {
                    // since we have no header, it is immortal, remove any option overrides
                    // so we only supply the genesisHash and no era to the construction
                    delete options.era;
                    delete options.blockHash;
                }
                return this._makeSignOptions(options, { nonce });
            }
            return this._makeSignOptions(options, {
                blockHash: header.hash,
                era: types_1.createType(this.registry, 'ExtrinsicEra', {
                    current: header.number,
                    period: options.era || DEFAULT_MORTAL_LENGTH
                }),
                nonce
            });
        }
        _getPrelimState(address, options) {
            return rxjs_1.combineLatest([
                // if we have a nonce already, don't retrieve the latest, use what is there
                util_1.isUndefined(options.nonce)
                    // FIXME This apparently is having issues on latest Kusama for nonce retrieval,
                    // hence we are using the accountNonce only
                    // ? this._api.rpc.account.nextIndex
                    //   ? this._api.rpc.account.nextIndex(address)
                    //   : this._api.query.system.accountNonce(address)
                    ? this._api.query.system.accountNonce(address)
                    : rxjs_1.of(types_1.createType(this.registry, 'Index', options.nonce)),
                // if we have an era provided already or eraLength is <= 0 (immortal)
                // don't get the latest block, just pass null, handle in mergeMap
                (util_1.isUndefined(options.era) || (util_1.isNumber(options.era) && options.era > 0))
                    ? this._api.rpc.chain.getHeader()
                    : rxjs_1.of(null)
            ]);
        }
        _updateSigner(updateId, status) {
            if ((updateId !== -1) && this._api.signer && this._api.signer.update) {
                this._api.signer.update(updateId, status);
            }
        }
        _statusObservable(status) {
            if (!status.isFinalized) {
                return rxjs_1.of(new Result_1.default({ status }));
            }
            const blockHash = status.asFinalized;
            return rxjs_1.combineLatest([
                this._api.rpc.chain.getBlock(blockHash),
                this._api.query.system.events.at(blockHash)
            ]).pipe(operators_1.map(([signedBlock, allEvents]) => new Result_1.default({
                events: util_2.filterEvents(this.hash, signedBlock, allEvents),
                status
            })));
        }
    };
}
exports.default = createClass;
