import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { isAddress } from '@ethersproject/address';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AbiItem } from 'web3-utils';
import { environment } from '../../../environments/environment';
import { BalanceContract, RecipientCommitment, SchnorrSignatureSet } from '../interfaces/balance-contract';
import { MethodCallback } from '../interfaces/callback';
import {
  InitData, InputParamsBalance, InputParamsBurnCost, InputParamsExchange,
  InputParamsPrice, OutputParamsBalance,
} from '../interfaces/contract-service-methods';
import { PriceContract } from '../interfaces/price-contract';
import { TokenContract } from '../interfaces/token-contract';
import { ShifterContract } from '../interfaces/shifter-contract';
import { StorageContract } from '../interfaces/storage-contract'
import { SwapRouterContract } from '../interfaces/swaprouter-contract'
import { Weth9Contract } from '../interfaces/weth9-contract'
import { ChainlinkContract } from '../interfaces/chainlink-contract'
import { TokenSwapContract } from '../interfaces/tokenswap-contract'
import { RewardContract } from '../interfaces/reward-contract';
import { QuoterV2Contract } from '../interfaces/quoterv2-contract'
import { OracleContract } from '../interfaces/oracle-contract';
import { RewardUnlockedContract } from '../interfaces/reward-unlocked-contract'
import { BaseService } from './base.service';
import {
  BALANCE_CONTRACT_ABI,
  PRICE_CONTRACT_ABI,
  TOKEN_CONTRACT_ABI,
  SHIFTER_CONTRACT_ABI,
  STORAGE_CONTRACT_ABI,
  AUSD_CONTRACT_ABI,
  SWAPROUTER_CONTRACT_ABI,
  WETH9_CONTRACT_ABI,
  CHAINLINK_CONTRACT_ABI,
  TOKENSWAP_CONTRACT_ABI,
  REWARDER_contract_abi,
  REWARDERUNLOCKED_CONTRACT_ABI,
  QUOTERV2_CONTRACT_ABI,
  V3STAKER_CONTRACT_ABI,
  ORACLE_CONTRACT_ABI
} from './contracts-abi';
import { WalletService } from './wallet.service';
import { Contract, EventData } from 'web3-eth-contract';
import { BigNumberValue, valueToBigNumber } from '../helpers';
import { TokensName } from '../enums';
import { buffPedersenHash, randomBN, toFixedHex, parseNote, parseSaveNote } from './crypto';
import { Dictionary } from '@ngrx/entity';
import { Buffer } from 'buffer';
import { TOKENS_MAP } from '../helpers/index'
import { local } from 'web3modal';
import { V3StakerContract } from '../interfaces/v3staker-contract';

const websnarkUtils = require('websnark-backup/src/utils')
const buildGroth16 = require('websnark-backup/src/groth16')
const stringifyBigInts = require('websnark-backup/tools/stringifybigint').stringifyBigInts
const snarkjs = require('snarkjs-backup')
const bigInt = snarkjs.bigInt
const MerkleTree = require('fixed-merkle-tree-backup')
const crypto = require('crypto')
const subtle = globalThis.crypto.subtle
const circomlib = require('circomlib-backup')
const { toBN, keccak256, fromWei, randomHex, randomBytes } = require('web3-utils')
const Web3 = require('web3')
const CUT_LENGTH = 31
const CONTRACT_MAP: Map<string, Contract> = new Map<string, Contract>([]);
const circuit = require("./withdraw.json")
const req = new XMLHttpRequest();
const rbigint = (nbytes: number) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes));
let provingKey: any
let groth16: any
const loadKey = () => {
  return new Promise((resolve, reject) => {
    req.open("GET", "./assets/withdraw_proving_key.bin", true);
    req.responseType = "arraybuffer";
    req.onload = (event) => {
      resolve(req.response);
    };
    req.send(null);
  })
}

@Injectable({
  providedIn: 'root'
})
export class ContractService extends BaseService {
  relayerConfig: any;
  constructor(
    store: Store,
    private readonly _http: HttpClient,
    private readonly _walletService: WalletService
  ) {
    super(store);
  }

  public isAddress = (address: string) => {
    return Web3.utils.isAddress(address);
  }

  public async relayWithdrawTransaction(
    saveNote: string,
    _recipient: string,
    shifterAddress: string,
    cb: MethodCallback = null
  ): Promise<any> {
    const { args, proof } = await this.prepareWithdraw(saveNote, _recipient, shifterAddress)

    // Post to the relay server
    const receipt = await this._http.post<any>(`${environment.relayerEndpoint}/relay`, { proof: proof, args: args, shifter: shifterAddress, callback: cb }).toPromise();
    return receipt;
  }

  // Populates relayerConfig with data from config file
  public async getRelayerConfig() {
    return this._http.get(environment.relayerEndpoint + "/config")
      .toPromise()
      .then(
        data => this.relayerConfig = data
      );
  }

  public getPrice(payload: InputParamsPrice): Observable<any> {
    return this._http.post<any>(this.apiUrl('getPrice'), payload);
  }

  public async getETHBalance(address: string): Promise<any> {
    return this._walletService.web3.eth.getBalance(address)
  }

  public async getDenomination(shifter: string): Promise<string> {
    const { methods } = this._getShifterContract(shifter);
    return await this.call(methods.denomination())
  }

  public async getBurnCost(shifter: string, amount: BigInt, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getShifterContract(shifter);

    return await this.call(methods.getCost(amount), cb);
  }

  public async getCost(shifter: string, amount: BigInt, user: string, cb: MethodCallback = null): Promise<any> {
    const shifterMethods = this._getShifterContract(shifter).methods
    const chainlinkFeed = await shifterMethods.chainlinkFeed().call({ from: user })
    const xftPool = await shifterMethods.xftPool().call({ from: user })
    const { methods } = this._getOracleContract();

    return methods.getCost(amount, chainlinkFeed, xftPool).call({from: user}, cb);
  }

  public async getCostSimpleShift(shifter: string, amount: BigInt, user: string, cb: MethodCallback = null): Promise<any> {
    const shifterMethods = this._getShifterContract(shifter).methods
    const chainlinkFeed = await shifterMethods.chainlinkFeed().call({ from: user })
    const xftPool = await shifterMethods.xftPool().call({ from: user })
    const tokenPool = await shifterMethods.tokenPool().call({ from: user })
    const { methods } = this._getOracleContract();

    return methods.getCostSimpleShift(amount, chainlinkFeed, xftPool, tokenPool).call({ from: user }, cb);
  }

  public async getBurnCostOracle(amount: BigInt, chainlinkFeed: string, xftPool: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getOracleContract();

    return await this.call(methods.getCost(amount, chainlinkFeed, xftPool), cb);
  }

  public exchange(payload: InputParamsExchange): Observable<any> {
    return this._http.post<any>(this.apiUrl('exchange'), payload);
  }

  public balances(payload: InputParamsBalance): Observable<OutputParamsBalance[]> {
    return new Observable(); // stub the call
    return this._http.post<OutputParamsBalance[]>(this.apiUrl('balances'), payload);
  }

  public init(payload: InitData): Observable<any> {
    return new Observable(); // stub the call
    return this._http.post<any>(this.apiUrl('init'), payload);
  }

  public async approve(
    amount: string,
    address: string,
    user: string,
    cb: MethodCallback = null
  ): Promise<any> {
    const { methods } = this._getWETH9Contract()
    return methods.approve(address, amount).send({ from: user }, cb)
  }

  public async approveXFT(
    amount: string,
    address: string,
    tokenAddress: string,
    user: string,
    cb: MethodCallback = null
  ): Promise<any> {
    const { methods } = this._getTokenContract(tokenAddress)
    return methods.approve(address, amount).send({ from: user }, cb)
  }

  public async allowance(
    token: string,
    owner: string,
    spender: string,
    cb: MethodCallback = null,
  ): Promise<any> {
    const { methods } = this._getTokenContract(token);

    return this.call(methods.allowance(owner, spender), cb);
  }

  public async safeApprove(
    amount: string,
    token: string,
    address: string,
    user: string,
    cb: MethodCallback = null
  ): Promise<any> {
    const { methods } = this._getTokenContract(token)
    return methods.safeApprove(address, amount).send({ from: user }, cb)
  }

  public xcrypt = async (data: string, password: string, shifter: string, address: string, nonce: number = null) => {

    const { methods } = this._getStorageContract();

    if (!nonce && nonce !== 0) nonce = (await methods.getDepositsLength(shifter, address, keccak256(password)).call());
    const iv = (await subtle.digest('SHA-256', new Uint8Array(Buffer.from((password + nonce))))).slice(0, 16);
    const dataBuffer = new Uint8Array(Buffer.from(data.slice(2), 'hex'));
    const keyBuffer = new Uint8Array(Buffer.from(password.slice(2), 'hex'));
    const key = await subtle.importKey('raw', keyBuffer, { name: 'AES-GCM' }, false, ['encrypt']);

    try {
      let encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, key, dataBuffer);
      return "0x" + Buffer.from(encrypted).toString('hex').slice(0, 124);
    } catch (err) {
      throw new Error("The data provided couldn't be encrypted or decrypted, please check the inputs");
    }
  }

  public async deposit(
    address: string,
    amount: BigNumberValue,
    currency: TokensName,
    cb: MethodCallback = null,
  ): Promise<any> {
    const { methods } = this._getBalanceContract();

    const {
      amountIn,
      assetEnum: asset,
      commitment,
      message,
      aggregatePubKey: publicKey,
      aggregaterR: ecR,
      aggregateS: s,
    } = await this._http.post<any>(this.apiUrl('deposit'), {
      address,
      amount: valueToBigNumber(amount).toNumber(),
      currency
    }).toPromise();

    const recipientCommitment: RecipientCommitment = { commitment, asset };
    const schnorrSignatureSet: SchnorrSignatureSet = { message, publicKey, ecR, s };

    return this.send(methods.deposit(amountIn, amount, recipientCommitment, schnorrSignatureSet), cb);
  }

  public async withdraw(
    address: string,
    amount: BigNumberValue,
    commitmentId: BigNumberValue,
    cb: MethodCallback = null,
  ): Promise<any> {
    const {
      message,
      aggregatePubKey: publicKey,
      aggregaterR: ecR,
      aggregateS: s,
    } = await this._http.post<any>(this.apiUrl('withdraw'), {
      address,
      amount: valueToBigNumber(amount).toNumber(),
      commitmentId: Number(commitmentId),
    }).toPromise();

    const schnorrSignatureSet: SchnorrSignatureSet = { message, publicKey, ecR, s };

    const { methods } = this._getBalanceContract();

    return this.send(methods.withdraw(amount, Number(commitmentId), schnorrSignatureSet), cb);
  }

  // Get all notes for a given address
  public async getAggregateDeposits(shifters: string[], address: string, decrypted: boolean): Promise<any> {

    const { methods } = this._getStorageContract();

    const password = this.getOrSetPasswordLocally(); // Should never be null
    let encryptedNotes: any[] = [];
    let decryptedNotes: any[] = [];
    let spentNotes: any[] = [];
    for (let shifter in shifters) {
      shifter = shifters[shifter];
      if (password) { // There should always be a password set by the time we reach this
        let passwordHash = keccak256(password);
        let shifterNotes = await this.call(methods.getDeposits(shifter, address, passwordHash));
        shifterNotes = shifterNotes.map((note: string, index: number) => { return { "note": note, "shifter": shifter, "index": index } });
        try {
          encryptedNotes = encryptedNotes.concat(shifterNotes);
        } catch (e) { console.log(e) };
      } else {
        throw ("No password set");
      }
    }
    let nullHashArray;
    let boolSpentArray: string[];
    let decryptedUnspentNotes;
    if (decrypted) {
      for (let index in encryptedNotes) {
        let thisNote = encryptedNotes[index];
        let thisDecryptedNote = (await this.xcrypt(thisNote.note, password, thisNote.shifter, address, parseInt(thisNote.index)));
        let deposited = await this._getShifterContract(thisNote.shifter).methods.commitments(parseSaveNote(thisDecryptedNote).commitmentHex).call()
        let spent = await this._getShifterContract(thisNote.shifter).methods.isSpent(parseSaveNote(thisDecryptedNote).nullifierHex).call();
        // Make sure the note is unspent
        if (!spent && deposited)
          decryptedNotes.push({
            "note": thisDecryptedNote,
            "shifter": thisNote.shifter
          })
        // Aggregates spent notes
        else if (spent && deposited)
          spentNotes.push({
            "note": thisDecryptedNote,
            "shifter": thisNote.shifter
          })
      }
      decryptedNotes = decryptedNotes
        .map((note, idx: number) => {
          return {
            "note": note.note,
            "shifter": note.shifter,
            "commitment": parseSaveNote(note.note).commitmentHex
          }
        });
      spentNotes = spentNotes
        .map((note, idx: number) => {
          return {
            "note": note.note,
            "shifter": note.shifter,
            "commitment": parseSaveNote(note.note).commitmentHex
          }
        });
    }
    return (decrypted ? { decryptedNotes, spentNotes } : encryptedNotes);
  }

  public async getLatestDeposit(shifter: string, address: string): Promise<any> {

    const { methods } = this._getStorageContract();
    const password = this.getOrSetPasswordLocally()
    let encryptedNote;
    if (password) {
      try {
        let passwordHash = keccak256(password)
        encryptedNote = await this.call(methods.getLatestDeposit(shifter, address, passwordHash))
      } catch (e) { console.log(e) };
    }

    if (!encryptedNote) {
      console.log("No note found")
      return Promise.resolve(null); // No note found
    }
    console.log(`Found note: ${encryptedNote}`)
    console.log(`Decrypted note: ${await this.xcrypt(encryptedNote, password, shifter, address)}`)
    return this.xcrypt(encryptedNote, password, shifter, address)
  }

  public async depositAnon(
    /*amount: BigNumberValue,
    currency: TokensName,*/
    address: string,
    shifterAddress: string,
    cb: MethodCallback = null,
  ): Promise<any> {

    let config = JSON.parse(localStorage.getItem('config'))
    let noStorage = config[localStorage.getItem('account')].noStorage
    const { methods } = this._getShifterContract(shifterAddress); 
    const { saveNote, hexNote, commitmentHex, password } = this.prepareDeposit();

    let passwordHash = keccak256(noStorage ? crypto.randomBytes(32) : password);
    const encryptedNote = await this.xcrypt(noStorage ? crypto.randomBytes(62) : saveNote, password, shifterAddress, address);
    let tx = await methods.deposit(commitmentHex, encryptedNote, passwordHash).send({ from: address, gas: 1300000, value: "50000000000000000" }, cb)

    // Download saveNote
    if (noStorage)
      this.download(saveNote, commitmentHex, shifterAddress);

    return tx;
  }

  public async withdrawAnon(
    saveNote: string,
    _recipient: string,
    shifterAddress: string,
    cb: MethodCallback = null
  ): Promise<any> {
    console.log("Save note: " + saveNote)
    const { methods } = this._getShifterContract(shifterAddress)
    const { args, proof } = await this.prepareWithdraw(saveNote, _recipient, shifterAddress)
    const {
      root,
      nullifierHash,
      recipient,
      relayer,
      fee,
      refund
    } = args

    return this.send(methods.withdraw(
      proof,
      root,
      nullifierHash,
      recipient,
      relayer,
      fee,
      refund
    ), cb)
  }

  public generatePassword = () => randomHex(32);

  public getOrSetPasswordLocally = () => {
    // add account object 
    let config = JSON.parse(localStorage.getItem('config'))
    let acc = config[localStorage.getItem('account')]
    if (!acc.password) {
      acc.password = this.generatePassword();
    }

    config[localStorage.getItem('account')] = acc;
    localStorage.setItem('config', JSON.stringify(config))

    return acc.password
  }

  public prepareDeposit() {
    const secret = rbigint(31)
    const nullifier = rbigint(31)

    const preimage = Buffer.concat([nullifier.leInt2Buff(31), secret.leInt2Buff(31)])

    const commitment = buffPedersenHash(preimage)
    const commitmentHex = toFixedHex(commitment)

    const saveNote = `0x${nullifier.toString(16, 'hex').padStart(62, '0')}${secret.toString(16, 'hex').padStart(62, '0')}`;
    const hexNote = `0x${preimage.toString('hex')}`

    const password = this.getOrSetPasswordLocally();
    return { saveNote, hexNote, commitmentHex, password }
  }

  async prepareWithdraw(
    saveNote: string,
    recipient: string,
    shifterAddress: string,
  ): Promise<any> {

    const { note, tree, root } = await this.buildTree(saveNote, 20, shifterAddress)
    const { args, proof } = await this.createSnarkProof(
      root,
      note,
      tree,
      recipient,
      tree.indexOf(note.commitmentHex)
    )

    return { args, proof }
  }

  async buildTree(
    saveNote: string,
    levels: number,
    shifterAddress: string,
    cb: MethodCallback = null
  ): Promise<any> {
    const note = parseSaveNote(saveNote)
    const { methods } = this._getShifterContract(shifterAddress);
    let leaves = await this.call(methods.commitmentList(), cb)
    const tree = new MerkleTree(levels, leaves)
    return { note, tree, root: tree.root() }
  }

  async createSnarkProof(
    root: string,
    note: any,
    tree: any,
    recipient: string,
    leafIndex: number,
  ): Promise<any> {
    const { pathElements, pathIndices } = tree.path(leafIndex)
    let relayer, fee, refund, chainId
    if (environment.useRelayer) {
      await this.getRelayerConfig();
      relayer = this.relayerConfig.address
      fee = this.relayerConfig.fee
      refund = this.relayerConfig.refund
      chainId = this.relayerConfig.chainId  // Not used yet
    } else {
      relayer = recipient
      fee = 0
      refund = 0
      chainId = 1
    }

    const input = stringifyBigInts({
      // public
      root: tree.root(),
      nullifierHash: note.nullifierHash,
      relayer,
      recipient: BigInt(recipient),
      fee,
      refund,
      // private
      nullifier: note.nullifier,
      secret: note.secret,
      pathElements: pathElements,
      pathIndices: pathIndices,
    })

    if (!provingKey) provingKey = await loadKey();
    if (!groth16) groth16 = await buildGroth16()

    let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, provingKey)
    const { proof } = await websnarkUtils.toSolidityInput(proofData)

    const args = {
      root: toFixedHex(input.root),
      nullifierHash: toFixedHex(input.nullifierHash),
      recipient: toFixedHex(input.recipient, 20),
      relayer: toFixedHex(input.relayer, 20),
      fee: toFixedHex(input.fee),
      refund: toFixedHex(input.refund)
    }
    return { args, proof }
  }

  public async simpleShift(
    amount: string,
    recipient: string,
    shifterAddress: string,
    cb: MethodCallback = null
  ): Promise<any> {
    const { methods } = this._getShifterContract(shifterAddress)
    return methods.simpleShift(amount, recipient).send({ from: recipient, gas: 350000 }, cb)
  }

  public async isDeposited(saveNote: string, shifterAddress: string): Promise<string> {
    const { methods } = this._getShifterContract(shifterAddress)
    const depositObject = parseSaveNote(saveNote)

    console.log("Commitment hex: " + depositObject.commitmentHex)
    console.log("Is commitment deposited: " + await this.call(methods.commitments(depositObject.commitmentHex)))

    return this.call(methods.commitments(depositObject.commitmentHex))
  }

  public async isSpent(saveNote: string, shifterAddress: string): Promise<string> {
    const { methods } = this._getShifterContract(shifterAddress);
    console.log(`The note being checked is: ${saveNote}`)
    console.log(`Note spent: ${await this.call(methods.isSpent(parseSaveNote(saveNote).nullifierHex))}`)
    return this.call(methods.isSpent(parseSaveNote(saveNote).nullifierHex));
  }


  public async exactOutputSingle(params: string[], recipient: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getSwapRouterContract()
    let gas = await methods.exactOutputSingle(params).estimateGas({ from: recipient }, cb)
    const out = methods.exactOutputSingle(params).send({ from: recipient, gas: gas }, cb)
    return out
  }

  public async exactOutputSingleCall(params: string[], recipient: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getSwapRouterContract()
    const out = methods.exactOutputSingle(params).call({ from: recipient }, cb)
    return out
  }

  /*
  struct QuoteExactOutputSingleParams {
        address tokenIn;
        address tokenOut;
        uint256 amount;
        uint24 fee;
        uint160 sqrtPriceLimitX96;
    }
  */
  public async quoteExactOutputSingle(params: string[], user: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getQuoterV2Contract();
    return methods.quoteExactOutputSingle(params).call({ from: user });
  }

  public async quoteExactInputSingle(params: string[], user: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getQuoterV2Contract();
    return methods.quoteExactInputSingle(params).call({ from: user });
  }

  public async wethDeposit(amount: string, user: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getWETH9Contract()
    return methods.deposit().send({ from: user, value: amount }, cb)
  }

  public async getChainlinkPriceETHUSD(amount: string, user: string, slippage: number, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getChainlinkContract("0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419") //Mainnet ETHUSD Chainlink feed
    let chainlinkPrice = await methods.latestAnswer().call({ from: user }, cb)
    const chainlinkDecimals = await methods.decimals().call({ from: user }, cb)
    let amountBigInt = BigInt(amount)
    // Putting the rate in the denominator so we can get ETH in the numerator
    let exchangeRate = (amountBigInt * BigInt(10 ** chainlinkDecimals)) / BigInt(chainlinkPrice)
    exchangeRate = exchangeRate * BigInt(100 + slippage) / BigInt(100) //Adds slippage amount to calculated price
    return exchangeRate.toString()
  }

  public async getChainlinkPriceBTCETH(amount: string, user: string, slippage: number, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getChainlinkContract("0xdeb288F737066589598e9214E782fa5A8eD689e8") //Mainnet BTCETH Chainlink feed
    let chainlinkPrice = await methods.latestAnswer().call({ from: user }, cb)
    const chainlinkDecimals = await methods.decimals().call({ from: user }, cb)
    let amountBigInt = BigInt(amount)

    // Rate goes in the numerator since BTCETH is the number of ETH per BTC
    let exchangeRate = (amountBigInt * BigInt(chainlinkPrice)) / BigInt(10 ** chainlinkDecimals)
    exchangeRate = exchangeRate * BigInt(100 + slippage) / BigInt(100) //Adds slippage amount to calculated price 
    return exchangeRate.toString()
  }

  public async tokenSwap(user: string, cb: MethodCallback = null): Promise<any> {
    const { methods } = this._getTokenSwapContract()
    const gasEstimate = await methods.upgrade().estimateGas({ from: user }, cb)
    return methods.upgrade().send({ from: user, gas: gasEstimate }, cb)
  }

  public async getV3IncentiveKey(user: string): Promise<any> {
    const { methods } = this._getRewardContract();
    const incentiveKey = await methods.incentiveKey().call({from: user});
    return incentiveKey;
  }

  public async getIncentiveId(user: string): Promise<any> {
    const { methods } = this._getRewardContract();
    const incentiveKey = await this.getV3IncentiveKey(user);
    const incentiveId = await methods.incentiveId().call({from: user});
    return incentiveId;
  }

  public async incentives(user: string): Promise<any> {
    const { methods } = this._getV3StakerContract();
    const incentiveId = await this.getIncentiveId(user);
    const incentives = await methods.incentives(incentiveId).call({from: user});
    return incentives;
  }

  public async getAPY(user: string): Promise<any> {
    const incentives = await this.incentives(user);
    const incentiveKey = await this.getV3IncentiveKey(user);
    const interval = BigInt(incentiveKey.endTime - Math.ceil(Date.now()/1000));
    const yearInSeconds = BigInt(365 * 24 * 3600); //one year, in seconds
    const quoteXFTWETH = await this.quoteExactInputSingle([
      TOKENS_MAP["XFT"].contract, 
      TOKENS_MAP["WETH"].contract, 
      incentives.totalRewardUnclaimed.toString(),
      "3000",
      "0"], user);
    const quote = BigInt(quoteXFTWETH.amountOut.toString());
    const rewardsPerYearETH = quote * yearInSeconds / interval;
    const ethBalPool = await this.getBalance(incentiveKey.pool, TOKENS_MAP["WETH"].contract);
    const anonUSDBalPool = await this.getBalance(incentiveKey.pool, TOKENS_MAP["anonUSD500"].contract);
    const anonUSDValue = await this.getRequiredETH(anonUSDBalPool.toString(), user);
    const poolValue = BigInt(anonUSDValue.toString()) + BigInt(ethBalPool.toString());
    const poolValueFromWei = Number(fromWei(poolValue.toString()));
    const rewardsFromWei = Number(fromWei(rewardsPerYearETH.toString()));
    const apy = rewardsFromWei * 100 / poolValueFromWei;
    return apy.toString();
  }

  public async getRewardInfo(tokenId: string, user: string): Promise<any> {
    const { methods } = this._getV3StakerContract();
    const incentiveKey = await this.getV3IncentiveKey(user);
    const params = [incentiveKey.rewardToken, incentiveKey.pool, incentiveKey.startTime, incentiveKey.endTime, incentiveKey.refundee];
    const getRewardInfo = await methods.getRewardInfo(params, tokenId).call({from: user});
    return getRewardInfo;
  }

  public async groupCommitment(
    address: string,
    commitmentsId: BigNumberValue[],
    cb: MethodCallback = null,
  ): Promise<any> {
    const {
      assetEnum,
      commitment,
      pkSum,
    } = await this._http.post<any>(this.apiUrl('group'), {
      address,
      commitmentsId,
    }).toPromise();

    const { methods } = this._getBalanceContract();

    return this.send(methods.groupCommitment(commitmentsId, assetEnum, commitment, pkSum), cb);
  }

  public async groupCommitmentsSpent(
    address: string,
    hashTx: string,
    commitmentsId: BigNumberValue[],
    cb: MethodCallback = null,
  ): Promise<any> {
    return await this._http.post<any>(this.apiUrl('group', 'spent'), {
      address,
      hashTx,
      commitmentsId,
    }).toPromise();
  }

  public async xftBalance(address: string): Promise<string> {
    const { methods } = this._getXFTContract();
    return this.call(methods.balanceOf(address));
  }

  public async usdBalance(address: string): Promise<string> {
    const { methods } = this._getUSDContract();
    return this.call(methods.balanceOf(address));
  }

  public async getBalance(userAddress: string, tokenAddress: string, cb: MethodCallback = null): Promise<string> {
    const { methods } = this._getTokenContract(tokenAddress)
    return methods.balanceOf(userAddress).call({ from: userAddress }, cb)
  }

  public async getLatestPrice(aggregator: number): Promise<{ price: string, decimals: number }> {
    const { methods } = this._getPriceContract();

    const latestPrice = 0;

    const [price, decimals] = Object.values(latestPrice);

    return { price, decimals: Number(decimals) };
  }

  public async getRequiredETH(amount: string, account: string): Promise<string> {
    const { methods } = this._getRewardContract();
    return methods.getETHAmount(amount).call({ from: account });
  }

  public async shift(amount: string, ether: string, account: string, cb: MethodCallback = null): Promise<void> {
    const { methods } = this._getRewardContract();
    await methods.shift(amount).send({ from: account, value: ether }, cb);
  }

  public async addLiquidity(amount: string, ether: string, account: string, cb: MethodCallback = null): Promise<void> {
    const { methods } = this._getRewardUnlockedContract();
    await methods.addLiquidity(amount).send({ from: account, value: ether }, cb);
  }

  public async liquidityUnlocked(user: string, cb: MethodCallback = null): Promise<boolean> {
    const { methods } = this._getRewardContract();
    const expiry = await methods.expiry().call({from: user});
    const blockNumber = await this._walletService.web3.eth.getBlockNumber();
    return (Number(blockNumber) > Number(expiry));
  }

  //withdraw the token from the contract
  public async liquidityWithdraw(tokenId: string, account: string): Promise<void> {
    const { methods } = this._getRewardContract();

    methods.withdraw(tokenId).send({ from: account });
  }

  public async liquidityWithdrawUnlocked(tokenId: string, account: string): Promise<void> {
    const { methods } = this._getRewardUnlockedContract();

    methods.withdraw(tokenId).send({ from: account });
  }

  // Get all the staked tokens in the contract that belong to the account
  public async queryTokenIds(accountAddress: string): Promise<any[]> {
    const contract = this._getRewardContract();
    const stakerContract = this._getV3StakerContract();
    const incentiveKey = await this.getV3IncentiveKey(accountAddress);
    const params = [incentiveKey.rewardToken, incentiveKey.pool, incentiveKey.startTime, incentiveKey.endTime, incentiveKey.refundee];
    let events: any[] = [];
    contract.events.NFT_LOCKED({
      filter: { owner: accountAddress },
      fromBlock: 0,
    }, async function(error : any, event: EventData){ 
      const deposit =  await contract.methods.deposits(event.returnValues['tokenId']).call();
      if (deposit.liquidity != 0){
        events.push({
          token: event.returnValues['tokenId'],
          locked: true,
          reward: fromWei((await stakerContract.methods.getRewardInfo(params, event.returnValues['tokenId']).call({from: accountAddress})).reward)
        });
      }
    });

    return events;
  }

  // Get all the staked tokens in the contract that belong to the account
  public async queryTokenIdsUnlocked(accountAddress: string): Promise<any[]> {
    const contract = this._getRewardUnlockedContract();
    const stakerContract = this._getV3StakerContract();
    const incentiveKey = await this.getV3IncentiveKey(accountAddress);
    const params = [incentiveKey.rewardToken, incentiveKey.pool, incentiveKey.startTime, incentiveKey.endTime, incentiveKey.refundee];
    let events: any[] = [];
    contract.events.NFT_LOCKED({
      filter: { owner: accountAddress },
      fromBlock: 0,
    }, async function(error : any, event: EventData){ 
      const deposit =  await contract.methods.deposits(event.returnValues['tokenId']).call();
      if (deposit.liquidity != 0){
        events.push({
          token: event.returnValues['tokenId'],
          locked: false,
          reward: fromWei((await stakerContract.methods.getRewardInfo(params, event.returnValues['tokenId']).call({from: accountAddress})).reward)
        });
      }
    });

    return events;
  }

  public download(note: string, commitment: string, shifter: string): void {
    let receipt = { "shifter": shifter, "commitment": commitment, "note": note }
    var file = new Blob([JSON.stringify(receipt)], { type: '.txt' });
    var a = document.createElement("a"),
      url = URL.createObjectURL(file);
    a.href = url;
    const timestamp = Date.now();
    a.download = `xft-note-${timestamp}`
    document.body.appendChild(a);
    a.click();
    setTimeout(function () {
      document.body.removeChild(a);
      window.URL.revokeObjectURL(url);
    }, 0);
  }

  private _getXFTContract(): TokenContract {
    const XFT = Object.values(TOKENS_MAP).find(value => value["zkSymbol"] === "XFT")
    const address = XFT["contract"]
    return this._getContractInstance(AUSD_CONTRACT_ABI, address);
  }

  private _getUSDContract(): TokenContract {
    const anonUSD = Object.values(TOKENS_MAP).find(value => value["name"] === "anonUSD")
    const address = anonUSD["contract"]
    return this._getContractInstance(AUSD_CONTRACT_ABI, address)
  }

  private _getTokenContract(address: string): TokenContract {
    return this._getContractInstance(AUSD_CONTRACT_ABI, address)
  }

  private _getPriceContract(): PriceContract {
    return this._getContractInstance(PRICE_CONTRACT_ABI, environment.priceContractAddress);
  }

  private _getBalanceContract(): BalanceContract {
    return this._getContractInstance(BALANCE_CONTRACT_ABI, environment.balanceContractAddress);
  }

  private _getShifterContract(addr: string = null): ShifterContract {
    return this._getContractInstance(SHIFTER_CONTRACT_ABI, addr);
  }

  private _getStorageContract(): StorageContract {
    return this._getContractInstance(STORAGE_CONTRACT_ABI, environment.storageContractAddress);
  }

  private _getSwapRouterContract(): SwapRouterContract {
    return this._getContractInstance(SWAPROUTER_CONTRACT_ABI, "0xE592427A0AEce92De3Edee1F18E0157C05861564"); // Hardcoded by Uniswap
  }

  private _getV3StakerContract(): V3StakerContract {
    return this._getContractInstance(V3STAKER_CONTRACT_ABI, "0xe34139463bA50bD61336E0c446Bd8C0867c6fE65"); //Hardcoded by Uniswap
  }

  private _getWETH9Contract(): Weth9Contract {
    return this._getContractInstance(WETH9_CONTRACT_ABI, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
  }

  private _getChainlinkContract(address: string): ChainlinkContract {
    return this._getContractInstance(CHAINLINK_CONTRACT_ABI, address)
  }

  private _getTokenSwapContract(): TokenSwapContract {
    return this._getContractInstance(TOKENSWAP_CONTRACT_ABI, environment.tokenSwapAddress)
  }

  private _getRewardContract(): RewardContract {
    return this._getContractInstance(REWARDER_contract_abi, environment.rewarderAddress);
  }

  private _getRewardUnlockedContract(): RewardUnlockedContract {
    return this._getContractInstance(REWARDERUNLOCKED_CONTRACT_ABI, environment.rewarderUnlockedAddress)
  }

  private _getOracleContract(): OracleContract {
    return this._getContractInstance(ORACLE_CONTRACT_ABI, environment.oracleAddress);
  }

  private _getQuoterV2Contract(): QuoterV2Contract {
    return this._getContractInstance(QUOTERV2_CONTRACT_ABI, "0x61fFE014bA17989E743c5F6cB21bF9697530B21e"); //Hardcoded uniswap deployment
  }

  private _getContractInstance<T extends Contract>(abi: AbiItem[], address: string): T {
    if (!isAddress(address)) {
      throw new Error('Contract address is not valid');
    }

    const contractInstance = new this._walletService.web3.eth.Contract(abi, address);

    CONTRACT_MAP.set(address, contractInstance);

    return contractInstance as T;
  }

  protected override apiUrl(...path: string[]): string {
    let url = `${super.apiUrl()} `;

    if (path && Array.isArray(path)) {
      url = path.reduce((cur, prev) => {
        cur += `/ ${prev} `;

        return cur;
      }, url);
    }

    return url;
  }
}