import { ethers } from 'ethers';
import { NexusClient } from '@biconomy/abstractjs';
import { BundlerClient } from 'viem/_types/account-abstraction';

interface RpcUrls {
  http: string;
  websocket: string;
}

type NetworkType = 'testnet' | 'mainnet';

type gaslessOptions = {
  type: 'biconomy' | 'coinbase';
  bundlerUrl: string;
  paymasterUrl: string;
};

interface ProviderDetails {
  name: string;
  region: string;
  attributes: string;
  hostUri: string;
  certificate: string;
  paymentsAccepted: string[];
  status: string;
  trust: number;
  timestamp: number;
}

declare enum TransactionStatus {
  SUCCESS = 'success',
  FAILURE = 'failure',
}

interface TransactionData {
  rewardWallet: string;
  tokenAddress: string;
  amount: number;
  decimals: number;
  onSuccessCallback?: (data: unknown) => void;
  onFailureCallback?: (data: unknown) => void;
}

interface DepositData {
  token: string;
  amount: number;
  onSuccessCallback?: (data: unknown) => void;
  onFailureCallback?: (data: unknown) => void;
}

interface WithdrawData extends DepositData {
  operator?: string;
}

interface TokenDetails {
  name: string;
  symbol: string;
  decimal: number;
}

interface UserBalance {
  lockedBalance: string;
  unlockedBalance: string;
  token: TokenDetails;
}

interface WithdrawEarningsData {
  providerAddress: string;
  fizzId: string;
  token: string;
  amount: number;
  isFizz: boolean;
}

interface DepositForOperatorData extends DepositData {
  operatorAddresses: string[];
}

type SmartWalletBundlerClient = BundlerClient | NexusClient;

declare class EscrowModule {
  private provider: ethers.Provider;
  private wallet: ethers.Wallet | undefined;
  private networkType: NetworkType;

  constructor(
    provider: ethers.Provider,
    wallet?: ethers.Wallet,
    networkType: NetworkType = 'mainnet',
    private smartWalletBundlerClientPromise?: Promise<SmartWalletBundlerClient>
  ) {
    this.provider = provider;
    this.wallet = wallet;
    this.networkType = networkType;
    this.smartWalletBundlerClientPromise = smartWalletBundlerClientPromise;
  }

  async getUserBalance(token: string, walletAddress?: string, isOperator: boolean = false) {
    const contractAbi = abiMap[this.networkType].escrow;
    try {
      const contractAddress = contractAddresses[this.networkType].escrow;
      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const tokenAddress: string = tokenDetails?.address || ethers.ZeroAddress;

      let userWalletAddress;
      if (walletAddress) {
        userWalletAddress = walletAddress;
      } else {
        if (this.wallet) {
          userWalletAddress = await this.wallet.getAddress();
        } else {
          throw new Error('No wallet address provided');
        }
      }

      const response = await contract.getUserData(userWalletAddress, tokenAddress, isOperator);

      const userData: UserBalance = {
        lockedBalance: response[0].toString(),
        unlockedBalance: response[1].toString(),
        token: {
          name: tokenDetails?.name,
          symbol: tokenDetails?.symbol,
          decimal: tokenDetails?.decimal,
        },
      };

      return userData;
    } catch (error) {
      const errorMessage = handleContractError(error, contractAbi);
      throw errorMessage;
    }
  }

  async getSmartWalletDetails(): Promise<{ accountAddress: string; balance: string }> {
    if (this.smartWalletBundlerClientPromise) {
      const smartWalletBundlerClient = await this.smartWalletBundlerClientPromise;
      const accountAddress = await smartWalletBundlerClient?.account?.address;
      if (!accountAddress) {
        throw new Error('Smart wallet account address not found');
      }

      const uSponToken = tokenMap[this.networkType].find(
        (token) => token.symbol.toLowerCase() === 'uspon'
      );
      if (!uSponToken) {
        throw new Error('uSPON token not found');
      }

      const tokenContract = new ethers.Contract(
        uSponToken.address,
        abiMap[this.networkType].testToken,
        this.provider
      );
      const balance = await tokenContract.balanceOf(accountAddress);

      return {
        accountAddress,
        balance: balance.toString(),
      };
    } else {
      throw new Error('Gasless options not provided');
    }
  }

  async depositBalance({ token, amount, onSuccessCallback, onFailureCallback }: DepositData) {
    const contractABI = abiMap[this.networkType].escrow;
    try {
      if (this.smartWalletBundlerClientPromise) {
        return await this.depositBalanceGasless({
          token,
          amount,
          onSuccessCallback,
          onFailureCallback,
        });
      }
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType].escrow;
      const tokenABI = abiMap[this.networkType].testToken;

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;

      const contract = new ethers.Contract(contractAddress, contractABI, signer);
      const tokenContract = new ethers.Contract(tokenAddress, tokenABI, signer);

      const finalAmount = Number(amount.toString());
      const depositAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const approvalTxn = await tokenContract.approve(contractAddress, depositAmount);
      await approvalTxn.wait();

      // poll for allowance till it is greater than deposit amount, try only 5 times
      const allowance = await tokenContract.allowance(signer.address, contractAddress);
      let allowanceValue = BigInt(allowance);
      let i = 0;
      while (allowanceValue < depositAmount && i < 5) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        allowanceValue = await tokenContract.allowance(signer.address, contractAddress);
        i++;
      }

      const result = await contract.deposit(tokenAddress, depositAmount);
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  private async depositBalanceGasless({
    token,
    amount,
    onSuccessCallback,
    onFailureCallback,
  }: DepositData) {
    const contractABI = abiMap[this.networkType].escrow;
    try {
      const contractAddress = contractAddresses[this.networkType].escrow;

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;
      const tokenABI = abiMap[this.networkType].testToken;

      const finalAmount = Number(amount.toString());
      const depositAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const smartWalletBundlerClient = await this.smartWalletBundlerClientPromise;

      const approveTxnHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: tokenABI,
            functionName: 'approve',
            to: tokenAddress as `0x${string}`,
            args: [contractAddress, depositAmount],
          },
        ],
      });
      await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: approveTxnHash!,
      });

      const tokenContract = new ethers.Contract(tokenAddress, tokenABI, this.provider);
      const accountAddress = await smartWalletBundlerClient?.account?.address;
      const allowance = await tokenContract.allowance(accountAddress, contractAddress);
      let allowanceValue = BigInt(allowance);

      // poll for allowance till it is greater than deposit amount, try only 5 times
      let i = 0;
      while (allowanceValue < depositAmount && i < 5) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        allowanceValue = await tokenContract.allowance(accountAddress, contractAddress);
        i++;
      }

      const network = await this.provider.getNetwork();
      const chainId = network.chainId;
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const signerAddress = signer.address;
      // Get the current nonce for the signer
      const contract = new ethers.Contract(contractAddress, contractABI, signer);
      const nonce = await contract.nonces(signerAddress);
      const deadline = Math.floor(Date.now() / 1000) + SIGNATURE_DEADLINE;

      const domain = {
        name: 'Spheron',
        version: '1',
        chainId,
        verifyingContract: contractAddress,
      };

      const types = {
        Deposit: [
          { name: 'token', type: 'address' },
          { name: 'amount', type: 'uint256' },
          { name: 'nonce', type: 'uint256' },
          { name: 'deadline', type: 'uint256' },
        ],
      };

      const value = {
        token: tokenAddress,
        amount: depositAmount,
        nonce,
        deadline,
      };

      // Sign the typed data using EIP-712
      const signature = await signer.signTypedData(domain, types, value);

      const txHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: contractABI,
            functionName: 'depositWithSignature',
            to: contractAddress as `0x${string}`,
            args: [tokenAddress, depositAmount, signerAddress, signature, nonce, deadline],
          },
        ],
      });
      const txReceipt = await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: txHash!,
      });

      if (onSuccessCallback) onSuccessCallback(txReceipt?.receipt);
      return txReceipt?.receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  async withdrawBalance({
    token,
    amount,
    operator = ethers.ZeroAddress,
    onSuccessCallback,
    onFailureCallback,
  }: WithdrawData) {
    const contractABI = abiMap[this.networkType].escrow;
    try {
      if (this.smartWalletBundlerClientPromise) {
        return await this.withdrawBalanceGasless({
          token,
          amount,
          operator,
          onSuccessCallback,
          onFailureCallback,
        });
      }
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType].escrow;

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;

      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      const finalAmount = Number(amount.toString());
      const withdrawAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const result = await contract.withdraw(tokenAddress, withdrawAmount, operator);
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  private async withdrawBalanceGasless({
    token,
    amount,
    operator = ethers.ZeroAddress,
    onSuccessCallback,
    onFailureCallback,
  }: WithdrawData) {
    const contractABI = abiMap[this.networkType].escrow;
    try {
      const network = await this.provider.getNetwork();
      const chainId = network.chainId;
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const signerAddress = signer.address;

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;

      const finalAmount = Number(amount.toString());
      const withdrawAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const contractAddress = contractAddresses[this.networkType].escrow;
      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      const deadline = Math.floor(Date.now() / 1000 + SIGNATURE_DEADLINE);

      // Get the current nonce for the signer
      const nonce = await contract.nonces(signerAddress);

      const domain = {
        name: 'Spheron',
        version: '1',
        chainId,
        verifyingContract: contractAddress,
      };

      const types = {
        Withdraw: [
          { name: 'token', type: 'address' },
          { name: 'amount', type: 'uint256' },
          { name: 'operator', type: 'address' },
          { name: 'nonce', type: 'uint256' },
          { name: 'deadline', type: 'uint256' },
        ],
      };

      const value = {
        token: tokenAddress,
        amount: withdrawAmount,
        operator,
        nonce,
        deadline,
      };

      // Sign the typed data using EIP-712
      const signature = await signer.signTypedData(domain, types, value);

      const smartWalletBundlerClient = await this.smartWalletBundlerClientPromise;

      const txHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: contractABI,
            functionName: 'withdrawWithSignature',
            to: contractAddress as `0x${string}`,
            args: [
              tokenAddress,
              withdrawAmount,
              operator,
              signerAddress,
              signature,
              nonce,
              deadline,
            ],
          },
        ],
      });
      const txReceipt = await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: txHash!,
      });

      if (onSuccessCallback) onSuccessCallback(txReceipt?.receipt);
      return txReceipt?.receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  async getProviderEarnings(providerAddress: string, tokenAddress: string) {
    const contractABI = abiMap[this.networkType].escrowProtocol;
    try {
      const contractAddress = contractAddresses[this.networkType].escrowProtocol;
      const contract = new ethers.Contract(contractAddress, contractABI, this.provider);

      const response = await contract.getProviderEarnings(providerAddress, tokenAddress);

      const providerEarnings: { earned: string; withdrawn: string; balance: string } = {
        earned: response[0].toString(),
        withdrawn: response[1].toString(),
        balance: response[2].toString(),
      };

      return providerEarnings;
    } catch (error) {
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  async getFizzEarnings(fizzAddress: string, tokenAddress: string) {
    const contractABI = abiMap[this.networkType].escrowProtocol;
    try {
      const contractAddress = contractAddresses[this.networkType].escrowProtocol;
      const contract = new ethers.Contract(contractAddress, contractABI, this.provider);

      const response = await contract.getFizzNodeEarnings(fizzAddress, tokenAddress);

      const fizzEarnings: { earned: string; withdrawn: string; balance: string } = {
        earned: response[0].toString(),
        withdrawn: response[1].toString(),
        balance: response[2].toString(),
      };

      return fizzEarnings;
    } catch (error) {
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  async withdrawEarnings({ providerAddress, fizzId = '0', token, amount }: WithdrawEarningsData) {
    const contractABI = abiMap[this.networkType].escrowProtocol;
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType].escrowProtocol;

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );

      if (!tokenDetails) {
        throw new Error('Provided token Symbol is invalid.');
      }

      const tokenAddress: string = tokenDetails?.address;

      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      const result = await contract.withdrawEarnings(providerAddress, fizzId, tokenAddress, amount);
      const receipt = await result.wait();
      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  async depositForOperators({
    token,
    amount,
    operatorAddresses,
    onSuccessCallback,
    onFailureCallback,
  }: DepositForOperatorData) {
    const contractABI = abiMap[this.networkType].escrow;

    try {
      if (this.smartWalletBundlerClientPromise) {
        return await this.depositForOperatorsGasless({
          token,
          amount,
          operatorAddresses,
          onSuccessCallback,
          onFailureCallback,
        });
      }
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const contractAddress = contractAddresses[this.networkType].escrow;
      const contract = new ethers.Contract(contractAddress, contractABI, signer);
      const tokenABI = abiMap[this.networkType].testToken;

      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );

      if (!tokenDetails) {
        throw new Error('Provided token Symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;

      const tokenAddress: string = tokenDetails?.address;

      const tokenContract = new ethers.Contract(tokenAddress, tokenABI, signer);

      const finalAmount = Number(amount.toString());
      const depositAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const approvalTxn = await tokenContract.approve(contractAddress, depositAmount);
      await approvalTxn.wait();

      const result = await contract.depositForOperators(tokenAddress, amount, operatorAddresses);
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, contractABI);
      throw errorMessage;
    }
  }

  private async depositForOperatorsGasless({
    token,
    amount,
    operatorAddresses,
    onFailureCallback,
    onSuccessCallback,
  }: DepositForOperatorData) {
    const contractAbi = abiMap[this.networkType].escrow;
    try {
      const contractAddress = contractAddresses[this.networkType].escrow;
      const tokenDetails = tokenMap[this.networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );

      if (!tokenDetails) {
        throw new Error('Provided token Symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;
      const tokenABI = abiMap[this.networkType].testToken;

      const finalAmount = Number(amount.toString());
      const depositAmount = ethers.parseUnits(finalAmount.toString(), decimals);

      const smartWalletBundlerClient = await this.smartWalletBundlerClientPromise;

      const approveTxnHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: tokenABI,
            functionName: 'approve',
            to: tokenAddress as `0x${string}`,
            args: [contractAddress, depositAmount],
          },
        ],
      });
      await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: approveTxnHash!,
      });

      const tokenContract = new ethers.Contract(tokenAddress, tokenABI, this.provider);
      const accountAddress = await smartWalletBundlerClient?.account?.address;
      const allowance = await tokenContract.allowance(accountAddress, contractAddress);
      let allowanceValue = BigInt(allowance);

      // poll for allowance till it is greater than deposit amount, try only 5 times  
      let i = 0;
      while (allowanceValue < depositAmount && i < 5) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        allowanceValue = await tokenContract.allowance(accountAddress, contractAddress);
        i++;
      }

      const network = await this.provider.getNetwork();
      const chainId = network.chainId;
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const signerAddress = signer.address;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);
      const nonce = await contract.nonces(signerAddress);

      const deadline = Math.floor(Date.now() / 1000) + SIGNATURE_DEADLINE * 1000;

      const domain = {
        name: 'Spheron',
        version: '1',
        chainId,
        verifyingContract: contractAddress,
      };

      const types = {
        Deposit: [
          { name: 'token', type: 'address' },
          { name: 'amount', type: 'uint256' },
          { name: 'nonce', type: 'uint256' },
          { name: 'deadline', type: 'uint256' },
        ],
      };

      const value = {
        token: tokenAddress,
        amount: depositAmount,
        nonce,
        deadline,
      };

      const signature = await signer.signTypedData(domain, types, value);

      const txHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: contractAbi,
            functionName: 'depositForOperatorsWithSignature',
            to: contractAddress as `0x${string}`,
            args: [
              tokenAddress,
              depositAmount,
              operatorAddresses,
              signerAddress,
              signature,
              nonce,
              deadline,
            ],
          },
        ],
      });
      const txReceipt = await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: txHash!,
      });

      if (onSuccessCallback) onSuccessCallback(txReceipt?.receipt);
      return txReceipt?.receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, contractAbi);
      throw errorMessage;
    }
  }
}

declare enum Tier {
  One,
  Two,
  Three,
  Four,
  Five,
  Six,
  Seven,
}

declare enum Mode {
  Fizz,
  Provider,
}

interface OrderDetails {
  maxPrice: bigint;
  numOfBlocks: bigint;
  token: string;
  spec: string;
  version: number | bigint;
  mode: Mode;
  tier: Tier[];
}

declare enum OrderState {
  OPEN = 'open',
  PROVISIONED = 'provisioned',
  CLOSED = 'closed',
  MATCHED = 'matched',
}

interface OrderSpecs {
  specs: string;
  version: string;
  mode: string;
  tier: Tier[];
}

interface InitialOrder {
  id: number;
  name: string;
  region: string;
  maxPrice: number;
  numOfBlocks: number;
  token?: {
    symbol?: string;
    decimal?: number;
    address: string;
  };
  creator: string;
  state: OrderState;
  specs: OrderSpecs;
}

interface OrderMatchedEvent {
  leaseId: string;
  providerAddress: string;
  fizzId: string | number | bigint;
  providerId: string | number | bigint;
  acceptedPrice: string | number | bigint;
  creatorAddress: string;
}

interface OrderUpdatedEvent {
  leaseId: string;
  providerAddress: string;
  tenantAddress: string;
  acceptedPrice: string | number | bigint;
}

interface OrderUpdateAcceptedEvent {
  leaseId: string;
  providerAddress: string;
}

declare class OrderModule {
  private provider: ethers.Provider;
  private createTimeoutId: NodeJS.Timeout | null;
  private updateTimeoutId: NodeJS.Timeout | null;
  private wallet: ethers.Wallet | undefined;
  private networkType: NetworkType | undefined;
  private rpcUrls: RpcUrls | undefined;

  constructor(
    provider: ethers.Provider,
    wallet?: ethers.Wallet,
    networkType?: NetworkType,
    private smartWalletBundlerClientPromise?: Promise<SmartWalletBundlerClient>,
    rpcUrls?: RpcUrls
  ) {
    this.provider = provider;
    this.createTimeoutId = null;
    this.updateTimeoutId = null;
    this.wallet = wallet;
    this.networkType = networkType;
    this.smartWalletBundlerClientPromise = smartWalletBundlerClientPromise;
    this.rpcUrls = rpcUrls;
  }

  async createOrder(orderDetails: OrderDetails): Promise<string | null> {
    const contractAbi = abiMap[this.networkType as NetworkType].orderRequest;
    try {
      if (this.smartWalletBundlerClientPromise) {
        return await this.createOrderWithPaymaster(orderDetails);
      }

      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].orderRequest;
      const contractAbi = abiMap[this.networkType as NetworkType].orderRequest;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx: ethers.ContractTransactionResponse = await contract.createOrder(orderDetails);
      const receipt: ethers.ContractTransactionReceipt | null = await tx.wait();
      return receipt?.hash || null;
    } catch (error) {
      if (this.smartWalletBundlerClientPromise) {
        throw error;
      }
      const errorMessage = handleContractError(error, contractAbi);
      throw errorMessage;
    }
  }

  async createOrderWithPaymaster(orderDetails: OrderDetails): Promise<string | null> {
    const network = await this.provider.getNetwork();
    const chainId = network.chainId;
    const { signer } = await initializeSigner({ wallet: this.wallet });
    const claimedSigner = signer.address;

    const contractAddress = contractAddresses[this.networkType as NetworkType].orderRequest;
    const contractAbi = abiMap[this.networkType as NetworkType].orderRequest;

    const contract = new ethers.Contract(contractAddress, contractAbi, signer);
    const nonce = await contract.nonces(claimedSigner);
    const deadline = Math.floor(Date.now() / 1000 + SIGNATURE_DEADLINE);

    const domain = {
      name: 'Spheron',
      version: '1',
      chainId,
      verifyingContract: contractAddress,
    };

    const types = {
      CreateOrder: [
        { name: 'maxPrice', type: 'uint256' },
        { name: 'numOfBlocks', type: 'uint64' },
        { name: 'token', type: 'address' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' },
      ],
    };

    const value = {
      maxPrice: orderDetails.maxPrice,
      numOfBlocks: orderDetails.numOfBlocks,
      token: orderDetails.token,
      nonce,
      deadline,
    };

    // Sign the typed data using EIP-712
    const signature = await signer.signTypedData(domain, types, value);

    const smartWalletBundlerClient = await this.smartWalletBundlerClientPromise;

    try {
      const txHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: contractAbi,
            functionName: 'createOrderWithSignature',
            to: contractAddress as `0x${string}`,
            args: [orderDetails, claimedSigner, signature, nonce, deadline],
          },
        ],
      });
      const txReceipt = await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: txHash!,
      });
      return txReceipt?.receipt.transactionHash || null;
    } catch (error) {
      throw error;
    }
  }

  async updateOrder(orderId: string, orderDetails: OrderDetails): Promise<string | null> {
    const contractAbi = abiMap[this.networkType as NetworkType].orderRequest;
    try {
      if (this.smartWalletBundlerClientPromise) {
        return await this.updateOrderWithPaymaster(orderId, orderDetails);
      }

      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].orderRequest;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx: ethers.ContractTransactionResponse = await contract.updateInitialOrder(
        orderId,
        orderDetails
      );
      const receipt: ethers.ContractTransactionReceipt | null = await tx.wait();
      return receipt?.hash || null;
    } catch (error) {
      if (this.smartWalletBundlerClientPromise) {
        throw error;
      }
      const errorMessage = handleContractError(error, contractAbi);
      throw errorMessage;
    }
  }

  async updateOrderWithPaymaster(
    orderId: string,
    orderDetails: OrderDetails
  ): Promise<string | null> {
    const network = await this.provider.getNetwork();
    const chainId = network.chainId;
    const { signer } = await initializeSigner({ wallet: this.wallet });
    const claimedSigner = signer.address;

    const contractAddress = contractAddresses[this.networkType as NetworkType].orderRequest;
    const contractAbi = abiMap[this.networkType as NetworkType].orderRequest;

    const contract = new ethers.Contract(contractAddress, contractAbi, signer);
    const nonce = await contract.nonces(claimedSigner);
    const deadline = Math.floor(Date.now() / 1000 + SIGNATURE_DEADLINE);

    const domain = {
      name: 'Spheron',
      version: '1',
      chainId,
      verifyingContract: contractAddress,
    };

    const types = {
      UpdateInitialOrder: [
        { name: 'orderId', type: 'uint64' },
        { name: 'maxPrice', type: 'uint256' },
        { name: 'numOfBlocks', type: 'uint64' },
        { name: 'token', type: 'address' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' },
      ],
    };

    const value = {
      orderId: orderId,
      maxPrice: orderDetails.maxPrice,
      numOfBlocks: orderDetails.numOfBlocks,
      token: orderDetails.token,
      nonce,
      deadline,
    };

    // Sign the typed data using EIP-712
    const signature = await signer.signTypedData(domain, types, value);

    const smartWalletBundlerClient = await this.smartWalletBundlerClientPromise;
    try {
      const txHash = await smartWalletBundlerClient?.sendUserOperation({
        calls: [
          {
            abi: contractAbi,
            functionName: 'updateInitialOrderWithSignature',
            to: contractAddress as `0x${string}`,
            args: [orderId, orderDetails, claimedSigner, signature, nonce, deadline],
          },
        ],
      });
      const txReceipt = await smartWalletBundlerClient?.waitForUserOperationReceipt({
        hash: txHash!,
      });
      return txReceipt?.receipt.transactionHash || null;
    } catch (error) {
      throw error;
    }
  }

  async getOrderDetails(leaseId: string): Promise<InitialOrder> {
    const contractAbi = abiMap[this.networkType as NetworkType].orderRequest;
    const contractAddress = contractAddresses[this.networkType as NetworkType].orderRequest;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
    const response = await contract.getOrderById(leaseId);

    const specs = {
      specs: response.specs.specs,
      version: response.specs.version,
      mode: response.specs.mode,
      tier: response.specs.tier.map((t: bigint) => Number(t)) as Tier[],
    };

    const tokenDetails = getTokenDetails(response.token, this.networkType as NetworkType);
    const token = {
      symbol: tokenDetails?.symbol,
      decimal: tokenDetails?.decimal,
      address: tokenDetails?.address,
    };

    return {
      id: response.id.toString(),
      maxPrice: Number(response.maxPrice),
      numOfBlocks: Number(response.numOfBlocks),
      token,
      creator: response.creator,
      state: getOrderStateAsString(response.state),
      specs,
    } as InitialOrder;
  }

  async listenToOrderCreated(
    timeoutTime = 60000,
    onSuccessCallback: (
      leaseId: string,
      providerAddress: string,
      fizzId: string | number | bigint,
      providerId: string | number | bigint,
      acceptedPrice: string | number | bigint,
      creatorAddress: string
    ) => void,
    onFailureCallback: () => void
  ): Promise<OrderMatchedEvent> {
    let orderWssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      orderWssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!orderWssProvider) {
      throw new Error('Order WSS provider not created');
    }
    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();

    const contractAbi = abiMap[this.networkType as NetworkType].bid;
    const contractAddress = contractAddresses[this.networkType as NetworkType].bid;

    const contract = new ethers.Contract(contractAddress, contractAbi, orderWssProvider);

    return new Promise((resolve, reject) => {
      this.createTimeoutId = setTimeout(() => {
        contract.off('OrderMatched');
        orderWssProvider.destroy();
        onFailureCallback();
        reject({ error: true, msg: 'Order not matched within timeout' });
      }, timeoutTime);

      contract.on(
        'OrderMatched',
        (
          leaseId: string,
          providerAddress: string,
          fizzId: string | number | bigint,
          providerId: string | number | bigint,
          acceptedPrice: string | number | bigint,
          creatorAddress: string
        ) => {
          if (creatorAddress.toString().toLowerCase() === account.toString().toLowerCase()) {
            onSuccessCallback(
              leaseId,
              providerAddress,
              fizzId,
              providerId,
              acceptedPrice,
              creatorAddress
            );
            contract.off('OrderMatched');
            orderWssProvider.destroy();
            clearTimeout(this.createTimeoutId as NodeJS.Timeout);
            resolve({
              leaseId,
              providerAddress,
              fizzId,
              providerId,
              acceptedPrice,
              creatorAddress,
            });
          }
        }
      );
    });
  }

  async listenToOrderUpdated(
    timeoutTime = 60000,
    onSuccessCallback: (
      leaseId: string,
      providerAddress: string,
      tenantAddress?: string,
      acceptedPrice?: string
    ) => void,
    onFailureCallback: () => void
  ): Promise<OrderUpdatedEvent> {
    let orderWssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      orderWssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!orderWssProvider) {
      throw new Error('Order WSS provider not created');
    }

    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();

    const contractAbi = abiMap[this.networkType as NetworkType].bid;
    const contractAddress = contractAddresses[this.networkType as NetworkType].bid;

    const contract = new ethers.Contract(contractAddress, contractAbi, orderWssProvider);

    return new Promise((resolve, reject) => {
      this.updateTimeoutId = setTimeout(() => {
        contract.off('LeaseUpdated');
        orderWssProvider.destroy();
        onFailureCallback();
        reject({ error: true, msg: 'Order update failed' });
      }, timeoutTime);

      contract.on('LeaseUpdated', (leaseId, providerAddress, tenantAddress, acceptedPrice) => {
        if (tenantAddress.toString().toLowerCase() === account.toString().toLowerCase()) {
          onSuccessCallback(leaseId, providerAddress, tenantAddress, acceptedPrice?.toString());
          contract.off('LeaseUpdated');
          orderWssProvider.destroy();
          clearTimeout(this.updateTimeoutId as NodeJS.Timeout);
          resolve({ leaseId, providerAddress, tenantAddress, acceptedPrice });
        }
      });
    });
  }

  async listenToOrderUpdateAccepted(
    timeoutTime = 60000,
    onSuccessCallback: (leaseId: string, providerAddress: string) => void,
    onFailureCallback: () => void
  ): Promise<OrderUpdateAcceptedEvent> {
    let orderWssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      orderWssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!orderWssProvider) {
      throw new Error('Order WSS provider not created');
    }

    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();

    const contractAbi = abiMap[this.networkType as NetworkType].bid;
    const contractAddress = contractAddresses[this.networkType as NetworkType].bid;

    const contract = new ethers.Contract(contractAddress, contractAbi, orderWssProvider);

    return new Promise((resolve, reject) => {
      this.updateTimeoutId = setTimeout(() => {
        contract.off('UpdateRequestAccepted');
        orderWssProvider.destroy();
        onFailureCallback();
        reject({ error: true, msg: 'Order not accepted within timeout' });
      }, timeoutTime);

      contract.on('UpdateRequestAccepted', async (leaseId, providerAddress, tenantAddress) => {
        if (tenantAddress.toString().toLowerCase() === account.toString().toLowerCase()) {
          await onSuccessCallback(leaseId, providerAddress);
          contract.off('UpdateRequestAccepted');
          orderWssProvider.destroy();
          clearTimeout(this.updateTimeoutId as NodeJS.Timeout);
          resolve({ leaseId, providerAddress });
        }
      });
    });
  }
}

declare enum LeaseState {
  ACTIVE = 'active',
  TERMINATED = 'terminated',
}

interface Lease {
  leaseId: string;
  fizzId: string;
  requestId: string;
  acceptedPrice: number;
  leaseHourlyCost: number;
  providerAddress: string;
  tenantAddress: string;
  startBlock: string;
  startTime: number;
  endTime: number;
  state: LeaseState;
}

interface LeaseWithOrderDetails extends Lease {
  name: string;
  region: string;
  tier: Tier[];
  token: {
    symbol?: string;
    decimal?: number;
  };
}

interface FizzParams {
  providerId: bigint;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  rewardWallet: string;
}

type RawFizzNode = [
  bigint, // fizzId
  bigint, // providerId
  string, // spec
  string, // walletAddress
  string[], // paymentsAccepted
  bigint, // status
  bigint, // joinTimestamp
  string // rewardWallet
];

interface FizzNode {
  fizzId: bigint;
  providerId: bigint;
  region: string;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  status: number;
  joinTimestamp: bigint;
  rewardWallet: string;
}

interface FizzDetails {
  region: string;
  providerId: bigint;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  status: bigint;
  joinTimestamp: bigint;
  rewardWallet: string;
}

interface ResourceCategory {
  name: string;
  registry: string;
  baseReward: bigint;
}

interface Resource {
  name: string;
  tier: string;
  multiplier: bigint;
}

interface ResourceAttributes {
  cpuUnits: bigint;
  cpuAttributes: string[];
  ramUnits: bigint;
  ramAttributes: string[];
  gpuUnits: bigint;
  gpuAttributes: string[];
  endpointsKind: number;
  endpointsSequenceNumber: number;
}

interface FizzLease {
  leaseId: bigint;
  fizzId: bigint;
  requestId: bigint;
  resourceAttribute: ResourceAttributes;
  acceptedPrice: bigint;
  providerAddress: string;
  tenantAddress: string;
  startBlock: bigint;
  startTime: bigint;
  endTime: bigint;
  state: string;
}

declare enum FizzProviderStatus {
  Unregistered,
  Registered,
  Active,
  Maintenance,
  Suspended,
  Deactivated,
}

declare enum FizzProviderTrustTier {
  One,
  Two,
  Three,
  Four,
  Five,
  Six,
  Seven,
}

interface FizzProvider {
  providerId: bigint;
  name: string;
  region: string;
  walletAddress: string;
  paymentsAccepted: string[];
  spec: string;
  hostUri: string;
  certificate: string;
  status: FizzProviderStatus;
  tier: FizzProviderTrustTier;
  joinTimestamp: bigint;
  rewardWallet: string;
}

interface FizzAttribute {
  id: bigint | string;
  units: bigint | string;
}

type RawFizzAttribute = [id: string, units: string];

interface FizzStatusResponse {
  name: string;
  allocatable: {
    cpu: number;
    gpu: number;
    gpu_infos:
      | null
      | {
          vendor: string;
          name: string;
          model_id: string;
          interface: string;
          memory_size: string;
          vram_available_percentage: number;
        }[];
    memory: number;
    storage_ephemeral: number;
  };
  available: {
    cpu: number;
    gpu: number;
    gpu_infos: null;
    memory: number;
    storage_ephemeral: number;
  };
  bandwidth: `${string};${string}`;
  version: `${string};${string}`;
  os: string;
  arch: string;
  cuda_version: string;
  nvidia_driver_version: string;
}

interface IProvider {
  providerId: string;
  spec: string;
  hostUri: string;
  certificate: string;
  paymentsAccepted: string[];
  status: string;
  trust: number;
  timestamp: number;
}

interface Attribute {
  id: bigint | string;
  units: bigint | string;
}

declare enum ProviderStatus {
  Unregistered,
  Registered,
  Active,
  Maintenance,
  Suspended,
  Deactivated,
}

declare enum ProviderTrustTier {
  One,
  Two,
  Three,
  Four,
  Five,
  Six,
  Seven,
}

interface Provider {
  providerId?: bigint;
  name: string;
  region: string;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  hostUri: string;
  certificate: string;
  status: ProviderStatus;
  tier: ProviderTrustTier;
  joinTimestamp: bigint;
  rewardWallet: string;
}

type Category = 'CPU' | 'GPU';

type RawProviderAttribute = [id: string, units: string];

declare class ProviderModule {
  private provider: ethers.Provider;
  private networkType: NetworkType | undefined;

  constructor(provider: ethers.Provider, networkType?: NetworkType) {
    this.provider = provider;
    this.networkType = networkType;
  }

  async getProviderDetails(providerAddress: string): Promise<IProvider> {
    if (!providerAddress) {
      throw new Error('Pass Provider Address');
    }

    if (!isValidEthereumAddress(providerAddress)) {
      throw new Error('Pass Valid Address');
    }
    try {
      const contractAbi = abiMap[this.networkType as NetworkType].providerRegistry;
      const contractAddress = contractAddresses[this.networkType as NetworkType].providerRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const response = await contract.getProviderByAddress(providerAddress);
      const providerId = await contract.addressToProviderId(providerAddress);

      const providerDetailsData: IProvider = {
        providerId: providerId.toString(),
        spec: response[0],
        hostUri: response[1],
        certificate: response[2],
        paymentsAccepted: response[3],
        status: response[4].toString(),
        trust: Number(response[5].toString()) + 1,
        timestamp: Number(response[6].toString()),
      };

      return providerDetailsData;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getProviderPendingAttributes(providerAddress: string, category: Category) {
    if (!providerAddress) {
      throw new Error('Pass Provider Address');
    }

    if (!isValidEthereumAddress(providerAddress)) {
      throw new Error('Pass Valid Address');
    }

    if (!category) {
      throw new Error('Please pass a category');
    }
    try {
      const contractAbi = abiMap[this.networkType as NetworkType].providerAttributeRegistry;
      const contractAddress =
        contractAddresses[this.networkType as NetworkType].providerAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const response = await contract.getProviderPendingAttributes(providerAddress, category);

      return response;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getProviderAttributes(providerAddress: string, category: Category) {
    if (!providerAddress) {
      throw new Error('Pass Provider Address');
    }

    if (!isValidEthereumAddress(providerAddress)) {
      throw new Error('Pass Valid Address');
    }

    if (!category) {
      throw new Error('Please pass a category');
    }
    try {
      const contractAbi = abiMap[this.networkType as NetworkType].providerAttributeRegistry;
      const contractAddress =
        contractAddresses[this.networkType as NetworkType].providerAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const response = await contract.getAttributes(providerAddress, category);

      return response;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getProvider(providerId: bigint): Promise<Provider> {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].providerRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].providerRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const providerData = await contract.getProvider(providerId);

      let name, region;
      try {
        const { Name, Region } = JSON.parse(providerData.spec);
        name = Name;
        region = Region;
      } catch {
        try {
          const { Name, Region } = decompressProviderSpec(providerData.spec) as {
            Name: string;
            Region: string;
          };
          name = Name;
          region = Region;
        } catch {
          name = '';
          region = '';
        }
      }

      return {
        name,
        region,
        spec: providerData.spec,
        hostUri: providerData.hostUri,
        certificate: providerData.certificate,
        paymentsAccepted: providerData.paymentsAccepted,
        status: providerData.status,
        tier: providerData.tier,
        joinTimestamp: providerData.joinTimestamp,
        walletAddress: providerData.walletAddress,
        rewardWallet: providerData.rewardWallet,
      };
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getProviderByAddress(walletAddress: string): Promise<Provider> {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].providerRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].providerRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const providerData = await contract.getProviderByAddress(walletAddress);

      let name, region;
      try {
        const { Name, Region } = JSON.parse(providerData.spec);
        name = Name;
        region = Region;
      } catch {
        try {
          const { Name, Region } = decompressProviderSpec(providerData.spec) as {
            Name: string;
            Region: string;
          };
          name = Name;
          region = Region;
        } catch {
          name = '';
          region = '';
        }
      }

      return {
        name,
        region,
        spec: providerData.spec,
        hostUri: providerData.hostUri,
        certificate: providerData.certificate,
        paymentsAccepted: providerData.paymentsAccepted,
        status: providerData.status,
        tier: providerData.tier,
        joinTimestamp: providerData.joinTimestamp,
        walletAddress,
        rewardWallet: providerData.rewardWallet,
      };
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getAllProviders(): Promise<Provider[]> {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].providerRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].providerRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const providersData = await contract.getAllProviders();

      const providers: Provider[] = providersData.map((provider: Provider) => {
        let name, region;
        try {
          const { Name, Region } = JSON.parse(provider.spec as string);
          name = Name;
          region = Region;
        } catch {
          try {
            const { Name, Region } = decompressProviderSpec(provider.spec as string) as {
              Name: string;
              Region: string;
            };
            name = Name;
            region = Region;
          } catch {
            name = '';
            region = '';
          }
        }
        return {
          name,
          region,
          providerId: (provider.providerId as bigint).toString(),
          walletAddress: provider.walletAddress,
          paymentsAccepted: provider.paymentsAccepted,
          spec: provider.spec,
          hostUri: provider.hostUri,
          certificate: provider.certificate,
          status: ProviderStatus[provider.status],
          tier: Number(provider.tier.toString()),
          // tier: ProviderTrustTier[provider.tier],
          joinTimestamp: Number(provider.joinTimestamp.toString()),
          rewardWallet: provider.rewardWallet,
        };
      });

      return providers;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getAttributes(providerAddress: string, category: string): Promise<Attribute[]> {
    try {
      const contractAddress =
        contractAddresses[this.networkType as NetworkType].providerAttributeRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].providerAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawProviderAttribute[] = await contract.getAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawProviderAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getPendingAttributes(providerAddress: string, category: string): Promise<Attribute[]> {
    try {
      const contractAddress =
        contractAddresses[this.networkType as NetworkType].providerAttributeRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].providerAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawProviderAttribute[] = await contract.getPendingAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawProviderAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }
}

declare class FizzModule {
  private provider: ethers.Provider;
  private timeoutId: NodeJS.Timeout | undefined;
  private wallet: ethers.Wallet | undefined;
  private providerModule: ProviderModule;
  private networkType: NetworkType | undefined;
  private rpcUrls: RpcUrls | undefined;

  constructor(
    provider: ethers.Provider,
    wallet?: ethers.Wallet,
    networkType?: NetworkType,
    rpcUrls?: RpcUrls
  ) {
    this.provider = provider;
    this.wallet = wallet;
    this.providerModule = new ProviderModule(provider, networkType);
    this.networkType = networkType;
    this.rpcUrls = rpcUrls;
  }

  async addFizzNode(fizzParams: FizzParams): Promise<unknown> {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const abi = abiMap[this.networkType as NetworkType].fizzRegistry;
      const contract = new ethers.Contract(contractAddress, abi, signer);

      const tx = await contract.addFizzNode(fizzParams);
      const receipt = await tx.wait();

      return { tx, receipt };
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzName(newName: string): Promise<unknown> {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      // Contract address (hardcoded or retrieved from an environment variable)
      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const abi = abiMap[this.networkType as NetworkType].fizzRegistry;
      const contract = new ethers.Contract(contractAddress, abi, signer);

      const tx = await contract.updateFizzName(newName);

      const receipt = await tx.wait();

      return { tx, receipt };
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getFizzById(fizzId: bigint): Promise<FizzDetails> {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const fizzDetails = await contract.getFizz(fizzId);

      const {
        providerId,
        spec,
        paymentsAccepted,
        status,
        joinTimestamp,
        walletAddress,
        rewardWallet,
      } = fizzDetails;

      return {
        region: spec?.split(',')?.[7] ?? '',
        providerId,
        spec,
        paymentsAccepted,
        status,
        joinTimestamp,
        walletAddress,
        rewardWallet,
      };
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getFizzNodeByAddress(walletAddress: string) {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const fizzId = await contract.addressToFizzId(walletAddress);
      const fizzNode: FizzDetails = await this.getFizzById(fizzId);

      const result: FizzNode = {
        region: fizzNode.spec?.split(',')?.[7] ?? '',
        fizzId,
        providerId: fizzNode.providerId,
        spec: fizzNode.spec,
        walletAddress: fizzNode.walletAddress,
        paymentsAccepted: fizzNode.paymentsAccepted,
        status: Number(fizzNode.status.toString()),
        joinTimestamp: fizzNode.joinTimestamp,
        rewardWallet: fizzNode.rewardWallet,
      };

      return result;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getTotalFizzNodes(): Promise<bigint> {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const fizzCounter = await contract.nextFizzId();
      return fizzCounter;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getAllFizzNodes(): Promise<FizzNode[]> {
    try {
      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const allFizzNodes = await contract.getAllFizzNodes();

      const fizzNodes: FizzNode[] = allFizzNodes.map((fizzNode: RawFizzNode) => ({
        fizzId: fizzNode[0],
        providerId: fizzNode[1],
        spec: fizzNode[2],
        walletAddress: fizzNode[3],
        paymentsAccepted: fizzNode[4],
        status: fizzNode[5],
        joinTimestamp: fizzNode[6],
        rewardWallet: fizzNode[7],
      }));

      return fizzNodes;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getActiveFizzNodes(
    providerProxyUrl: string,
    timeout: number = 2000
  ): Promise<FizzStatusResponse[]> {
    if (!this.wallet) throw new Error('Wallet not found');
    if (!this.networkType) throw new Error('Network type not found');
    try {
      let providers: Awaited<ReturnType<typeof subgraphGetProviders>> = await subgraphGetProviders(
        this.networkType
      );

      const authToken = createAuthorizationToken(this.wallet);

      const fizzNodes = (
        await Promise.allSettled(
          providers
            .filter(
              (p) =>
                p.hostUri !== 'localhost' && p.status === 'Active' && p.region !== 'dev-spheron'
            )
            .map(async (p) => {
              const reqBody = {
                certificate: p.certificate,
                authToken,
                method: 'GET',
                url: `https://${p.hostUri}:8543/status`,
              };

              const url = `${providerProxyUrl}`;
              try {
                const response = await requestPipeline({
                  url,
                  method: 'POST',
                  body: JSON.stringify(reqBody),
                  options: {
                    signal: AbortSignal.timeout(timeout),
                  },
                });
                return response;
              } catch (error) {
                return { cluster: { inventory: { available: { nodes: [] } } } };
              }
            })
        )
      ).map((result) => {
        if (result.status === 'fulfilled') {
          if (Object.keys(result.value.cluster.inventory.available).length === 0) {
            return { nodes: [] };
          }
          return result.value.cluster.inventory.available;
        } else {
          console.error(`Failed to get fizz nodes for provider: ${result.reason}`);
          return { nodes: [] };
        }
      });

      const fizzNodesWithAddress = fizzNodes
        .map((i) =>
          i.nodes.map(async (node: { name: string }) => {
            const walletAddress = `0x${node.name}`;
            return { walletAddress, ...node };
          })
        )
        .flat()
        .filter(Boolean);

      const subgraphNodes = await subgraphGetFizzNodeIds(this.networkType);

      const mappedNodes = await Promise.all(
        fizzNodesWithAddress.map(async (nPromise) => {
          const n = await nPromise;
          return [n.walletAddress.toLowerCase(), null] as [string, null];
        })
      );
      const activeNodeAddressIdMap = new Map<string, number | null>(mappedNodes);

      subgraphNodes.forEach((node) => {
        const address = node.walletAddress.toLowerCase();
        if (activeNodeAddressIdMap.has(address)) activeNodeAddressIdMap.set(address, node.fizzId);
      });

      const fizzNodesWithId = await Promise.all(
        fizzNodesWithAddress.map(async (nodePromise) => {
          const node = await nodePromise;
          return { ...node, id: activeNodeAddressIdMap.get(node.walletAddress.toLowerCase()) };
        })
      );

      return fizzNodesWithId;
    } catch (error) {
      throw error;
    }
  }

  async getAttributes(providerAddress: string, category: string): Promise<FizzAttribute[]> {
    try {
      const contractAddress =
        contractAddresses[this.networkType as NetworkType].fizzAttributeRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawFizzAttribute[] = await contract.getAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawFizzAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getPendingAttributes(providerAddress: string, category: string): Promise<FizzAttribute[]> {
    try {
      const contractAddress =
        contractAddresses[this.networkType as NetworkType].fizzAttributeRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawFizzAttribute[] = await contract.getPendingAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawFizzAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getResource(resourceID: bigint, category: string): Promise<Resource> {
    try {
      const contractAbi = abiMap[this.networkType as NetworkType].resourceRegistry;
      const contractAddress =
        category === 'CPU'
          ? contractAddresses[this.networkType as NetworkType].fizzResourceRegistryCPU
          : contractAddresses[this.networkType as NetworkType].fizzResourceRegistryGPU;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const [name, tier, multiplier]: [string, string, bigint] = await contract.getResource(
        resourceID
      );

      const resource: Resource = { name, tier, multiplier };

      return resource;
    } catch (error) {
      const errorMessage = handleContractError(error, ResourceRegistryAbi);
      throw errorMessage;
    }
  }

  async getFizzLeases(
    fizzId: bigint,
    providerId: bigint,
    state?: string
  ): Promise<FizzLease[] | unknown> {
    try {
      const providerData = await this.providerModule.getProvider(providerId);
      const walletAddress = providerData.walletAddress;

      const leaseContractAddress = contractAddresses[this.networkType as NetworkType].computeLease;
      const leaseContractAbi = abiMap[this.networkType as NetworkType].computeLease;
      const leaseContract = new ethers.Contract(
        leaseContractAddress,
        leaseContractAbi,
        this.provider
      );
      const [activeLeases, allLeases] = await leaseContract.getProviderLeases(walletAddress);

      const selectedLeases = state === 'ACTIVE' ? activeLeases : allLeases;
      const leases: FizzLease[] = [];

      for (const leaseId of selectedLeases) {
        const leaseData = await leaseContract.leases(leaseId);

        // Filter by fizzId
        if (leaseData.fizzId === fizzId) {
          const lease: FizzLease = {
            leaseId: leaseData.leaseId,
            fizzId: leaseData.fizzId,
            requestId: leaseData.requestId,
            resourceAttribute: leaseData.resourceAttributes,
            acceptedPrice: leaseData.acceptedPrice,
            providerAddress: leaseData.providerAddress,
            tenantAddress: leaseData.tenantAddress,
            startBlock: leaseData.startBlock,
            startTime: leaseData.startTime,
            endTime: leaseData.endTime,
            state: leaseData.state,
          };

          leases.push(lease);
        }
      }

      return leases;
    } catch (error) {
      const errorMessage = handleContractError(error, ComputeLeaseAbi);
      throw errorMessage;
    }
  }

  async listenToFizzCreated(
    onSuccessCallback: (fizzId: bigint, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
    const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi);

      return new Promise((resolve, reject) => {
        this.timeoutId = setTimeout(() => {
          contract.off('FizzNodeAdded');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz creation failed' });
        }, timeoutTime);

        contract.on('FizzNodeAdded', (fizzId: bigint, walletAddress: string) => {
          if (walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()) {
            onSuccessCallback(fizzId, walletAddress);
            contract.off('FizzNodeAdded');
            clearTimeout(this.timeoutId as NodeJS.Timeout);
            resolve({ fizzId, walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzSpecs(specs: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.updateFizzSpec(specs);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenSpecUpdated(
    onSuccessCallback: (fizzId: bigint, specs: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    let wssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      wssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!wssProvider) {
      throw new Error('Fizz WSS provider not created');
    }
    const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
    const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, wssProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('FizzNodeSpecUpdated');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('FizzNodeSpecUpdated', async (fizzId: bigint, specs: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, specs, fizz.walletAddress);
            contract.off('FizzNodeSpecUpdated');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, specs, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzRegion(region: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.updateFizzRegion(region);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenRegionUpdated(
    onSuccessCallback: (fizzId: bigint, region: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    let wssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      wssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!wssProvider) {
      throw new Error('Fizz WSS provider not created');
    }

    const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
    const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, wssProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('FizzNodeRegionUpdated');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('FizzNodeRegionUpdated', async (fizzId: bigint, region: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, region, fizz.walletAddress);
            contract.off('FizzNodeRegionUpdated');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, region, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzProvider(providerId: bigint) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.updateFizzProviderId(providerId);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenProviderUpdated(
    onSuccessCallback: (fizzId: bigint, providerId: bigint, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    let wssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      wssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!wssProvider) {
      throw new Error('Fizz WSS provider not created');
    }

    const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
    const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, wssProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('FizzNodeProviderIdUpdated');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('FizzNodeProviderIdUpdated', async (fizzId: bigint, providerId: bigint) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, providerId, fizz.walletAddress);
            contract.off('FizzNodeProviderIdUpdated');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, providerId, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async addAcceptedPayment(tokenAddress: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.addAcceptedPayment(tokenAddress);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenToAddAcceptedPayment(
    onSuccessCallback: (fizzId: bigint, tokenAddress: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    let wssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      wssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!wssProvider) {
      throw new Error('Fizz WSS provider not created');
    }

    const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
    const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, wssProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('PaymentAdded');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('PaymentAdded', async (fizzId: bigint, tokenAddress: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, tokenAddress, fizz.walletAddress);
            contract.off('PaymentAdded');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, tokenAddress, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async removeAcceptedPayment(tokenAddress: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
      const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.removeAcceptedPayment(tokenAddress);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenToRemoveAcceptedPayment(
    onSuccessCallback: (fizzId: bigint, tokenAddress: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    let wssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      wssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!wssProvider) {
      throw new Error('Fizz WSS provider not created');
    }

    const contractAddress = contractAddresses[this.networkType as NetworkType].fizzRegistry;
    const contractAbi = abiMap[this.networkType as NetworkType].fizzRegistry;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, wssProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('PaymentRemoved');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('PaymentRemoved', async (fizzId: bigint, tokenAddress: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, tokenAddress, fizz.walletAddress);
            contract.off('PaymentRemoved');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, tokenAddress, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }
}

declare class LeaseModule {
  private provider: ethers.Provider;
  private orderModule: OrderModule;
  private fizzModule: FizzModule;
  private providerModule: ProviderModule;
  private leaseCloseTimeoutId: NodeJS.Timeout | null;
  private wallet: ethers.Wallet | undefined;
  private networkType: NetworkType | undefined;
  private rpcUrls: RpcUrls | undefined;

  constructor(
    provider: ethers.Provider,
    wallet?: ethers.Wallet,
    networkType?: NetworkType,
    private smartWalletBundlerClientPromise?: Promise<SmartWalletBundlerClient>,
    rpcUrls?: RpcUrls
  ) {
    this.provider = provider;
    this.getLeaseDetails = this.getLeaseDetails.bind(this);
    this.orderModule = new OrderModule(
      provider,
      wallet,
      networkType,
      smartWalletBundlerClientPromise,
      rpcUrls
    );
    this.fizzModule = new FizzModule(provider, wallet, networkType, rpcUrls);
    this.providerModule = new ProviderModule(provider, networkType);
    this.leaseCloseTimeoutId = null;
    this.wallet = wallet;
    this.networkType = networkType;
    this.smartWalletBundlerClientPromise = smartWalletBundlerClientPromise;
  }

  async getLeaseDetails(leaseId: string): Promise<Lease> {
    const contractAbi = abiMap[this.networkType as NetworkType].computeLease;
    const contractAddress = contractAddresses[this.networkType as NetworkType].computeLease;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
    const response = await contract.leases(leaseId);
    const leaseHourlyCost = (Number(response.acceptedPrice) * 1800) / 10 ** 18;

    const lease: Lease = {
      leaseId: response.leaseId.toString(),
      fizzId: response.fizzId.toString(),
      requestId: response.requestId.toString(),
      acceptedPrice: Number(response.acceptedPrice),
      leaseHourlyCost,
      providerAddress: response.providerAddress.toString(),
      tenantAddress: response.tenantAddress.toString(),
      startBlock: response.startBlock.toString(),
      startTime: Number(response.startTime),
      endTime: Number(response.endTime),
      state: getLeaseStateAsString(response.state.toString()) as LeaseState,
    };

    return lease;
  }

  async getLeaseIds(address: string) {
    const contractAbi = abiMap[this.networkType as NetworkType].computeLease;
    const contractAddress = contractAddresses[this.networkType as NetworkType].computeLease;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
    const response = await contract.getTenantLeases(address);

    const activeLeaseIds = response[0].map((id: bigint) => id.toString()) as string[];
    const allLeaseIds = response[1].map((id: bigint) => id.toString()) as string[];

    const terminatedLeaseIds = allLeaseIds.filter((lId) => {
      return !activeLeaseIds.includes(lId);
    });

    return {
      activeLeaseIds,
      allLeaseIds,
      terminatedLeaseIds,
    };
  }

  async getLeasesByState(
    address: string,
    options?: { state?: LeaseState; page?: number; pageSize?: number }
  ) {
    const { activeLeaseIds, terminatedLeaseIds, allLeaseIds } = await this.getLeaseIds(address);

    let leaseIds = allLeaseIds;
    const totalCount = allLeaseIds.length;
    const terminatedCount = terminatedLeaseIds.length;
    const activeCount = activeLeaseIds.length;

    if (options?.state) {
      switch (options.state) {
        case LeaseState.ACTIVE:
          leaseIds = activeLeaseIds;
          break;
        case LeaseState.TERMINATED:
          leaseIds = terminatedLeaseIds;
          break;
      }
    }

    leaseIds.sort((a, b) => Number(b) - Number(a));

    if (options?.page) {
      const pageSize = options.pageSize || DEFAULT_PAGE_SIZE;
      leaseIds = leaseIds.slice((options.page - 1) * pageSize, options.page * pageSize);
    }

    const filteredLeases = await Promise.all(leaseIds.map((lId) => this.getLeaseDetails(lId)));
    const orderDetails = await Promise.all(
      leaseIds.map((lId) => this.orderModule.getOrderDetails(lId))
    );

    const leaseWithToken: LeaseWithOrderDetails[] = await Promise.all(
      filteredLeases.map(async (lease, index) => {
        const order = orderDetails[index];
        let tokenDetails;
        if (order.token?.address)
          tokenDetails = getTokenDetails(order.token.address, this.networkType as NetworkType);

        let region;
        if (lease.fizzId.toString() !== '0') {
          const fizz: FizzDetails = await this.fizzModule.getFizzById(BigInt(lease.fizzId));
          region = fizz?.region;
        } else {
          const provider: Provider = await this.providerModule.getProviderByAddress(
            lease.providerAddress
          );
          region = provider?.region;
        }

        return {
          ...lease,
          name: order.name,
          tier: order.specs.tier,
          region: region,
          token: {
            symbol: tokenDetails?.symbol,
            decimal: tokenDetails?.decimal,
          },
        };
      })
    );

    return {
      leases: leaseWithToken,
      activeCount,
      terminatedCount,
      totalCount,
    };
  }

  async closeLease(leaseId: string): Promise<string | null> {
    if (this.smartWalletBundlerClientPromise) {
      return await this.closeLeaseWithPaymaster(leaseId);
    }
    const contractAbi = abiMap[this.networkType as NetworkType].computeLease;
    const contractAddress = contractAddresses[this.networkType as NetworkType].computeLease;
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const contract = new ethers.Contract(contractAddress, contractAbi, signer);
      const tx = await contract.closeLease(leaseId);
      const receipt = await tx.wait();
      return receipt?.hash || null;
    } catch (error) {
      const errorMessage = handleContractError(error, contractAbi);
      throw errorMessage;
    }
  }

  async closeLeaseWithPaymaster(leaseId: string): Promise<string | null> {
    const contractAddress = contractAddresses[this.networkType as NetworkType].computeLease;
    const contractAbi = abiMap[this.networkType as NetworkType].computeLease;

    const network = await this.provider.getNetwork();
    const chainId = network.chainId;

    const bundlerClient = await this.smartWalletBundlerClientPromise;

    const { signer } = await initializeSigner({ wallet: this.wallet });
    const claimedSigner = signer.address;

    const contract = new ethers.Contract(contractAddress, contractAbi, signer);

    const nonce = await contract.nonces(claimedSigner);
    const deadline = Math.floor(Date.now() / 1000 + SIGNATURE_DEADLINE);

    const domain = {
      name: 'Spheron',
      version: '1',
      chainId,
      verifyingContract: contractAddress,
    };

    const types = {
      CloseLease: [
        { name: 'leaseId', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' },
      ],
    };

    const value = {
      leaseId,
      nonce,
      deadline,
    };

    // Sign the typed data using EIP-712
    const signature = await signer.signTypedData(domain, types, value);

    try {
      const txHash = await bundlerClient?.sendUserOperation({
        calls: [
          {
            abi: contractAbi,
            functionName: 'closeLeaseWithSignature',
            to: contractAddress as `0x${string}`,
            args: [leaseId, claimedSigner, signature, nonce, deadline],
          },
        ],
      });
      const txReceipt = await bundlerClient?.waitForUserOperationReceipt({ hash: txHash! });
      return txReceipt?.receipt.transactionHash || null;
    } catch (error) {
      throw error;
    }
  }

  async listenToLeaseClosedEvent(
    onSuccessCallback: ({
      leaseId,
      providerAddress,
      tenantAddress,
    }: {
      leaseId: string;
      providerAddress: string;
      tenantAddress: string;
    }) => void,
    onFailureCallback: () => void,
    timeout = 60000
  ) {
    let leaseWssProvider: WebSocketProvider | null = null;
    if (this.rpcUrls?.websocket) {
      leaseWssProvider = new ethers.WebSocketProvider(this.rpcUrls?.websocket);
    }
    if (!leaseWssProvider) {
      throw new Error('Lease WSS provider not created');
    }

    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();
    const contractAbi = abiMap[this.networkType as NetworkType].computeLease;
    const contractAddress = contractAddresses[this.networkType as NetworkType].computeLease;

    const contract = new ethers.Contract(contractAddress, contractAbi, leaseWssProvider);

    return new Promise((resolve, reject) => {
      this.leaseCloseTimeoutId = setTimeout(() => {
        contract.off('LeaseClosed');
        onFailureCallback();
        reject({ error: true, msg: 'Lease Close Failed' });
      }, timeout);

      contract.on(
        'LeaseClosed',
        (orderId: string, providerAddress: string, tenantAddress: string) => {
          if (
            providerAddress.toString().toLowerCase() === account.toString().toLowerCase() ||
            tenantAddress.toString().toLowerCase() === account.toString().toLowerCase()
          ) {
            onSuccessCallback({ leaseId: orderId, providerAddress, tenantAddress });
            contract.off('LeaseClosed');
            clearTimeout(this.leaseCloseTimeoutId as NodeJS.Timeout);
            resolve({ leaseId: orderId, providerAddress, tenantAddress });
          }
        }
      );
    });
  }
}

interface CreateDeploymentResponse {
  leaseId: string;
  transactionHash: string | null;
}

interface UpdateDeploymentResponse {
  leaseId: string;
  providerAddress: string;
  transactionHash: string | null;
}

interface LeaseStatusResponse {
  services: Record<string, ServiceDetails> | null;
  forwarded_ports: Record<string, ForwardedPort[]> | null;
  ips: string[] | null;
}

interface DeploymentResponse extends LeaseStatusResponse {
  secureUrls: Record<string, string[]>;
}

interface ServiceDetails {
  name: string;
  available: number;
  total: number;
  uris: string[] | null;
  observed_generation: number;
  target_replicas: number;
  ready_replicas: number;
  available_replicas: number;
  container_statuses: ContainerStatus[];
  creationTimestamp: string;
}

interface ContainerStatus {
  name: string;
  state: ContainerState;
  lastState: Record<string, unknown>;
  ready: boolean;
  restartCount: number;
  image: string;
  imageID: string;
  containerID: string;
  started: boolean;
}

interface ContainerState {
  running?: { startedAt: string };
  terminated?: { exitCode: number; reason: string; finishedAt: string };
  waiting?: { reason: string; message: string };
}

interface ForwardedPort {
  host: string;
  port: number;
  externalPort: number;
  proto: string;
  name: string;
}

declare class DeploymentModule {
  private wallet: ethers.Wallet | undefined;
  private escrowModule: EscrowModule;
  private orderModule: OrderModule;
  private leaseModule: LeaseModule;
  private providerModule: ProviderModule;
  private networkType: NetworkType;

  constructor(
    provider: ethers.Provider,
    wallet?: ethers.Wallet,
    networkType: NetworkType = 'mainnet',
    smartWalletBundlerClientPromise?: Promise<SmartWalletBundlerClient>,
    rpcUrls?: RpcUrls
  ) {
    this.wallet = wallet;
    this.escrowModule = new EscrowModule(provider, wallet, networkType);
    this.orderModule = new OrderModule(
      provider,
      wallet,
      networkType,
      smartWalletBundlerClientPromise,
      rpcUrls
    );
    this.leaseModule = new LeaseModule(
      provider,
      wallet,
      networkType,
      smartWalletBundlerClientPromise
    );
    this.providerModule = new ProviderModule(provider, networkType);
    this.networkType = networkType;
  }

  async createDeployment(
    iclYaml: string,
    providerProxyUrl: string,
    createOrderMatchedCallback?: (transactionHash: string) => void,
    createOrderFailedCallback?: (transactionHash: string) => void,
    isOperator: boolean = false
  ): Promise<CreateDeploymentResponse> {
    try {
      const { error, orderDetails: details } = yamlToOrderDetails(iclYaml, this.networkType);
      if (error || typeof details === 'undefined') {
        throw new Error('Please verify YAML format');
      }

      // Detect YAML version and generate appropriate manifest
      const icl = yaml.load(iclYaml) as any;
      const isV2 = Number(icl.version) === 2;
      const sdlManifest = isV2 ? getManifestV2(iclYaml) : getManifestIcl(iclYaml);
      const { token, maxPrice, numOfBlocks, mode } = details;
      
      // Check if this is a fizz deployment (mode === 0) and validate token
      if (mode === 0) {
        const sponTokenAddress = contractAddresses[this.networkType].SPON;
        if (token.toLowerCase() !== sponTokenAddress.toLowerCase()) {
          throw new Error('For fizz deployments, only SPON token is allowed');
        }
      }
      
      const tokenDetails = getTokenDetails(token, this.networkType as NetworkType);
      const decimal = 18;
      const totalCost = (Number(maxPrice.toString()) / 10 ** decimal) * Number(numOfBlocks);
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }
      const account = await this.wallet.getAddress();

      const tokenBalance = await this.escrowModule.getUserBalance(
        tokenDetails?.symbol || '',
        account,
        isOperator
      );
      if (Number(tokenBalance.unlockedBalance) / 10 ** (tokenDetails?.decimal || 0) < totalCost) {
        throw new Error('Insufficient Balance');
      }
      try {
        const transactionHash = await this.orderModule.createOrder(details);
        const newOrderEvent: Promise<OrderMatchedEvent> = this.orderModule.listenToOrderCreated(
          60_000,
          () => {
            createOrderMatchedCallback?.(transactionHash || '');
          },
          () => {
            createOrderFailedCallback?.(transactionHash || '');
          }
        );
        const { leaseId, providerAddress } = await newOrderEvent;

        const providerDetails: IProvider = await this.providerModule.getProviderDetails(
          providerAddress
        );
        const { certificate, hostUri } = providerDetails;
        const authToken = await createAuthorizationToken(this.wallet);
        const port = details.mode === 0 ? 8543 : 8443;

        try {
          if (isV2) {
            const spheronProviderV2 = new SpheronProviderModuleV2(
              `https://${hostUri}:${port}`,
              providerProxyUrl
            );
            await spheronProviderV2.submitManfiest(
              certificate,
              authToken,
              leaseId.toString(),
              sdlManifest as V2Manifest
            );
          } else {
            const spheronProvider = new SpheronProviderModule(
              `https://${hostUri}:${port}`,
              providerProxyUrl
            );
            await spheronProvider.submitManfiest(
              certificate,
              authToken,
              leaseId.toString(),
              sdlManifest as { name: string; services: ServiceManifest[] }[]
            );
          }
          return { leaseId, transactionHash };
        } catch (error) {
          throw new Error('Error occurred in sending manifest');
        }
      } catch (error) {
        throw error;
      }
    } catch (error) {
      throw error;
    }
  }

  async updateDeployment(
    leaseId: string,
    iclYaml: string,
    providerProxyUrl: string,
    updatedOrderLeaseCallback?: (leaseId: string, providerAddress: string) => void,
    updatedOrderLeaseFailedCallback?: () => void,
    updateOrderAcceptedCallback?: (leaseId: string) => void,
    updateOrderFailedCallback?: () => void,
    isOperator: boolean = false
  ): Promise<UpdateDeploymentResponse> {
    try {
      const { error, orderDetails: details } = yamlToOrderDetails(iclYaml, this.networkType);
      if (error || typeof details === 'undefined') {
        throw new Error('Please verify YAML format');
      }

      // Detect YAML version and generate appropriate manifest
      const icl = yaml.load(iclYaml) as any;
      const isV2 = icl.version === '2.0';
      const sdlManifest = isV2 ? getManifestV2(iclYaml) : getManifestIcl(iclYaml);
      const { token, maxPrice, numOfBlocks, mode } = details;
      
      // Check if this is a fizz deployment (mode === 0) and validate token
      if (mode === 0) {
        const sponTokenAddress = contractAddresses[this.networkType].SPON;
        if (token.toLowerCase() !== sponTokenAddress.toLowerCase()) {
          throw new Error('For fizz deployments, only SPON token is allowed');
        }
      }
      
      const tokenDetails = getTokenDetails(token, this.networkType as NetworkType);
      const decimal = 18;
      const totalCost =
        Number(Number(maxPrice.toString()) / 10 ** (decimal || 0)) * Number(numOfBlocks);

      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }
      const account = await this.wallet.getAddress();

      const tokenBalance = await this.escrowModule.getUserBalance(
        tokenDetails?.symbol || '',
        account,
        isOperator
      );
      if (Number(tokenBalance.unlockedBalance) / 10 ** (tokenDetails?.decimal || 0) < totalCost) {
        throw new Error('Insufficient Balance');
      }

      const transactionHash = await this.orderModule.updateOrder(leaseId, details);
      let updateLeaseResponse: OrderUpdatedEvent | undefined;

      const updatedOrderLease: Promise<OrderUpdatedEvent> = this.orderModule.listenToOrderUpdated(
        120_000,
        (leaseId, providerAddress) => {
          updatedOrderLeaseCallback?.(leaseId, providerAddress);
        },
        () => {
          updatedOrderLeaseFailedCallback?.();
        }
      );
      const updateOrderAcceptance: Promise<OrderUpdateAcceptedEvent> =
        this.orderModule.listenToOrderUpdateAccepted(
          60_000,
          async (leaseId: string, providerAddress: string) => {
            updateOrderAcceptedCallback?.(leaseId);

            const providerDetails: IProvider = await this.providerModule.getProviderDetails(
              providerAddress
            );
            const { certificate, hostUri } = providerDetails;
            const port = details.mode === 0 ? 8543 : 8443;
            const authToken = await createAuthorizationToken(this.wallet!);
            if (isV2) {
              const spheronProviderV2 = new SpheronProviderModuleV2(
                `https://${hostUri}:${port}`,
                providerProxyUrl
              );
              await spheronProviderV2.updateManfiest(
                certificate,
                authToken,
                leaseId as string,
                sdlManifest as V2Manifest
              );
            } else {
              const spheronProvider = new SpheronProviderModule(
                `https://${hostUri}:${port}`,
                providerProxyUrl
              );
              await spheronProvider.submitManfiest(
                certificate,
                authToken,
                leaseId as string,
                sdlManifest as { name: string; services: ServiceManifest[] }[]
              );
            }
            const updateOrderLeaseResponse: OrderUpdatedEvent = await updatedOrderLease;
            updateLeaseResponse = { ...updateOrderLeaseResponse };
          },
          () => {
            updateOrderFailedCallback?.();
          }
        );
      await updateOrderAcceptance;
      return {
        transactionHash,
        leaseId,
        providerAddress: updateLeaseResponse?.providerAddress || '',
      };
    } catch (error) {
      throw error;
    }
  }

  async getDeployment(leaseId: string, providerProxyUrl: string): Promise<DeploymentResponse> {
    try {
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }

      if (!leaseId) {
        throw new Error('Provider Lease Id');
      }
      const { providerAddress, fizzId } = await this.leaseModule.getLeaseDetails(leaseId);
      const port = fizzId?.toString() !== '0' ? 8543 : 8443;
      const providerDetails: IProvider = await this.providerModule.getProviderDetails(
        providerAddress
      );
      const { certificate, hostUri } = providerDetails;

      const spheronProvider = new SpheronProviderModuleV2(
        `https://${hostUri}:${port}`,
        providerProxyUrl
      );
      const authToken = await createAuthorizationToken(this.wallet);
      const leaseInfo: LeaseStatusResponse = await spheronProvider.getLeaseStatus(
        certificate,
        authToken,
        leaseId
      );

      const service = leaseInfo?.services || {};
      const leaseServiceKeys = Object.keys(service);
      leaseServiceKeys.forEach((serviceName: string) => {
        let uris: string[] = [];
        if (service?.[serviceName] && (service?.[serviceName]?.uris || []).length > 0) {
          (service?.[serviceName]?.uris || []).forEach((url: string) => {
            url = replaceDomain(url, hostUri);
            uris.push(url);
          });
        }
        service[serviceName].uris = uris;
      });
      const forwardedPorts = leaseInfo?.forwarded_ports || {};
      const serviceKeys = Object.keys(forwardedPorts);
      const secureUrls: Record<string, string[]> = {};
      if (serviceKeys.length > 0) {
        serviceKeys.forEach((serviceName: string) => {
          if (forwardedPorts?.[serviceName]?.length > 0) {
            leaseInfo.forwarded_ports?.[serviceName].forEach((forwardedPort) => {
              const secureUrl = getSecureProxyUrl(
                forwardedPort.externalPort,
                providerDetails.providerId
              );
              if (secureUrls[serviceName]?.length > 0) {
                secureUrls[serviceName].push(secureUrl);
              } else {
                secureUrls[serviceName] = [secureUrl];
              }
            });
          }
        });
      }
      const leaseWithSecureUrls = {
        ...leaseInfo,
        secureUrls,
        hostUri,
      };
      return leaseWithSecureUrls;
    } catch (error) {
      throw error;
    }
  }

  async getDeploymentLogs(
    leaseId: string,
    providerProxyUrl: string,
    logsOptions?: { service?: string; tail?: number; startup?: boolean }
  ): Promise<string[]> {
    try {
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }

      if (!leaseId) {
        throw new Error('Provider Lease Id');
      }
      const { providerAddress, fizzId } = await this.leaseModule.getLeaseDetails(leaseId);
      const port = fizzId?.toString() !== '0' ? 8543 : 8443;
      const providerDetails: IProvider = await this.providerModule.getProviderDetails(
        providerAddress
      );
      const { certificate, hostUri } = providerDetails;

      const spheronProvider = new SpheronProviderModuleV2(
        `https://${hostUri}:${port}`,
        `${providerProxyUrl}/ws-data`
      );
      const authToken = await createAuthorizationToken(this.wallet);
      const leaseLogs = await spheronProvider.getLeaseLogs(
        certificate,
        authToken,
        leaseId,
        logsOptions?.service,
        logsOptions?.tail,
        logsOptions?.startup
      );
      return leaseLogs;
    } catch (error) {
      throw error;
    }
  }

  async closeDeployment(leaseId: string): Promise<{ transactionHash: string | null }> {
    try {
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }

      if (!leaseId) {
        throw new Error('Provider Lease Id');
      }

      const closeLeaseResponse = await this.leaseModule.closeLease(leaseId);

      return { transactionHash: closeLeaseResponse };
    } catch (error) {
      throw error;
    }
  }
}

declare class InventoryModule {
  private wallet: ethers.Wallet | undefined;
  private providerModule: ProviderModule;
  private fizzModule: FizzModule;
  private networkType: NetworkType;
  private rpcUrls: RpcUrls | undefined;

  constructor(
    provider: ethers.Provider,
    wallet?: ethers.Wallet,
    networkType: NetworkType = 'mainnet',
    rpcUrls?: RpcUrls | undefined
  ) {
    this.wallet = wallet;
    this.rpcUrls = rpcUrls;
    this.providerModule = new ProviderModule(provider, networkType);
    this.fizzModule = new FizzModule(provider, wallet, networkType, this.rpcUrls);
    this.networkType = networkType;
  }

  async getFizzInventory(
    providerProxyUrl: string,
    options?: { groupBy?: 'fizzAddress' | 'gpu'; timeout?: number }
  ) {
    try {
      const activeFizzNodes = await this.fizzModule.getActiveFizzNodes(
        providerProxyUrl,
        options?.timeout || 2000
      );

      if (!options) options = { groupBy: 'gpu' };
      if (!options.groupBy) options.groupBy = 'gpu';

      if (options.groupBy === 'fizzAddress') {
        const fizzInventory = activeFizzNodes.reduce((acc, fizzNode) => {
          const allocatableGpuCount = fizzNode.allocatable.gpu;
          const availableGpuCount = fizzNode.available.gpu;

          if (availableGpuCount <= 0 || allocatableGpuCount <= 0) return acc;

          const gpuInfos = fizzNode.allocatable.gpu_infos;

          if (!gpuInfos || gpuInfos.length === 0) return acc;

          const availableGPUPerType = availableGpuCount / gpuInfos.length;
          const foundGpu = gpuInfos.find((info) => !!info.name);

          if (!foundGpu) return acc;

          const fizzGpuCountMap: Record<
            string,
            { available: number; allocatable: number; gpuShortName: string; gpuVendor: string }
          > = {};
          const key = `${foundGpu?.vendor || 'nvidia'}|${foundGpu.name}`;

          gpuInfos.forEach(() => {
            const gpuCount = fizzGpuCountMap[key] ?? { available: 0, allocatable: 0 };
            fizzGpuCountMap[key] = {
              available: gpuCount.available + availableGPUPerType,
              allocatable: gpuCount.allocatable + 1,
              gpuShortName: foundGpu.name,
              gpuVendor: foundGpu.vendor,
            };
          });

          const fizzAddress = (
            fizzNode.name.startsWith('0x') ? fizzNode.name : `0x${fizzNode.name}`
          ).toLowerCase();

          acc[fizzAddress] = Object.values(fizzGpuCountMap);
          return acc;
        }, {} as Record<string, { available: number; allocatable: number; gpuShortName: string; gpuVendor: string }[]>);

        return { fizzInventory };
      } else if (options.groupBy === 'gpu') {
        const fizzGpuCountMap = new Map<string, { available: number; allocatable: number }>();

        activeFizzNodes.forEach((fizzNode) => {
          const allocatableGpuCount = fizzNode.allocatable.gpu;
          const availableGpuCount = fizzNode.available.gpu;

          if (availableGpuCount <= 0 || allocatableGpuCount <= 0) return;

          const gpuInfos = fizzNode.allocatable.gpu_infos;

          if (!gpuInfos || gpuInfos.length === 0) return;

          const availableGPUPerType = availableGpuCount / gpuInfos.length;
          const foundGpu = gpuInfos.find((info) => !!info.name);

          if (!foundGpu) return;

          const key = `${foundGpu?.vendor || 'nvidia'}|${foundGpu.name}`;
          gpuInfos.forEach(() => {
            const gpuCount = fizzGpuCountMap.get(key) ?? { available: 0, allocatable: 0 };
            fizzGpuCountMap.set(key, {
              available: gpuCount.available + availableGPUPerType,
              allocatable: gpuCount.allocatable + 1,
            });
          });
        });

        const fizzInventory = Array.from(fizzGpuCountMap.keys()).reduce((acc, key) => {
          const [vendor, gpuShortName] = key.split('|');
          if (!vendor || !gpuShortName) return acc;

          const gpuConfig = GpuConfig.find(
            (gpu) => gpu.shortName === gpuShortName && gpu.vendor === vendor
          );
          if (!gpuConfig) return acc;

          const gpuCount = fizzGpuCountMap.get(key)!;

          const { fizzGpuPricePerHour, fizzGpuPricePerMonth } = gpuConfig;

          acc[gpuShortName] = {
            available: gpuCount.available,
            allocatable: gpuCount.allocatable,
            pricePerHr: fizzGpuPricePerHour,
            pricePerMonth: fizzGpuPricePerMonth,
          };

          return acc;
        }, {} as Record<string, { allocatable: number; available: number; pricePerHr: number; pricePerMonth: number }>);

        return { fizzInventory };
      }
    } catch (error) {
      throw error;
    }
  }

  async getProviderInventory(
    providerProxyUrl?: string,
    options?: { groupBy?: 'providerAddress' | 'gpu'; timeout?: number }
  ) {
    try {
      if (!this.wallet) throw new Error('Wallet is required');
      let providers: Awaited<ReturnType<typeof subgraphGetProviders>> | null | Provider[] = null;
      if (this.networkType) {
        providers = await subgraphGetProviders(this.networkType);
      } else {
        providers = await this.providerModule.getAllProviders();
      }

      if (!options) options = { groupBy: 'gpu' };
      if (!options.groupBy) options.groupBy = 'gpu';

      const activeProviders = providers.filter(
        (p) =>
          (p.status === 'Active' || p.status === ProviderStatus.Active) &&
          p.region !== 'dev-spheron' &&
          p.hostUri !== 'localhost'
      );

      const authToken = createAuthorizationToken(this.wallet);

      const activeProviderDetails = (
        await Promise.allSettled(
          activeProviders.map(async (provider) => {
            const reqBody = {
              certificate: provider.certificate,
              authToken,
              method: 'GET',
              url: `https://${provider.hostUri}:8443/status`,
            };

            const url = `${providerProxyUrl}`;
            try {
              const response = await requestPipeline({
                url,
                method: 'POST',
                body: JSON.stringify(reqBody),
                options: {
                  signal: AbortSignal.timeout(options?.timeout || 2000),
                },
              });
              return { walletAddress: provider.walletAddress, response };
            } catch (error) {
              return {
                walletAddress: provider.walletAddress,
                response: { cluster: { inventory: { available: { nodes: [] } } } },
              };
            }
          })
        )
      )
        .map((result) => {
          if (result.status === 'fulfilled') {
            if (
              result.value.response &&
              result.value.response.cluster &&
              result.value.response.cluster.inventory
            ) {
              return {
                ...result.value.response.cluster.inventory.available,
                walletAddress: result.value.walletAddress.toLowerCase(),
              };
            }
            return null;
          }
          return null;
        })
        .filter(Boolean);

      if (options.groupBy === 'providerAddress') {
        const providerInventory = activeProviderDetails.reduce((acc, provider) => {
          const fizzGpuCountMap: Record<
            string,
            { available: number; allocatable: number; gpuShortName: string; gpuVendor: string }
          > = {};

          provider.nodes?.forEach((node: any) => {
            const allocatableGpuCount = node.allocatable?.gpu ?? 0;
            const availableGpuCount = node.available?.gpu ?? 0;

            if (availableGpuCount <= 0 || allocatableGpuCount <= 0) return acc;

            const gpuInfos = node.allocatable.gpu_infos;

            if (!gpuInfos || gpuInfos.length === 0) return acc;

            const availableGPUPerType = availableGpuCount / gpuInfos.length;
            const foundGpu = gpuInfos.find((info: { name?: string }) => !!info.name);

            if (!foundGpu) return acc;

            const key = `${foundGpu?.vendor || 'nvidia'}|${foundGpu.name}`;

            gpuInfos.forEach(() => {
              const gpuCount = fizzGpuCountMap[key] ?? { available: 0, allocatable: 0 };
              fizzGpuCountMap[key] = {
                available: gpuCount.available + availableGPUPerType,
                allocatable: gpuCount.allocatable + 1,
                gpuShortName: foundGpu.name,
                gpuVendor: foundGpu.vendor,
              };
            });
          });

          const fizzAddress = (
            provider.walletAddress.startsWith('0x')
              ? provider.walletAddress
              : `0x${provider.walletAddress}`
          ).toLowerCase();

          acc[fizzAddress] = Object.values(fizzGpuCountMap);
          return acc;
        }, {} as Record<string, { available: number; allocatable: number; gpuShortName: string; gpuVendor: string }[]>);

        const filteredProviders = Object.keys(providerInventory).reduce((acc, providerAddress) => {
          if (providerInventory[providerAddress] && providerInventory[providerAddress].length > 0)
            acc[providerAddress] = providerInventory[providerAddress];
          return acc;
        }, {} as typeof providerInventory);

        return { providerInventory: filteredProviders };
      } else if (options.groupBy === 'gpu') {
        const fizzGpuCountMap = new Map<string, { available: number; allocatable: number }>();

        activeProviderDetails.forEach((provider) => {
          provider.nodes?.forEach((node: any) => {
            const allocatableGpuCount = node.allocatable?.gpu ?? 0;
            const availableGpuCount = node.available?.gpu ?? 0;

            if (availableGpuCount <= 0 || allocatableGpuCount <= 0) return;

            const gpuInfos = node.allocatable.gpu_infos;

            if (!gpuInfos || gpuInfos.length === 0) return;

            const availableGPUPerType = availableGpuCount / gpuInfos.length;
            const foundGpu = gpuInfos.find((info: { name?: string }) => !!info.name);

            if (!foundGpu) return;

            const key = `${foundGpu?.vendor || 'nvidia'}|${foundGpu.name}`;
            gpuInfos.forEach(() => {
              const gpuCount = fizzGpuCountMap.get(key) ?? { available: 0, allocatable: 0 };
              fizzGpuCountMap.set(key, {
                available: gpuCount.available + availableGPUPerType,
                allocatable: gpuCount.allocatable + 1,
              });
            });
          });
        });

        const providerInventory = Array.from(fizzGpuCountMap.keys()).reduce(
          (acc, key) => {
            const [vendor, gpuShortName] = key.split('|');
            if (!vendor || !gpuShortName) return acc;

            const gpuConfig = GpuConfig.find(
              (gpu) => gpu.shortName === gpuShortName && gpu.vendor === vendor
            );
            if (!gpuConfig) return acc;

            const gpuCount = fizzGpuCountMap.get(key)!;

            // const { gpuPricePerHour, gpuPricePerMonth } = gpuConfig;

            acc[gpuShortName] = {
              available: gpuCount.available,
              allocatable: gpuCount.allocatable,
              // pricePerHr: gpuPricePerHour,
              // pricePerMonth: gpuPricePerMonth,
            };

            return acc;
          },
          {} as Record<
            string,
            {
              allocatable: number;
              available: number;
              // pricePerHr: number; pricePerMonth: number
            }
          >
        );

        return { providerInventory };
      }
    } catch (error) {
      throw error;
    }
  }
}

interface ICpuConfig {
    id: number;
    name: string;
    vendor: 'general' | 'apple';
    shortName: string;
    multiplier: number;
    tier: 'Medium 1' | 'Low 2' | 'Low 1' | 'Medium 2';
    sp: number;
    fn: number;
    fizzCpuPricePerHour: number;
    fizzCpuPricePerMonth: number;
    fizzRamPricePerGbPerMonth: number;
    fizzStoragePricePerGbPerMonth: number;
}
declare const CpuConfig: ICpuConfig[];

interface IGpuConfig {
    id: number;
    name: string;
    tier: 'Entry 1' | 'Entry 2' | 'Low 1' | 'Low 2' | 'Medium 1' | 'Medium 2' | 'High 1' | 'High 2' | 'Ultra High 1' | 'Ultra High 2' | 'Ultra High 3';
    shortName: string;
    multiplier: number;
    baseFnPoints: number;
    fizzGpuPricePerHour: number;
    fizzGpuPricePerMonth: number;
    fizzCpuPricePerMonth: number;
    fizzRamPricePerGbPerMonth: number;
    fizzStoragePricePerGbPerMonth: number;
    vendor: 'nvidia';
}
declare const GpuConfig: IGpuConfig[];

declare class SpheronSDK {
    leases: LeaseModule;
    orders: OrderModule;
    escrow: EscrowModule;
    provider: ProviderModule;
    fizz: FizzModule;
    deployment: DeploymentModule;
    inventory: InventoryModule;
    private smartWalletBundlerClientPromise?;
    constructor({ networkType, privateKey, rpcUrls, gaslessOptions, }: {
        networkType: NetworkType;
        privateKey?: string;
        rpcUrls?: RpcUrls;
        gaslessOptions?: gaslessOptions;
    });
}

export { type Attribute, type Category, CpuConfig, type DepositData, type DepositForOperatorData, type FizzAttribute, type FizzDetails, type FizzLease, type FizzNode, type FizzParams, type FizzProvider, FizzProviderStatus, FizzProviderTrustTier, type FizzStatusResponse, GpuConfig, type ICpuConfig, type IGpuConfig, type IProvider, type InitialOrder, type Lease, LeaseState, type LeaseWithOrderDetails, Mode, type OrderDetails, type OrderMatchedEvent, OrderState, type OrderUpdateAcceptedEvent, type OrderUpdatedEvent, type Provider, type ProviderDetails, ProviderStatus, ProviderTrustTier, type RawFizzAttribute, type RawFizzNode, type RawProviderAttribute, type Resource, type ResourceAttributes, type ResourceCategory, SpheronSDK, Tier, type TokenDetails, type TransactionData, TransactionStatus, type UserBalance, type WithdrawData, type WithdrawEarningsData };
