"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const long_1 = tslib_1.__importDefault(require("long"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const darc_1 = require("../darc");
const darc_2 = tslib_1.__importDefault(require("../darc/darc"));
const identity_ed25519_1 = tslib_1.__importDefault(require("../darc/identity-ed25519"));
const log_1 = tslib_1.__importDefault(require("../log"));
const network_1 = require("../network");
const skipchain_1 = require("../skipchain");
const config_1 = tslib_1.__importDefault(require("./config"));
const darc_instance_1 = tslib_1.__importDefault(require("./contracts/darc-instance"));
const proof_1 = tslib_1.__importStar(require("./proof"));
const proto_1 = require("./proto");
const check_auth_1 = tslib_1.__importStar(require("./proto/check-auth"));
const requests_1 = require("./proto/requests");
const stream_1 = require("./proto/stream");
exports.currentVersion = 5;
exports.versionViewchange = 4;
exports.versionPreID = 5;
exports.CONFIG_INSTANCE_ID = Buffer.alloc(32, 0);
/**
 * ByzCoinRPC represents one byzcoin-representation.
 *
 * TODO:
 * Split this into two classes:
 * - one with only static methods mapped to the API of byzcoin
 * - one representing a single byzcoin-instance
 */
class ByzCoinRPC {
    constructor() {
        this.cache = new Map();
    }
    get genesisID() {
        return this.genesis.computeHash();
    }
    get latest() {
        return new skipchain_1.SkipBlock(this._latest);
    }
    set latest(sb) {
        // @ts-ignore
        if (!global.jasmine) {
            log_1.default.warn("This should only be used in tests");
        }
        this._latest = sb;
        this.updateInstances.next(sb.index);
    }
    /**
     * Helper to create a genesis darc
     * @param signers       Authorized signers
     * @param roster        Roster that will be used
     * @param description   An optional description for the chain
     */
    static makeGenesisDarc(signers, roster, description) {
        if (signers.length === 0) {
            throw new Error("no identities");
        }
        const d = darc_2.default.createBasic(signers, signers, Buffer.from(description || "Genesis darc"));
        roster.list.forEach((srvid) => {
            d.addIdentity("invoke:config.view_change", identity_ed25519_1.default.fromPoint(srvid.getPublic()), darc_1.Rule.OR);
        });
        signers.forEach((signer) => {
            d.addIdentity("spawn:darc", signer, darc_1.Rule.OR);
            d.addIdentity("invoke:config.update_config", signer, darc_1.Rule.OR);
        });
        return d;
    }
    /**
     * Recreate a byzcoin RPC from a given roster
     * @param nodes either a roster or an initialized connection
     * @param skipchainID   The genesis block identifier
     * @param waitMatch how many times to wait for a match - useful if its called just after an addTransactionAndWait.
     * @param interval how long to wait between two attempts in waitMatch.
     * @param latest if given, use this to prove the current state of the blockchain. Needs to be trusted!
     * @param storage to be used to store instance caches
     * @param verify will skip checking of links if false
     * @returns a promise that resolves with the initialized ByzCoin instance
     */
    static fromByzcoin(nodes, skipchainID, waitMatch = 0, interval = 1000, latest, storage, verify = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const rpc = new ByzCoinRPC();
            if (nodes instanceof network_1.Roster) {
                rpc.conn = new network_1.RosterWSConnection(nodes, ByzCoinRPC.serviceName);
            }
            else {
                rpc.conn = nodes.copy(ByzCoinRPC.serviceName);
            }
            const skipchain = new skipchain_1.SkipchainRPC(rpc.conn);
            rpc.genesis = yield skipchain.getSkipBlock(skipchainID);
            rpc._latest = latest !== undefined ? latest : rpc.genesis;
            const ccProof = yield rpc.getProofFromLatest(exports.CONFIG_INSTANCE_ID, waitMatch, interval, verify);
            rpc.config = config_1.default.fromProof(ccProof);
            const di = yield darc_instance_1.default.fromByzcoin(rpc, ccProof.stateChangeBody.darcID, waitMatch, interval);
            rpc.genesisDarc = di.darc;
            rpc.db = storage ? storage : new LocalCache();
            return rpc;
        });
    }
    /**
     * Create a new byzcoin chain and returns an associated RPC
     * @param roster        The roster to use to create the genesis block
     * @param darc          The genesis darc
     * @param blockInterval The interval of block creation in nanoseconds
     * @param storage       To be used to store instance caches
     */
    static newByzCoinRPC(roster, darc, blockInterval, storage) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const leader = new network_1.LeaderConnection(roster, ByzCoinRPC.serviceName);
            const req = new requests_1.CreateGenesisBlock({
                blockInterval,
                darcContractIDs: [darc_instance_1.default.contractID],
                genesisDarc: darc,
                roster,
                version: exports.currentVersion,
            });
            const ret = yield leader.send(req, requests_1.CreateGenesisBlockResponse);
            return ByzCoinRPC.fromByzcoin(roster, ret.skipblock.hash, undefined, undefined, undefined, storage);
        });
    }
    /**
     * Getter for the genesis darc
     * @returns the genesis darc
     */
    getDarc() {
        return this.genesisDarc;
    }
    /**
     * Getter for the chain configuration
     * @returns the configuration
     */
    getConfig() {
        return this.config;
    }
    /**
     * Getter for the genesis block
     * @returns the genesis block
     */
    getGenesis() {
        return this.genesis;
    }
    /**
     * Getter for the ByzCoin protocol version of the
     * latest block known by the RPC client.
     */
    getProtocolVersion() {
        const header = proto_1.DataHeader.decode(this._latest.data);
        return header.version;
    }
    /**
     * Returns an observable proof for an instance that will be
     * automatically updated whenever the instance is changed.
     * It returns a BehaviorSubject, which is a special observable that
     * keeps the current value.
     * It also caches all requests, so if there are two requests for the
     * same id, no new observable will be created.
     * It connects to `getNewBlocks` to be informed whenever a new block is
     * created.
     * @param id of the instance to return
     * @throws an error if the instance does not exist
     */
    instanceObservable(id) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const idStr = id.toString("hex");
            const bs = this.cache.get(idStr);
            if (bs !== undefined) {
                return bs;
            }
            // Need to initialize it anyway, even if the instance is not in the cache.
            const updateInst = yield this.getUpdateInstances();
            // Check if the db already has a version, which might be outdated,
            // but still better than to wait for the network.
            // This makes it possible to have a quick display of a value that
            // might be outdated, but be informed as soon as the correct value arrives.
            const ipBuf = yield this.db.get(idStr);
            let dbIP;
            if (ipBuf !== undefined) {
                dbIP = proof_1.InclusionProof.decode(ipBuf);
            }
            else {
                try {
                    const gp = yield this.getUpdates([new requests_1.IDVersion({ id, version: long_1.default.fromNumber(0) })], requests_1.GetUpdatesRequest.sendVersion0);
                    if (gp.proofs.length !== 1) {
                        throw new Error("couldn't find this instance");
                    }
                    dbIP = gp.proofs[0];
                    yield this.db.set(idStr, Buffer.from(proof_1.InclusionProof.encode(dbIP).finish()));
                }
                catch (e) {
                    log_1.default.error("couldn't getUpdates", e);
                    throw new Error(e);
                }
            }
            if (!dbIP.exists(id)) {
                throw new Error("this instance does not exist");
            }
            // Create a new BehaviorSubject with the proof, which might not be
            // current, but a best guess from the db of a previous session.
            const bsNew = new rxjs_1.BehaviorSubject(new proof_1.default({ inclusionproof: dbIP, links: [], latest: this.latest }));
            this.cache.set(idStr, bsNew);
            if (ipBuf !== undefined) {
                // If the instance came from the cache, schedule update of all instances by simulating a new block.
                // Adding a 0.5 to the block index here makes sure that this only happens once between two blocks.
                // If there are more queries for instances, only cached values will be shown until the next new block.
                updateInst.next(this.latest.index + 0.5);
            }
            // Return the BehaviorSubject. The subscription on the getNewBlocks will take care
            // of all updates.
            return bsNew;
        });
    }
    /**
     * Defines how many nodes will be contacted in parallel when sending a message
     * @param p nodes to contact in parallel
     */
    setParallel(p) {
        this.conn.setParallel(p);
    }
    /**
     * Sends a transaction to byzcoin and waits for up to 'wait' blocks for the
     * transaction to be included in the global state. If more than 'wait' blocks
     * are created and the transaction is not included, an exception will be raised.
     *
     * @param transaction is the client transaction holding
     * one or more instructions to be sent to byzcoin.
     * @param wait indicates the number of blocks to wait for the
     * transaction to be included
     * @returns a promise that gets resolved if the block has been included
     */
    sendTransactionAndWait(transaction, wait = 10) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.AddTxRequest({
                inclusionwait: wait,
                skipchainID: this.genesis.hash,
                transaction,
                version: exports.currentVersion,
            });
            const counters = this.counters();
            // The error might be in the response, so we look for it and then reject the promise.
            const resp = yield this.conn.send(req, requests_1.AddTxResponse);
            // If the transaction has been successful, we update all cached counters. If the transaction
            // failed, all involved counters are reset and will have to be fetched again.
            transaction.instructions.forEach((instruction) => {
                instruction.signerIdentities.forEach((signer, i) => {
                    if (resp.error.length === 0) {
                        counters.set(signer.toString(), instruction.signerCounter[i]);
                    }
                    else {
                        counters.delete(signer.toString());
                    }
                });
            });
            if (resp.error.length === 0) {
                return Promise.resolve(resp);
            }
            return Promise.reject(new Error(resp.error));
        });
    }
    /**
     * Get the latest configuration for the chain and update the local
     * cache
     */
    updateConfig() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const pr = yield this.getProofFromLatest(exports.CONFIG_INSTANCE_ID);
            this.config = config_1.default.fromProof(pr);
            const darcIID = pr.stateChangeBody.darcID;
            const genesisDarcInstance = yield darc_instance_1.default.fromByzcoin(this, darcIID);
            this.genesisDarc = genesisDarcInstance.darc;
        });
    }
    /**
     * Gets a proof from byzcoin to show that a given instance is in the
     * global state. The proof always starts from the genesis block.
     *
     * @param id the instance key
     * @param waitMatch number of milliseconds to wait if the proof is false
     * @param interval how long to wait before checking for a match again
     * @param verify will skip checking of links if false
     * @return a promise that resolves with the proof, rejecting otherwise
     */
    getProof(id, waitMatch = 0, interval = 1000, verify = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!this.genesis) {
                throw new Error("RPC not initialized with the genesis block");
            }
            return this.getProofFrom(this.genesis, id, waitMatch, interval, verify);
        });
    }
    /**
     * Gets a proof from byzcoin to show that a given instance is in the
     * global state. The proof starts from the latest known block.
     * Caution: If you need to pass the Proof onwards to another server,
     * you must use getProof in order to create a complete standalone
     * proof starting from the genesis block.
     *
     * @param id the instance key
     * @param waitMatch number of milliseconds to wait if the proof is false
     * @param interval how long to wait before checking for a match again
     * @param verify will skip checking of links if false
     * @return a promise that resolves with the proof, rejecting otherwise
     */
    getProofFromLatest(id, waitMatch = 0, interval = 1000, verify = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this._latest === undefined) {
                throw new Error("no latest block found");
            }
            return this.getProofFrom(this._latest, id, waitMatch, interval, verify);
        });
    }
    /**
     * Gets a proof from byzcoin to show that a given instance is in the
     * global state. The proof starts from the block given in parameter.
     * Caution: If you need to pass the Proof onwards to another server,
     * you must use getProof in order to create a complete standalone
     * proof starting from the genesis block.
     *
     * @param from skipblock to start
     * @param id the instance key
     * @param waitMatch number of milliseconds to wait if the proof is false
     * @param interval how long to wait before checking for a match again
     * @param verify will skip checking of links if false
     * @return a promise that resolves with the proof, rejecting otherwise
     */
    getProofFrom(from, id, waitMatch = 0, interval = 1000, verify = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.GetProof({
                id: from.hash,
                key: id,
                version: exports.currentVersion,
            });
            const reply = yield this.conn.send(req, requests_1.GetProofResponse);
            if (waitMatch > 0 && !reply.proof.exists(id)) {
                return new Promise((resolve, reject) => {
                    setTimeout(() => {
                        this.getProofFrom(from, id, waitMatch - interval, interval).then(resolve, reject);
                    }, interval);
                });
            }
            if (verify) {
                const err = reply.proof.verifyFrom(from);
                if (err) {
                    throw err;
                }
            }
            this._latest = reply.proof.latest;
            return reply.proof;
        });
    }
    /**
     * Gets one or more proofs for updated instances. The client can ask an array of instances and their latest
     * known version, and the service will return all instances that have been updated in the meantime.
     *
     * Instead of returning full proofs, it only returns the InclusionProofs.
     *
     * @param instances that will be queried for changes
     * @param flags are ORed to indicate the following behaviors:
     *    - GetUpdatesRequest.sendVersion0 (=1) will make GetUpdates to send all instances with
     *     version 0, even those that are note updated.
     *    - GetUpdatesRequest.sendMissingProofs (=2) will make GetUpdates send proofs for missing
     *     instances. If not present, missing instances are ignored.
     */
    getUpdates(instances, flags = long_1.default.fromNumber(0)) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.GetUpdatesRequest({
                flags,
                instances,
                skipchainid: this.genesisID,
            });
            return this.conn.send(req, requests_1.GetUpdatesReply);
        });
    }
    /**
     * Get the latest counter for the given signers and increase it with a given value
     *
     * @param ids The identifiers of the signers
     * @param add The increment
     * @returns the ordered list of counters
     */
    getSignerCounters(ids, add = 0) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.GetSignerCounters({
                signerIDs: ids.map((id) => id.toString()),
                skipchainID: this.genesis.hash,
            });
            const rep = yield this.conn.send(req, requests_1.GetSignerCountersResponse);
            const counters = this.counters();
            ids.forEach((counter, i) => counters.set(counter.toString(), rep.counters[i]));
            return rep.counters.map((c) => c.add(add));
        });
    }
    /**
     * This keeps track of the know counters, thus speeding up signing with known keys. It
     * also solves the problem of counters sometimes being out of synch with the chain.
     * After the call returns, all signers are in ClientTransaction.counters.
     *
     * @param rpc should allow updates for counters
     * @param signers the signers needed
     */
    updateCachedCounters(signers) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const counters = this.counters();
            const newSigners = signers.filter((signer) => {
                return counters.has(signer.toString()) === false;
            });
            if (newSigners.length > 0) {
                yield this.getSignerCounters(newSigners, 0);
            }
            return signers.map((signer) => {
                return counters.get(signer.toString());
            });
        });
    }
    /**
     * Clears the counters, in case of an error.
     */
    clearCounters() {
        this.counters().clear();
    }
    /**
     * Returns the next value for the counter and updates the cache.
     */
    getNextCounter(signer) {
        const counters = this.counters();
        const c = counters.get(signer.toString()).add(1);
        counters.set(signer.toString(), c);
        return c;
    }
    /**
     * checks the authorization of a set of identities with respect to a given darc. This calls
     * an OmniLedger node and trusts it to return the name of the actions that a hypothetical set of
     * signatures from the given identities can execute using the given darc.
     *
     * This is useful if a darc delegates one or more actions to other darc, who delegate also, so
     * this call will test what actions are possible to be executed.
     *
     * @param darcID the base darc whose actions are verified
     * @param identities the set of identities that are hypothetically signing
     */
    checkAuthorization(byzCoinID, darcID, ...identities) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new check_auth_1.default({
                byzcoinID: byzCoinID,
                darcID,
                identities,
                version: exports.currentVersion,
            });
            const reply = yield this.conn.send(req, check_auth_1.CheckAuthorizationResponse);
            return reply.actions;
        });
    }
    /**
     * The returned BehaviorSubject replays all new blocks to all listeners.
     * The streaming is attached to the first node of the connectionlist.
     */
    getNewBlocks() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.newBlock !== undefined) {
                return this.newBlock;
            }
            // Get the latest block - this is faster than getUpdateBlocks
            yield this.getProofFromLatest(exports.CONFIG_INSTANCE_ID);
            this.newBlock = new rxjs_1.BehaviorSubject(this._latest);
            this.listenNewBlocks();
            return this.newBlock;
        });
    }
    /**
     * Closes an eventual newBlock websocket. All connected BehaviorSubjects
     * will get a 'completed' message.
     */
    closeNewBlocks() {
        if (this.newBlock) {
            this.newBlock = undefined;
            if (this.newBlockWS) {
                this.newBlockWS.close(1000);
            }
        }
    }
    /**
     * Returns the latest version of an instance and the block-number it's been created or modified in. The node
     * might not have the version available.
     *
     * @param instanceid
     * @param version
     */
    getInstanceVersion(instanceid, version) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.GetInstanceVersion({
                instanceid,
                skipchainid: this.genesisID,
                version,
            });
            return this.conn.send(req, requests_1.GetInstanceVersionResponse);
        });
    }
    /**
     * Returns the latest version of an instance and the block-number it's been created or modified in.
     *
     * @param instanceid
     * @param version
     */
    getLastInstanceVersion(instanceid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.GetLastInstanceVersion({
                instanceid,
                skipchainid: this.genesisID,
            });
            return this.conn.send(req, requests_1.GetInstanceVersionResponse);
        });
    }
    /**
     * Returns a given version of an instance and the block-number it's in. The node might not have the version
     * available.
     *
     * @param instanceid
     * @param version
     */
    getAllInstanceVersions(instanceid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const req = new requests_1.GetAllInstanceVersion({
                instanceid,
                skipchainid: this.genesisID,
            });
            const response = yield this.conn.send(req, requests_1.GetAllInstanceVersionResponse);
            return response.statechanges;
        });
    }
    /**
     * For every new block received, this method calls getUpdates with all instance-IDs of the cache.
     * If an instance has been updated onchain, it calls 'next' with the new value.
     * This means that for every instance returned from instanceObservable, it will automatically be updated.
     */
    getUpdateInstances() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.updateInstances === undefined) {
                // For every new block, send the latest known version of all instances, and then
                // call `next` for all received updates.
                this.updateInstances = new rxjs_1.BehaviorSubject(this.latest.index);
                (yield this.getNewBlocks()).subscribe((block) => this.updateInstances.next(block.index));
                this.updateInstances.pipe(operators_1.distinctUntilChanged((a, b) => a === b), operators_1.flatMap(() => {
                    const idvs = [...this.cache.values()].map((pr) => new requests_1.IDVersion({
                        id: pr.getValue().key,
                        version: pr.getValue().stateChangeBody.version,
                    }));
                    return this.getUpdates(idvs);
                })).subscribe({
                    next: (updates) => {
                        updates.proofs.forEach((update) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                            const ids = update.key.toString("hex");
                            yield this.db.set(ids, Buffer.from(proof_1.InclusionProof.encode(update).finish()));
                            const pr = new proof_1.default({
                                inclusionproof: update,
                                latest: updates.latest,
                                links: [],
                            });
                            this.cache.get(ids).next(pr);
                        }));
                        log_1.default.lvl2("Processed", updates.proofs.length);
                    },
                });
            }
            return this.updateInstances;
        });
    }
    listenNewBlocks() {
        const msgBlock = new stream_1.StreamingRequest({
            id: this.genesisID,
        });
        if (this.subscriberListenOnline === undefined) {
            if (typeof window !== "undefined") {
                try {
                    this.subscriberListenOnline = rxjs_1.fromEvent(window, "online").subscribe(() => {
                        log_1.default.warn("Got online on", new Date());
                        this.listenNewBlocks();
                        this.updateInstances.next(this.latest.index);
                    });
                }
                catch (e) {
                    log_1.default.error(e);
                }
            }
        }
        if (this.subscriberNewBlocks !== undefined) {
            this.subscriberNewBlocks.unsubscribe();
        }
        log_1.default.lvl3("Starting new subscription for newBlocks");
        this.subscriberNewBlocks = this.conn.sendStream(msgBlock, stream_1.StreamingResponse).pipe(operators_1.map(([sr, ws]) => {
            this.newBlockWS = ws;
            return sr.block;
        }), operators_1.tap((sb) => this._latest = sb)).subscribe({
            complete: () => {
                setTimeout(() => this.listenNewBlocks(), 1000);
            },
            error: (err) => {
                log_1.default.error("while listening to blocks:", err);
                setTimeout(() => this.listenNewBlocks(), 1000);
            },
            next: (sb) => {
                if (this.newBlock) {
                    log_1.default.lvl2("Got new block", sb.index);
                    this.newBlock.next(sb);
                }
            },
        });
    }
    counters() {
        const idStr = this.genesisID.toString("hex");
        if (!ByzCoinRPC.staticCounters.has(idStr)) {
            ByzCoinRPC.staticCounters.set(idStr, new Map());
        }
        return ByzCoinRPC.staticCounters.get(idStr);
    }
}
exports.default = ByzCoinRPC;
ByzCoinRPC.serviceName = "ByzCoin";
ByzCoinRPC.staticCounters = new Map();
/**
 * LocalCache wraps a Map<string, Buffer> to be used by the Marshaller.
 */
class LocalCache {
    constructor() {
        this.cache = new Map();
    }
    get(key) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return this.cache.get(key);
        });
    }
    set(key, value) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            this.cache.set(key, value);
        });
    }
}
exports.LocalCache = LocalCache;
