From 40edca4e5f2622c4339cde04e70560360bfcb290 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 13 Feb 2026 00:19:06 +1300 Subject: [PATCH] add rpc selector --- frontend/public/wallets/keplr.svg | 12 + frontend/public/wallets/leap.svg | 17 ++ frontend/public/wallets/metamask.svg | 26 ++ frontend/public/wallets/rabby.svg | 36 +++ frontend/src/App.tsx | 23 ++ frontend/src/components/Header.tsx | 32 ++- frontend/src/components/RpcModal.tsx | 338 ++++++++++++++++++++++++ frontend/src/components/RpcSelector.tsx | 184 +++++++++++++ frontend/src/index.css | 3 + frontend/src/services/contracts.ts | 17 +- frontend/src/store/rpcStore.ts | 140 ++++++++++ frontend/src/store/useStore.ts | 16 +- 12 files changed, 831 insertions(+), 13 deletions(-) create mode 100644 frontend/public/wallets/keplr.svg create mode 100644 frontend/public/wallets/leap.svg create mode 100644 frontend/public/wallets/metamask.svg create mode 100644 frontend/public/wallets/rabby.svg create mode 100644 frontend/src/components/RpcModal.tsx create mode 100644 frontend/src/components/RpcSelector.tsx create mode 100644 frontend/src/store/rpcStore.ts diff --git a/frontend/public/wallets/keplr.svg b/frontend/public/wallets/keplr.svg new file mode 100644 index 0000000..215632d --- /dev/null +++ b/frontend/public/wallets/keplr.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/wallets/leap.svg b/frontend/public/wallets/leap.svg new file mode 100644 index 0000000..8dce658 --- /dev/null +++ b/frontend/public/wallets/leap.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/wallets/metamask.svg b/frontend/public/wallets/metamask.svg new file mode 100644 index 0000000..66cbc40 --- /dev/null +++ b/frontend/public/wallets/metamask.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/frontend/public/wallets/rabby.svg b/frontend/public/wallets/rabby.svg new file mode 100644 index 0000000..4f1855f --- /dev/null +++ b/frontend/public/wallets/rabby.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ee6be82..4347066 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,8 @@ import { useEffect, useState, useRef } from 'react' import { useStore } from './store/useStore' +import { setMsgBroadcasterEndpoints } from './store/useStore' +import { useRpcStore } from './store/rpcStore' +import { setGrpcEndpoint } from './services/contracts' import Header from './components/Header' import HeroSection from './components/HeroSection' import StakingPanel from './components/StakingPanel' @@ -115,6 +118,26 @@ function App() { } }, [isConnected]) + // Sync gRPC endpoint from rpcStore (on mount + when user switches) + const activeEndpoint = useRpcStore((s) => s.endpoints[s.activeIndex]) + const activeGrpc = activeEndpoint?.grpc + const prevGrpcRef = useRef(activeGrpc) + useEffect(() => { + if (!activeGrpc) return + setGrpcEndpoint(activeGrpc) + setMsgBroadcasterEndpoints({ grpc: activeGrpc }) + // Refetch data when endpoint actually changes (skip initial mount — handled above) + if (prevGrpcRef.current && prevGrpcRef.current !== activeGrpc) { + fetchContractData() + fetchDraws() + if (isConnected) { + fetchBalances() + fetchUserData() + } + } + prevGrpcRef.current = activeGrpc + }, [activeGrpc]) + // Poll for fresh data every 30s useEffect(() => { const interval = setInterval(() => { diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index aa322d2..dbf4c0f 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -2,6 +2,8 @@ import React, { useState, useEffect, useRef } from 'react' import { Sparkles, ChevronDown, Wallet, LogOut, Copy, Check, Menu, X } from 'lucide-react' import { useStore } from '../store/useStore' import type { WalletType } from '../store/useStore' +import RpcSelector from './RpcSelector' +import RpcModal from './RpcModal' const navLinks = [ { href: '#/stake', label: 'Stake' }, @@ -70,10 +72,10 @@ export default function Header() { }, [mobileMenuOpen]) const wallets: { id: WalletType; name: string; icon: string }[] = [ - { id: 'keplr', name: 'Keplr', icon: '🔑' }, - { id: 'leap', name: 'Leap', icon: '🦘' }, - { id: 'metamask', name: 'MetaMask', icon: '🦊' }, - { id: 'rabby', name: 'Rabby', icon: '🐰' }, + { id: 'keplr', name: 'Keplr', icon: '/wallets/keplr.svg' }, + { id: 'leap', name: 'Leap', icon: '/wallets/leap.svg' }, + { id: 'metamask', name: 'MetaMask', icon: '/wallets/metamask.svg' }, + { id: 'rabby', name: 'Rabby', icon: '/wallets/rabby.svg' }, ] const handleNavClick = () => { @@ -113,6 +115,11 @@ export default function Header() { ))} + {/* Desktop RPC selector */} +
+ +
+ {/* Desktop wallet section */}
{!isConnected ? ( @@ -142,7 +149,7 @@ export default function Header() { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }} > - {w.icon} + {w.name} {w.name} ))} @@ -266,6 +273,13 @@ export default function Header() {
+ {/* Mobile RPC selector */} +
+ +
+ +
+ {/* Mobile wallet section */}
{!isConnected ? ( @@ -281,7 +295,7 @@ export default function Header() { setMobileMenuOpen(false) }} > - {w.icon} + {w.name} {w.name} ))} @@ -331,6 +345,8 @@ export default function Header() { )}
+ + ) } @@ -450,7 +466,9 @@ const styles: Record = { transition: 'background 0.15s', }, walletIcon: { - fontSize: 18, + width: 22, + height: 22, + borderRadius: 6, }, accountButton: { display: 'flex', diff --git a/frontend/src/components/RpcModal.tsx b/frontend/src/components/RpcModal.tsx new file mode 100644 index 0000000..53b70b1 --- /dev/null +++ b/frontend/src/components/RpcModal.tsx @@ -0,0 +1,338 @@ +import React, { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { X, Plus, Trash2, Radio } from 'lucide-react' +import { useRpcStore, type Endpoint } from '../store/rpcStore' + +export default function RpcModal() { + const { endpoints, activeIndex, modalOpen, selectEndpoint, addCustomEndpoint, removeCustomEndpoint, closeModal } = + useRpcStore() + + const [grpcInput, setGrpcInput] = useState('') + const [restInput, setRestInput] = useState('') + + // Lock body scroll while open + useEffect(() => { + if (modalOpen) document.body.style.overflow = 'hidden' + else document.body.style.overflow = '' + return () => { document.body.style.overflow = '' } + }, [modalOpen]) + + // Close on escape + useEffect(() => { + if (!modalOpen) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') closeModal() + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [modalOpen, closeModal]) + + if (!modalOpen) return null + + const handleAdd = () => { + if (!grpcInput.trim()) return + addCustomEndpoint(grpcInput, restInput || undefined) + setGrpcInput('') + setRestInput('') + } + + const handleSelect = (i: number) => { + selectEndpoint(i) + closeModal() + } + + return createPortal( +
+ {/* Modal */} +
e.stopPropagation()}> + {/* Header */} +
+ RPC Endpoints + +
+ + {/* Endpoint list */} +
+ {endpoints.map((ep, i) => ( + handleSelect(i)} + onRemove={ep.custom ? () => removeCustomEndpoint(i) : undefined} + /> + ))} +
+ + {/* Add custom */} +
+ Add Custom Endpoint + setGrpcInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + /> + setRestInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + /> + +
+
+
, + document.body, + ) +} + +function EndpointRow({ + endpoint, + active, + onSelect, + onRemove, +}: { + endpoint: Endpoint + active: boolean + onSelect: () => void + onRemove?: () => void +}) { + const hostname = endpoint.grpc.replace(/^https?:\/\//, '').split('/')[0] + + return ( +
+
+
+ {active && ( +
+ )} +
+
+ + {endpoint.label || 'Custom'} + {endpoint.custom && ( + custom + )} + + {hostname} +
+
+ {onRemove && ( + + )} +
+ ) +} + +const styles: Record = { + overlay: { + position: 'fixed', + inset: 0, + background: 'rgba(0, 0, 0, 0.6)', + backdropFilter: 'blur(4px)', + zIndex: 300, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + animation: 'fadeIn 0.15s ease-out', + }, + modal: { + width: 420, + maxWidth: 'calc(100vw - 32px)', + maxHeight: 'calc(100vh - 64px)', + background: '#1A1A22', + border: '1px solid #2A2A38', + borderRadius: 16, + display: 'flex', + flexDirection: 'column' as const, + boxShadow: '0 24px 64px rgba(0, 0, 0, 0.6)', + animation: 'scaleIn 0.15s ease-out', + overflow: 'hidden', + }, + header: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '16px 20px', + borderBottom: '1px solid #2A2A38', + }, + title: { + fontSize: 15, + fontWeight: 600, + color: '#F0F0F5', + }, + closeBtn: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + borderRadius: 8, + background: 'transparent', + border: 'none', + color: '#8E8EA0', + cursor: 'pointer', + transition: 'color 0.15s', + }, + list: { + padding: '8px 12px', + overflowY: 'auto' as const, + flex: 1, + display: 'flex', + flexDirection: 'column' as const, + gap: 6, + }, + row: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '12px 14px', + borderRadius: 12, + border: '1px solid #2A2A38', + cursor: 'pointer', + transition: 'all 0.15s', + }, + rowLeft: { + display: 'flex', + alignItems: 'center', + gap: 12, + minWidth: 0, + flex: 1, + }, + rowInfo: { + display: 'flex', + flexDirection: 'column' as const, + gap: 2, + minWidth: 0, + }, + rowLabel: { + fontSize: 13, + fontWeight: 600, + color: '#F0F0F5', + display: 'flex', + alignItems: 'center', + gap: 8, + }, + customBadge: { + fontSize: 10, + fontWeight: 500, + color: '#8E8EA0', + background: '#252530', + padding: '1px 6px', + borderRadius: 4, + }, + rowHost: { + fontSize: 11, + color: '#8E8EA0', + fontFamily: "'JetBrains Mono', monospace", + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' as const, + }, + removeBtn: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 28, + height: 28, + borderRadius: 6, + background: 'transparent', + border: 'none', + color: '#8E8EA0', + cursor: 'pointer', + transition: 'color 0.15s', + flexShrink: 0, + }, + addSection: { + padding: '12px 16px 16px', + borderTop: '1px solid #2A2A38', + display: 'flex', + flexDirection: 'column' as const, + gap: 8, + }, + addLabel: { + fontSize: 12, + fontWeight: 600, + color: '#8E8EA0', + textTransform: 'uppercase' as const, + letterSpacing: '0.04em', + }, + input: { + width: '100%', + padding: '9px 12px', + borderRadius: 8, + background: '#13131a', + border: '1px solid #2A2A38', + color: '#F0F0F5', + fontSize: 12, + fontFamily: "'JetBrains Mono', monospace", + outline: 'none', + transition: 'border-color 0.15s', + boxSizing: 'border-box' as const, + }, + addBtn: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + padding: '9px 16px', + borderRadius: 8, + background: 'rgba(139, 111, 255, 0.1)', + border: '1px solid rgba(139, 111, 255, 0.2)', + color: '#8B6FFF', + fontSize: 13, + fontWeight: 600, + cursor: 'pointer', + transition: 'all 0.15s', + }, +} diff --git a/frontend/src/components/RpcSelector.tsx b/frontend/src/components/RpcSelector.tsx new file mode 100644 index 0000000..07a1c0d --- /dev/null +++ b/frontend/src/components/RpcSelector.tsx @@ -0,0 +1,184 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useRpcStore, useCurrentRest, useCurrentGrpc } from '../store/rpcStore' + +interface BlockInfo { + height: number + time: string +} + +type Health = 'good' | 'warn' | 'bad' | 'offline' + +const STATUS_COLORS: Record = { + good: { bg: '#22c55e', shadow: 'rgba(34, 197, 94, 0.4)' }, + warn: { bg: '#f59e0b', shadow: 'rgba(245, 158, 11, 0.4)' }, + bad: { bg: '#ef4444', shadow: 'rgba(239, 68, 68, 0.4)' }, + offline: { bg: '#ef4444', shadow: 'rgba(239, 68, 68, 0.4)' }, +} + +function getHealth(blockTime: string | undefined): { health: Health; label: string } { + if (!blockTime) return { health: 'offline', label: '' } + const ageSec = (Date.now() - new Date(blockTime).getTime()) / 1000 + if (ageSec < 0 || isNaN(ageSec)) return { health: 'offline', label: '' } + + let label: string + if (ageSec < 60) label = `${Math.round(ageSec)}s ago` + else if (ageSec < 3600) label = `${Math.floor(ageSec / 60)}m ago` + else label = `${Math.floor(ageSec / 3600)}h ago` + + if (ageSec <= 15) return { health: 'good', label } + if (ageSec <= 60) return { health: 'warn', label } + return { health: 'bad', label } +} + +async function fetchLatestBlock(restEndpoint: string): Promise { + const url = `${restEndpoint}/cosmos/base/tendermint/v1beta1/blocks/latest` + const res = await fetch(url, { signal: AbortSignal.timeout(5000) }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const json = await res.json() + const header = json.block?.header ?? json.sdk_block?.header + return { + height: parseInt(header.height, 10), + time: header.time, + } +} + +export default function RpcSelector() { + const openModal = useRpcStore((s) => s.openModal) + const currentGrpc = useCurrentGrpc() + const currentRest = useCurrentRest() + + const [block, setBlock] = useState(null) + const [online, setOnline] = useState(false) + const [now, setNow] = useState(Date.now()) + const displayHeight = useAnimatedNumber(block?.height ?? 0) + + const poll = useCallback(async () => { + if (!currentRest) { + try { + await fetch(currentGrpc, { + method: 'POST', + signal: AbortSignal.timeout(5000), + }) + setOnline(true) + } catch { + setOnline(false) + } + setBlock(null) + return + } + try { + const b = await fetchLatestBlock(currentRest) + setBlock(b) + setOnline(true) + } catch { + setOnline(false) + setBlock(null) + } + }, [currentGrpc, currentRest]) + + useEffect(() => { + poll() + const id = setInterval(poll, 5_000) + return () => clearInterval(id) + }, [poll]) + + // Tick every second so the "Xs ago" label stays fresh + useEffect(() => { + const id = setInterval(() => setNow(Date.now()), 1_000) + return () => clearInterval(id) + }, []) + + const { health, label: ageLabel } = online + ? getHealth(block?.time) + : { health: 'offline' as Health, label: '' } + + const colors = STATUS_COLORS[health] + const dotStyle: React.CSSProperties = { + width: 7, + height: 7, + borderRadius: '50%', + background: colors.bg, + boxShadow: `0 0 8px ${colors.shadow}`, + animation: 'pulse 2s ease-in-out infinite', + flexShrink: 0, + } + + return ( + + ) +} + +/** Smoothly animates a number toward its target value. */ +function useAnimatedNumber(target: number): number { + const [display, setDisplay] = useState(target) + const rafRef = useRef(0) + + useEffect(() => { + if (target === 0) { + setDisplay(0) + return + } + let current = display || target + const step = () => { + const diff = target - current + if (Math.abs(diff) < 1) { + setDisplay(target) + return + } + current += diff * 0.25 + setDisplay(Math.round(current)) + rafRef.current = requestAnimationFrame(step) + } + rafRef.current = requestAnimationFrame(step) + return () => cancelAnimationFrame(rafRef.current) + }, [target]) + + return display +} + +const styles: Record = { + button: { + display: 'flex', + flexWrap: 'nowrap' as const, + alignItems: 'center', + gap: 8, + padding: '6px 12px', + borderRadius: 8, + background: 'rgba(26, 26, 34, 0.8)', + border: '1px solid #2A2A38', + color: '#8E8EA0', + fontSize: 12, + fontFamily: "'JetBrains Mono', monospace", + cursor: 'pointer', + transition: 'border-color 0.2s', + whiteSpace: 'nowrap' as const, + width: '100%', + }, + height: { + color: '#8B6FFF', + fontWeight: 600, + }, + age: { + fontSize: 11, + fontWeight: 500, + }, + host: { + color: '#8E8EA0', + overflow: 'hidden', + textOverflow: 'ellipsis', + minWidth: 0, + flex: 1, + }, +} diff --git a/frontend/src/index.css b/frontend/src/index.css index e75d24b..0950676 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -244,6 +244,9 @@ input:focus, textarea:focus { .header-wallet-desktop { display: none !important; } + .header-rpc-desktop { + display: none !important; + } .header-burger { display: flex !important; } diff --git a/frontend/src/services/contracts.ts b/frontend/src/services/contracts.ts index f6476a3..8bf7cb4 100644 --- a/frontend/src/services/contracts.ts +++ b/frontend/src/services/contracts.ts @@ -8,13 +8,20 @@ import { } from "@injectivelabs/sdk-ts"; import { CONTRACTS, ENDPOINTS, INJ_DENOM, getCsinjDenom } from "../config"; -// ---------- API instances ---------- -const wasmApi = new ChainGrpcWasmApi(ENDPOINTS.grpc); -const bankApi = new ChainGrpcBankApi(ENDPOINTS.grpc); -const stakingApi = new ChainGrpcStakingApi(ENDPOINTS.grpc); -const mintApi = new ChainGrpcMintApi(ENDPOINTS.grpc); +// ---------- API instances (reinitializable for RPC switching) ---------- +let wasmApi = new ChainGrpcWasmApi(ENDPOINTS.grpc); +let bankApi = new ChainGrpcBankApi(ENDPOINTS.grpc); +let stakingApi = new ChainGrpcStakingApi(ENDPOINTS.grpc); +let mintApi = new ChainGrpcMintApi(ENDPOINTS.grpc); const explorerApi = new IndexerGrpcExplorerApi(ENDPOINTS.indexer); +export function setGrpcEndpoint(grpc: string) { + wasmApi = new ChainGrpcWasmApi(grpc); + bankApi = new ChainGrpcBankApi(grpc); + stakingApi = new ChainGrpcStakingApi(grpc); + mintApi = new ChainGrpcMintApi(grpc); +} + // ---------- Types ---------- export interface ExchangeRateResponse { rate: string; diff --git a/frontend/src/store/rpcStore.ts b/frontend/src/store/rpcStore.ts new file mode 100644 index 0000000..f984fac --- /dev/null +++ b/frontend/src/store/rpcStore.ts @@ -0,0 +1,140 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { getNetworkEndpoints, Network } from "@injectivelabs/networks"; +import { NETWORK } from "../config"; + +export interface Endpoint { + grpc: string; + rest?: string; + label?: string; + custom?: boolean; +} + +interface RpcState { + endpoints: Endpoint[]; + activeIndex: number; + modalOpen: boolean; + + selectEndpoint: (i: number) => void; + addCustomEndpoint: (grpc: string, rest?: string) => void; + removeCustomEndpoint: (i: number) => void; + openModal: () => void; + closeModal: () => void; +} + +const sdkEndpoints = getNetworkEndpoints(NETWORK); + +const defaultEndpoints: Endpoint[] = + NETWORK === Network.Testnet + ? [ + { + grpc: sdkEndpoints.grpc, + rest: "https://testnet.sentry.lcd.injective.network", + label: "Injective Official", + }, + { + grpc: "https://injective-testnet-grpc.publicnode.com", + rest: "https://injective-testnet-rest.publicnode.com", + label: "PublicNode", + }, + ] + : [ + { + grpc: sdkEndpoints.grpc, + rest: "https://sentry.lcd.injective.network", + label: "Injective Official", + }, + { + grpc: "https://injective-grpc.publicnode.com", + rest: "https://injective-rest.publicnode.com", + label: "PublicNode", + }, + ]; + +export const useRpcStore = create()( + persist( + (set, get) => ({ + endpoints: [...defaultEndpoints], + activeIndex: 0, + modalOpen: false, + + selectEndpoint: (i) => { + const { endpoints } = get(); + if (i >= 0 && i < endpoints.length) { + set({ activeIndex: i }); + } + }, + + addCustomEndpoint: (grpc, rest) => { + grpc = grpc.trim().replace(/\/+$/, ""); + if (!/^https?:\/\/.+/i.test(grpc)) return; + + const { endpoints } = get(); + if (endpoints.some((e) => e.grpc === grpc)) return; + + const list = [ + ...endpoints, + { + grpc, + rest: rest?.trim().replace(/\/+$/, "") || undefined, + label: "Custom", + custom: true, + }, + ]; + set({ endpoints: list, activeIndex: list.length - 1 }); + }, + + removeCustomEndpoint: (i) => { + const { endpoints, activeIndex } = get(); + if (!endpoints[i]?.custom) return; + const list = endpoints.filter((_, idx) => idx !== i); + set({ + endpoints: list, + activeIndex: + activeIndex >= list.length + ? list.length - 1 + : activeIndex > i + ? activeIndex - 1 + : activeIndex, + }); + }, + + openModal: () => set({ modalOpen: true }), + closeModal: () => set({ modalOpen: false }), + }), + { + name: "rpc-store", + partialize: (state) => ({ + endpoints: state.endpoints, + activeIndex: state.activeIndex, + }), + merge: (persisted, current) => { + const p = persisted as Partial | undefined; + if (!p?.endpoints) return current; + + // Ensure default endpoints are always present + const merged = [...defaultEndpoints]; + const defaultGrpcs = new Set( + defaultEndpoints.map((e) => e.grpc), + ); + for (const ep of p.endpoints) { + if (ep.custom && !defaultGrpcs.has(ep.grpc)) { + merged.push(ep); + } + } + + const activeIndex = Math.min( + p.activeIndex ?? 0, + merged.length - 1, + ); + return { ...current, endpoints: merged, activeIndex }; + }, + }, + ), +); + +export const useCurrentGrpc = () => + useRpcStore((s) => s.endpoints[s.activeIndex]?.grpc ?? sdkEndpoints.grpc); + +export const useCurrentRest = () => + useRpcStore((s) => s.endpoints[s.activeIndex]?.rest ?? ""); diff --git a/frontend/src/store/useStore.ts b/frontend/src/store/useStore.ts index 9921f48..dd25f09 100644 --- a/frontend/src/store/useStore.ts +++ b/frontend/src/store/useStore.ts @@ -123,7 +123,7 @@ const walletStrategy = new WalletStrategy({ strategies: {}, }); -const msgBroadcaster = new MsgBroadcaster({ +let msgBroadcaster = new MsgBroadcaster({ walletStrategy, simulateTx: true, network: NETWORK, @@ -131,6 +131,20 @@ const msgBroadcaster = new MsgBroadcaster({ gasBufferCoefficient: 1.1, }); +export function setMsgBroadcasterEndpoints(overrides: { + grpc?: string; + rest?: string; +}) { + const base = getNetworkEndpoints(NETWORK); + msgBroadcaster = new MsgBroadcaster({ + walletStrategy, + simulateTx: true, + network: NETWORK, + endpoints: { ...base, ...overrides }, + gasBufferCoefficient: 1.1, + }); +} + const WALLET_MAP: Record = { metamask: Wallet.Metamask, keplr: Wallet.Keplr,