"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_browserify_1 = require("crypto-browserify");
const elliptic_1 = require("elliptic");
const keccak_1 = tslib_1.__importDefault(require("keccak"));
const long_1 = tslib_1.__importDefault(require("long"));
const byzcoin_1 = require("../byzcoin");
const log_1 = tslib_1.__importDefault(require("../log"));
const bevm_rpc_1 = require("./bevm-rpc");
const ethereumjs_tx_1 = require("ethereumjs-tx");
const rlp = tslib_1.__importStar(require("rlp"));
const abi_1 = require("@ethersproject/abi");
/**
 * Ethereum account
 */
class EvmAccount {
    /**
     * Create a new EVM account
     *
     * @param name      Account name
     * @param privKey   Private key for the account (32 bytes); if not
     *                  specified, a new one is generated randomly
     * @param nonce     Initial nonce for the account; should normally not be specified
     */
    constructor(name, privKey, nonce = 0) {
        this.name = name;
        if (privKey === undefined) {
            privKey = crypto_browserify_1.randomBytes(32);
        }
        this.key = EvmAccount.EC.keyFromPrivate(privKey);
        this.address = EvmAccount.computeAddress(this.key);
        this._nonce = nonce;
    }
    static deserialize(obj) {
        return new EvmAccount(obj.name, obj.privateKey, obj.nonce);
    }
    static computeAddress(key) {
        // Marshal public key to binary
        const pubBytes = Buffer.from(key.getPublic("hex"), "hex");
        const h = new keccak_1.default("keccak256");
        h.update(pubBytes.slice(1));
        const address = h.digest().slice(12);
        log_1.default.lvl3("Computed account address", address.toString("hex"));
        return address;
    }
    get nonce() {
        return this._nonce;
    }
    sign(tx) {
        const privKey = Buffer.from(this.key.getPrivate("hex"), "hex");
        tx.sign(privKey);
    }
    incNonce() {
        this._nonce += 1;
    }
    serialize() {
        return {
            name: this.name,
            nonce: this.nonce,
            privateKey: this.key.getPrivate("hex"),
        };
    }
}
exports.EvmAccount = EvmAccount;
EvmAccount.EC = new elliptic_1.ec("secp256k1");
/**
 * Ethereum smart contract
 */
class EvmContract {
    /**
     * Create a new EVM contract
     *
     * @param name      Contract name
     * @param bytecode  Contract bytecode; pass `undefined` if the contract
     *                  does not need deploying
     * @param abiJson   Contract ABI (JSON-encoded)
     * @param addresses Array of existing (already deployed) contract
     *                  instances; if not provided, the contract will need to
     *                  deploy instances before exeuting transactions or
     *                  view method calls
     */
    constructor(name, bytecode = Buffer.from(""), abiJson, addresses = []) {
        this.name = name;
        this.bytecode = bytecode;
        this.abiJson = abiJson;
        this.addresses = addresses;
        this.methodAbi = new Map();
        this.transactions = [];
        this.viewMethods = [];
        const abiObj = JSON.parse(abiJson);
        abiObj.forEach((item) => {
            switch (item.type) {
                case "constructor": {
                    this.methodAbi.set("", item);
                    break;
                }
                case "function": {
                    this.methodAbi.set(item.name, item);
                    if (item.stateMutability === "view") {
                        this.viewMethods.push(item.name);
                    }
                    else {
                        this.transactions.push(item.name);
                    }
                    break;
                }
            }
        });
    }
    static deserialize(obj) {
        const addresses = obj.addresses.map((elem) => Buffer.from(elem, "hex"));
        const bytecode = Buffer.from(obj.bytecode, "hex");
        const contract = new EvmContract(obj.name, bytecode, obj.abi, addresses);
        return contract;
    }
    static computeAddress(data, nonce) {
        const buf = rlp.encode([data, nonce]);
        const h = new keccak_1.default("keccak256");
        h.update(buf);
        const address = h.digest().slice(12);
        log_1.default.lvl3("Computed contract address", address.toString("hex"));
        return address;
    }
    getMethodAbi(method) {
        const entry = this.methodAbi.get(method);
        if (entry === undefined) {
            throw Error(`Method "${method}" does not exist in the ABI`);
        }
        return entry;
    }
    encodeDeployData(...args) {
        if (this.bytecode.length === 0) {
            throw Error("This contract cannot be deployed (no provided bytecode)");
        }
        const abiIface = new abi_1.Interface(this.abiJson);
        const encodedArgs = abiIface.encodeDeploy(args);
        const encodedArgsBuf = Buffer.from(encodedArgs.substring(2), "hex");
        const callDataBuf = Buffer.concat([this.bytecode, encodedArgsBuf]);
        return callDataBuf;
    }
    encodeCallData(method, ...args) {
        const abiIface = new abi_1.Interface(this.abiJson);
        const fragment = this.getMethodFragment(method);
        const callData = abiIface.encodeFunctionData(fragment, args);
        const callDataBuf = Buffer.from(callData.substring(2), "hex");
        return callDataBuf;
    }
    decodeCallResult(method, result) {
        const abiIface = new abi_1.Interface(this.abiJson);
        const fragment = this.getMethodFragment(method);
        const decodedResult = abiIface.decodeFunctionResult(fragment, result);
        return decodedResult;
    }
    /**
     * Add a new instance to this conctract, with the address determine by the
     * provided account.
     *
     * @param account Account that spawned the instance
     */
    addNewInstance(account) {
        const newAddress = EvmContract.computeAddress(account.address, account.nonce);
        this.addresses.push(newAddress);
    }
    serialize() {
        return {
            abi: this.abiJson,
            addresses: this.addresses.map((address) => address.toString("hex")),
            bytecode: this.bytecode.toString("hex"),
            name: this.name,
        };
    }
    getMethodFragment(method) {
        const entry = this.getMethodAbi(method);
        const types = entry.inputs.map((arg) => arg.type);
        const fragment = `${method}(${types.join(",")})`;
        return fragment;
    }
}
exports.EvmContract = EvmContract;
// Number of WEIs in one ETHER
exports.WEI_PER_ETHER = long_1.default.fromString("1000000000000000000");
/**
 * BEvm client
 */
class BEvmInstance extends byzcoin_1.Instance {
    constructor(byzcoinRPC, inst) {
        super(inst);
        this.byzcoinRPC = byzcoinRPC;
        if (inst.contractID.toString() !== BEvmInstance.contractID) {
            throw new Error(`mismatch contract name: ${inst.contractID} vs ${BEvmInstance.contractID}`);
        }
        // Use first cothority server for service
        const bevmRPC = new bevm_rpc_1.BEvmRPC(byzcoinRPC.getConfig().roster.list[0]);
        bevmRPC.setTimeout(1000);
        this.bevmRPC = bevmRPC;
    }
    /**
     * Spawn a new BEvm instance
     *
     * @param bc        ByzCoin RPC to use
     * @param darcID    DARC instance ID
     * @param signers   List of signers for the ByzCoin transaction
     *
     * @return New BEvm instance
     */
    static spawn(bc, darcID, signers) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const inst = byzcoin_1.Instruction.createSpawn(darcID, BEvmInstance.contractID, []);
            const ctx = byzcoin_1.ClientTransaction.make(bc.getProtocolVersion(), inst);
            yield ctx.updateCountersAndSign(bc, [signers]);
            yield bc.sendTransactionAndWait(ctx);
            return BEvmInstance.fromByzcoin(bc, ctx.instructions[0].deriveId(), 2);
        });
    }
    /**
     * Retrieve an existing BEvm instance
     *
     * @param bc    ByzCoin RPC to use
     * @param iid   BEvm instance ID
     *
     * @returns BEvm instance
     */
    static fromByzcoin(bc, iid, waitMatch = 0, interval = 1000) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const instance = yield byzcoin_1.Instance.fromByzcoin(bc, iid, waitMatch, interval);
            return new BEvmInstance(bc, instance);
        });
    }
    /**
     * Deploy an EVM smart contract
     *
     * @param signers   ByzCoin identities signing the ByzCoin transaction
     * @param gasLimit  Gas limit for the transaction
     * @param gasPrice  Gas price for the transaction
     * @param amount    Amount transferred during the transaction
     * @param account   EVM account
     * @param contract  EVM contract to deploy
     * @param args      Arguments for the smart contract constructor
     * @param wait      Number of blocks to wait for the ByzCoin transaction to be included
     *
     * The `args` are passed as an array of values, one per argument. The types
     * of the arguments must match the types in the constructor ABI. In
     * particular, for large number types (e.g. uint256), the argument can be
     * passed as a number, string or BN value.
     */
    deploy(signers, gasLimit, gasPrice, amount, account, contract, args = [], wait) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const callData = contract.encodeDeployData(...args);
            const ethTx = new ethereumjs_tx_1.Transaction({
                data: callData,
                gasLimit,
                gasPrice,
                nonce: account.nonce,
                value: amount,
            });
            account.sign(ethTx);
            yield this.invoke(BEvmInstance.commandTransaction, [
                new byzcoin_1.Argument({ name: BEvmInstance.argumentTx,
                    value: ethTx.serialize() }),
            ], signers, wait);
            contract.addNewInstance(account);
            account.incNonce();
        });
    }
    /**
     * Execute an EVM transaction on a deployed smart contract (R/W method)
     *
     * @param signers       ByzCoin identities signing the ByzCoin transaction
     * @param gasLimit      Gas limit for the transaction
     * @param gasPrice      Gas price for the transaction
     * @param amount        Amount transferred during the transaction
     * @param account       EVM account
     * @param contract      EVM contract
     * @param instanceIndex Index of the deployed smart contract instance
     * @param method        Name of the method to execute
     * @param args          Arguments for the smart contract method
     * @param wait          Number of blocks to wait for the ByzCoin transaction to be included
     *
     * See `deploy()` for a description of `args`.
     */
    transaction(signers, gasLimit, gasPrice, amount, account, contract, instanceIndex, method, args = [], wait) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (instanceIndex >= contract.addresses.length) {
                throw Error(`instance ${instanceIndex} does not exist`);
            }
            const callData = contract.encodeCallData(method, ...args);
            const ethTx = new ethereumjs_tx_1.Transaction({
                data: callData,
                gasLimit,
                gasPrice,
                nonce: account.nonce,
                to: contract.addresses[instanceIndex],
                value: amount,
            });
            account.sign(ethTx);
            yield this.invoke(BEvmInstance.commandTransaction, [
                new byzcoin_1.Argument({ name: BEvmInstance.argumentTx,
                    value: ethTx.serialize() }),
            ], signers, wait);
            account.incNonce();
        });
    }
    /**
     * Execute a view method on a deployed smart contract (read-only method)
     *
     * A view method is executed on a conode on behalf of the client. This
     * conode does not necessarily belong to the ByzCoin cothority. Therefore,
     * the actual cothority configuration, the BEvm ID as well as the ByzCoin
     * ID that contains it must be provided.
     *
     * @param account           EVM account
     * @param contract          EVM contract
     * @param instanceIndex     Index of the deployed smart contract instance
     * @param method            Name of the view method to execute
     * @param args              Arguments for the smart contract method
     *
     * @return Result of the view method execution, as an array of values. The
     * types of the values will match the return values in the view method ABI.
     * In particular, values for large number types (e.g. uint256) will be BNs.
     *
     * See `deploy()` for a description of `args`.
     */
    call(account, contract, instanceIndex, method, args = []) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (instanceIndex >= contract.addresses.length) {
                throw Error(`instance ${instanceIndex} does not exist`);
            }
            const callData = contract.encodeCallData(method, ...args);
            const response = yield this.bevmRPC.viewCall(this.byzcoinRPC.genesisID, this.id, account.address, contract.addresses[instanceIndex], callData);
            const decodedResult = contract.decodeCallResult(method, response.result);
            return decodedResult;
        });
    }
    /**
     * Credit an EVM account by the specified amount
     *
     * @param signers   ByzCoin identities signing the ByzCoin transaction
     * @param account   EVM account
     * @param amount    Amount to credit on the account
     * @param wait      Number of blocks to wait for the ByzCoin transaction to be included
     */
    creditAccount(signers, account, amount, wait) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const amountBuf = Buffer.from(amount.toBytesBE());
            yield this.invoke(BEvmInstance.commandCredit, [
                new byzcoin_1.Argument({ name: BEvmInstance.argumentAddress,
                    value: account.address }),
                new byzcoin_1.Argument({ name: BEvmInstance.argumentAmount,
                    value: amountBuf }),
            ], signers, wait);
        });
    }
    invoke(command, args, signers, wait) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const ctx = byzcoin_1.ClientTransaction.make(this.byzcoinRPC.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(this.id, BEvmInstance.contractID, command, args));
            yield ctx.updateCountersAndSign(this.byzcoinRPC, [signers]);
            yield this.byzcoinRPC.sendTransactionAndWait(ctx, wait);
        });
    }
}
exports.BEvmInstance = BEvmInstance;
BEvmInstance.contractID = "bevm";
BEvmInstance.commandTransaction = "transaction";
BEvmInstance.argumentTx = "txRlp";
BEvmInstance.commandCredit = "credit";
BEvmInstance.argumentAddress = "address";
BEvmInstance.argumentAmount = "amount";
