"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_1 = require("crypto");
const elliptic_1 = require("elliptic");
const keccak_1 = tslib_1.__importDefault(require("keccak"));
const long_1 = tslib_1.__importDefault(require("long"));
const client_transaction_1 = tslib_1.__importStar(require("../byzcoin/client-transaction"));
const instance_1 = tslib_1.__importDefault(require("../byzcoin/instance"));
const log_1 = tslib_1.__importDefault(require("../log"));
/**
 * 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_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(hash) {
        /* WARNING: The "canonical" option is crucial to have the same
        * signature as Ethereum */
        const sig = this.key.sign(hash, { canonical: true });
        const r = Buffer.from(sig.r.toArray("be", 32));
        const s = Buffer.from(sig.s.toArray("be", 32));
        const len = r.length + s.length + 1;
        const buf = Buffer.concat([r, s], len);
        buf.writeUInt8(sig.recoveryParam, len - 1);
        return buf;
    }
    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
     * @param abi       Contract ABI (JSON-encoded)
     * @param addresses Array of deployed contract instances; should normally not be specified
     */
    constructor(name, bytecode, abi, addresses = []) {
        this.name = name;
        this.bytecode = bytecode;
        this.abi = abi;
        this.addresses = addresses;
        const abiObj = JSON.parse(abi);
        const transactions = abiObj.filter((elem) => {
            return elem.type === "function" && elem.stateMutability !== "view";
        }).map((elem) => elem.name);
        this.transactions = transactions;
        const viewMethods = abiObj.filter((elem) => {
            return elem.type === "function" && elem.stateMutability === "view";
        }).map((elem) => elem.name);
        this.viewMethods = viewMethods;
    }
    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 = EvmContract.erlEncode(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;
    }
    // Translated from the Go Ethereum code
    static erlEncode(address, nonce) {
        const bufNonce = Buffer.alloc(8);
        bufNonce.writeUInt32BE(nonce / (Math.pow(2, 32)), 0);
        bufNonce.writeUInt32BE(nonce % (Math.pow(2, 32)), 4);
        let size = 8;
        for (let i = 0; (i < 8) && (bufNonce[i] === 0); i++) {
            size--;
        }
        const addressLen = address.length + 1;
        const nonceLen = (nonce < 128 ? 1 : size + 1);
        const buf = Buffer.alloc(1 + addressLen + nonceLen);
        let pos = 0;
        buf.writeUInt8(0xc0 + addressLen + nonceLen, pos++);
        buf.writeUInt8(0x80 + address.length, pos++);
        address.copy(buf, 2);
        pos += address.length;
        if ((nonce === 0) || (nonce >= 128)) {
            buf.writeUInt8(0x80 + size, pos++);
        }
        bufNonce.copy(buf, pos, 8 - size);
        return buf;
    }
    createNewAddress(account) {
        const newAddress = EvmContract.computeAddress(account.address, account.nonce);
        this.addresses.push(newAddress);
    }
    serialize() {
        return {
            abi: this.abi,
            addresses: this.addresses.map((address) => address.toString("hex")),
            bytecode: this.bytecode.toString("hex"),
            name: this.name,
        };
    }
}
exports.EvmContract = EvmContract;
// Number of WEIs in one ETHER
exports.WEI_PER_ETHER = long_1.default.fromString("1000000000000000000");
/**
 * BEvm client
 */
class BEvmClient extends instance_1.default {
    constructor(byzcoinRPC, inst) {
        super(inst);
        this.byzcoinRPC = byzcoinRPC;
        if (inst.contractID.toString() !== BEvmClient.contractID) {
            throw new Error(`mismatch contract name: ${inst.contractID} vs ${BEvmClient.contractID}`);
        }
    }
    /**
     * 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 = client_transaction_1.Instruction.createSpawn(darcID, BEvmClient.contractID, []);
            const ctx = client_transaction_1.default.make(bc.getProtocolVersion(), inst);
            yield ctx.updateCountersAndSign(bc, [signers]);
            yield bc.sendTransactionAndWait(ctx);
            return BEvmClient.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 instance_1.default.fromByzcoin(bc, iid, waitMatch, interval);
            return new BEvmClient(bc, instance);
        });
    }
    /**
     * Set the BEvm service to use
     */
    setBEvmService(bevmService) {
        this.bevmService = bevmService;
    }
    /**
     * 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 strings, one per argument, each of
     * them JSON-encoded.
     * The following argument types are currently supported:
     *
     *   Solidity type          | JSON type | Example
     *   --------------------------------------------
     *   uint, uint256, uint128 | string    | "12345"
     *   int, int256, int128    | string    | "-12345"
     *   uint32, uint16, uint8  | number    | 12345
     *   int32, int16, int8     | number    | -12345
     *   address                | string    | "112233445566778899aabbccddeeff0011223344"
     *   string                 | string    | "look at me I am a string"
     *   array, e.g. uint[2]    | array     | ["123", "456"]
     */
    deploy(signers, gasLimit, gasPrice, amount, account, contract, args, wait) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const unsignedTx = yield this.bevmService.prepareDeployTx(gasLimit, gasPrice, amount, account.nonce, contract.bytecode, contract.abi, args);
            const signature = account.sign(Buffer.from(unsignedTx.transactionHash));
            const signedTx = yield this.bevmService.finalizeTx(Buffer.from(unsignedTx.transaction), signature);
            yield this.invoke(BEvmClient.commandTransaction, [
                new client_transaction_1.Argument({ name: BEvmClient.argumentTx,
                    value: Buffer.from(signedTx.transaction) }),
            ], signers, wait);
            contract.createNewAddress(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* () {
            const contractAddress = contract.addresses[instanceIndex];
            const unsignedTx = yield this.bevmService.prepareTransactionTx(gasLimit, gasPrice, amount, contractAddress, account.nonce, contract.abi, method, args);
            const signature = account.sign(Buffer.from(unsignedTx.transactionHash));
            const signedTx = yield this.bevmService.finalizeTx(Buffer.from(unsignedTx.transaction), signature);
            yield this.invoke(BEvmClient.commandTransaction, [
                new client_transaction_1.Argument({ name: BEvmClient.argumentTx,
                    value: Buffer.from(signedTx.transaction) }),
            ], 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 byzcoinId         ByzCoin ID
     * @param serverConfig      Cothority server config in TOML
     * @param bevmInstanceId    BEvm instance ID
     * @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
     *
     * See `deploy()` for a description of `args`.
     */
    call(byzcoinId, serverConfig, bevmInstanceId, account, contract, instanceIndex, method, args) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const contractAddress = contract.addresses[instanceIndex];
            const response = yield this.bevmService.performCall(byzcoinId, serverConfig, bevmInstanceId, account.address, contractAddress, contract.abi, method, args);
            return JSON.parse(response.result);
        });
    }
    /**
     * 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(BEvmClient.commandCredit, [
                new client_transaction_1.Argument({ name: BEvmClient.argumentAddress,
                    value: account.address }),
                new client_transaction_1.Argument({ name: BEvmClient.argumentAmount,
                    value: amountBuf }),
            ], signers, wait);
        });
    }
    invoke(command, args, signers, wait) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const ctx = client_transaction_1.default.make(this.byzcoinRPC.getProtocolVersion(), client_transaction_1.Instruction.createInvoke(this.id, BEvmClient.contractID, command, args));
            yield ctx.updateCountersAndSign(this.byzcoinRPC, [signers]);
            yield this.byzcoinRPC.sendTransactionAndWait(ctx, wait);
        });
    }
}
exports.BEvmClient = BEvmClient;
BEvmClient.contractID = "bevm";
BEvmClient.commandTransaction = "transaction";
BEvmClient.argumentTx = "tx";
BEvmClient.commandCredit = "credit";
BEvmClient.argumentAddress = "address";
BEvmClient.argumentAmount = "amount";
