"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_browserify_1 = require("crypto-browserify");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const long_1 = tslib_1.__importDefault(require("long"));
const light_1 = require("protobufjs/light");
const identity_wrapper_1 = tslib_1.__importDefault(require("../darc/identity-wrapper"));
const protobuf_1 = require("../protobuf");
const beautifier_1 = require("./beautifier");
const instance_1 = tslib_1.__importDefault(require("./instance"));
/**
 * List of instructions to send to a byzcoin chain
 */
class ClientTransaction extends light_1.Message {
    /**
     * @deprecated Use the static function make to create transactions compatible with
     * latest versions of the ByzCoin protocol.
     */
    constructor(props) {
        super(props);
        this.instructions = this.instructions || [];
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.ClientTransaction", ClientTransaction, Instruction);
    }
    /**
     * Create a transaction from the list of instructions.
     * @param version   Version of the ByzCoin protocol
     * @param instrs    List of instructions
     */
    static make(version, ...instrs) {
        if (version >= 1) {
            instrs = instrs.map((i) => new InstructionV1(i));
        }
        return new ClientTransaction({ instructions: instrs });
    }
    /**
     * Sign the hash of the instructions using the list of signers
     * @param signers List of signers
     */
    signWith(signers) {
        const ctxHash = this.hash();
        if (signers.length !== this.instructions.length) {
            throw new Error("need same number of signers as instructions");
        }
        this.instructions.forEach((instr, i) => instr.signWith(ctxHash, signers[i]));
    }
    /**
     * Fetch the counters and update the instructions accordingly
     * @param rpc       The RPC to use to fetch
     * @param signers   List of signers
     */
    updateCounters(rpc, signers) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.instructions.length === 0) {
                return;
            }
            if (signers.length !== this.instructions.length) {
                return Promise.reject("length of signers and instructions do not match");
            }
            // Get all counters from all signers of all instructions and map them into an object.
            const uniqueSigners = lodash_1.default.uniq(lodash_1.default.flatten(signers));
            yield rpc.updateCachedCounters(uniqueSigners);
            // Iterate over the instructions, and store the appropriate signers and counters, while
            // increasing those that have been used.
            for (let i = 0; i < this.instructions.length; i++) {
                signers[i].forEach((signer) => {
                    this.instructions[i].signerIdentities.push(identity_wrapper_1.default.fromIdentity(signer));
                    this.instructions[i].signerCounter.push(rpc.getNextCounter(signer));
                });
            }
        });
    }
    updateCountersAndSign(rpc, signers) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            // Extend the signers to the number of instructions if there is only one signer.
            if (signers.length === 1) {
                for (let i = 1; i < this.instructions.length; i++) {
                    signers.push(signers[0]);
                }
            }
            yield this.updateCounters(rpc, signers);
            this.signWith(signers);
        });
    }
    /**
     * Hash the instructions' hash
     * @returns a buffer of the hash
     */
    hash() {
        const h = crypto_browserify_1.createHash("sha256");
        this.instructions.forEach((i) => h.update(i.hash()));
        return h.digest();
    }
}
exports.ClientTransaction = ClientTransaction;
/**
 * An instruction represents one action
 */
class Instruction extends light_1.Message {
    constructor(props) {
        super(props);
        this.signerCounter = this.signerCounter || [];
        this.signerIdentities = this.signerIdentities || [];
        this.signatures = this.signatures || [];
        /* Protobuf aliases */
        Object.defineProperty(this, "instanceid", {
            get() {
                return this.instanceID;
            },
            set(value) {
                this.instanceID = value;
            },
        });
        Object.defineProperty(this, "signercounter", {
            get() {
                return this.signerCounter;
            },
            set(value) {
                this.signerCounter = value;
            },
        });
        Object.defineProperty(this, "signeridentities", {
            get() {
                return this.signerIdentities;
            },
            set(value) {
                this.signerIdentities = value;
            },
        });
    }
    /**
     * Get the type of the instruction
     * @returns the type as a number
     */
    get type() {
        if (this.spawn) {
            return Instruction.typeSpawn;
        }
        if (this.invoke) {
            return Instruction.typeInvoke;
        }
        if (this.delete) {
            return Instruction.typeDelete;
        }
        throw new Error("instruction without type");
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.Instruction", Instruction, identity_wrapper_1.default, Spawn, Invoke, Delete);
    }
    /**
     * Helper to create a spawn instruction
     * @param iid           The instance ID
     * @param contractID    The contract name
     * @param args          Arguments for the instruction
     * @returns the instruction
     */
    static createSpawn(iid, contractID, args) {
        return new Instruction({
            instanceID: iid,
            signerCounter: [],
            spawn: new Spawn({ contractID, args }),
        });
    }
    /**
     * Helper to create a invoke instruction
     * @param iid           The instance ID
     * @param contractID    The contract name
     * @param command       The command to invoke
     * @param args          The list of arguments
     * @returns the instruction
     */
    static createInvoke(iid, contractID, command, args) {
        return new Instruction({
            instanceID: iid,
            invoke: new Invoke({ command, contractID, args }),
            signerCounter: [],
        });
    }
    /**
     * Helper to create a delete instruction
     * @param iid           The instance ID
     * @param contractID    The contract name
     * @returns the instruction
     */
    static createDelete(iid, contractID) {
        return new Instruction({
            delete: new Delete({ contractID }),
            instanceID: iid,
            signerCounter: [],
        });
    }
    /**
     * Use the signers to make a signature of the hash
     * @param ctxHash The client transaction hash
     * @param signers The list of signers
     */
    signWith(ctxHash, signers) {
        // @ts-ignore
        this.signatures = signers.map((s) => s.sign(ctxHash));
    }
    /**
     * Set the signer counters and identities
     * @param counters      List of counters
     * @param identities    List of identities
     */
    setCounters(counters, identities) {
        // @ts-ignore
        this.signerCounter = counters;
        // @ts-ignore
        this.signerIdentities = identities;
    }
    /**
     * Fetch and update the counters
     * @param rpc       the RPC to use to fetch
     * @param signers   the list of signers
     */
    updateCounters(rpc, signers) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const counters = yield rpc.getSignerCounters(signers, 1);
            this.setCounters(counters, signers.map((s) => identity_wrapper_1.default.fromIdentity(s)));
        });
    }
    /**
     * Hash the instruction
     * @returns a buffer of the hash
     */
    hash() {
        return this.hashForVersion(0);
    }
    /**
     * Get the unique identifier of the instruction
     * @returns the id as a buffer
     */
    deriveId(what = "") {
        const h = crypto_browserify_1.createHash("sha256");
        h.update(this.hash());
        const b = Buffer.alloc(4);
        b.writeUInt32LE(this.signatures.length, 0);
        h.update(b);
        this.signatures.forEach((sig) => {
            b.writeUInt32LE(sig.length, 0);
            h.update(b);
            h.update(sig);
        });
        h.update(Buffer.from(what));
        return h.digest();
    }
    /**
     * Returns the instance ID of the new instance to be spawned. If the argument 'arg' exists, it is used to
     * calculate a client-defined ID. If the argument 'arg' doesn't exist, the normal deriveId is used.
     * It throws an error if the instruction is not of type spawn.
     *
     * @param what to be used if arg is not given
     * @param arg holding the pre-image of the instanceID
     */
    deriveIdArg(what = "", arg = "preID") {
        if (this.type !== Instruction.typeSpawn) {
            throw new Error("can only search args for spawn-instruction");
        }
        const id = this.spawn.args.find((a) => a.name === arg);
        if (id) {
            return instance_1.default.calcInstID(this.spawn.contractID, id.value);
        }
        return this.deriveId(what);
    }
    /**
     * Return a general-purpose json representation of an instruction that is
     * contracts aware, ie. knows how to render specific contracts arguments. It
     * returns the following status:
     *
     * 0: the default hex representation is used
     * 1: has been specifically interpreted
     *
     * The JSON has the following structure:
     *
     *  {
     *      status: 0 | 1
     *      type: "spawn|invoke|delete",
     *      contract: "config|darc|..."
     *      args: [
     *          {
     *              "name": "<NAME>",
     *              "value": "<VALUE>",    // Arg value or its summary
     *              "full": "<FULL VALUE>" // Optional. useful if the argument
     *                                     // have a very long representation.
     *          }
     *          ...
     *      ]
     *  }
     *
     */
    beautify() {
        const res = { status: 0, type: "spawn", contract: "", args: [] };
        switch (this.type) {
            case Instruction.typeSpawn:
                res.type = "spawn";
                switch (this.spawn.contractID) {
                    case "config":
                        res.contract = "config";
                        res.args = beautifier_1.ConfigBeautifier.Spawn(this.spawn.args);
                        break;
                    case "darc":
                        res.contract = "darc";
                        res.args = beautifier_1.DarcBeautifier.Spawn(this.spawn.args);
                        break;
                    case "coin":
                        res.contract = "coin";
                        res.args = beautifier_1.CoinBeautifier.Spawn(this.spawn.args);
                        break;
                    case "credential":
                        res.contract = "credential";
                        res.args = beautifier_1.CredentialBeautifier.Spawn(this.spawn.args);
                        break;
                    default:
                        res.contract = this.spawn.contractID;
                        res.status = 1;
                        res.args = beautifier_1.DefaultBeautifier.Hex(this.spawn.args);
                }
                break;
            case Instruction.typeInvoke:
                res.type = "invoke";
                switch (this.invoke.contractID) {
                    case "config":
                        res.contract = "config";
                        res.args = beautifier_1.ConfigBeautifier.Invoke(this.invoke.args);
                        break;
                    case "darc":
                        res.contract = "darc";
                        res.args = beautifier_1.DarcBeautifier.Invoke(this.invoke.args);
                        break;
                    case "coin":
                        res.contract = "coin";
                        res.args = beautifier_1.CoinBeautifier.Invoke(this.invoke.args);
                        break;
                    case "credential":
                        res.contract = "credential";
                        res.args = beautifier_1.CredentialBeautifier.Invoke(this.invoke.args);
                        break;
                    default:
                        res.contract = this.invoke.contractID;
                        res.status = 1;
                        res.args = beautifier_1.DefaultBeautifier.Hex(this.invoke.args);
                }
                break;
            case Instruction.typeDelete:
                res.type = "delete";
                switch (this.delete.contractID) {
                    default:
                        res.contract = this.delete.contractID;
                        res.status = 1;
                }
                break;
            default:
                throw new Error("unknown instruction type");
        }
        return res;
    }
    hashForVersion(version) {
        const h = crypto_browserify_1.createHash("sha256");
        h.update(this.instanceID);
        h.update(Buffer.from([this.type]));
        let args = [];
        switch (this.type) {
            case Instruction.typeSpawn:
                h.update(this.spawn.contractID);
                args = this.spawn.args;
                break;
            case Instruction.typeInvoke:
                h.update(this.invoke.contractID);
                if (version >= 1) {
                    h.update(this.invoke.command);
                }
                args = this.invoke.args;
                break;
            case Instruction.typeDelete:
                h.update(this.delete.contractID);
                break;
        }
        args.forEach((arg) => {
            const nameBuf = Buffer.from(arg.name);
            const nameLenBuf = Buffer.from(long_1.default.fromNumber(nameBuf.length).toBytesLE());
            h.update(nameLenBuf);
            h.update(arg.name);
            const valueLenBuf = Buffer.from(long_1.default.fromNumber(arg.value.length).toBytesLE());
            h.update(valueLenBuf);
            h.update(arg.value);
        });
        this.signerCounter.forEach((sc) => {
            h.update(Buffer.from(sc.toBytesLE()));
        });
        this.signerIdentities.forEach((si) => {
            const buf = si.toBytes();
            const lenBuf = Buffer.from(long_1.default.fromNumber(buf.length).toBytesLE());
            h.update(lenBuf);
            h.update(si.toBytes());
        });
        return h.digest();
    }
}
exports.Instruction = Instruction;
Instruction.typeSpawn = 0;
Instruction.typeInvoke = 1;
Instruction.typeDelete = 2;
/**
 * Extension of the initial version of an instruction to include the
 * invoke command in the hash.
 * Use ClientTransaction.make in order to create compatible transactions.
 */
class InstructionV1 extends Instruction {
    hash() {
        return this.hashForVersion(1);
    }
}
/**
 * Argument of an instruction
 */
class Argument extends light_1.Message {
    constructor(props) {
        super(props);
        this.value = Buffer.from(this.value || protobuf_1.EMPTY_BUFFER);
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.Argument", Argument);
    }
}
exports.Argument = Argument;
/**
 * Spawn instruction that will create instances
 */
class Spawn extends light_1.Message {
    constructor(props) {
        super(props);
        this.args = this.args || [];
        /* Protobuf aliases */
        Object.defineProperty(this, "contractid", {
            get() {
                return this.contractID;
            },
            set(value) {
                this.contractID = value;
            },
        });
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.Spawn", Spawn, Argument);
    }
}
exports.Spawn = Spawn;
/**
 * Invoke instruction that will update an existing instance
 */
class Invoke extends light_1.Message {
    constructor(props) {
        super(props);
        this.args = this.args || [];
        /* Protobuf aliases */
        Object.defineProperty(this, "contractid", {
            get() {
                return this.contractID;
            },
            set(value) {
                this.contractID = value;
            },
        });
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.Invoke", Invoke, Argument);
    }
}
exports.Invoke = Invoke;
/**
 * Delete instruction that will delete an instance
 */
class Delete extends light_1.Message {
    constructor(props) {
        super(props);
        Object.defineProperty(this, "contractid", {
            get() {
                return this.contractID;
            },
            set(value) {
                this.contractID = value;
            },
        });
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("byzcoin.Delete", Delete);
    }
}
exports.Delete = Delete;
ClientTransaction.register();
Instruction.register();
Argument.register();
Spawn.register();
Invoke.register();
Delete.register();
