soft3.js/src/cyberclient.ts

import {
  Code,
  CodeDetails,
  Contract,
  ContractCodeHistoryEntry,
  JsonObject,
  setupWasmExtension,
  WasmExtension,
} from "@cosmjs/cosmwasm-stargate";
import { fromAscii, toHex } from "@cosmjs/encoding";
import { Uint53 } from "@cosmjs/math";
import {
  Account,
  accountFromAny,
  AuthExtension,
  BankExtension,
  Block,
  Coin,
  DeliverTxResponse,
  DistributionExtension,
  GovExtension,
  GovParamsType,
  GovProposalId,
  IbcExtension,
  IndexedTx,
  QueryClient,
  SequenceResponse,
  setupAuthExtension,
  setupBankExtension,
  setupDistributionExtension,
  setupGovExtension,
  setupIbcExtension,
  setupStakingExtension,
  setupTxExtension,
  StakingExtension,
  TxExtension,
} from "@cosmjs/stargate";
import {
  BlockResultsResponse,
  Tendermint34Client,
  TendermintClient,
  toRfc3339WithNanoseconds,
} from "@cosmjs/tendermint-rpc";
import { assert } from "@cosmjs/utils";
import {
  QueryCommunityPoolResponse,
  QueryDelegationRewardsResponse,
  QueryDelegationTotalRewardsResponse,
  QueryDelegatorValidatorsResponse as QueryDelegatorValidatorsResponseDistribution,
  QueryDelegatorWithdrawAddressResponse,
  QueryParamsResponse as QueryParamsResponseDistribution,
  QueryValidatorCommissionResponse,
  QueryValidatorOutstandingRewardsResponse,
  QueryValidatorSlashesResponse,
} from "cosmjs-types/cosmos/distribution/v1beta1/query";
import { ProposalStatus } from "cosmjs-types/cosmos/gov/v1beta1/gov";
import {
  QueryDepositResponse,
  QueryDepositsResponse,
  QueryParamsResponse as QueryParamsResponseGovernance,
  QueryProposalResponse,
  QueryProposalsResponse,
  QueryTallyResultResponse,
  QueryVoteResponse,
  QueryVotesResponse,
} from "cosmjs-types/cosmos/gov/v1beta1/query";
import {
  QueryDelegationResponse,
  QueryDelegatorDelegationsResponse,
  QueryDelegatorUnbondingDelegationsResponse,
  QueryDelegatorValidatorResponse,
  QueryDelegatorValidatorsResponse,
  QueryHistoricalInfoResponse,
  QueryParamsResponse as QueryParamsResponseStaking,
  QueryPoolResponse,
  QueryRedelegationsResponse,
  QueryUnbondingDelegationResponse,
  QueryValidatorDelegationsResponse,
  QueryValidatorResponse,
  QueryValidatorsResponse,
  QueryValidatorUnbondingDelegationsResponse,
} from "cosmjs-types/cosmos/staking/v1beta1/query";
import { BondStatus } from "cosmjs-types/cosmos/staking/v1beta1/staking";
import {
  CodeInfoResponse,
  QueryCodesResponse,
  QueryContractsByCodeResponse,
} from "cosmjs-types/cosmwasm/wasm/v1/query";
import { ContractCodeHistoryOperationType } from "cosmjs-types/cosmwasm/wasm/v1/types";
import {
  QueryDenomTraceResponse,
  QueryDenomTracesResponse,
} from "cosmjs-types/ibc/applications/transfer/v1/query";

import {
  QueryLoadResponse,
  QueryNeuronBandwidthResponse,
  QueryPriceResponse,
} from "./codec/cyber/bandwidth/v1beta1/query";
import { QueryGraphStatsResponse } from "./codec/cyber/graph/v1beta1/query";
import {
  QueryParamsResponse as QueryParamsResponseEnergy,
  QueryRoutedEnergyResponse,
  QueryRouteResponse,
  QueryRoutesResponse,
} from "./codec/cyber/grid/v1beta1/query";
import {
  QueryKarmaResponse,
  QueryLinkExistResponse,
  QueryNegentropyResponse,
  QueryRankResponse,
  QuerySearchResponse,
} from "./codec/cyber/rank/v1beta1/query";
import { QueryParamsResponse as QueryParamsResponseResources } from "./codec/cyber/resources/v1beta1/query";
import {
  QueryLiquidityPoolResponse,
  QueryLiquidityPoolsResponse,
  QueryParamsResponse as QueryParamsResponseLiquidity,
} from "./codec/tendermint/liquidity/v1beta1/query";
import {
  BandwidthExtension,
  GraphExtension,
  GridExtension,
  LiquidityExtension,
  RankExtension,
  ResourcesExtension,
  setupBandwidthExtension,
  setupGraphExtension,
  setupGridExtension,
  setupLiquidityExtension,
  setupRankExtension,
  setupResourcesExtension,
} from "./queries/index";
import { CyberSearchTxFilter, CyberSearchTxQuery } from "./types";

export { Code, CodeDetails, Contract, ContractCodeHistoryEntry };

type QueryClientType = QueryClient &
  AuthExtension &
  BankExtension &
  DistributionExtension &
  StakingExtension &
  GraphExtension &
  RankExtension &
  BandwidthExtension &
  GridExtension &
  WasmExtension &
  LiquidityExtension &
  GovExtension &
  ResourcesExtension &
  TxExtension &
  IbcExtension;

export interface PrivateCyberClient {
  readonly tmClient: TendermintClient | undefined;
  readonly queryClient: QueryClientType | undefined;
}

export declare type BondStatusString = Exclude<
  keyof typeof BondStatus,
  "BOND_STATUS_UNSPECIFIED" | "UNRECOGNIZED"
>;

export class CyberClient {
  private readonly tmClient: TendermintClient | undefined;
  private readonly queryClient: QueryClientType | undefined;
  private readonly codesCache = new Map<number, CodeDetails>();
  private chainId: string | undefined;

  public static async connect(endpoint: string): Promise<CyberClient> {
    const tmClient = await Tendermint34Client.connect(endpoint);
    return new CyberClient(tmClient);
  }

  protected constructor(tmClient: Tendermint34Client | undefined) {
    if (tmClient) {
      this.tmClient = tmClient;
      this.queryClient = QueryClient.withExtensions(
        tmClient,
        setupAuthExtension,
        setupBankExtension,
        setupDistributionExtension,
        setupStakingExtension,
        setupGraphExtension,
        setupRankExtension,
        setupBandwidthExtension,
        setupGridExtension,
        setupWasmExtension,
        setupLiquidityExtension,
        setupGovExtension,
        setupResourcesExtension,
        setupTxExtension,
        setupIbcExtension,
      );
    }
  }

  protected getTmClient(): TendermintClient | undefined {
    return this.tmClient;
  }

  protected forceGetTmClient(): TendermintClient {
    if (!this.tmClient) {
      throw new Error(
        "Tendermint client not available. You cannot use online functionality in offline mode.",
      );
    }
    return this.tmClient;
  }

  protected getQueryClient(): QueryClientType | undefined {
    return this.queryClient;
  }

  protected forceGetQueryClient(): QueryClient &
    AuthExtension &
    BankExtension &
    DistributionExtension &
    StakingExtension &
    GraphExtension &
    RankExtension &
    BandwidthExtension &
    GridExtension &
    WasmExtension &
    LiquidityExtension &
    GovExtension &
    ResourcesExtension &
    IbcExtension &
    TxExtension {
    if (!this.queryClient) {
      throw new Error("Query client not available. You cannot use online functionality in offline mode.");
    }
    return this.queryClient;
  }

  public async getChainId(): Promise<string> {
    if (!this.chainId) {
      const response = await this.forceGetTmClient().status();
      const chainId = response.nodeInfo.network;
      if (!chainId) throw new Error("Chain ID must not be empty");
      this.chainId = chainId;
    }

    return this.chainId;
  }

  public async getHeight(): Promise<number> {
    const status = await this.forceGetTmClient().status();
    return status.syncInfo.latestBlockHeight;
  }

  public async getAccount(searchAddress: string): Promise<Account | null> {
    try {
      const account = await this.forceGetQueryClient().auth.account(searchAddress);
      return account ? accountFromAny(account) : null;
    } catch (error) {
      if (/rpc error: code = NotFound/i.test(String(error))) {
        return null;
      }
      throw error;
    }
  }

  public async getSequence(address: string): Promise<SequenceResponse> {
    const account = await this.getAccount(address);
    if (!account) {
      throw new Error(
        "Account does not exist on chain. Send some tokens there before trying to query sequence.",
      );
    }
    return {
      accountNumber: account.accountNumber,
      sequence: account.sequence,
    };
  }

  public async getBlock(height?: number): Promise<Block> {
    const response = await this.forceGetTmClient().block(height);
    return {
      id: toHex(response.blockId.hash).toUpperCase(),
      header: {
        version: {
          block: new Uint53(response.block.header.version.block).toString(),
          app: new Uint53(response.block.header.version.app).toString(),
        },
        height: response.block.header.height,
        chainId: response.block.header.chainId,
        time: toRfc3339WithNanoseconds(response.block.header.time),
      },
      txs: response.block.txs,
    };
  }

  public async getBlockResults(height?: number): Promise<BlockResultsResponse> {
    return this.forceGetTmClient().blockResults(height);
  }

  public async getBalance(address: string, searchDenom: string): Promise<Coin> {
    return this.forceGetQueryClient().bank.balance(address, searchDenom);
  }

  public async getAllBalances(address: string): Promise<readonly Coin[]> {
    return this.forceGetQueryClient().bank.allBalances(address);
  }

  public async totalSupply(): Promise<Coin[]> {
    const result = await this.forceGetQueryClient().bank.totalSupply();
    return result.supply;
  }

  public async getTx(id: string): Promise<IndexedTx | null> {
    const results = await this.txsQuery(`tx.hash='${id}'`);
    return results[0] ?? null;
  }

  public async searchTx(
    query: CyberSearchTxQuery,
    filter: CyberSearchTxFilter = {},
  ): Promise<readonly IndexedTx[]> {
    const minHeight = filter.minHeight || 0;
    const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;

    if (maxHeight < minHeight) return []; // optional optimization

    function withFilters(originalQuery: string): string {
      return `${originalQuery} AND tx.height>=${minHeight} AND tx.height<=${maxHeight}`;
    }

    let txs: readonly IndexedTx[];

    if (query?.height !== undefined) {
      txs =
        query.height >= minHeight && query.height <= maxHeight
          ? await this.txsQuery(`tx.height=${query.height}`)
          : [];
    } else if (query?.sentFromOrTo !== undefined) {
      const sentQuery = withFilters(`message.module='bank' AND transfer.sender='${query.sentFromOrTo}'`);
      const receivedQuery = withFilters(
        `message.module='bank' AND transfer.recipient='${query.sentFromOrTo}'`,
      );
      const [sent, received] = await Promise.all(
        [sentQuery, receivedQuery].map((rawQuery) => this.txsQuery(rawQuery)),
      );
      const sentHashes = sent.map((t) => t.hash);
      txs = [...sent, ...received.filter((t) => !sentHashes.includes(t.hash))];
    } else if (query?.tags !== undefined) {
      const rawQuery = withFilters(query.tags.map((t) => `${t.key}='${t.value}'`).join(" AND "));
      txs = await this.txsQuery(rawQuery);
    } else {
      throw new Error("Unknown query type");
    }

    const filtered = txs.filter((tx) => tx.height >= minHeight && tx.height <= maxHeight);
    return filtered;
  }

  public disconnect(): void {
    if (this.tmClient) this.tmClient.disconnect();
  }

  public async broadcastTx(tx: Uint8Array): Promise<DeliverTxResponse> {
    const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx });
    const transactionId = toHex(broadcasted.hash).toUpperCase();

    return {
      code: broadcasted.code,
      height: 0,
      rawLog: broadcasted.log,
      transactionHash: transactionId,
      gasUsed: broadcasted.gasUsed,
      gasWanted: broadcasted.gasWanted,
      txIndex: 0,
      events: [], // TODO:  broadcasted.events,
      msgResponses: [],
    };
  }

  // Graph module

  public async graphStats(): Promise<QueryGraphStatsResponse> {
    const response = await this.forceGetQueryClient().graph.graphStats();
    return QueryGraphStatsResponse.toJSON(response) as QueryGraphStatsResponse;
  }

  // Rank module

  public async search(particle: string, page?: number, perPage?: number): Promise<QuerySearchResponse> {
    const response = await this.forceGetQueryClient().rank.search(particle, page, perPage);
    return QuerySearchResponse.toJSON(response) as QuerySearchResponse;
  }

  public async backlinks(particle: string, page?: number, perPage?: number): Promise<QuerySearchResponse> {
    const response = await this.forceGetQueryClient().rank.backlinks(particle, page, perPage);
    return QuerySearchResponse.toJSON(response) as QuerySearchResponse;
  }

  public async rank(particle: string): Promise<QueryRankResponse> {
    const response = await this.forceGetQueryClient().rank.rank(particle);
    return QueryRankResponse.toJSON(response) as QueryRankResponse;
  }

  public async karma(neuron: string): Promise<QueryKarmaResponse> {
    const response = await this.forceGetQueryClient().rank.karma(neuron);
    return QueryKarmaResponse.toJSON(response) as QueryKarmaResponse;
  }

  public async isLinkExist(from: string, to: string, agent: string): Promise<QueryLinkExistResponse> {
    const response = await this.forceGetQueryClient().rank.isLinkExist(from, to, agent);
    return QueryLinkExistResponse.toJSON(response) as QueryLinkExistResponse;
  }

  public async isAnyLinkExist(from: string, to: string): Promise<QueryLinkExistResponse> {
    const response = await this.forceGetQueryClient().rank.isAnyLinkExist(from, to);
    return QueryLinkExistResponse.toJSON(response) as QueryLinkExistResponse;
  }

  public async negentropy(): Promise<QueryNegentropyResponse> {
    const response = await this.forceGetQueryClient().rank.negentropy();
    return QueryNegentropyResponse.toJSON(response) as QueryNegentropyResponse;
  }

  // Bandwidth module

  public async load(): Promise<QueryLoadResponse> {
    const response = await this.forceGetQueryClient().bandwidth.load();
    return QueryLoadResponse.toJSON(response) as QueryLoadResponse;
  }

  public async price(): Promise<QueryPriceResponse> {
    const response = await this.forceGetQueryClient().bandwidth.price();
    return QueryPriceResponse.toJSON(response) as QueryPriceResponse;
  }

  public async accountBandwidth(agent: string): Promise<QueryNeuronBandwidthResponse> {
    const response = await this.forceGetQueryClient().bandwidth.account(agent);
    return QueryNeuronBandwidthResponse.toJSON(response) as QueryNeuronBandwidthResponse;
  }

  // Staking module

  public async delegation(
    delegatorAddress: string,
    validatorAddress: string,
  ): Promise<QueryDelegationResponse> {
    const response = await this.forceGetQueryClient().staking.delegation(delegatorAddress, validatorAddress);
    return QueryDelegationResponse.toJSON(response) as QueryDelegationResponse;
  }

  public async delegatorDelegations(
    delegatorAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryDelegatorDelegationsResponse> {
    const response = await this.forceGetQueryClient().staking.delegatorDelegations(
      delegatorAddress,
      paginationKey,
    );
    return QueryDelegatorDelegationsResponse.toJSON(response) as QueryDelegatorDelegationsResponse;
  }

  public async delegatorUnbondingDelegations(
    delegatorAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryDelegatorUnbondingDelegationsResponse> {
    const response = await this.forceGetQueryClient().staking.delegatorUnbondingDelegations(
      delegatorAddress,
      paginationKey,
    );
    return QueryDelegatorUnbondingDelegationsResponse.toJSON(
      response,
    ) as QueryDelegatorUnbondingDelegationsResponse;
  }

  public async delegatorValidator(
    delegatorAddress: string,
    validatorAddress: string,
  ): Promise<QueryDelegatorValidatorResponse> {
    const response = await this.forceGetQueryClient().staking.delegatorValidator(
      delegatorAddress,
      validatorAddress,
    );
    return QueryDelegatorValidatorResponse.toJSON(response) as QueryDelegatorValidatorResponse;
  }

  public async delegatorValidators(
    delegatorAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryDelegatorValidatorsResponse> {
    const response = await this.forceGetQueryClient().staking.delegatorValidators(
      delegatorAddress,
      paginationKey,
    );
    return QueryDelegatorValidatorsResponse.toJSON(response) as QueryDelegatorValidatorsResponse;
  }

  public async historicalInfo(height: number): Promise<QueryHistoricalInfoResponse> {
    const response = await this.forceGetQueryClient().staking.historicalInfo(height);
    return QueryHistoricalInfoResponse.toJSON(response) as QueryHistoricalInfoResponse;
  }

  public async stakingParams(): Promise<QueryParamsResponseStaking> {
    const response = await this.forceGetQueryClient().staking.params();
    return QueryParamsResponseStaking.toJSON(response) as QueryParamsResponseStaking;
  }

  public async stakingPool(): Promise<QueryPoolResponse> {
    const response = await this.forceGetQueryClient().staking.pool();
    return QueryPoolResponse.toJSON(response) as QueryPoolResponse;
  }

  public async redelegations(
    delegatorAddress: string,
    sourceValidatorAddress: string,
    destinationValidatorAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryRedelegationsResponse> {
    const response = await this.forceGetQueryClient().staking.redelegations(
      delegatorAddress,
      sourceValidatorAddress,
      destinationValidatorAddress,
      paginationKey,
    );
    return QueryRedelegationsResponse.toJSON(response) as QueryRedelegationsResponse;
  }
  public async unbondingDelegation(
    delegatorAddress: string,
    validatorAddress: string,
  ): Promise<QueryUnbondingDelegationResponse> {
    const response = await this.forceGetQueryClient().staking.unbondingDelegation(
      delegatorAddress,
      validatorAddress,
    );
    return QueryUnbondingDelegationResponse.toJSON(response) as QueryUnbondingDelegationResponse;
  }
  public async validator(validatorAddress: string): Promise<QueryValidatorResponse> {
    const response = await this.forceGetQueryClient().staking.validator(validatorAddress);
    return QueryValidatorResponse.toJSON(response) as QueryValidatorResponse;
  }
  public async validatorDelegations(
    validatorAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryValidatorDelegationsResponse> {
    const response = await this.forceGetQueryClient().staking.validatorDelegations(
      validatorAddress,
      paginationKey,
    );
    return QueryValidatorDelegationsResponse.toJSON(response) as QueryValidatorDelegationsResponse;
  }

  public async validators(
    status: BondStatusString,
    paginationKey?: Uint8Array,
  ): Promise<QueryValidatorsResponse> {
    const response = await this.forceGetQueryClient().staking.validators(status, paginationKey);
    return QueryValidatorsResponse.toJSON(response) as QueryValidatorsResponse;
  }

  public async validatorUnbondingDelegations(
    validatorAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryValidatorUnbondingDelegationsResponse> {
    const response = await this.forceGetQueryClient().staking.validatorUnbondingDelegations(
      validatorAddress,
      paginationKey,
    );
    return QueryValidatorUnbondingDelegationsResponse.toJSON(
      response,
    ) as QueryValidatorUnbondingDelegationsResponse;
  }

  // Distribution module

  public async communityPool(): Promise<QueryCommunityPoolResponse> {
    const response = await this.forceGetQueryClient().distribution.communityPool();
    return QueryCommunityPoolResponse.toJSON(response) as QueryCommunityPoolResponse;
  }

  public async delegationRewards(
    delegatorAddress: string,
    validatorAddress: string,
  ): Promise<QueryDelegationRewardsResponse> {
    const response = await this.forceGetQueryClient().distribution.delegationRewards(
      delegatorAddress,
      validatorAddress,
    );
    return QueryDelegationRewardsResponse.toJSON(response) as QueryDelegationRewardsResponse;
  }

  public async delegationTotalRewards(
    delegatorAddress: string,
  ): Promise<QueryDelegationTotalRewardsResponse> {
    const response = await this.forceGetQueryClient().distribution.delegationTotalRewards(delegatorAddress);
    return QueryDelegationTotalRewardsResponse.toJSON(response) as QueryDelegationTotalRewardsResponse;
  }

  public async delegatorValidatorsDistribution(
    delegatorAddress: string,
  ): Promise<QueryDelegatorValidatorsResponseDistribution> {
    const response = await this.forceGetQueryClient().distribution.delegatorValidators(delegatorAddress);
    return QueryDelegatorValidatorsResponseDistribution.toJSON(
      response,
    ) as QueryDelegatorValidatorsResponseDistribution;
  }

  public async delegatorWithdrawAddress(
    delegatorAddress: string,
  ): Promise<QueryDelegatorWithdrawAddressResponse> {
    const response = await this.forceGetQueryClient().distribution.delegatorWithdrawAddress(delegatorAddress);
    return QueryDelegatorWithdrawAddressResponse.toJSON(response) as QueryDelegatorWithdrawAddressResponse;
  }

  public async distributionParams(): Promise<QueryParamsResponseDistribution> {
    const response = await this.forceGetQueryClient().distribution.params();
    return QueryParamsResponseDistribution.toJSON(response) as QueryParamsResponseDistribution;
  }

  public async validatorCommission(validatorAddress: string): Promise<QueryValidatorCommissionResponse> {
    const response = await this.forceGetQueryClient().distribution.validatorCommission(validatorAddress);
    return QueryValidatorCommissionResponse.toJSON(response) as QueryValidatorCommissionResponse;
  }

  public async validatorOutstandingRewards(
    validatorAddress: string,
  ): Promise<QueryValidatorOutstandingRewardsResponse> {
    const response = await this.forceGetQueryClient().distribution.validatorOutstandingRewards(
      validatorAddress,
    );
    return QueryValidatorOutstandingRewardsResponse.toJSON(
      response,
    ) as QueryValidatorOutstandingRewardsResponse;
  }

  public async validatorSlashes(
    validatorAddress: string,
    startingHeight: number,
    endingHeight: number,
    paginationKey?: Uint8Array,
  ): Promise<QueryValidatorSlashesResponse> {
    const response = await this.forceGetQueryClient().distribution.validatorSlashes(
      validatorAddress,
      startingHeight,
      endingHeight,
      paginationKey,
    );
    return QueryValidatorSlashesResponse.toJSON(response) as QueryValidatorSlashesResponse;
  }

  // Grid module

  public async sourceRoutes(source: string): Promise<QueryRoutesResponse> {
    const response = await this.forceGetQueryClient().grid.sourceRoutes(source);
    return QueryRoutesResponse.toJSON(response) as QueryRoutesResponse;
  }

  public async destinationRoutes(destination: string): Promise<QueryRoutesResponse> {
    const response = await this.forceGetQueryClient().grid.destinationRoutes(destination);
    return QueryRoutesResponse.toJSON(response) as QueryRoutesResponse;
  }

  public async destinationRoutedEnergy(destination: string): Promise<QueryRoutedEnergyResponse> {
    const response = await this.forceGetQueryClient().grid.destinationRoutedEnergy(destination);
    return QueryRoutedEnergyResponse.toJSON(response) as QueryRoutedEnergyResponse;
  }

  public async sourceRoutedEnergy(source: string): Promise<QueryRoutedEnergyResponse> {
    const response = await this.forceGetQueryClient().grid.sourceRoutedEnergy(source);
    return QueryRoutedEnergyResponse.toJSON(response) as QueryRoutedEnergyResponse;
  }

  public async route(source: string, destination: string): Promise<QueryRouteResponse> {
    const response = await this.forceGetQueryClient().grid.route(source, destination);
    return QueryRouteResponse.toJSON(response) as QueryRouteResponse;
  }

  public async routes(): Promise<QueryRoutesResponse> {
    const response = await this.forceGetQueryClient().grid.routes();
    return QueryRoutesResponse.toJSON(response) as QueryRoutesResponse;
  }

  public async energyParams(): Promise<QueryParamsResponseEnergy> {
    const response = await this.forceGetQueryClient().grid.params();
    return QueryParamsResponseEnergy.toJSON(response) as QueryParamsResponseEnergy;
  }

  // Resources module

  public async resourcesParams(): Promise<QueryParamsResponseResources> {
    const response = await this.forceGetQueryClient().resources.params();
    return QueryParamsResponseResources.toJSON(response) as QueryParamsResponseResources;
  }

  // Liquidity module

  public async liquidityParams(): Promise<QueryParamsResponseLiquidity> {
    const response = await this.forceGetQueryClient().liquidity.params();
    return QueryParamsResponseLiquidity.toJSON(response) as QueryParamsResponseLiquidity;
  }

  public async pool(id: number): Promise<QueryLiquidityPoolResponse> {
    const response = await this.forceGetQueryClient().liquidity.pool(id);
    return QueryLiquidityPoolResponse.toJSON(response) as QueryLiquidityPoolResponse;
  }

  public async pools(): Promise<QueryLiquidityPoolsResponse> {
    const response = await this.forceGetQueryClient().liquidity.pools();
    return QueryLiquidityPoolsResponse.toJSON(response) as QueryLiquidityPoolsResponse;
  }

  // Gov module

  public async govParams(parametersType: GovParamsType): Promise<QueryParamsResponseGovernance> {
    const response = await this.forceGetQueryClient().gov.params(parametersType);
    return QueryParamsResponseGovernance.toJSON(response) as QueryParamsResponseGovernance;
  }

  public async proposals(
    proposalStatus: ProposalStatus,
    depositorAddress: string,
    voterAddress: string,
    paginationKey?: Uint8Array,
  ): Promise<QueryProposalsResponse> {
    const response = await this.forceGetQueryClient().gov.proposals(
      proposalStatus,
      depositorAddress,
      voterAddress,
      paginationKey,
    );
    return QueryProposalsResponse.toJSON(response) as QueryProposalsResponse;
  }

  public async proposal(proposalId: GovProposalId): Promise<QueryProposalResponse> {
    const response = await this.forceGetQueryClient().gov.proposal(proposalId);
    return QueryProposalResponse.toJSON(response) as QueryProposalResponse;
  }

  public async deposits(proposalId: GovProposalId): Promise<QueryDepositsResponse> {
    const response = await this.forceGetQueryClient().gov.deposits(proposalId);
    return QueryDepositsResponse.toJSON(response) as QueryDepositsResponse;
  }

  public async deposit(proposalId: GovProposalId, depositorAddress: string): Promise<QueryDepositResponse> {
    const response = await this.forceGetQueryClient().gov.deposit(proposalId, depositorAddress);
    return QueryDepositResponse.toJSON(response) as QueryDepositResponse;
  }

  public async tally(proposalId: GovProposalId): Promise<QueryTallyResultResponse> {
    const response = await this.forceGetQueryClient().gov.tally(proposalId);
    return QueryTallyResultResponse.toJSON(response) as QueryTallyResultResponse;
  }

  public async votes(proposalId: GovProposalId): Promise<QueryVotesResponse> {
    const response = await this.forceGetQueryClient().gov.votes(proposalId);
    return QueryVotesResponse.toJSON(response) as QueryVotesResponse;
  }

  public async vote(proposalId: GovProposalId, voterAddress: string): Promise<QueryVoteResponse> {
    const response = await this.forceGetQueryClient().gov.vote(proposalId, voterAddress);
    return QueryVoteResponse.toJSON(response) as QueryVoteResponse;
  }

  // Wasm module

  public async getCodes(): Promise<readonly Code[]> {
    const allCodes = [];

    let startAtKey: Uint8Array | undefined = undefined;
    do {
      const { codeInfos, pagination }: QueryCodesResponse =
        await this.forceGetQueryClient().wasm.listCodeInfo(startAtKey);
      const loadedCodes = codeInfos || [];
      allCodes.push(...loadedCodes);
      startAtKey = pagination?.nextKey;
    } while (startAtKey?.length !== 0);

    return allCodes.map((entry: CodeInfoResponse): Code => {
      assert(entry.creator && entry.codeId && entry.dataHash, "entry incomplete");
      return {
        id: entry.codeId.toNumber(),
        creator: entry.creator,
        checksum: toHex(entry.dataHash),
      };
    });
  }

  public async getCodeDetails(codeId: number): Promise<CodeDetails> {
    const cached = this.codesCache.get(codeId);
    if (cached) return cached;

    const { codeInfo, data } = await this.forceGetQueryClient().wasm.getCode(codeId);
    assert(
      codeInfo && codeInfo.codeId && codeInfo.creator && codeInfo.dataHash && data,
      "codeInfo missing or incomplete",
    );
    const codeDetails: CodeDetails = {
      id: codeInfo.codeId.toNumber(),
      creator: codeInfo.creator,
      checksum: toHex(codeInfo.dataHash),
      data: data,
    };
    this.codesCache.set(codeId, codeDetails);
    return codeDetails;
  }

  public async getContracts(codeId: number): Promise<readonly string[]> {
    const allContracts = [];
    let startAtKey: Uint8Array | undefined = undefined;
    do {
      const { contracts, pagination }: QueryContractsByCodeResponse =
        await this.forceGetQueryClient().wasm.listContractsByCodeId(codeId, startAtKey);
      const loadedContracts = contracts || [];
      allContracts.push(...loadedContracts);
      startAtKey = pagination?.nextKey;
    } while (startAtKey?.length !== 0 && startAtKey !== undefined);

    return allContracts;
  }

  // IBC module
  public async allDenomTraces(): Promise<QueryDenomTracesResponse> {
    const response = await this.forceGetQueryClient().ibc.transfer.allDenomTraces();
    return response;
  }

  public async denomTrace(hash: string): Promise<QueryDenomTraceResponse> {
    const response = await this.forceGetQueryClient().ibc.transfer.denomTrace(hash);
    return response;
  }

  /**
   * Throws an error if no contract was found at the address
   */
  public async getContract(address: string): Promise<Contract> {
    const { address: retrievedAddress, contractInfo } = await this.forceGetQueryClient().wasm.getContractInfo(
      address,
    );
    if (!contractInfo) throw new Error(`No contract found at address "${address}"`);
    assert(retrievedAddress, "address missing");
    assert(contractInfo.codeId && contractInfo.creator && contractInfo.label, "contractInfo incomplete");
    return {
      address: retrievedAddress,
      codeId: contractInfo.codeId.toNumber(),
      creator: contractInfo.creator,
      admin: contractInfo.admin || undefined,
      label: contractInfo.label,
      ibcPortId: contractInfo.ibcPortId || undefined,
    };
  }

  /**
   * Throws an error if no contract was found at the address
   */
  public async getContractCodeHistory(address: string): Promise<readonly ContractCodeHistoryEntry[]> {
    const result = await this.forceGetQueryClient().wasm.getContractCodeHistory(address);
    if (!result) throw new Error(`No contract history found for address "${address}"`);
    const operations: Record<number, "Init" | "Genesis" | "Migrate"> = {
      [ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT]: "Init",
      [ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_GENESIS]: "Genesis",
      [ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE]: "Migrate",
    };
    return (result.entries || []).map((entry): ContractCodeHistoryEntry => {
      assert(entry.operation && entry.codeId && entry.msg);
      return {
        operation: operations[entry.operation],
        codeId: entry.codeId.toNumber(),
        msg: JSON.parse(fromAscii(entry.msg)),
      };
    });
  }

  /**
   * Returns the data at the key if present (raw contract dependent storage data)
   * or null if no data at this key.
   *
   * Promise is rejected when contract does not exist.
   */
  public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
    // just test contract existence
    await this.getContract(address);

    const { data } = await this.forceGetQueryClient().wasm.queryContractRaw(address, key);
    return data ?? null;
  }

  /**
   * Makes a smart query on the contract, returns the parsed JSON document.
   *
   * Promise is rejected when contract does not exist.
   * Promise is rejected for invalid query format.
   * Promise is rejected for invalid response format.
   */
  public async queryContractSmart(address: string, queryMsg: Record<string, unknown>): Promise<JsonObject> {
    try {
      return await this.forceGetQueryClient().wasm.queryContractSmart(address, queryMsg);
    } catch (error) {
      if (error instanceof Error) {
        if (error.message.startsWith("not found: contract")) {
          throw new Error(`No contract found at address "${address}"`);
        } else {
          throw error;
        }
      } else {
        throw error;
      }
    }
  }

  private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
    const results = await this.forceGetTmClient().txSearchAll({ query: query });
    return results.txs.map((tx) => {
      return {
        height: tx.height,
        hash: toHex(tx.hash).toUpperCase(),
        code: tx.result.code,
        rawLog: tx.result.log || "",
        tx: tx.tx,
        gasUsed: tx.result.gasUsed,
        gasWanted: tx.result.gasWanted,
        txIndex: tx.index,
        events: [], // TODO:  tx.result.events || [],
        msgResponses: [],
      };
    });
  }
}

Neighbours