cyb/src/utils/offlineSigner.ts

import {
  DirectSecp256k1HdWallet,
  DirectSecp256k1HdWalletOptions,
} from '@cosmjs/proto-signing/build/directsecp256k1hdwallet';
import { Bip39, EnglishMnemonic } from '@cosmjs/crypto';
import { toBase64, toUtf8 } from '@cosmjs/encoding';
import {
  Secp256k1HdWallet,
  makeSignDoc,
} from '@cosmjs/amino';
import defaultNetworks from 'src/constants/defaultNetworks';
import networkList from './networkListIbc';

export interface ArbSignResult {
  pub_key: { type: string; value: string };
  signature: string;
}

export class CybOfflineSigner extends DirectSecp256k1HdWallet {
  private _aminoWallet: Secp256k1HdWallet | null = null;

  public static async fromMnemonic(
    mnemonic: string,
    options: Partial<DirectSecp256k1HdWalletOptions> = {}
  ): Promise<CybOfflineSigner> {
    const mnemonicChecked = new EnglishMnemonic(mnemonic);
    const seed = await Bip39.mnemonicToSeed(
      mnemonicChecked,
      options.bip39Password
    );

    // Parent constructor expects internal Mnemonic type โ€” EnglishMnemonic satisfies it at runtime
    return new CybOfflineSigner(mnemonicChecked as unknown as string, {
      ...options,
      seed,
    });
  }

  private async getAminoWallet(): Promise<Secp256k1HdWallet> {
    if (!this._aminoWallet) {
      const [{ address }] = await this.getAccounts();
      const prefix = address.substring(0, address.indexOf('1'));
      this._aminoWallet = await Secp256k1HdWallet.fromMnemonic(
        this.mnemonic,
        { prefix }
      );
    }
    return this._aminoWallet;
  }

  /**
   * ADR-036 arbitrary message signing โ€” mirrors Keplr's signArbitrary API.
   */
  async signArbitrary(
    _chainId: string,
    signerAddress: string,
    data: string
  ): Promise<ArbSignResult> {
    const aminoWallet = await this.getAminoWallet();

    // ADR-036 sign doc: wrap data as MsgSignData
    const signDoc = makeSignDoc(
      [
        {
          type: 'sign/MsgSignData',
          value: {
            signer: signerAddress,
            data: toBase64(toUtf8(data)),
          },
        },
      ],
      { amount: [], gas: '0' },
      '',
      '',
      0,
      0
    );

    const res = await aminoWallet.signAmino(signerAddress, signDoc);
    return res.signature;
  }
}

/** Type guard: checks if signer supports ADR-036 signArbitrary */
export function hasSignArbitrary(
  signer: unknown
): signer is CybOfflineSigner {
  return signer != null && typeof (signer as CybOfflineSigner).signArbitrary === 'function';
}

export async function generateMnemonic(): Promise<string> {
  const wallet = await DirectSecp256k1HdWallet.generate(24);
  return wallet.mnemonic;
}

export const getOfflineSigner = (mnemonic: string, network?: string) => {
  const prefix =
    network && networkList[network]?.prefix
      ? networkList[network].prefix
      : defaultNetworks.bostrom.BECH32_PREFIX;

  return CybOfflineSigner.fromMnemonic(mnemonic, { prefix });
};

Neighbours