"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_browserify_1 = require("crypto-browserify");
const long_1 = tslib_1.__importDefault(require("long"));
const light_1 = require("protobufjs/light");
const byzcoin_1 = require("../byzcoin");
const byzcoin_rpc_1 = tslib_1.__importStar(require("../byzcoin/byzcoin-rpc"));
const coin_instance_1 = tslib_1.__importStar(require("../byzcoin/contracts/coin-instance"));
const darc_instance_1 = tslib_1.__importDefault(require("../byzcoin/contracts/darc-instance"));
const value_instance_1 = tslib_1.__importDefault(require("../byzcoin/contracts/value-instance"));
const instance_1 = tslib_1.__importDefault(require("../byzcoin/instance"));
const calypso_1 = require("../calypso");
const calypso_instance_1 = require("../calypso/calypso-instance");
const darc_1 = tslib_1.__importDefault(require("../darc/darc"));
const rules_1 = require("../darc/rules");
const log_1 = tslib_1.__importDefault(require("../log"));
const protobuf_1 = require("../protobuf");
const credentials_instance_1 = tslib_1.__importDefault(require("./credentials-instance"));
const pop_party_instance_1 = require("./pop-party-instance");
const ro_pa_sci_instance_1 = tslib_1.__importStar(require("./ro-pa-sci-instance"));
exports.SPAWNER_COIN = Buffer.alloc(32, 0);
exports.SPAWNER_COIN.write("SpawnerCoin");
class SpawnerInstance extends instance_1.default {
    /**
     * Creates a new SpawnerInstance
     * @param rpc        The ByzCoinRPC instance
     * @param inst   A valid spawner instance
     */
    constructor(rpc, inst) {
        super(inst);
        this.rpc = rpc;
        if (inst.contractID.toString() !== SpawnerInstance.contractID) {
            throw new Error(`mismatch contract name: ${inst.contractID} vs ${SpawnerInstance.contractID}`);
        }
        this.struct = SpawnerStruct.decode(inst.data);
    }
    get costs() {
        return new SpawnerStruct(this.struct);
    }
    /**
     * Spawn a spawner instance. It takes either an ICreateSpawner as single argument, or all the arguments
     * separated.
     *
     * @param params The ByzCoinRPC to use or an ICreateSpawner
     * @param darcID The darc instance ID
     * @param signers The list of signers
     * @param costs The different cost for new instances
     * @param beneficiary The beneficiary of the costs
     * @param preID Can be used to define the InstanceID of the credential
     */
    static spawn(params, darcID, signers, costs, beneficiary, preID) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let bc;
            if (params instanceof byzcoin_rpc_1.default) {
                bc = params;
            }
            else {
                ({ bc, darcID, signers, costs, beneficiary } = params);
            }
            const args = [
                ...Object.keys(costs).map((k) => {
                    const value = new coin_instance_1.Coin({ name: exports.SPAWNER_COIN, value: costs[k] }).toBytes();
                    return new byzcoin_1.Argument({ name: k, value });
                }),
                new byzcoin_1.Argument({ name: "beneficiary", value: beneficiary }),
            ];
            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, this.contractID, args);
            const ctx = byzcoin_1.ClientTransaction.make(bc.getProtocolVersion(), inst);
            yield ctx.updateCountersAndSign(bc, [signers]);
            yield bc.sendTransactionAndWait(ctx);
            return this.fromByzcoin(bc, ctx.instructions[0].deriveIdArg(), 1);
        });
    }
    /**
     * Initializes using an existing coinInstance from ByzCoin
     *
     * @param bc an initialized byzcoin RPC instance
     * @param iid the instance-ID of the spawn-instance
     * @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 SpawnerInstance(bc, yield instance_1.default.fromByzcoin(bc, iid, waitMatch, interval));
        });
    }
    /**
     * Update the data of this instance
     *
     * @returns a promise that resolves once the data is up-to-date
     */
    update() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const proof = yield this.rpc.getProofFromLatest(this.id);
            this.struct = SpawnerStruct.decode(proof.value);
            return this;
        });
    }
    /**
     * Create the instructions necessary to spawn one or more darcs. This is separated from the
     * spanDarcs method itself, so that a caller can create a bigger ClientTransaction with
     * multiple sets of instructions inside.
     *
     * @param coin where to take the coins from
     * @param darcs the darcs to create
     */
    spawnDarcInstructions(coin, ...darcs) {
        const cost = this.struct.costDarc.value.mul(darcs.length);
        const ret = [
            byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [new byzcoin_1.Argument({ name: coin_instance_1.default.argumentCoins, value: Buffer.from(cost.toBytesLE()) })]),
            ...darcs.map((darc) => byzcoin_1.Instruction.createSpawn(this.id, darc_instance_1.default.contractID, [new byzcoin_1.Argument({ name: SpawnerInstance.argumentDarc, value: darc.toBytes() })])),
        ];
        return ret;
    }
    /**
     * Spawns one or more darc and returns an array of all the spawned darcs.
     *
     * @param coin      The coin instance to take coins from
     * @param signers   The signers for the transaction
     * @param darcs... All the darcs to spawn using the spawner. The coin instance must have enough
     * coins to pay for all darcs.
     * @returns a promise that resolves with the new array of the instantiated darc instances
     */
    spawnDarcs(coin, signers, ...darcs) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), ...this.spawnDarcInstructions(coin, ...darcs));
            yield ctx.updateCountersAndSign(this.rpc, [signers]);
            yield this.rpc.sendTransactionAndWait(ctx);
            const dis = [];
            for (const da of darcs) {
                try {
                    // Because this call is directly after the sendTransactionAndWait, the new block might not be
                    // applied to all nodes yet.
                    dis.push(yield darc_instance_1.default.fromByzcoin(this.rpc, da.getBaseID(), 2, 1000));
                    break;
                }
                catch (e) {
                    log_1.default.warn("couldn't get proof - perhaps still updating?");
                }
            }
            return dis;
        });
    }
    /**
     * Creates all the necessary instruction to create a new coin - either with a 0 balance, or with
     * a given balance by the caller.
     *
     * @param coin where to take the coins to create the instance
     * @param darcID the responsible darc for the new coin
     * @param coinID the type of coin - must be the same as the `coin` in case of balance > 0
     * @param balance how many coins to transfer to the new coin
     */
    spawnCoinInstructions(coin, darcID, coinID, balance) {
        balance = balance || long_1.default.fromNumber(0);
        const valueBuf = this.struct.costCoin.value.add(balance).toBytesLE();
        return [
            byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [new byzcoin_1.Argument({ name: coin_instance_1.default.argumentCoins, value: Buffer.from(valueBuf) })]),
            byzcoin_1.Instruction.createSpawn(this.id, coin_instance_1.default.contractID, [
                new byzcoin_1.Argument({ name: SpawnerInstance.argumentCoinName, value: exports.SPAWNER_COIN }),
                new byzcoin_1.Argument({ name: SpawnerInstance.argumentCoinID, value: coinID }),
                new byzcoin_1.Argument({ name: SpawnerInstance.argumentDarcID, value: darcID }),
                new byzcoin_1.Argument({ name: SpawnerInstance.argumentCoinValue, value: Buffer.from(balance.toBytesLE()) }),
            ]),
        ];
    }
    /**
     * Create a coin instance for a given darc
     *
     * @param coin      The coin instance to take the coins from
     * @param signers   The signers for the transaction
     * @param darcID    The darc responsible for this coin
     * @param coinID    The instance-ID for the coin - will be calculated as sha256("coin" | coinID)
     * @param balance   The starting balance
     * @returns a promise that resolves with the new coin instance
     */
    spawnCoin(coin, signers, darcID, coinID, balance) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), ...this.spawnCoinInstructions(coin, darcID, coinID, balance));
            yield ctx.updateCountersAndSign(this.rpc, [signers, []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            return coin_instance_1.default.fromByzcoin(this.rpc, coin_instance_1.default.coinIID(coinID), 2);
        });
    }
    /**
     * Creates the instructions necessary to create a credential. This is separated from the spawnCredential
     * method, so that a caller can get the instructions separated and then put all the instructions
     * together in a big ClientTransaction.
     *
     * @param coin coin-instance to pay for the credential
     * @param darcID responsible darc for the credential
     * @param cred the credential structure
     * @param credID if given, used to calculate the iid of the credential, else the darcID will be used
     */
    spawnCredentialInstruction(coin, darcID, cred, credID = null) {
        const valueBuf = this.struct.costCredential.value.toBytesLE();
        const credArgs = [
            new byzcoin_1.Argument({ name: SpawnerInstance.argumentDarcID, value: darcID }),
            new byzcoin_1.Argument({ name: SpawnerInstance.argumentCredential, value: cred.toBytes() }),
        ];
        if (credID) {
            credArgs.push(new byzcoin_1.Argument({ name: SpawnerInstance.argumentCredID, value: credID }));
        }
        return [
            byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [new byzcoin_1.Argument({ name: coin_instance_1.default.argumentCoins, value: Buffer.from(valueBuf) })]),
            byzcoin_1.Instruction.createSpawn(this.id, credentials_instance_1.default.contractID, credArgs),
        ];
    }
    /**
     * Create a credential instance for the given darc
     *
     * @param coin      The coin instance to take coins from
     * @param signers   The signers for the transaction
     * @param darcID    The darc instance ID
     * @param cred      The starting credentials
     * @param credID    The instance-ID for this credential - will be calculated as sha256("credential" | credID)
     * @returns a promise that resolves with the new credential instance
     */
    spawnCredential(coin, signers, darcID, cred, credID = null) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), ...this.spawnCredentialInstruction(coin, darcID, cred, credID));
            yield ctx.updateCountersAndSign(this.rpc, [signers, []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            const finalCredID = credentials_instance_1.default.credentialIID(credID || darcID);
            return credentials_instance_1.default.fromByzcoin(this.rpc, finalCredID, 2);
        });
    }
    /**
     * Create a PoP party
     *
     * @param params structure of {coin, signers, orgs, desc, reward}
     * @returns a promise tha resolves with the new pop party instance
     */
    spawnPopParty(params) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { coin, signers, orgs, desc, reward } = params;
            const valueBuf = this.struct.costDarc.value.add(this.struct.costParty.value).toBytesLE();
            const orgDarc = pop_party_instance_1.PopPartyInstance.preparePartyDarc(orgs, "party-darc " + desc.name);
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [new byzcoin_1.Argument({ name: coin_instance_1.default.argumentCoins, value: Buffer.from(valueBuf) })]), byzcoin_1.Instruction.createSpawn(this.id, darc_instance_1.default.contractID, [new byzcoin_1.Argument({ name: SpawnerInstance.argumentDarc, value: orgDarc.toBytes() })]), byzcoin_1.Instruction.createSpawn(this.id, pop_party_instance_1.PopPartyInstance.contractID, [
                new byzcoin_1.Argument({ name: "darcID", value: orgDarc.getBaseID() }),
                new byzcoin_1.Argument({ name: "description", value: desc.toBytes() }),
                new byzcoin_1.Argument({ name: "miningReward", value: Buffer.from(reward.toBytesLE()) }),
            ]));
            yield ctx.updateCountersAndSign(this.rpc, [signers, [], []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            return pop_party_instance_1.PopPartyInstance.fromByzcoin(this.rpc, ctx.instructions[2].deriveId(), 2);
        });
    }
    /**
     * Create a Rock-Paper-scissors game instance
     *
     * @param params structure of {desc, coin, signers, stake, choice, fillup}
     * @returns a promise that resolves with the new instance
     */
    spawnRoPaSci(params) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { desc, coin, signers, stake, choice, fillup, calypso } = params;
            if (fillup.length !== 31) {
                throw new Error("need exactly 31 bytes for fillUp");
            }
            const c = new coin_instance_1.Coin({ name: coin.name, value: stake.add(this.struct.costRoPaSci.value) });
            if (coin.value.lessThan(c.value)) {
                throw new Error("account balance not high enough for that stake");
            }
            const preHash = Buffer.allocUnsafe(32);
            preHash.writeInt8(choice % 3, 0);
            fillup.copy(preHash, 1);
            const fph = crypto_browserify_1.createHash("sha256");
            fph.update(preHash);
            const rps = new ro_pa_sci_instance_1.RoPaSciStruct({
                description: desc,
                firstPlayer: -1,
                firstPlayerAccount: calypso !== undefined ? coin.id : undefined,
                firstPlayerHash: fph.digest(),
                secondPlayer: -1,
                secondPlayerAccount: Buffer.alloc(32),
                stake: c,
            });
            const rpsArgs = [new byzcoin_1.Argument({ name: "struct", value: rps.toBytes() })];
            if (calypso !== undefined) {
                const wcH = crypto_browserify_1.createHash("sha256");
                wcH.update(rps.firstPlayerHash);
                const writeCommit = wcH.digest();
                const w = yield calypso_instance_1.Write.createWrite(calypso.id, writeCommit, calypso.X, preHash.slice(0, 28));
                const writeBuf = calypso_instance_1.Write.encode(w).finish();
                rpsArgs.push(new byzcoin_1.Argument({ name: "secret", value: Buffer.from(writeBuf) }));
            }
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [new byzcoin_1.Argument({ name: coin_instance_1.default.argumentCoins, value: Buffer.from(c.value.toBytesLE()) })]), byzcoin_1.Instruction.createSpawn(this.id, ro_pa_sci_instance_1.default.contractID, rpsArgs));
            yield ctx.updateCountersAndSign(this.rpc, [signers, []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            const rpsi = yield ro_pa_sci_instance_1.default.fromByzcoin(this.rpc, ctx.instructions[1].deriveId(), 2);
            rpsi.setChoice(choice, fillup);
            return rpsi;
        });
    }
    /**
     * Creates a new calypso write instance for a given key. The key will be encrypted under the
     * aggregated public key. This method creates both the darc that will be protecting the
     * calypsoWrite, as well as the calypsoWrite instance.
     *
     * @param coinInst this coin instance will pay for spawning the calypso write
     * @param signers allow the `invoke:coin.fetch` call on the coinInstance
     * @param lts the id of the long-term-secret that will re-encrypt the key
     * @param key the symmetric key that will be stored encrypted on-chain - not more than 31 bytes.
     * @param ident allowed to re-encrypt the symmetric key
     * @param data additionl data that will be stored AS-IS on chain! So it must be either encrypted using
     * the symmetric 'key', or meta data that is public.
     */
    spawnCalypsoWrite(coinInst, signers, lts, key, ident, data) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (coinInst.value.lessThan(this.struct.costDarc.value.add(this.struct.costCWrite.value))) {
                throw new Error("account balance not high enough for spawning a darc + calypso writer");
            }
            const cwDarc = darc_1.default.createBasic([ident[0]], [ident[0]], Buffer.from("calypso write protection " + crypto_browserify_1.randomBytes(8).toString("hex")), ["spawn:" + calypso_1.CalypsoReadInstance.contractID]);
            ident.slice(1).forEach((id) => cwDarc.rules.appendToRule("spawn:calypsoRead", id, rules_1.Rule.OR));
            const d = yield this.spawnDarcs(coinInst, signers, cwDarc);
            const write = yield calypso_instance_1.Write.createWrite(lts.id, d[0].id, lts.X, key);
            write.cost = this.struct.costCRead;
            if (data) {
                write.data = data;
            }
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(coinInst.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [
                new byzcoin_1.Argument({
                    name: coin_instance_1.default.argumentCoins,
                    value: Buffer.from(this.struct.costCWrite.value.toBytesLE()),
                }),
            ]), byzcoin_1.Instruction.createSpawn(this.id, calypso_instance_1.CalypsoWriteInstance.contractID, [
                new byzcoin_1.Argument({
                    name: calypso_instance_1.CalypsoWriteInstance.argumentWrite,
                    value: Buffer.from(calypso_instance_1.Write.encode(write).finish()),
                }),
                new byzcoin_1.Argument({ name: "darcID", value: d[0].id }),
            ]));
            yield ctx.updateCountersAndSign(this.rpc, [signers, []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            return calypso_instance_1.CalypsoWriteInstance.fromByzcoin(this.rpc, ctx.instructions[1].deriveId(), 2);
        });
    }
    /**
     * Creates all the necessary instruction to create a new value - either with a 0 balance, or with
     * a given balance by the caller.
     *
     * @param coin where to take the coins to create the instance
     * @param darcID the responsible darc for the new coin
     * @param value the value to store in the instance
     */
    spawnValueInstructions(coin, darcID, value) {
        const valueBuf = this.struct.costValue.value.toBytesLE();
        return [
            byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [new byzcoin_1.Argument({ name: coin_instance_1.default.argumentCoins, value: Buffer.from(valueBuf) })]),
            byzcoin_1.Instruction.createSpawn(this.id, value_instance_1.default.contractID, [
                new byzcoin_1.Argument({ name: value_instance_1.default.argumentValue, value }),
            ]),
        ];
    }
    /**
     * Create a value instance for a given darc
     *
     * @param coin      The coin instance to take the coins from
     * @param signers   The signers for the transaction
     * @param darcID    The darc responsible for this coin
     * @param value     The value to store in the instance
     * @returns a promise that resolves with the new coin instance
     */
    spawnValue(coin, signers, darcID, value) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), ...this.spawnValueInstructions(coin, darcID, value));
            yield ctx.updateCountersAndSign(this.rpc, [signers, []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            return value_instance_1.default.fromByzcoin(this.rpc, ctx.instructions[1].deriveId(), 2);
        });
    }
}
exports.default = SpawnerInstance;
SpawnerInstance.contractID = "spawner";
SpawnerInstance.argumentCredential = "credential";
SpawnerInstance.argumentCredID = "credID";
SpawnerInstance.argumentDarc = "darc";
SpawnerInstance.argumentDarcID = "darcID";
SpawnerInstance.argumentCoinID = "coinID";
SpawnerInstance.argumentCoinName = "coinName";
SpawnerInstance.argumentCoinValue = "coinValue";
/**
 * Data of a spawner instance
 */
class SpawnerStruct extends light_1.Message {
    constructor(props) {
        super(props);
        /* Protobuf aliases */
        Object.defineProperty(this, "costdarc", {
            get() {
                return this.costDarc;
            },
            set(value) {
                this.costDarc = value;
            },
        });
        Object.defineProperty(this, "costcoin", {
            get() {
                return this.costCoin;
            },
            set(value) {
                this.costCoin = value;
            },
        });
        Object.defineProperty(this, "costcredential", {
            get() {
                return this.costCredential;
            },
            set(value) {
                this.costCredential = value;
            },
        });
        Object.defineProperty(this, "costparty", {
            get() {
                return this.costParty;
            },
            set(value) {
                this.costParty = value;
            },
        });
        Object.defineProperty(this, "costropasci", {
            get() {
                return this.costRoPaSci;
            },
            set(value) {
                this.costRoPaSci = value;
            },
        });
        Object.defineProperty(this, "costcread", {
            get() {
                return this.costCRead;
            },
            set(value) {
                this.costCRead = value;
            },
        });
        Object.defineProperty(this, "costcwrite", {
            get() {
                return this.costCWrite;
            },
            set(value) {
                this.costCWrite = value;
            },
        });
        Object.defineProperty(this, "costvalue", {
            get() {
                return this.costValue;
            },
            set(value) {
                this.costValue = value;
            },
        });
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("personhood.SpawnerStruct", SpawnerStruct, coin_instance_1.Coin);
    }
}
exports.SpawnerStruct = SpawnerStruct;
SpawnerStruct.register();
