"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_browserify_1 = require("crypto-browserify");
const light_1 = require("protobufjs/light");
const byzcoin_1 = require("../byzcoin");
const byzcoin_rpc_1 = require("../byzcoin/byzcoin-rpc");
const instance_1 = tslib_1.__importDefault(require("../byzcoin/instance"));
const protobuf_1 = require("../protobuf");
class CredentialsInstance extends instance_1.default {
    constructor(rpc, inst) {
        super(inst);
        this.rpc = rpc;
        this.inst = inst;
        if (inst.contractID.toString() !== CredentialsInstance.contractID) {
            throw new Error(`mismatch contract name: ${inst.contractID} vs ${CredentialsInstance.contractID}`);
        }
        this.credential = CredentialStruct.decode(inst.data);
    }
    /**
     * Generate the credential instance ID for a given public key
     *
     * @param buf the public key in marshalBinary form
     * @returns the id as a buffer
     */
    static credentialIID(buf) {
        const h = crypto_browserify_1.createHash("sha256");
        h.update(Buffer.from(CredentialsInstance.contractID));
        h.update(buf);
        return h.digest();
    }
    /**
     * Spawn a new credential instance from a darc
     *
     * @param bc        The RPC to use
     * @param darcID    The darc instance ID
     * @param signers   The list of signers for the transaction
     * @param cred      The credential to store
     * @param credID    the instanceID will be sha256("credential" | pub)
     * @param credDarcID replaces the darc stored in the new credential with credDarcID
     * @param preID     replaces credID for byzcoin-versions >= VersionPreID
     * @returns a promise that resolves with the new instance
     */
    static spawn(bc, darcID, signers, cred, credID, credDarcID, preID) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const args = [new byzcoin_1.Argument({ name: CredentialsInstance.argumentCredential, value: cred.toBytes() })];
            if (credID !== undefined) {
                args.push(new byzcoin_1.Argument({ name: CredentialsInstance.argumentCredID, value: credID }));
            }
            if (credDarcID !== undefined) {
                args.push(new byzcoin_1.Argument({ name: CredentialsInstance.argumentDarcID, value: credDarcID }));
            }
            if (preID && bc.getProtocolVersion() >= byzcoin_rpc_1.versionPreID) {
                args.push(new byzcoin_1.Argument({ name: "preID", value: preID }));
            }
            const inst = byzcoin_1.Instruction.createSpawn(darcID, CredentialsInstance.contractID, args);
            yield inst.updateCounters(bc, signers);
            const ctx = byzcoin_1.ClientTransaction.make(bc.getProtocolVersion(), inst);
            ctx.signWith([signers]);
            yield bc.sendTransactionAndWait(ctx, 10);
            return CredentialsInstance.fromByzcoin(bc, ctx.instructions[0].deriveId(), 1);
        });
    }
    /**
     * Create a new credential instance from a darc
     *
     * @param bc        The RPC to use
     * @param darcID    The darc instance ID
     * @param cred      The credential to store
     * @param credID    the instanceID will be sha256("credential" | credID)
     * @returns the new instance
     */
    static create(bc, darcID, cred, credID) {
        if (credID === undefined) {
            credID = crypto_browserify_1.randomBytes(32);
        }
        const inst = new instance_1.default({
            contractID: CredentialsInstance.contractID,
            darcID,
            data: cred.toBytes(),
            id: CredentialsInstance.credentialIID(credID),
        });
        return new CredentialsInstance(bc, inst);
    }
    /**
     * Get an existing credential instance using its instance ID by fetching
     * the proof.
     * @param bc    the byzcoin RPC
     * @param iid   the instance ID
     * @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.
     */
    static fromByzcoin(bc, iid, waitMatch = 0, interval = 1000) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new CredentialsInstance(bc, yield instance_1.default.fromByzcoin(bc, iid, waitMatch, interval));
        });
    }
    /**
     * Update the data of the crendetial instance by fetching the proof
     *
     * @returns a promise resolving with the instance on success
     */
    update() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const inst = yield instance_1.default.fromByzcoin(this.rpc, this.id);
            this.data = inst.data;
            this.darcID = inst.darcID;
            this.credential = CredentialStruct.decode(this.data);
            return this;
        });
    }
    /**
     * Get a credential attribute
     *
     * @param credential    The name of the credential
     * @param attribute     The name of the attribute
     * @returns the value of the attribute if it exists, undefined otherwise
     */
    getAttribute(credential, attribute) {
        return this.credential.getAttribute(credential, attribute);
    }
    /**
     * Set or update a credential attribute locally. The new credential is not sent to
     * the blockchain, for this you need to call sendUpdate.
     *
     * @param credential    Name of the credential
     * @param attribute     Name of the attribute
     * @param value         The value to set
     */
    setAttribute(credential, attribute, value) {
        this.credential.setAttribute(credential, attribute, value);
    }
    /**
     * Creates a transaction to update the credential and sends it to ByzCoin.
     *
     * @param owners a list of signers to fulfill the expression of the `invoke:credential.update` rule.
     * @param newCred the new credentialStruct to store in the instance.
     * @return the updated CredentialInstance
     */
    sendUpdate(owners, newCred) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (newCred !== undefined) {
                this.credential = newCred.copy();
            }
            const instr = byzcoin_1.Instruction.createInvoke(this.id, CredentialsInstance.contractID, CredentialsInstance.commandUpdate, [new byzcoin_1.Argument({ name: CredentialsInstance.argumentCredential, value: this.credential.toBytes() })]);
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), instr);
            yield ctx.updateCountersAndSign(this.rpc, [owners]);
            yield this.rpc.sendTransactionAndWait(ctx);
            return this;
        });
    }
    /**
     * Recovers an identity by giving a list of signatures from trusted people.
     *
     * @param pubKey the new public key for the identity. It will be stored as the new expression for the
     * signer-rule.
     * @param signatures a threshold list of signatures on the public key and the instanceID.
     */
    recoverIdentity(pubKey, signatures) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const sigBuf = Buffer.alloc(RecoverySignature.pubSig * signatures.length);
            signatures.forEach((s, i) => s.signature.copy(sigBuf, RecoverySignature.pubSig * i));
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(this.id, CredentialsInstance.contractID, "recover", [
                new byzcoin_1.Argument({ name: "signatures", value: sigBuf }),
                new byzcoin_1.Argument({ name: "public", value: pubKey.toProto() }),
            ]));
            yield this.rpc.sendTransactionAndWait(ctx);
        });
    }
}
exports.default = CredentialsInstance;
CredentialsInstance.contractID = "credential";
CredentialsInstance.commandUpdate = "update";
CredentialsInstance.argumentCredential = "credential";
CredentialsInstance.argumentCredID = "credentialID";
CredentialsInstance.argumentDarcID = "darcIDBuf";
/**
 * Data of a credential instance. It contains none, one or multiple
 * credentials.
 */
class CredentialStruct extends light_1.Message {
    constructor(properties) {
        super(properties);
        if (this.credentials === undefined) {
            this.credentials = [];
        }
        this.credentials = this.credentials.slice();
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("personhood.CredentialStruct", CredentialStruct, Credential);
    }
    /**
     * Get a credential attribute
     *
     * @param credential    The name of the credential
     * @param attribute     The name of the attribute
     * @returns the value of the attribute if it exists, undefined otherwise
     */
    getAttribute(credential, attribute) {
        const cred = this.credentials.find((c) => c.name === credential);
        if (cred === undefined) {
            return undefined;
        }
        const att = cred.attributes.find((a) => a.name === attribute);
        if (att === undefined) {
            return undefined;
        }
        return att.value;
    }
    /**
     * getCredential returns the credential with the given name, or undefined if
     * nothing found.
     * @param credential name of the credential to return
     */
    getCredential(credential) {
        return this.credentials.find((c) => c.name === credential);
    }
    /**
     * Overwrites the credential with name 'name' with the given credential.
     * If it doesn't exist, it will be appended to the list.
     *
     * @param name the name of the credential
     * @param cred the credential to store
     */
    setCredential(name, cred) {
        const index = this.credentials.findIndex((c) => c.name === name);
        if (index === -1) {
            this.credentials.push(cred);
        }
        else {
            this.credentials[index] = cred;
        }
    }
    /**
     * Set or update a credential attribute locally. The update is not sent to the blockchain.
     * For this you need to call CredentialInstance.sendUpdate().
     *
     * @param owner         Signer to use for the transaction
     * @param credential    Name of the credential
     * @param attribute     Name of the attribute
     * @param value         The value to set
     */
    setAttribute(credential, attribute, value) {
        if (value === undefined) {
            value = Buffer.from("");
        }
        else if (typeof value === "string") {
            value = Buffer.from(value);
        }
        const attr = new Attribute({ name: attribute, value });
        const cred = this.credentials.find((c) => c.name === credential);
        if (cred === undefined) {
            this.credentials.push(new Credential({ name: credential, attributes: [attr] }));
        }
        else {
            const idx = cred.attributes.findIndex((a) => a.name === attribute);
            if (idx === -1) {
                cred.attributes.push(attr);
            }
            else {
                cred.attributes[idx] = attr;
            }
        }
    }
    /**
     * Removes the attribute from the given credential. If the credential or the
     * attribute doesn't exist, it returns 'undefined', else it returns the
     * content of the deleted attribute.
     *
     * @param credential the name of the credential
     * @param attribute the attribute to be deleted
     */
    deleteAttribute(credential, attribute) {
        const cred = this.getCredential(credential);
        if (!cred) {
            return undefined;
        }
        const index = cred.attributes.findIndex((att) => att.name === attribute);
        if (index < 0) {
            return undefined;
        }
        return cred.attributes.splice(index, 1)[0].value;
    }
    /**
     * Copy returns a new CredentialStruct with copies of all internal data.
     */
    copy() {
        return CredentialStruct.decode(this.toBytes());
    }
    /**
     * Helper to encode the struct using protobuf
     * @returns encoded struct as a buffer
     */
    toBytes() {
        return Buffer.from(CredentialStruct.encode(this).finish());
    }
}
exports.CredentialStruct = CredentialStruct;
/**
 * A credential has a given name used as a key and one or more attributes
 */
class Credential extends light_1.Message {
    constructor(props) {
        super(props);
        if (this.attributes === undefined) {
            this.attributes = [];
        }
        this.attributes = this.attributes.slice();
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("personhood.Credential", Credential, Attribute);
    }
    /**
     * Returns a credential with only the given name/key = value stored in it.
     *
     * @param name the name of the attribute
     * @param key the key to store
     * @param value the value that will be stored in the key
     */
    static fromNameAttr(name, key, value) {
        return new Credential({ name, attributes: [new Attribute({ name: key, value })] });
    }
    toString() {
        return `${this.name}= ` +
            this.attributes.map((a) => `${a.name}=${a.value.toString("hex")}`).join(" - ");
    }
}
exports.Credential = Credential;
/**
 * Attribute of a credential
 */
class Attribute extends light_1.Message {
    constructor(props) {
        super(props);
        if (this.value === undefined) {
            this.value = protobuf_1.EMPTY_BUFFER;
        }
        this.value = Buffer.from(this.value);
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("personhood.Attribute", Attribute);
    }
}
exports.Attribute = Attribute;
class RecoverySignature {
    constructor(credentialIID, signature) {
        this.credentialIID = credentialIID;
        this.signature = signature;
    }
}
exports.RecoverySignature = RecoverySignature;
RecoverySignature.sig = 64;
RecoverySignature.pub = 32;
RecoverySignature.credIID = 32;
RecoverySignature.version = 8;
RecoverySignature.pubSig = RecoverySignature.pub + RecoverySignature.sig;
RecoverySignature.msgBuf = RecoverySignature.credIID + RecoverySignature.pub + RecoverySignature.version;
CredentialStruct.register();
Credential.register();
Attribute.register();
