import "crypto"
import { BarretenbergBackend } from '@noir-lang/backend_barretenberg';
import { Noir, InputMap } from '@noir-lang/noir_js';
import { zeroPadValue, TypedDataEncoder } from 'ethers';
import json2toml from "json2toml"
import * as types from './types.js';
import { TransactionInputs, create_transaction } from '../circuits/helpers/codegen/create_transaction';
import { UTXO_New } from '../circuits/helpers/codegen/utxo_to_commitment';
import { MerkleTree } from './MerkleTree';
import { keccak_tx_no_deposit } from '../circuits/helpers/codegen/keccak_tx_no_deposit';
import { tx_as_hash } from '../circuits/helpers/codegen/tx_as_hash';
import { Momiji__factory, XFTMock__factory, UltraVerifier__factory, IUniswapV3Pool__factory } from './typechain-types';
import { transaction, transaction_wrapper } from './transaction_circuits';
import { Fr, Barretenberg } from "@aztec/bb.js"
import { Buffer } from "buffer"
import { rollup_transaction, RecursionInputs, Verifier } from "../circuits/helpers/codegen/rollup_transaction";
import './database';

const MAX_FIELD_VALUE: string = (new types.NoirFr(types.NoirFr.MAX_VALUE)).toString();

export class TransactionBuilder {
  initialized: boolean = false;
  circuits: types.Circuits;
  backends: types.Backends;
  noirs: types.Noirs;
  contracts!: types.Contracts;
  sendRpc: string | undefined;
  verbose: boolean = false;
  config: types.GlobalConfig;

  constructor(config: types.GlobalConfig) {
    this.config = config;
    this.verbose = (config.verbose) ? config.verbose : false;
    const circuits: types.Circuits = {
      transaction: transaction,
      transaction_wrapper: transaction_wrapper,
    };
    let backendOptions: types.NoirBackendOptions | undefined = (this.config.threads) ? { threads: this.config.threads } : undefined;
    const backends: types.Backends = {
      transaction: new BarretenbergBackend(circuits.transaction, backendOptions),
      transaction_wrapper: new BarretenbergBackend(circuits.transaction_wrapper, backendOptions),
    };
    const noirs: types.Noirs = {
      transaction: new Noir(circuits.transaction, backends.transaction),
      transaction_wrapper: new Noir(circuits.transaction_wrapper, backends.transaction_wrapper),
    };
    this.circuits = circuits;
    this.backends = backends;
    this.noirs = noirs;
  }

  async initializeTransactionBuilder(): Promise<void> {
    let _runner = this.config.signer ? this.config.signer : this.config.provider
    let _state = Momiji__factory.connect("0xd66E7B1B008a560ad8be69494ce101DE188FbCd3", _runner) 
    this.contracts = {
      state: _state,
      verifier: UltraVerifier__factory.connect(await _state.verifier(), _runner),
      token: XFTMock__factory.connect(await _state.xft(), _runner),
      pool: IUniswapV3Pool__factory.connect(await _state.xftPool(), _runner)
    }
    this.initialized = true;
  }

  emptyTransaction = async (): Promise<types.Transaction> => {
    return { 
      proof_artifacts: {
        proofData: {
          proof: Uint8Array.from(new Array(2144).fill(255)),
          publicInputs: [MAX_FIELD_VALUE]
        },
        proofAsFields: new Array(93).fill(MAX_FIELD_VALUE),
        vkAsFields: new Array(114).fill(MAX_FIELD_VALUE),
        vkHash: await this.contracts.state.txKeyHash()
      },
      contract_inputs: {
        current_root: await this.contracts.state.merkleRoot(),
        utxo_root: MAX_FIELD_VALUE,
        price_limit: MAX_FIELD_VALUE,
        timestamp: await this.config.provider.getBlock("latest")
          .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60), true)),
        deadline: await this.config.provider!.getBlock("latest")
          .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60) + (60 * 60 * 24 * 7), true)),
        amount: MAX_FIELD_VALUE,
        encrypted_utxo: new Array(16).fill({
          secret: MAX_FIELD_VALUE,
          amount: MAX_FIELD_VALUE,
          data: MAX_FIELD_VALUE
        }),
        withdrawals: new Array(16).fill(types.ZERO_HEX),
        commitments_in: new Array(16).fill(types.ZERO_VALUE),
        commitments: new Array(16).fill(types.NoirFr.random().toString()),
        recipients: new Array(16).fill(types.ZERO_HEX),
        nullifier_hashes: new Array(16).fill(types.ZERO_VALUE),
        uids: new Array(16).fill(types.ZERO_HEX),
        swap_amounts: new Array(16).fill(types.ZERO_HEX),
        deposit: {
          signature: "0x00",
          pi_hash: MAX_FIELD_VALUE
        }
      },
      public_inputs: {
        current_root: await this.contracts.state.merkleRoot(),
        utxo_root: MAX_FIELD_VALUE,
        deposit_amount: types.ZERO_HEX,
        withdrawals: types.ZERO_HEX,
        commitment_in: new Array(16).fill(types.ZERO_VALUE),
        commitment_out: new Array(16).fill(types.ZERO_VALUE),
        nullifier_hashes: new Array(16).fill(types.ZERO_VALUE),
        contract_only_inputs: MAX_FIELD_VALUE
      }
  }
  };

  async _padBatch(tx: types.Transaction[]): Promise<types.Batch> {
    const txContractInputs = tx.map(_tx => _tx.contract_inputs)
    const _fill = new Array(16 - tx.length).fill((await this.emptyTransaction()).contract_inputs)
    const txFill = txContractInputs.concat(_fill)
    let _batch: types.Batch = {
      tx_key_hash: await this.contracts.state.txKeyHash(),
      recursive_key_hash: await this.contracts.state.recursiveKeyHash(),
      new_root: MAX_FIELD_VALUE,
      old_hist_root: await this.contracts.state.histRoot(),
      new_hist_root: MAX_FIELD_VALUE,
      oracle: MAX_FIELD_VALUE,
      historic_path: new Array(20).fill(MAX_FIELD_VALUE),
      aggregation_object: new Array(16).fill(MAX_FIELD_VALUE),
      transactions: txFill
    }
    console.log(_batch.transactions)
    return _batch
  }

  async getTransactionsGas(tx: types.Transaction[] | types.Transaction): Promise<bigint> {
    const SUBSIDY_FACTOR = 16n 
    const _verifyCost = 430_000n; 
    if (!Array.isArray(tx)) tx = [tx]
    console.log("pad now")
    const _batch = await this._padBatch(tx)
    console.log(_batch)
    let _batchSim = _batch;
    let _emptySim = _batch;
    try {
      _batchSim.transactions = _batch.transactions.slice(tx.length);
      _emptySim.transactions = _batch.transactions = [];
      let _fullGas = (await this.contracts.state.simulatePublish.estimateGas(_batch));
      let _stubGas = (await this.contracts.state.simulatePublish.estimateGas(_batchSim));
      let _emptyGas = (await this.contracts.state.simulatePublish.estimateGas(_emptySim));
      console.log(_fullGas, _stubGas, _emptyGas);
      return (_fullGas - _stubGas) + ((_verifyCost + _emptyGas) / SUBSIDY_FACTOR);
    } catch (e) {
      console.log(e);
      return 0n;
    }
  }

  async _generateUTXOEncrypted(utxo_input: types.UTXO_Input[]): Promise<types.UTXO_Encrypted[]> {
    const utxos: types.UTXO_Encrypted[] = utxo_input.map((utxo) => {
      return {
        secret: utxo.secret ? utxo.secret : Fr.random().toString(),
        amount: utxo.amount,
        asset_type: utxo.asset_type ? utxo.asset_type : types.ZERO_VALUE,
        secret_encrypted: types.ZERO_VALUE,
        amount_encrypted: types.ZERO_VALUE,
        asset_type_encrypted: types.ZERO_VALUE,
        uid: types.ZERO_VALUE
      }
    })
    return utxos
  }

  async utxo_to_commitment(utxo: UTXO_New): Promise<string> {
    const barretenberg: types._Barretenberg = await Barretenberg.new()
    const hash: string = await barretenberg.pedersenHash([Fr.fromString(utxo.secret)], 0)
      .then((owner: types._NoirFr) => barretenberg.pedersenHash([owner, Fr.fromString(utxo.amount), Fr.fromString(utxo.asset_type)], 0))
      .then((hashFr: types._NoirFr) => hashFr.toString())
    await barretenberg.destroy()
    return hash
  }

  async pedersen_left_right(left: string, right: string): Promise<string> {
    const barretenberg: types._Barretenberg = await Barretenberg.new()
    const hash: string = await barretenberg.pedersenHash([Fr.fromString(left), Fr.fromString(right)], 0)
      .then(hashFr => hashFr.toString())
    await barretenberg.destroy()
    return hash
  }

  _calculateDepositAmount(utxo_commitments?: types.UTXO_Commitment[], utxo_new?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[]): string {
    const utxo_in_total: bigint = utxo_commitments ? utxo_commitments.map(utxo => (utxo.amount === types.ZERO_VALUE ? BigInt(0) : BigInt(utxo.amount))).reduce((prev, curr) => BigInt(prev) + BigInt(curr)) : BigInt(0)
    const utxo_out_total: bigint = utxo_new ? utxo_new.map(utxo => (utxo.amount === types.ZERO_VALUE ? BigInt(0) : BigInt(utxo.amount))).reduce((prev, curr) => BigInt(prev) + BigInt(curr)) : BigInt(0)
    const withdrawal_total: bigint = withdrawals ? withdrawals.map(w => BigInt(w.amount)).reduce((prev, curr) => BigInt(prev) + BigInt(curr)) : BigInt(0)
    let total: string = ((utxo_out_total + withdrawal_total) > utxo_in_total)
      ? (new Fr(utxo_out_total + withdrawal_total - utxo_in_total)).toString()
      : (new Fr(BigInt(0))).toString();
    return total
  }

  async _getPriceLimitX96(slippage_percentage: number): Promise<string> {
    if (slippage_percentage > 24) return Promise.reject("Slippage too high");
    if (slippage_percentage < 0) return Promise.reject("Slippage cannot be less than zero")
    const _sqrtPriceX96: bigint = await this.contracts.pool.slot0().then(slot0 => slot0.sqrtPriceX96)
    const _sqrtPriceLimitX96: bigint = _sqrtPriceX96 * BigInt(100_000 - Math.floor(slippage_percentage * 1_000)) / BigInt(100_000)
    return (new Fr(_sqrtPriceLimitX96)).toString()
  }

  async _getTransactionFeeInXFT(gasUsed: bigint): Promise<bigint> {
    const _sqrtPriceX96: bigint = await this.contracts.pool.slot0().then(slot0 => slot0.sqrtPriceX96)
    const _xftPerEth: bigint = BigInt(2**192 * 1e18) / (_sqrtPriceX96 * _sqrtPriceX96)
    const _feeData = await this.config.provider.getFeeData()
    if (!_feeData.maxFeePerGas || !_feeData.maxPriorityFeePerGas) return Promise.reject("Fee data cannot be fetched")
    const _transactionFeeInXft: bigint = BigInt(gasUsed) * (_feeData.maxFeePerGas! + _feeData.maxPriorityFeePerGas!) * _xftPerEth / BigInt(1e18)
    return _transactionFeeInXft
  }

  async _getEtherFromXFT(xft: bigint): Promise<bigint> {
    const sqrtPriceX96: bigint = await this.contracts.pool.slot0().then(slot0 => slot0.sqrtPriceX96)
    const xftToEther: bigint = xft * sqrtPriceX96 * sqrtPriceX96 / BigInt(2**192)
    return xftToEther
  }

  async _generateWithdrawals(_withdrawals: types.Withdrawal[], slippage_percentage: number): Promise<types.WithdrawalSwap[]> {
    const priceLimitX96: string = await this._getPriceLimitX96(slippage_percentage)
    const withdrawals: types.WithdrawalSwap[] = _withdrawals.map(
      (withdrawal) => {
        const _amount: string = (withdrawal.amount === types.ZERO_VALUE) ? types.ZERO_HEX : withdrawal.amount;
        return {
          amount: _amount,
          recipient: zeroPadValue(withdrawal.recipient, 32),
          swap_amount: (new Fr(BigInt(_amount) * BigInt(Math.floor(withdrawal.swap_percentage * 1_000)) / BigInt(100_000))).toString(),
          price_limit: priceLimitX96
        } as types.WithdrawalSwap
      }
    )
    return withdrawals
  }

  async _generateRawTransactionInputs(utxo_commitments?: types.UTXO_Commitment[], utxo_encrypted?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[]): Promise<types.TransactionInputsRaw> {
    const inputs: types.TransactionInputsRaw = {
      current_root: await this.contracts.state.histRoot(),
      deposit_amount: this._calculateDepositAmount(utxo_commitments, utxo_encrypted, withdrawals),
      withdrawals: (withdrawals)
        ? new Array(16).fill("0x").map((_, i, __) => (withdrawals[i]) ? withdrawals[i].amount : types.ZERO_HEX)
        : new Array(16).fill(types.ZERO_HEX),
      utxo_spendable: await this._generateUTXOSpendable(utxo_commitments),
      utxo_new: (utxo_encrypted)
        ? new Array(16).fill({
          secret: types.ZERO_VALUE,
          amount: types.ZERO_VALUE,
          asset_type: types.ZERO_VALUE
        })
          .map((_, i, __) => {
            if (utxo_encrypted[i]) {
              return {
                secret: utxo_encrypted[i].secret,
                amount: utxo_encrypted[i].amount,
                asset_type: utxo_encrypted[i].asset_type
              }
            } else {
              return {
                secret: types.ZERO_HEX,
                amount: types.ZERO_VALUE,
                asset_type: types.ZERO_VALUE
              }
            }
          })
        : new Array(16).fill({
          secret: types.ZERO_VALUE,
          amount: types.ZERO_VALUE,
          asset_type: types.ZERO_VALUE
        }),
      contract_only_inputs: {
        timestamp: await this.config.provider.getBlock("latest")
          .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60), true)),
        deadline: await this.config.provider!.getBlock("latest")
          .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60) + (60 * 60 * 24 * 7), true)), 
        signature_hash: types.ZERO_HEX,
        price_limit: (withdrawals)
          ? withdrawals.reduce((prev, curr) => ((BigInt(curr.price_limit) > BigInt(prev.price_limit)) ? curr : prev)).price_limit
          : types.ZERO_HEX,
        recipients: (withdrawals)
          ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].recipient : types.ZERO_HEX))
          : new Array(16).fill(types.ZERO_HEX),
        swap_amounts: (withdrawals)
          ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].swap_amount : types.ZERO_HEX))
          : new Array(16).fill(types.ZERO_HEX),
        uids: (utxo_encrypted)
          ? new Array(16).fill("0x").map((_, i, __) => ((utxo_encrypted[i]) ? utxo_encrypted[i].uid : types.ZERO_VALUE))
          : new Array(16).fill(types.ZERO_VALUE),
        encrypted_utxo: (utxo_encrypted)
          ? new Array(16).fill("0x")
            .map((_, i, __) => {
              if (utxo_encrypted[i]) {
                return {
                  secret: utxo_encrypted[i].secret_encrypted,
                  amount: utxo_encrypted[i].amount_encrypted,
                  data: utxo_encrypted[i].asset_type_encrypted
                }
              } else {
                return {
                  secret: types.ZERO_VALUE,
                  amount: types.ZERO_VALUE,
                  data: types.ZERO_VALUE
                }
              }
            })
          : new Array(16).fill({
            secret: types.ZERO_VALUE,
            amount: types.ZERO_VALUE,
            data: types.ZERO_VALUE
          })
      }
    }
    const _domain: types.TypedDataDomain = {
      name: "Momiji",
      version: "1",
      chainId: BigInt(1),
      verifyingContract: await this.contracts.state.getAddress()
    }
    const _types: types.TypedDataTypes = {
      DepositHash: [
        { name: "pi_hash", type: "bytes32" }
      ]
    }
    const _values: types.TypedDataValues = {
      pi_hash: types.ZERO_HEX
    }
    inputs.contract_only_inputs.signature_hash = await keccak_tx_no_deposit(
      inputs.current_root,
      inputs.deposit_amount,
      inputs.withdrawals,
      inputs.utxo_spendable,
      inputs.utxo_new,
      inputs.contract_only_inputs
    ).then(hash => {
      _values.pi_hash = zeroPadValue(hash, 32)
      if (inputs.deposit_amount === types.toFixedHex(0, true)) return types.toFixedHex(0, true);
      else return zeroPadValue(
        Fr.fromBufferReduce(
          Buffer.from(
            TypedDataEncoder.hash(_domain, _types, _values).slice(2),
            "hex"
          )
        ).toString(),
        32
      );
    })
    return inputs
  }

  async _generateRawTransactionInputsExpert(deposit?: bigint, utxo_commitments?: types.UTXO_Commitment[], utxo_encrypted?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[]): Promise<types.TransactionInputsRaw> {
    const inputs: types.TransactionInputsRaw = {
      current_root: await this.contracts.state.histRoot(),
      deposit_amount: (deposit) 
      ? (new types.NoirFr(deposit)).toString()
      : types.toFixedHex(0, true),
      withdrawals: (withdrawals)
        ? new Array(16).fill("0x").map((_, i, __) => (withdrawals[i]) ? withdrawals[i].amount : types.ZERO_HEX)
        : new Array(16).fill(types.ZERO_HEX),
      utxo_spendable: await this._generateUTXOSpendable(utxo_commitments),
      utxo_new: (utxo_encrypted)
        ? new Array(16).fill({
          secret: types.ZERO_VALUE,
          amount: types.ZERO_VALUE,
          asset_type: types.ZERO_VALUE
        })
          .map((_, i, __) => {
            if (utxo_encrypted[i]) {
              return {
                secret: utxo_encrypted[i].secret,
                amount: utxo_encrypted[i].amount,
                asset_type: utxo_encrypted[i].asset_type
              }
            } else {
              return {
                secret: types.ZERO_HEX,
                amount: types.ZERO_VALUE,
                asset_type: types.ZERO_VALUE
              }
            }
          })
        : new Array(16).fill({
          secret: types.ZERO_VALUE,
          amount: types.ZERO_VALUE,
          asset_type: types.ZERO_VALUE
        }),
      contract_only_inputs: {
        timestamp: await this.config.provider.getBlock("latest")
          .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60), true)),
        deadline: await this.config.provider!.getBlock("latest")
          .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60) + (60 * 60 * 24 * 7), true)), 
        signature_hash: types.ZERO_HEX,
        price_limit: (withdrawals)
          ? withdrawals.reduce((prev, curr) => ((BigInt(curr.price_limit) > BigInt(prev.price_limit)) ? curr : prev)).price_limit
          : types.ZERO_HEX,
        recipients: (withdrawals)
          ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].recipient : types.ZERO_HEX))
          : new Array(16).fill(types.ZERO_HEX),
        swap_amounts: (withdrawals)
          ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].swap_amount : types.ZERO_HEX))
          : new Array(16).fill(types.ZERO_HEX),
        uids: (utxo_encrypted)
          ? new Array(16).fill("0x").map((_, i, __) => ((utxo_encrypted[i]) ? utxo_encrypted[i].uid : types.ZERO_VALUE))
          : new Array(16).fill(types.ZERO_VALUE),
        encrypted_utxo: (utxo_encrypted)
          ? new Array(16).fill("0x")
            .map((_, i, __) => {
              if (utxo_encrypted[i]) {
                return {
                  secret: utxo_encrypted[i].secret_encrypted,
                  amount: utxo_encrypted[i].amount_encrypted,
                  data: utxo_encrypted[i].asset_type_encrypted
                }
              } else {
                return {
                  secret: types.ZERO_VALUE,
                  amount: types.ZERO_VALUE,
                  data: types.ZERO_VALUE
                }
              }
            })
          : new Array(16).fill({
            secret: types.ZERO_VALUE,
            amount: types.ZERO_VALUE,
            data: types.ZERO_VALUE
          })
      }
    }
    const _domain: types.TypedDataDomain = {
      name: "Momiji",
      version: "1",
      chainId: BigInt(1),
      verifyingContract: await this.contracts.state.getAddress()
    }
    const _types: types.TypedDataTypes = {
      DepositHash: [
        { name: "pi_hash", type: "bytes32" }
      ]
    }
    const _values: types.TypedDataValues = {
      pi_hash: types.ZERO_HEX
    }
    console.log(inputs)
    inputs.contract_only_inputs.signature_hash = await keccak_tx_no_deposit(
      inputs.current_root,
      inputs.deposit_amount,
      inputs.withdrawals,
      inputs.utxo_spendable,
      inputs.utxo_new,
      inputs.contract_only_inputs
    ).then(hash => {
      _values.pi_hash = zeroPadValue(hash, 32)
      if (inputs.deposit_amount === types.toFixedHex(0, true)) return types.toFixedHex(0, true);
      else return zeroPadValue(
        Fr.fromBufferReduce(
          Buffer.from(
            TypedDataEncoder.hash(_domain, _types, _values).slice(2),
            "hex"
          )
        ).toString(),
        32
      );
    })
    return inputs
  }

  async getSiblingsFromChain(utxo: UTXO_New): Promise<string[]> {
    const commitment: string = await this.utxo_to_commitment(utxo).then(c => zeroPadValue(c, 32))
    const old_root: string = await this.contracts.state.utxoPrevRoots(commitment)
    const siblings: string[] = await this.contracts.state.queryFilter(
      this.contracts.state.filters.TransactionPublish(undefined, old_root, undefined)
    )
      .then((events: any) => events.map((event: any) => event.args[0].commitments))
      .then((commitments: any) => commitments.find((c: any) => c.includes(commitment)))
    if (!siblings) return Promise.reject("Siblings not found.")
    return siblings
  }

  async getUtxoCommitment(utxo: UTXO_New): Promise<string> {
    const commitment: string = await this.utxo_to_commitment(utxo)
    return commitment
  }

  async getTxHashFromUtxo(utxo: UTXO_New): Promise<string> {
    const commitment: string = await this.getUtxoCommitment(utxo)
    const oldRoot: string = await this.contracts.state.utxoPrevRoots(commitment)
    const txHash: string = await this.contracts.state.queryFilter(
      this.contracts.state.filters.TransactionPublish(undefined, oldRoot, undefined)
    )
      .then(events => events.find(event => event.args[0].commitments.includes(commitment)))
      .then(event => (event ? event.transactionHash : types.toFixedHex(0, true)))
    return txHash
  }

  async _generateUTXOCommitments(inputs: types.TransactionInputsRaw, spend_in_same_batch: boolean[]): Promise<types.UTXO_Commitment[]> {
    const formattedInputs: TransactionInputs = await create_transaction(
      inputs.current_root,
      inputs.deposit_amount,
      inputs.withdrawals,
      inputs.utxo_spendable,
      inputs.utxo_new,
      inputs.contract_only_inputs
    )
    const utxo_commitments: types.UTXO_Commitment[] = inputs.utxo_new.map((utxo, i, _) => {
      return {
        commitment: formattedInputs.public_inputs.commitment_out[i],
        secret: utxo.secret,
        amount: utxo.amount,
        asset_type: utxo.asset_type,
        siblings: formattedInputs.public_inputs.commitment_out,
        spend_in_same_batch: spend_in_same_batch[i] ? true : false
      } as types.UTXO_Commitment
    })
    return utxo_commitments
  }

  async _generateFormattedTransactionInputs(inputs: types.TransactionInputsRaw): Promise<TransactionInputs> {
    const formattedInputs: TransactionInputs = await create_transaction(
      inputs.current_root,
      inputs.deposit_amount,
      inputs.withdrawals,
      inputs.utxo_spendable,
      inputs.utxo_new,
      inputs.contract_only_inputs
    )
    return formattedInputs
  }

  async _generateMerkleProof(
    utxo_input: types.MerkleProofInput,
    tx_input: types.MerkleProofInput,
    state_input: types.MerkleProofInput
  ): Promise<types.Merkle_Proof_Struct> {
    const merkleProof: types.Merkle_Proof_Struct = {
      path_utxo: await utxo_input.tree.proof(utxo_input.tree.getIndex(utxo_input.leaf))
        .then(proof => proof.pathElements),
      path_tx: await tx_input.tree.proof(tx_input.tree.getIndex(tx_input.leaf))
        .then(proof => proof.pathElements),
      path_historic: await state_input.tree.proof(state_input.tree.getIndex(state_input.leaf))
        .then(proof => proof.pathElements),
      index_utxo: types.toFixedHex(utxo_input.tree.getIndex(utxo_input.leaf), true),
      index_tx: types.toFixedHex(tx_input.tree.getIndex(tx_input.leaf), true),
      index_historic: types.toFixedHex(state_input.tree.getIndex(state_input.leaf), true)
    }
    return merkleProof
  }

  async getNewStateRoot(old_state_root: string): Promise<string> {
    const validRoots: string[] = await this.contracts.state.getValidRoots()
    const oldRootIndex: number = validRoots.indexOf(old_state_root)
    if (oldRootIndex === undefined) return Promise.reject("Root not found")
    const newStateRoot: string = validRoots[oldRootIndex + 1]
    return newStateRoot
  }

  async _generateUTXOSpendableSingle(utxo: types.UTXO_Commitment, hist_tree: MerkleTree): Promise<types.UTXO_Spendable_Struct> {
    const empty_utxo_spendable: types.UTXO_Spendable_Struct = {
      secret: types.ZERO_HEX,
      amount: types.ZERO_VALUE,
      asset_type: types.ZERO_VALUE,
      oracle: types.ZERO_VALUE,
      old_root_proof: types.ZERO_VALUE,
      merkle_proof: {
        path_utxo: new Array(types.treeConfig.utxoDepth).fill(types.ZERO_HEX),
        path_tx: new Array(types.treeConfig.txDepth).fill(types.ZERO_HEX),
        path_historic: new Array(types.treeConfig.stateDepth).fill(types.ZERO_HEX),
        index_utxo: types.ZERO_HEX,
        index_tx: types.ZERO_HEX,
        index_historic: types.ZERO_HEX
      },
      spend_in_same_batch: false
    }

    if (!utxo.spend_in_same_batch) utxo.spend_in_same_batch = false;
    if (!utxo.commitment) utxo.commitment = await this.utxo_to_commitment(utxo);
    if (!utxo.siblings && !utxo.spend_in_same_batch) utxo.siblings = await this.getSiblingsFromChain(utxo);

    if (utxo.commitment === types.ZERO_VALUE) return empty_utxo_spendable
    if (utxo.spend_in_same_batch) return {
      secret: utxo.secret,
      amount: utxo.amount,
      asset_type: utxo.asset_type,
      oracle: types.ZERO_VALUE,
      old_root_proof: types.ZERO_HEX,
      merkle_proof: {
        path_utxo: new Array(types.treeConfig.utxoDepth).fill(types.ZERO_HEX),
        path_tx: new Array(types.treeConfig.txDepth).fill(types.ZERO_HEX),
        path_historic: new Array(types.treeConfig.stateDepth).fill(types.ZERO_HEX),
        index_utxo: types.ZERO_HEX,
        index_tx: types.ZERO_HEX,
        index_historic: types.ZERO_HEX
      },
      spend_in_same_batch: true
    }
    const utxo_tree: MerkleTree = new MerkleTree(
      types.treeConfig.utxoDepth,
      utxo.siblings
    )
    await utxo_tree.init()

    const old_root: string = (utxo.commitment !== types.ZERO_VALUE && utxo.spend_in_same_batch === false)
      ? await this.contracts.state.utxoPrevRoots(zeroPadValue(utxo.commitment, 32))
      : types.ZERO_HEX

    const new_root: string = (old_root === types.ZERO_HEX) ? types.ZERO_HEX : await this.getNewStateRoot(old_root)

    const tx_tree: MerkleTree = await this.getBatchTransactions(old_root)
      .then(transactions => new MerkleTree(types.treeConfig.txDepth, transactions))
    await tx_tree.init()

    const utxo_input: types.MerkleProofInput = {
      leaf: utxo.commitment,
      tree: utxo_tree
    }
    const tx_input: types.MerkleProofInput = {
      leaf: utxo_tree.root(),
      tree: tx_tree
    }
    const state_input: types.MerkleProofInput = {
      leaf: new_root,
      tree: hist_tree
    }

    const utxo_spendable: types.UTXO_Spendable_Struct = {
      secret: utxo.secret,
      amount: utxo.amount,
      asset_type: utxo.asset_type,
      oracle: await this.getBatchOracle(old_root),
      old_root_proof: old_root,
      merkle_proof: await this._generateMerkleProof(utxo_input, tx_input, state_input),
      spend_in_same_batch: utxo.spend_in_same_batch
    }

    return utxo_spendable
  }

  async _generateUTXOSpendable(utxos?: types.UTXO_Commitment[]): Promise<types.UTXO_Spendable_Struct[]> {
    const empty_utxo_spendable: types.UTXO_Spendable_Struct = {
      secret: types.ZERO_HEX,
      amount: types.ZERO_VALUE,
      asset_type: types.ZERO_VALUE,
      oracle: types.ZERO_VALUE,
      old_root_proof: types.ZERO_VALUE,
      merkle_proof: {
        path_utxo: new Array(types.treeConfig.utxoDepth).fill(types.ZERO_HEX),
        path_tx: new Array(types.treeConfig.txDepth).fill(types.ZERO_HEX),
        path_historic: new Array(types.treeConfig.stateDepth).fill(types.ZERO_HEX),
        index_utxo: types.ZERO_HEX,
        index_tx: types.ZERO_HEX,
        index_historic: types.ZERO_HEX
      },
      spend_in_same_batch: false
    }
    const utxo_spendable_empty_array: types.UTXO_Spendable_Struct[] = new Array(16).fill(empty_utxo_spendable)
    if (!utxos) return utxo_spendable_empty_array;

    const hist_tree: MerkleTree = await this.contracts.state.getValidRoots()
      .then((roots: any) => new MerkleTree(types.treeConfig.stateDepth, roots))
    await hist_tree.init()

    let utxo_spendable_array: types.UTXO_Spendable_Struct[] = await Promise.all(
      utxos.map(utxo => this._generateUTXOSpendableSingle(utxo, hist_tree))
    )

    utxo_spendable_array = utxo_spendable_array.concat(new Array(16 - utxos.length).fill(empty_utxo_spendable))

    return utxo_spendable_array
  }

  async getBatchTransactions(old_root: string): Promise<string[]> {
    const transactions: string[] = await this.contracts.state.queryFilter(
      this.contracts.state.filters.TransactionPublish(undefined, old_root, undefined)
    )
      .then((events: any) => events.map((event: any) => event.args[0].utxo_root))
    return transactions
  }

  async getBatchOracle(root: string): Promise<string> {
    const index = await this.contracts.state.getValidRoots()
      .then((roots: any) => roots.indexOf(root))
    const oracle: string = await this.contracts.state.queryFilter(
      this.contracts.state.filters.BatchPublish(index, undefined, undefined, undefined, undefined)
    )
      .then((events: any) => {
        if (events.length === 0) return Promise.reject("No batch for this root");
        else return events[0].args[3];
      })
    return oracle
  }

  async _generateTransactionDepositSignature(inputs: types.TransactionInputsRaw): Promise<types.Deposit> {
    if (!this.config.signer) throw new Error("Signer not initialized");
    const _domain: types.TypedDataDomain = {
      name: "Momiji",
      version: "1",
      chainId: BigInt(1),
      verifyingContract: await this.contracts.state.getAddress()
    }
    const _types: types.TypedDataTypes = {
      DepositHash: [
        { name: "pi_hash", type: "bytes32" }
      ]
    }
    const _values: types.TypedDataValues = {
      pi_hash: await keccak_tx_no_deposit(
        inputs.current_root,
        inputs.deposit_amount,
        inputs.withdrawals,
        inputs.utxo_spendable,
        inputs.utxo_new,
        inputs.contract_only_inputs
      ).then(hash => zeroPadValue(hash, 32))
    }
    const deposit: types.Deposit = {
      signature: (BigInt(inputs.deposit_amount) === BigInt(types.ZERO_HEX)) ? "0x00" : await this.config.signer.signTypedData(_domain, _types, _values),
      pi_hash: inputs.contract_only_inputs.signature_hash
    }
    return deposit

  }

  async _generateTransactionContractInputs(inputs: types.TransactionInputsRaw): Promise<types.ContractTransaction> {
    const formattedInputs = await this._generateFormattedTransactionInputs(inputs)
    const deposit = await this._generateTransactionDepositSignature(inputs)
    const contract_inputs: types.ContractTransaction = {
      current_root: await this.contracts.state.histRoot(),
      utxo_root: zeroPadValue(formattedInputs.public_inputs.utxo_root, 32),
      price_limit: zeroPadValue(inputs.contract_only_inputs.price_limit, 32),
      timestamp: zeroPadValue(inputs.contract_only_inputs.timestamp, 32),
      deadline: zeroPadValue(inputs.contract_only_inputs.deadline, 32),
      amount: zeroPadValue(formattedInputs.public_inputs.deposit_amount, 32),
      encrypted_utxo: inputs.contract_only_inputs.encrypted_utxo
        .map(utxo => {
          return {
            secret: zeroPadValue(utxo.secret, 32),
            amount: zeroPadValue(utxo.amount, 32),
            data: zeroPadValue(utxo.data, 32)
          } as types.EncryptedUTXO
        }),
      withdrawals: inputs.withdrawals,
      commitments_in: formattedInputs.public_inputs.commitment_in.map(c => zeroPadValue(c, 32)),
      commitments: formattedInputs.public_inputs.commitment_out.map(c => zeroPadValue(c, 32)),
      recipients: inputs.contract_only_inputs.recipients.map(r => zeroPadValue(r, 32)),
      nullifier_hashes: formattedInputs.public_inputs.nullifier_hashes.map(hash => zeroPadValue(hash, 32)),
      uids: inputs.contract_only_inputs.uids.map(uid => zeroPadValue(uid, 32)),
      swap_amounts: inputs.contract_only_inputs.swap_amounts.map(swap => zeroPadValue(swap, 32)),
      deposit: deposit
    }
    return contract_inputs
  }

  async _generateTransactionProofArtifacts(inputs: types.TransactionInputsRaw): Promise<types.ProofArtifacts> {
    const formattedInputs: TransactionInputs = await create_transaction(
      inputs.current_root,
      inputs.deposit_amount,
      inputs.withdrawals,
      inputs.utxo_spendable,
      inputs.utxo_new,
      inputs.contract_only_inputs
    )
    const toml = json2toml(formattedInputs)
    let proof_artifacts: types.ProofArtifacts;
    let tx_proof = await this.noirs.transaction.execute(formattedInputs)
      .then(w => this.backends.transaction.generateProof(w.witness)) 

    let artifacts = await this.backends.transaction.generateRecursiveProofArtifacts(tx_proof, 1)

    proof_artifacts = {
      proofData: tx_proof,
      proofAsFields: artifacts.proofAsFields,
      vkAsFields: artifacts.vkAsFields,
      vkHash: artifacts.vkHash
    }
    return proof_artifacts
  }

  async _generateTransactionProof(
    utxo_commitments?: types.UTXO_Commitment[],
    utxo_encrypted?: types.UTXO_Encrypted[],
    withdrawals?: types.WithdrawalSwap[]
  ): Promise<types.Transaction> {
    const inputs: types.TransactionInputsRaw = await this._generateRawTransactionInputs(
      utxo_commitments,
      utxo_encrypted,
      withdrawals
    )
    return {
      public_inputs: await this._generateFormattedTransactionInputs(inputs).then(formatted => formatted.public_inputs),
      contract_inputs: await this._generateTransactionContractInputs(inputs),
      proof_artifacts: await this._generateTransactionProofArtifacts(inputs)
    }
  }

  async _generateTransactionProofExpert(
    deposit?: bigint,
    utxo_commitments?: types.UTXO_Commitment[],
    utxo_encrypted?: types.UTXO_Encrypted[],
    withdrawals?: types.WithdrawalSwap[]
  ): Promise<types.Transaction> {
    const inputs: types.TransactionInputsRaw = await this._generateRawTransactionInputsExpert(
      deposit,
      utxo_commitments,
      utxo_encrypted,
      withdrawals
    )
    return {
      public_inputs: await this._generateFormattedTransactionInputs(inputs).then(formatted => formatted.public_inputs),
      contract_inputs: await this._generateTransactionContractInputs(inputs),
      proof_artifacts: await this._generateTransactionProofArtifacts(inputs)
    }
  }

  async generateTransactions(inputs: types.TransactionIO[]): Promise<types.Transaction[]> {
    const transactions: types.Transaction[] = new Array(inputs.length)
    for (let i = 0; i < inputs.length; i++) {
      console.log(`Transaction proof ${i}`)
      transactions[i] = await this._generateTransactionProof(
        inputs[i].utxo_commitment,
        inputs[i].utxo_encrypted,
        inputs[i].withdrawal
      )
    }
    return transactions
  }

  async wrapTransaction(transaction: types.Transaction, txProofArtifacts: types.ProofArtifacts): Promise<Verifier> {
    const public_inputs_hash: string = await tx_as_hash(transaction.public_inputs)
    const wrapperInputs: InputMap = {
      public_inputs_hash: public_inputs_hash,
      transaction_verifier: {
        key_hash: txProofArtifacts.vkHash,
        verification_key: txProofArtifacts.vkAsFields,
        proof: txProofArtifacts.proofAsFields,
      }
    }
    let tx_aggregation_object: string[]
    let wrapperArtifacts: types.ProofArtifacts;
    let wrapper_proof = await this.noirs.transaction_wrapper.execute(wrapperInputs)
      .then((wrapper_witness: any) => this.backends.transaction_wrapper.generateProof(wrapper_witness.witness));

    tx_aggregation_object = wrapper_proof.publicInputs.slice(1);
    let artifacts = await this.backends.transaction_wrapper.generateRecursiveProofArtifacts(wrapper_proof, 17);
    wrapperArtifacts = {
      proofData: wrapper_proof,
      proofAsFields: artifacts.proofAsFields,
      vkAsFields: artifacts.vkAsFields,
      vkHash: artifacts.vkHash
    }

    const txWrapperVerifier: Verifier = {
      key_hash: wrapperArtifacts.vkHash,
      proof: wrapperArtifacts.proofAsFields,
      verification_key: wrapperArtifacts.vkAsFields,
      aggregation_object: tx_aggregation_object
    }
    txWrapperVerifier.proof = tx_aggregation_object.concat(txWrapperVerifier.proof)
    return txWrapperVerifier
  }

}