soft3.js/src/signingcyberclient.ts

import {
  AminoMsg,
  encodeSecp256k1Pubkey,
  isSecp256k1Pubkey,
  makeSignDoc as makeSignDocAmino,
  makeStdTx,
  rawSecp256k1PubkeyToRawAddress,
  serializeSignDoc,
  StdTx,
} from "@cosmjs/amino";
import { OfflineAminoSigner } from "@cosmjs/amino";
import {
  createWasmAminoConverters,
  InstantiateOptions,
  MsgClearAdminEncodeObject,
  MsgExecuteContractEncodeObject,
  MsgInstantiateContractEncodeObject,
  MsgMigrateContractEncodeObject,
  MsgStoreCodeEncodeObject,
  MsgUpdateAdminEncodeObject,
} from "@cosmjs/cosmwasm-stargate";
import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto";
import { fromBase64, fromBech32, toBase64, toUtf8 } from "@cosmjs/encoding";
import { Int53, Uint53 } from "@cosmjs/math";
import {
  AccountData,
  EncodeObject,
  encodePubkey,
  GeneratedType,
  makeAuthInfoBytes,
  makeSignDoc,
  OfflineDirectSigner,
  Registry,
  TxBodyEncodeObject,
} from "@cosmjs/proto-signing";
import {
  AminoConverters,
  AminoTypes,
  Coin,
  createAuthzAminoConverters,
  createBankAminoConverters,
  createDistributionAminoConverters,
  createGovAminoConverters,
  createIbcAminoConverters,
  createStakingAminoConverters,
  defaultRegistryTypes,
  DeliverTxResponse,
  MsgDelegateEncodeObject,
  MsgSendEncodeObject,
  MsgTransferEncodeObject,
  MsgUndelegateEncodeObject,
  MsgWithdrawDelegatorRewardEncodeObject,
  SignerData,
  StdFee,
} from "@cosmjs/stargate";
import { longify } from "@cosmjs/stargate/build/queryclient";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { arrayContentEquals, assert, assertDefined } from "@cosmjs/utils";
import { Grant } from "cosmjs-types/cosmos/authz/v1beta1/authz";
import { MsgExec, MsgGrant, MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx";
import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx";
import { TextProposal, VoteOption } from "cosmjs-types/cosmos/gov/v1beta1/gov";
import { MsgDeposit, MsgSubmitProposal, MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx";
import { MsgBeginRedelegate, MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing";
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import {
  MsgClearAdmin,
  MsgExecuteContract,
  MsgInstantiateContract,
  MsgMigrateContract,
  MsgStoreCode,
  MsgUpdateAdmin,
} from "cosmjs-types/cosmwasm/wasm/v1/tx";
import { Any } from "cosmjs-types/google/protobuf/any";
import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx";
import { Height } from "cosmjs-types/ibc/core/client/v1/client";
import equals from "fast-deep-equal";
import Long from "long";
import pako from "pako";

import { createCyberAminoConverters, isMsgSignData, MsgSignData } from "./aminomsgs";
import { MsgCyberlink } from "./codec/cyber/graph/v1beta1/tx";
// import { Link } from "./codec/cyber/graph/v1beta1/types";
import {
  MsgCreateRoute,
  MsgDeleteRoute,
  MsgEditRoute,
  MsgEditRouteName,
} from "./codec/cyber/grid/v1beta1/tx";
import { MsgInvestmint } from "./codec/cyber/resources/v1beta1/tx";
import {
  MsgCreatePool,
  MsgDepositWithinBatch,
  MsgSwapWithinBatch,
  MsgWithdrawWithinBatch,
} from "./codec/tendermint/liquidity/v1beta1/tx";
import { CyberClient } from "./cyberclient";
import {
  MsgBeginRedelegateEncodeObject,
  MsgCreatePoolEncodeObject,
  MsgCreateRouteEncodeObject,
  MsgCyberlinkEncodeObject,
  MsgDeleteRouteEncodeObject,
  MsgDepositEncodeObject,
  MsgDepositWithinBatchEncodeObject,
  MsgEditRouteEncodeObject,
  MsgEditRouteNameEncodeObject,
  MsgExecEncodeObject,
  MsgGrantEncodeObject,
  MsgInvestmintEncodeObject,
  MsgRevokeEncodeObject,
  MsgSubmitProposalEncodeObject,
  MsgSwapWithinBatchEncodeObject,
  MsgVoteEncodeObject,
  MsgWithdrawWithinBatchEncodeObject,
} from "./encodeobjects";
import {
  CosmosRegistryTypes,
  CosmwasmRegistryTypes,
  CyberRegistryTypes,
  TendermintRegistryTypes,
} from "./registryTypes";
import { renderItems } from "./renderItems";
import { Link } from "./types";

export function link(from: string, to: string): Link {
  return { from: from, to: to };
}

export function links(from: string, to: string): Link[] {
  return [link(from, to)];
}

export function chain(particles: string[]): Link[] {
  const chainResult = [];
  for (let i = 0; i < particles.length - 1; i++) {
    chainResult.push({
      from: particles[i],
      to: particles[i + 1],
    });
  }
  return chainResult;
}

// Experimental for remote dapps with cyb's signer integration
export type OfflineSigner = OfflineAminoSigner | OfflineDirectSigner | OfflineDappSigner;

export function isOfflineAminoSigner(signer: OfflineSigner): signer is OfflineAminoSigner {
  return (signer as OfflineAminoSigner).signAmino !== undefined;
}
export function isOfflineDirectSigner(signer: OfflineSigner): signer is OfflineDirectSigner {
  return (signer as OfflineDirectSigner).signDirect !== undefined;
}
export function isOfflineDappSigner(signer: OfflineSigner): signer is OfflineDappSigner {
  return (signer as OfflineDappSigner).getAccounts !== undefined;
}

export interface OfflineDappSigner {
  readonly getAccounts: () => Promise<readonly AccountData[]>;
}
export class OfflineDappWallet implements OfflineDappSigner {
  // export class  OfflineDappSigner {
  public async getAccounts(): Promise<readonly AccountData[]> {
    return [
      {
        algo: "secp256k1",
        address: "",
        pubkey: new Uint8Array(),
      },
    ];
  }
}

export const cyberRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [
  [CyberRegistryTypes.MsgCyberlink, MsgCyberlink],
  [CyberRegistryTypes.MsgInvestmint, MsgInvestmint],
  [CyberRegistryTypes.MsgCreateRoute, MsgCreateRoute],
  [CyberRegistryTypes.MsgEditRoute, MsgEditRoute],
  [CyberRegistryTypes.MsgEditRouteName, MsgEditRouteName],
  [CyberRegistryTypes.MsgDeleteRoute, MsgDeleteRoute],

  [CosmosRegistryTypes.MsgDeposit, MsgDeposit],
  [CosmosRegistryTypes.MsgExec, MsgExec],
  [CosmosRegistryTypes.MsgGrant, MsgGrant],
  [CosmosRegistryTypes.MsgRevoke, MsgRevoke],

  [CosmwasmRegistryTypes.MsgClearAdmin, MsgClearAdmin],
  [CosmwasmRegistryTypes.MsgMigrateContract, MsgMigrateContract],
  [CosmwasmRegistryTypes.MsgUpdateAdmin, MsgUpdateAdmin],
  [CosmwasmRegistryTypes.MsgExecuteContract, MsgExecuteContract],
  [CosmwasmRegistryTypes.MsgInstantiateContract, MsgInstantiateContract],
  [CosmwasmRegistryTypes.MsgStoreCode, MsgStoreCode],

  [TendermintRegistryTypes.MsgSwapWithinBatch, MsgSwapWithinBatch],
  [TendermintRegistryTypes.MsgDepositWithinBatch, MsgDepositWithinBatch],
  [TendermintRegistryTypes.MsgWithdrawWithinBatch, MsgWithdrawWithinBatch],
  [TendermintRegistryTypes.MsgCreatePool, MsgCreatePool],
];

function createDefaultRegistry(): Registry {
  return new Registry([...defaultRegistryTypes, ...cyberRegistryTypes]);
}

export interface SigningCyberClientOptions {
  readonly registry?: Registry;
  readonly aminoTypes?: AminoTypes;
  readonly broadcastTimeoutMs?: number;
  readonly broadcastPollIntervalMs?: number;
}

function createAminoTypes(): AminoConverters {
  return {
    ...createCyberAminoConverters(),
    ...createWasmAminoConverters(),
    ...createBankAminoConverters(),
    ...createDistributionAminoConverters(),
    ...createStakingAminoConverters(),
    ...createGovAminoConverters(),
    ...createIbcAminoConverters(),
    ...createAuthzAminoConverters(),
  };
}

export class SigningCyberClient extends CyberClient {
  public readonly registry: Registry;
  public readonly broadcastTimeoutMs: number | undefined;
  public readonly broadcastPollIntervalMs: number | undefined;

  private readonly signer: OfflineSigner;
  private readonly aminoTypes: AminoTypes;

  public static async connectWithSigner(
    endpoint: string,
    signer: OfflineSigner,
    options: SigningCyberClientOptions = {},
  ): Promise<SigningCyberClient> {
    const tmClient = await Tendermint34Client.connect(endpoint);
    return new SigningCyberClient(tmClient, signer, options);
  }

  /**
   * Creates a client in offline mode.
   *
   * This should only be used in niche cases where you know exactly what you're doing,
   * e.g. when building an offline signing application.
   *
   * When you try to use online functionality with such a signer, an
   * exception will be raised.
   */
  public static async offline(
    signer: OfflineSigner,
    options: SigningCyberClientOptions = {},
  ): Promise<SigningCyberClient> {
    return new SigningCyberClient(undefined, signer, options);
  }

  public render(): string {
    const arr: Array<Record<string, any>> = [];

    renderItems.forEach((i) => {
      arr.push({
        [i.typeUrl.toString()]: {
          proto: {
            type: i.typeUrl,
            value: i.value.fromPartial(i.data),
          },
          amino: {
            type: this.aminoTypes.toAmino({ typeUrl: i.typeUrl, value: i.value.fromPartial(i.data) }).type,
            value: this.aminoTypes.toAmino({ typeUrl: i.typeUrl, value: i.value.fromPartial(i.data) }).value,
          },
        },
      });
    });

    return JSON.stringify(arr);
  }

  public static async remotedapp(
    signer: OfflineSigner,
    options: SigningCyberClientOptions = {},
  ): Promise<SigningCyberClient> {
    return new SigningCyberClient(undefined, signer, options);
  }

  protected constructor(
    tmClient: Tendermint34Client | undefined,
    signer: OfflineSigner,
    options: SigningCyberClientOptions,
  ) {
    super(tmClient);
    const { registry = createDefaultRegistry(), aminoTypes = new AminoTypes(createAminoTypes()) } = options;
    this.registry = registry;
    this.aminoTypes = aminoTypes;
    this.signer = signer;
  }

  public async simulate(
    signerAddress: string,
    messages: readonly EncodeObject[],
    memo: string | undefined,
  ): Promise<number> {
    const anyMsgs = messages.map((m) => this.registry.encodeAsAny(m));
    const accountFromSigner = (await this.signer.getAccounts()).find(
      (account) => account.address === signerAddress,
    );
    if (!accountFromSigner) {
      throw new Error("Failed to retrieve account from signer");
    }
    const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey);
    const { sequence } = await this.getSequence(signerAddress);
    const { gasInfo } = await this.forceGetQueryClient().tx.simulate(anyMsgs, memo, pubkey, sequence);
    assertDefined(gasInfo);
    return Uint53.fromString(gasInfo.gasUsed.toString()).toNumber();
  }

  // Graph module

  public async cyberlink(
    neuron: string,
    from: string,
    to: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const cyberlinkMsg: MsgCyberlinkEncodeObject = {
      typeUrl: "/cyber.graph.v1beta1.MsgCyberlink",
      value: MsgCyberlink.fromPartial({
        neuron: neuron,
        links: links(from, to),
      }),
    };

    return this.signAndBroadcast(neuron, [cyberlinkMsg], fee, memo);
  }

  public async motif(
    neuron: string,
    linkchain: Link[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const cyberlinkMsg: MsgCyberlinkEncodeObject = {
      typeUrl: "/cyber.graph.v1beta1.MsgCyberlink",
      value: MsgCyberlink.fromPartial({
        neuron: neuron,
        links: linkchain,
      }),
    };

    return this.signAndBroadcast(neuron, [cyberlinkMsg], fee, memo);
  }

  public async linkchain(
    neuron: string,
    particles: string[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const cyberlinkMsg: MsgCyberlinkEncodeObject = {
      typeUrl: "/cyber.graph.v1beta1.MsgCyberlink",
      value: MsgCyberlink.fromPartial({
        neuron: neuron,
        links: chain(particles),
      }),
    };

    return this.signAndBroadcast(neuron, [cyberlinkMsg], fee, memo);
  }

  // Resources module

  public async investmint(
    senderAddress: string,
    amount: Coin,
    resource: string,
    length: number,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const investmintMsg: MsgInvestmintEncodeObject = {
      typeUrl: "/cyber.resources.v1beta1.MsgInvestmint",
      value: MsgInvestmint.fromPartial({
        neuron: senderAddress,
        amount: amount,
        resource: resource,
        length: Long.fromString(new Uint53(length).toString()),
      }),
    };
    return this.signAndBroadcast(senderAddress, [investmintMsg], fee, memo);
  }

  // Energy module

  public async createEnergyRoute(
    senderAddress: string,
    destination: string,
    name: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const createEnergyRouteMsg: MsgCreateRouteEncodeObject = {
      typeUrl: "/cyber.grid.v1beta1.MsgCreateRoute",
      value: MsgCreateRoute.fromPartial({
        source: senderAddress,
        destination: destination,
        name: name,
      }),
    };
    return this.signAndBroadcast(senderAddress, [createEnergyRouteMsg], fee, memo);
  }

  public async editEnergyRoute(
    senderAddress: string,
    destination: string,
    value: Coin,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const editEnergyRouteMsg: MsgEditRouteEncodeObject = {
      typeUrl: "/cyber.grid.v1beta1.MsgEditRoute",
      value: MsgEditRoute.fromPartial({
        source: senderAddress,
        destination: destination,
        value: value,
      }),
    };
    return this.signAndBroadcast(senderAddress, [editEnergyRouteMsg], fee, memo);
  }

  public async deleteEnergyRoute(
    senderAddress: string,
    destination: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const deleteEnergyRouteMsg: MsgDeleteRouteEncodeObject = {
      typeUrl: "/cyber.grid.v1beta1.MsgDeleteRoute",
      value: MsgDeleteRoute.fromPartial({
        source: senderAddress,
        destination: destination,
      }),
    };
    return this.signAndBroadcast(senderAddress, [deleteEnergyRouteMsg], fee, memo);
  }

  public async editEnergyRouteName(
    senderAddress: string,
    destination: string,
    name: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const editEnergyRouteNameMsg: MsgEditRouteNameEncodeObject = {
      typeUrl: "/cyber.grid.v1beta1.MsgEditRouteName",
      value: MsgEditRouteName.fromPartial({
        source: senderAddress,
        destination: destination,
        name: name,
      }),
    };

    return this.signAndBroadcast(senderAddress, [editEnergyRouteNameMsg], fee, memo);
  }

  // Wasm module

  /** Uploads code and returns a receipt, including the code ID */
  public async upload(
    senderAddress: string,
    wasmCode: Uint8Array,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const compressed = pako.gzip(wasmCode, { level: 9 });
    const storeCodeMsg: MsgStoreCodeEncodeObject = {
      typeUrl: "/cosmwasm.wasm.v1.MsgStoreCode",
      value: MsgStoreCode.fromPartial({
        sender: senderAddress,
        wasmByteCode: compressed,
      }),
    };

    return this.signAndBroadcast(senderAddress, [storeCodeMsg], fee, memo);
  }

  public async instantiate(
    senderAddress: string,
    codeId: number,
    msg: Record<string, unknown>,
    label: string,
    fee: StdFee | "auto" | number,
    options: InstantiateOptions = {},
  ): Promise<DeliverTxResponse | string[]> {
    const instantiateContractMsg: MsgInstantiateContractEncodeObject = {
      typeUrl: "/cosmwasm.wasm.v1.MsgInstantiateContract",
      value: MsgInstantiateContract.fromPartial({
        sender: senderAddress,
        codeId: Long.fromString(new Uint53(codeId).toString()),
        label: label,
        msg: toUtf8(JSON.stringify(msg)),
        funds: [...(options.funds || [])],
        admin: options.admin,
      }),
    };
    return this.signAndBroadcast(senderAddress, [instantiateContractMsg], fee, options.memo);
  }

  public async updateAdmin(
    senderAddress: string,
    contractAddress: string,
    newAdmin: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const updateAdminMsg: MsgUpdateAdminEncodeObject = {
      typeUrl: "/cosmwasm.wasm.v1.MsgUpdateAdmin",
      value: MsgUpdateAdmin.fromPartial({
        sender: senderAddress,
        contract: contractAddress,
        newAdmin: newAdmin,
      }),
    };
    return this.signAndBroadcast(senderAddress, [updateAdminMsg], fee, memo);
  }

  public async clearAdmin(
    senderAddress: string,
    contractAddress: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const clearAdminMsg: MsgClearAdminEncodeObject = {
      typeUrl: "/cosmwasm.wasm.v1.MsgClearAdmin",
      value: MsgClearAdmin.fromPartial({
        sender: senderAddress,
        contract: contractAddress,
      }),
    };
    return this.signAndBroadcast(senderAddress, [clearAdminMsg], fee, memo);
  }

  public async migrate(
    senderAddress: string,
    contractAddress: string,
    codeId: number,
    migrateMsg: Record<string, unknown>,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const migrateContractMsg: MsgMigrateContractEncodeObject = {
      typeUrl: "/cosmwasm.wasm.v1.MsgMigrateContract",
      value: MsgMigrateContract.fromPartial({
        sender: senderAddress,
        contract: contractAddress,
        codeId: Long.fromString(new Uint53(codeId).toString()),
        msg: toUtf8(JSON.stringify(migrateMsg)),
      }),
    };
    return this.signAndBroadcast(senderAddress, [migrateContractMsg], fee, memo);
  }

  public async execute(
    senderAddress: string,
    contractAddress: string,
    msg: Record<string, unknown>,
    fee: StdFee | "auto" | number,
    memo = "",
    funds?: readonly Coin[],
  ): Promise<DeliverTxResponse | string[]> {
    const executeContractMsg: MsgExecuteContractEncodeObject = {
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: MsgExecuteContract.fromPartial({
        sender: senderAddress,
        contract: contractAddress,
        msg: toUtf8(JSON.stringify(msg)),
        funds: [...(funds || [])],
      }),
    };

    return this.signAndBroadcast(senderAddress, [executeContractMsg], fee, memo);
  }

  public async executeArray(
    senderAddress: string,
    contractAddress: string,
    msg: string[],
    fee: StdFee | "auto" | number,
    memo = "",
    funds?: readonly Coin[],
  ): Promise<DeliverTxResponse | string[]> {
    const msgs = msg.map((item) => ({
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: MsgExecuteContract.fromPartial({
        sender: senderAddress,
        contract: contractAddress,
        msg: toUtf8(JSON.stringify(item)),
        funds: [...(funds || [])],
      }),
    }));

    return this.signAndBroadcast(senderAddress, msgs, fee, memo);
  }

  // Bank module

  public async sendTokens(
    senderAddress: string,
    recipientAddress: string,
    amount: readonly Coin[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const sendMsg: MsgSendEncodeObject = {
      typeUrl: "/cosmos.bank.v1beta1.MsgSend",
      value: {
        fromAddress: senderAddress,
        toAddress: recipientAddress,
        amount: [...amount],
      },
    };
    return this.signAndBroadcast(senderAddress, [sendMsg], fee, memo);
  }

  // Distribution module

  public async delegateTokens(
    delegatorAddress: string,
    validatorAddress: string,
    amount: Coin,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const delegateMsg: MsgDelegateEncodeObject = {
      typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
      value: MsgDelegate.fromPartial({
        delegatorAddress: delegatorAddress,
        validatorAddress,
        amount,
      }),
    };
    return this.signAndBroadcast(delegatorAddress, [delegateMsg], fee, memo);
  }

  public async redelegateTokens(
    delegatorAddress: string,
    validatorSrcAddress: string,
    validatorDstAddress: string,
    amount: Coin,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const redelegateMsg: MsgBeginRedelegateEncodeObject = {
      typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate",
      value: MsgBeginRedelegate.fromPartial({
        delegatorAddress: delegatorAddress,
        validatorSrcAddress,
        validatorDstAddress,
        amount,
      }),
    };
    return this.signAndBroadcast(delegatorAddress, [redelegateMsg], fee, memo);
  }

  public async undelegateTokens(
    delegatorAddress: string,
    validatorAddress: string,
    amount: Coin,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const undelegateMsg: MsgUndelegateEncodeObject = {
      typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate",
      value: MsgUndelegate.fromPartial({
        delegatorAddress: delegatorAddress,
        validatorAddress,
        amount,
      }),
    };
    return this.signAndBroadcast(delegatorAddress, [undelegateMsg], fee, memo);
  }

  public async withdrawRewards(
    delegatorAddress: string,
    validatorAddress: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const withdrawDelegatorRewardMsg: MsgWithdrawDelegatorRewardEncodeObject = {
      typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
      value: MsgWithdrawDelegatorReward.fromPartial({ delegatorAddress: delegatorAddress, validatorAddress }),
    };
    return this.signAndBroadcast(delegatorAddress, [withdrawDelegatorRewardMsg], fee, memo);
  }

  public async withdrawAllRewards(
    delegatorAddress: string,
    validatorAddresses: string[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const msgs = validatorAddresses.map((validatorAddress) => {
      return {
        typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
        value: MsgWithdrawDelegatorReward.fromPartial({
          delegatorAddress: delegatorAddress,
          validatorAddress: validatorAddress,
        }),
      };
    });

    return this.signAndBroadcast(delegatorAddress, msgs, fee, memo);
  }

  // Gov module

  public async voteProposal(
    voter: string,
    proposalId: number,
    option: number,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const voteMsg: MsgVoteEncodeObject = {
      typeUrl: "/cosmos.gov.v1beta1.MsgVote",
      value: MsgVote.fromPartial({
        proposalId: longify(proposalId),
        voter: voter,
        option: option as VoteOption | undefined,
      }),
    };
    return this.signAndBroadcast(voter, [voteMsg], fee, memo);
  }

  public async submitProposal(
    proposer: string,
    content: {
      typeUrl: string;
      value: TextProposal;
    },
    initialDeposit: Coin[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const sumbitProposalMsg: MsgSubmitProposalEncodeObject = {
      typeUrl: "/cosmos.gov.v1beta1.MsgSubmitProposal",
      value: MsgSubmitProposal.fromPartial({
        content: Any.fromPartial({
          typeUrl: content.typeUrl,
          value: Uint8Array.from(TextProposal.encode(content.value).finish()),
        }),
        initialDeposit: initialDeposit,
        proposer: proposer,
      }),
    };
    return this.signAndBroadcast(proposer, [sumbitProposalMsg], fee, memo);
  }

  public async depositProposal(
    depositor: string,
    proposalId: number,
    amount: Coin[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const depositMsg: MsgDepositEncodeObject = {
      typeUrl: "/cosmos.gov.v1beta1.MsgDeposit",
      value: MsgDeposit.fromPartial({
        depositor: depositor,
        proposalId: longify(proposalId),
        amount: amount,
      }),
    };
    return this.signAndBroadcast(depositor, [depositMsg], fee, memo);
  }

  // IBC module

  public async sendIbcTokens(
    senderAddress: string,
    recipientAddress: string,
    transferAmount: Coin,
    sourcePort: string,
    sourceChannel: string,
    timeoutHeight: Height | undefined,
    /** timeout in seconds */
    timeoutTimestamp: number | undefined,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const timeoutTimestampNanoseconds = timeoutTimestamp
      ? Long.fromNumber(timeoutTimestamp).multiply(1_000_000_000)
      : undefined;
    const transferMsg: MsgTransferEncodeObject = {
      typeUrl: "/ibc.applications.transfer.v1.MsgTransfer",
      value: MsgTransfer.fromPartial({
        sourcePort: sourcePort,
        sourceChannel: sourceChannel,
        sender: senderAddress,
        receiver: recipientAddress,
        token: transferAmount,
        timeoutHeight: timeoutHeight,
        timeoutTimestamp: timeoutTimestampNanoseconds,
      }),
    };
    return this.signAndBroadcast(senderAddress, [transferMsg], fee, memo);
  }

  // Liquidity module

  public async swapWithinBatch(
    swapRequesterAddress: string,
    poolId: number,
    swapTypeId: number,
    offerCoin: Coin,
    demandCoinDenom: string,
    offerCoinFee: Coin,
    orderPrice: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const swapWithinBatchMsg: MsgSwapWithinBatchEncodeObject = {
      typeUrl: "/tendermint.liquidity.v1beta1.MsgSwapWithinBatch",
      value: MsgSwapWithinBatch.fromPartial({
        swapRequesterAddress: swapRequesterAddress,
        poolId: poolId,
        swapTypeId: swapTypeId,
        offerCoin: offerCoin,
        demandCoinDenom: demandCoinDenom,
        offerCoinFee: offerCoinFee,
        orderPrice: orderPrice,
      }),
    };
    return this.signAndBroadcast(swapRequesterAddress, [swapWithinBatchMsg], fee, memo);
  }

  public async depositWithinBatch(
    depositorAddress: string,
    poolId: number,
    depositCoins: Coin[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const depositWithinBatchMsg: MsgDepositWithinBatchEncodeObject = {
      typeUrl: "/tendermint.liquidity.v1beta1.MsgDepositWithinBatch",
      value: MsgDepositWithinBatch.fromPartial({
        depositorAddress: depositorAddress,
        poolId: poolId,
        depositCoins: depositCoins,
      }),
    };
    return this.signAndBroadcast(depositorAddress, [depositWithinBatchMsg], fee, memo);
  }

  public async withdwawWithinBatch(
    withdrawerAddress: string,
    poolId: number,
    poolCoin: Coin,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const withdrawWithinBatchMsg: MsgWithdrawWithinBatchEncodeObject = {
      typeUrl: "/tendermint.liquidity.v1beta1.MsgWithdrawWithinBatch",
      value: MsgWithdrawWithinBatch.fromPartial({
        withdrawerAddress: withdrawerAddress,
        poolId: poolId,
        poolCoin: poolCoin,
      }),
    };
    return this.signAndBroadcast(withdrawerAddress, [withdrawWithinBatchMsg], fee, memo);
  }

  public async createPool(
    poolCreatorAddress: string,
    poolTypeId: number,
    depositCoins: Coin[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const createPoolMsg: MsgCreatePoolEncodeObject = {
      typeUrl: "/tendermint.liquidity.v1beta1.MsgCreatePool",
      value: MsgCreatePool.fromPartial({
        poolCreatorAddress: poolCreatorAddress,
        poolTypeId: poolTypeId,
        depositCoins: depositCoins,
      }),
    };
    return this.signAndBroadcast(poolCreatorAddress, [createPoolMsg], fee, memo);
  }

  public async exec(
    granteeAddress: string,
    msgs: Any[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const execMsg: MsgExecEncodeObject = {
      typeUrl: "/cosmos.authz.v1beta1.MsgExec",
      value: MsgExec.fromPartial({
        grantee: granteeAddress,
        msgs: msgs,
      }),
    };
    return this.signAndBroadcast(granteeAddress, [execMsg], fee, memo);
  }

  public async grant(
    granterAddress: string,
    granteeAddress: string,
    grant: Grant,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const grantMsg: MsgGrantEncodeObject = {
      typeUrl: "/cosmos.authz.v1beta1.MsgGrant",
      value: MsgGrant.fromPartial({
        granter: granterAddress,
        grantee: granteeAddress,
        grant: grant,
      }),
    };
    return this.signAndBroadcast(granterAddress, [grantMsg], fee, memo);
  }

  public async revoke(
    granterAddress: string,
    granteeAddress: string,
    urlMsgType: string,
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    const revokeMsg: MsgRevokeEncodeObject = {
      typeUrl: "/cosmos.authz.v1beta1.MsgRevoke",
      value: MsgRevoke.fromPartial({
        granter: granterAddress,
        grantee: granteeAddress,
        msgTypeUrl: urlMsgType,
      }),
    };
    return this.signAndBroadcast(granterAddress, [revokeMsg], fee, memo);
  }

  /**
   * Creates a transaction with the given messages, fee and memo. Then signs and broadcasts the transaction.
   *
   * @param signerAddress The address that will sign transactions using this instance. The signer must be able to sign with this address.
   * @param messages
   * @param fee
   * @param memo
   */
  public async signAndBroadcast(
    signerAddress: string,
    messages: readonly EncodeObject[],
    fee: StdFee | "auto" | number,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    // Experimental for remote dapps with cyb's signer integration
    // if (isOfflineDappSigner(this.signer)) {
    //   return messages.map((m) => toBase64(Buffer.from(JSON.stringify(m), "utf-8")));
    // }

    let usedFee: StdFee;
    if (fee == "auto" || typeof fee === "number") {
      const gasEstimation = await this.simulate(signerAddress, messages, memo);
      const multiplier = typeof fee === "number" ? fee : 1.3;
      usedFee = {
        amount: [],
        gas: Math.round(gasEstimation * multiplier).toString(),
      };
    } else {
      usedFee = fee;
    }

    const txRaw = await this.sign(signerAddress, messages, usedFee, memo);
    const txBytes = TxRaw.encode(txRaw).finish();
    return this.broadcastTx(txBytes);
  }

  // Experimental
  // Allows to pass not EncodeObject[] but AminoMsg[]
  // Converts amino to proto
  public async signAndBroadcastWithAmino(
    signerAddress: string,
    messages: readonly AminoMsg[],
    fee: StdFee,
    memo = "",
  ): Promise<DeliverTxResponse | string[]> {
    // Experimental for remote dapps with cyb's signer integration
    const msgs = messages.map((msg) => this.aminoTypes.fromAmino({ type: msg.type, value: msg.value }));
    if (isOfflineDappSigner(this.signer)) {
      return msgs.map((m) => toBase64(Buffer.from(JSON.stringify(m), "utf-8")));
    }
    const txRaw = await this.sign(signerAddress, msgs, fee, memo);
    const txBytes = TxRaw.encode(txRaw).finish();
    return this.broadcastTx(txBytes);
  }

  public async sign(
    signerAddress: string,
    messages: readonly EncodeObject[],
    fee: StdFee,
    memo: string,
    explicitSignerData?: SignerData,
  ): Promise<TxRaw> {
    let signerData: SignerData;
    if (explicitSignerData) {
      signerData = explicitSignerData;
    } else {
      const { accountNumber, sequence } = await this.getSequence(signerAddress);
      const chainId = await this.getChainId();
      signerData = {
        accountNumber: accountNumber,
        sequence: sequence,
        chainId: chainId,
      };
    }

    return isOfflineDirectSigner(this.signer)
      ? this.signDirect(signerAddress, messages, fee, memo, signerData)
      : this.signAmino(signerAddress, messages, fee, memo, signerData);
  }

  private async signAmino(
    signerAddress: string,
    messages: readonly EncodeObject[],
    fee: StdFee,
    memo: string,
    { accountNumber, sequence, chainId }: SignerData,
  ): Promise<TxRaw> {
    assert(isOfflineAminoSigner(this.signer));
    const accountFromSigner = (await this.signer.getAccounts()).find(
      (account) => account.address === signerAddress,
    );
    if (!accountFromSigner) {
      throw new Error("Failed to retrieve account from signer");
    }
    const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey));
    const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON;

    const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg));
    const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence);
    const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc);

    const signedTxBody = {
      messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)),
      memo: signed.memo,
    };

    const signedTxBodyEncodeObject: TxBodyEncodeObject = {
      typeUrl: "/cosmos.tx.v1beta1.TxBody",
      value: signedTxBody,
    };
    const signedTxBodyBytes = this.registry.encode(signedTxBodyEncodeObject);
    const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber();
    const signedSequence = Int53.fromString(signed.sequence).toNumber();
    const signedAuthInfoBytes = makeAuthInfoBytes(
      [{ pubkey, sequence: signedSequence }],
      signed.fee.amount,
      signedGasLimit,
      fee.granter,
      fee.payer,
      signMode,
    );
    return TxRaw.fromPartial({
      bodyBytes: signedTxBodyBytes,
      authInfoBytes: signedAuthInfoBytes,
      signatures: [fromBase64(signature.signature)],
    });
  }

  private async signDirect(
    signerAddress: string,
    messages: readonly EncodeObject[],
    fee: StdFee,
    memo: string,
    { accountNumber, sequence, chainId }: SignerData,
  ): Promise<TxRaw> {
    assert(isOfflineDirectSigner(this.signer));
    const accountFromSigner = (await this.signer.getAccounts()).find(
      (account) => account.address === signerAddress,
    );
    if (!accountFromSigner) {
      throw new Error("Failed to retrieve account from signer");
    }
    const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey));
    const txBodyEncodeObject: TxBodyEncodeObject = {
      typeUrl: "/cosmos.tx.v1beta1.TxBody",
      value: {
        messages: messages,
        memo: memo,
      },
    };
    const txBodyBytes = this.registry.encode(txBodyEncodeObject);
    const gasLimit = Int53.fromString(fee.gas).toNumber();
    const authInfoBytes = makeAuthInfoBytes(
      [{ pubkey, sequence }],
      fee.amount,
      gasLimit,
      fee.granter,
      fee.payer,
    );
    const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber);
    const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc);
    return TxRaw.fromPartial({
      bodyBytes: signed.bodyBytes,
      authInfoBytes: signed.authInfoBytes,
      signatures: [fromBase64(signature.signature)],
    });
  }

  public async signData(signerAddress: string, data: Uint8Array | Uint8Array[]): Promise<StdTx> {
    const accountNumber = 0;
    const sequence = 0;
    const chainId = "";
    const fee: StdFee = {
      gas: "0",
      amount: [],
    };
    const memo = "";

    const datas = Array.isArray(data) ? data : [data];

    const msgs: MsgSignData[] = datas.map(
      (d): MsgSignData => ({
        type: "sign/MsgSignData",
        value: {
          signer: signerAddress,
          data: toBase64(d),
        },
      }),
    );

    assert(isOfflineAminoSigner(this.signer));
    const accountFromSigner = (await this.signer.getAccounts()).find(
      (account) => account.address === signerAddress,
    );
    if (!accountFromSigner) {
      throw new Error("Failed to retrieve account from signer");
    }
    const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence);
    const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc);
    if (!equals(signDoc, signed)) {
      throw new Error(
        "The signed document differs from the signing instruction. This is not supported for ADR-036.",
      );
    }

    return makeStdTx(signDoc, signature);
  }

  public static async verifySignedData(signed: StdTx): Promise<boolean> {
    // Restrictions from ADR-036
    if (signed.memo !== "") throw new Error("Memo must be empty.");
    if (signed.fee.gas !== "0") throw new Error("Fee gas must 0.");
    if (signed.fee.amount.length !== 0) throw new Error("Fee amount must be an empty array.");

    const accountNumber = 0;
    const sequence = 0;
    const chainId = "";

    // Check `msg` array
    const signedMessages = signed.msg;
    if (!signedMessages.every(isMsgSignData)) {
      throw new Error(`Found message that is not the expected type.`);
    }
    if (signedMessages.length === 0) {
      throw new Error("No message found. Without messages we cannot determine the signer address.");
    }
    // TODO: restrict number of messages?

    const signatures = signed.signatures;
    if (signatures.length !== 1) throw new Error("Must have exactly one signature to be supported.");
    const signature = signatures[0];
    if (!isSecp256k1Pubkey(signature.pub_key)) {
      throw new Error("Only secp256k1 signatures are supported.");
    }

    const signBytes = serializeSignDoc(
      makeSignDocAmino(signed.msg, signed.fee, chainId, signed.memo, accountNumber, sequence),
    );
    const prehashed = sha256(signBytes);

    const secpSignature = Secp256k1Signature.fromFixedLength(fromBase64(signature.signature));
    const rawSecp256k1Pubkey = fromBase64(signature.pub_key.value);
    const rawSignerAddress = rawSecp256k1PubkeyToRawAddress(rawSecp256k1Pubkey);

    if (
      signedMessages.some((msg) => !arrayContentEquals(fromBech32(msg.value.signer).data, rawSignerAddress))
    ) {
      throw new Error("Found mismatch between signer in message and public key");
    }

    const ok = await Secp256k1.verifySignature(secpSignature, prehashed, rawSecp256k1Pubkey);
    return ok;
  }
}

Neighbours