bostrom-mcp/src/clients/signing.ts

import { createRequire } from "node:module";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import {
  SigningStargateClient,
  defaultRegistryTypes,
  GasPrice,
  type DeliverTxResponse,
} from "@cosmjs/stargate";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { Registry } from "@cosmjs/proto-signing";
import type { EncodeObject, GeneratedType } from "@cosmjs/proto-signing";

// CJS sub-path imports โ€” cyber-ts lacks ESM "exports" field.
// Lazy-loaded to avoid crashing Smithery's CJS scan bundler.
let _cyberProtoRegistry: ReadonlyArray<[string, GeneratedType]> | null = null;
let _osmosisProtoRegistry: ReadonlyArray<[string, GeneratedType]> | null = null;

function loadProtoRegistries() {
  if (_cyberProtoRegistry) return;
  const req = createRequire(import.meta.url);
  _cyberProtoRegistry = (req("@cybercongress/cyber-ts/cyber/client") as {
    cyberProtoRegistry: ReadonlyArray<[string, GeneratedType]>;
  }).cyberProtoRegistry;
  _osmosisProtoRegistry = (req("@cybercongress/cyber-ts/osmosis/client") as {
    osmosisProtoRegistry: ReadonlyArray<[string, GeneratedType]>;
  }).osmosisProtoRegistry;
}

const RPC_ENDPOINT = process.env.BOSTROM_RPC ?? "https://rpc.bostrom.cybernode.ai";
const ADDRESS_PREFIX = "bostrom";
const GAS_PRICE_STR = process.env.BOSTROM_GAS_PRICE ?? "0.01boot";
const GAS_MULTIPLIER = Number(process.env.BOSTROM_GAS_MULTIPLIER ?? "1.4");

let wallet: DirectSecp256k1HdWallet | null = null;
let address: string | null = null;
let stargateClient: SigningStargateClient | null = null;
let cosmwasmClient: SigningCosmWasmClient | null = null;

function requireMnemonic(): string {
  const mnemonic = process.env.BOSTROM_MNEMONIC;
  if (!mnemonic) {
    throw new Error(
      "BOSTROM_MNEMONIC environment variable is not set. " +
      "Write tools require a wallet. Set BOSTROM_MNEMONIC to enable signing.",
    );
  }
  return mnemonic;
}

async function initWallet(): Promise<void> {
  if (wallet) return;
  const mnemonic = requireMnemonic();
  wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
    prefix: ADDRESS_PREFIX,
  });
  const [account] = await wallet.getAccounts();
  address = account.address;
}

/** Get the wallet address (initializes wallet on first call) */
export async function getWalletAddress(): Promise<string> {
  await initWallet();
  return address!;
}

/** Build a merged registry: default stargate + cyber + osmosis types */
function buildRegistry(): Registry {
  loadProtoRegistries();
  return new Registry([
    ...defaultRegistryTypes,
    ..._cyberProtoRegistry!,
    ..._osmosisProtoRegistry!,
  ]);
}

/**
 * Get a SigningStargateClient with cyber + osmosis + default types registered.
 * Used for chain-native messages (bank, staking, gov, cyberlink, tokenfactory, etc.)
 */
export async function getStargateClient(): Promise<SigningStargateClient> {
  await initWallet();
  if (!stargateClient) {
    const registry = buildRegistry();
    stargateClient = await SigningStargateClient.connectWithSigner(
      RPC_ENDPOINT,
      wallet!,
      {
        registry,
        gasPrice: GasPrice.fromString(GAS_PRICE_STR),
      },
    );
  }
  return stargateClient;
}

/**
 * Get a SigningCosmWasmClient for contract execution.
 * Includes wasm types (MsgExecuteContract, etc.) automatically.
 */
export async function getCosmWasmClient(): Promise<SigningCosmWasmClient> {
  await initWallet();
  if (!cosmwasmClient) {
    cosmwasmClient = await SigningCosmWasmClient.connectWithSigner(
      RPC_ENDPOINT,
      wallet!,
      {
        gasPrice: GasPrice.fromString(GAS_PRICE_STR),
      },
    );
  }
  return cosmwasmClient;
}

const MIN_GAS = Number(process.env.BOSTROM_MIN_GAS ?? "100000");

/** Sign and broadcast messages using the stargate client with auto gas */
export async function signAndBroadcast(
  msgs: EncodeObject[],
  memo?: string,
): Promise<DeliverTxResponse> {
  const client = await getStargateClient();
  const addr = await getWalletAddress();
  const gasEstimate = await client.simulate(addr, msgs, memo);
  const gasLimit = Math.max(Math.ceil(gasEstimate * GAS_MULTIPLIER), MIN_GAS);
  const fee = {
    amount: [{ denom: "boot", amount: String(Math.ceil(gasLimit * 0.01)) }],
    gas: String(gasLimit),
  };
  const result = await client.signAndBroadcast(addr, msgs, fee, memo);
  if (result.code !== 0) {
    throw new Error(`Transaction failed (code ${result.code}): ${result.rawLog}`);
  }
  return result;
}

/** Format a DeliverTxResponse for tool output */
export function formatTxResult(result: DeliverTxResponse) {
  return {
    txHash: result.transactionHash,
    height: result.height,
    gasUsed: result.gasUsed,
    gasWanted: result.gasWanted,
    code: result.code,
  };
}

/** Check amount against BOSTROM_MAX_SEND_AMOUNT circuit breaker */
export function checkAmountLimit(amount: string, denom: string): void {
  const maxStr = process.env.BOSTROM_MAX_SEND_AMOUNT;
  if (!maxStr) return;
  const max = BigInt(maxStr);
  const val = BigInt(amount);
  if (val > max) {
    throw new Error(
      `Amount ${amount} ${denom} exceeds BOSTROM_MAX_SEND_AMOUNT (${maxStr}). ` +
      "Increase the limit or reduce the amount.",
    );
  }
}

Synonyms

cyber-ts/packages/cyber-ts/src/cosmos/tx/signing/v1beta1/signing.ts

Neighbours