Web dashboard for Stellar TxWatch - register contracts and manage real-time alert rules.
Part of the Tx-wats GitHub org.
- Register Soroban contracts on Mainnet, Testnet, or Futurenet
- Configure alert rules: large transfers, function calls, admin actions, failed transactions
- Receive instant webhook payloads when rules fire
- Monitor delivery history and alert logs per contract
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS |
| Wallet | Freighter (Stellar identity) |
| Blockchain | @stellar/stellar-sdk + Horizon API |
git clone https://github.com/Tx-wats/web
cd web
npm install
cp .env.example .env.local
npm run devOpen http://localhost:3000.
- Browsers: Chrome, Firefox, Edge, Safari (latest versions)
- Wallet: Freighter extension required for contract registration and transaction signing
- Install from freighter.app
- Supported on Chrome, Firefox, and Edge
- Must be connected to the same network (Mainnet, Testnet, or Futurenet) as the contract you're registering
app/
page.tsx # Landing page
dashboard/page.tsx # Stats + contract grid
contracts/
page.tsx # Contract list
new/page.tsx # Add contract form
[id]/page.tsx # Contract detail + alert history
components/
ContractCard.tsx # Contract summary card
RuleBuilder.tsx # Alert rule configuration UI
WebhookLog.tsx # Webhook delivery history table
NetworkBadge.tsx # Mainnet/Testnet/Futurenet badge
AlertRuleBadge.tsx # Rule type display badge
FreighterConnect.tsx # Wallet connection button
CopyButton.tsx # Clipboard copy utility
MobileNav.tsx # Mobile navigation drawer
EmptyState.tsx # Empty list placeholder
lib/
stellar.ts # Horizon + Soroban RPC helpers
storage.ts # localStorage contract registry
api.ts # Fetch wrapper + test webhook
useContracts.ts # React hook for contract state
types/
index.ts # Shared types (mirrors core Rust structs)
The dashboard follows a standard Next.js App Router structure:
- Landing page (
app/page.tsx) — unauthenticated entry point with feature overview - Dashboard (
app/dashboard/page.tsx) — authenticated view showing contract stats and grid - Contract list (
app/contracts/page.tsx) — all registered contracts - Add contract (
app/contracts/new/page.tsx) — registration form with Freighter signing - Contract detail (
app/contracts/[id]/page.tsx) — alert rules, webhook logs, and history
- localStorage (
lib/storage.ts) — persists registered contracts, alert history, and user preferences - Freighter wallet — stores user identity and network selection
- Backend API (
lib/api.ts) — optional txwatch-core integration for webhook delivery logs
The app integrates with Stellar at three points:
- Horizon REST API — fetch account balances, transaction history, and network status
- Soroban RPC — contract simulation, ledger entry reads, and address validation
- Freighter wallet — user authentication, transaction signing, and network switching
See Stellar integration for implementation details.
| Variable | Description |
|---|---|
NEXT_PUBLIC_API_URL |
Base URL of the txwatch-core API. When empty, the app uses local-only mode. |
NEXT_PUBLIC_SOROBAN_RPC_URL |
Optional Soroban RPC URL used for every network unless overridden per network. |
NEXT_PUBLIC_SOROBAN_RPC_URL_MAINNET |
Optional Mainnet Soroban RPC URL. |
NEXT_PUBLIC_SOROBAN_RPC_URL_TESTNET |
Optional Testnet Soroban RPC URL. |
NEXT_PUBLIC_SOROBAN_RPC_URL_FUTURENET |
Optional Futurenet Soroban RPC URL. |
NEXT_PUBLIC_HORIZON_URL |
Optional Horizon URL used for every network unless overridden per network. |
NEXT_PUBLIC_HORIZON_URL_MAINNET |
Optional Mainnet Horizon URL. |
NEXT_PUBLIC_HORIZON_URL_TESTNET |
Optional Testnet Horizon URL. |
NEXT_PUBLIC_HORIZON_URL_FUTURENET |
Optional Futurenet Horizon URL. |
This section documents how the dashboard integrates with the Stellar network and where to extend each layer.
lib/stellar.ts exports the Horizon REST and Soroban RPC base URLs for all three networks:
import { HORIZON_URLS, SOROBAN_RPC_URLS } from '@/lib/stellar'
// Horizon REST - account balances, transaction history
HORIZON_URLS.mainnet // https://horizon.stellar.org
HORIZON_URLS.testnet // https://horizon-testnet.stellar.org
HORIZON_URLS.futurenet // https://horizon-futurenet.stellar.org
// Soroban JSON-RPC - contract simulation, ledger entries
SOROBAN_RPC_URLS.mainnet // https://soroban-rpc.mainnet.stellar.gateway.fm
SOROBAN_RPC_URLS.testnet // https://soroban-testnet.stellar.org
SOROBAN_RPC_URLS.futurenet // https://rpc-futurenet.stellar.orgUse horizonUrl(network) and sorobanRpcUrl(network) rather than the constants: they apply the overrides below.
SDF runs public RPC for testnet and futurenet only. For mainnet the dashboard defaults to Gateway.fm's keyless public endpoint, so contract lookups work out of the box. Public endpoints are rate-limited and best-effort, so for production set your own:
| Provider | Mainnet endpoint | Key needed |
|---|---|---|
| Gateway.fm (default) | https://soroban-rpc.mainnet.stellar.gateway.fm |
No |
| Lightsail | https://rpc.lightsail.network |
No |
| Ankr | https://rpc.ankr.com/stellar_soroban |
No (public tier); yes for higher limits |
| Validation Cloud | https://mainnet.stellar.validationcloud.io/v1/<API_KEY>/soroban/rpc |
Yes |
| QuickNode, Blockdaemon, … | provider-specific | Yes |
Configure endpoints in .env.local. Per-network variables win over the global one, and the Content-Security-Policy connect-src allow-list picks up whatever you set:
NEXT_PUBLIC_SOROBAN_RPC_URL_MAINNET=https://mainnet.stellar.validationcloud.io/v1/<API_KEY>/soroban/rpc
NEXT_PUBLIC_SOROBAN_RPC_URL_TESTNET= # default: https://soroban-testnet.stellar.org
NEXT_PUBLIC_SOROBAN_RPC_URL= # fallback for every networkThe endpoint must allow cross-origin requests (CORS), because contract verification runs in the browser. Keys in NEXT_PUBLIC_* variables are visible to users, so use a key restricted to your domain or proxy the RPC.
Use the @stellar/stellar-sdk Horizon.Server to fetch on-chain data. Add new queries to lib/stellar.ts:
import { Horizon } from '@stellar/stellar-sdk'
import { HORIZON_URLS } from '@/lib/stellar'
import { Network } from '@/types'
export function getHorizonServer(network: Network) {
return new Horizon.Server(HORIZON_URLS[network])
}
// Fetch recent transactions for a contract account
export async function fetchContractTransactions(
network: Network,
contractId: string
) {
const server = getHorizonServer(network)
const { records } = await server
.transactions()
.forAccount(contractId)
.order('desc')
.limit(20)
.call()
return records
}Use SorobanRpc.Server to read contract storage or simulate invocations:
import { SorobanRpc, Contract, xdr } from '@stellar/stellar-sdk'
import { SOROBAN_RPC_URLS } from '@/lib/stellar'
import { Network } from '@/types'
export function getSorobanServer(network: Network) {
return new SorobanRpc.Server(SOROBAN_RPC_URLS[network])
}
// Read a contract ledger entry by key
export async function getContractData(
network: Network,
contractId: string,
key: xdr.ScVal
) {
const server = getSorobanServer(network)
const contract = new Contract(contractId)
return server.getContractData(contract, key, SorobanRpc.Durability.Persistent)
}components/FreighterConnect.tsx wraps the @stellar/freighter-api package, which talks to the browser extension for us. lib/freighter.ts is the only module that imports the package, and it is what the rest of the app uses to read the connected public key and network:
import { readFreighterConnection, getWalletNetwork } from '@/lib/freighter'
// Read the key after connection (passed by FreighterConnect via onConnect callback)
const connection = await readFreighterConnection()
const publicKey = connection?.publicKey ?? null
const network = await getWalletNetwork() // e.g. "TESTNET"Wallet state is shared through a txwatch:wallet CustomEvent rather than per-component state, so connecting or disconnecting from one button updates every other wallet-aware component. Prefer the hook over reading the global directly:
import { useWalletState } from '@/hooks/useWalletState'
const { publicKey, initialized, publish } = useWalletState()publish(key) connects and publish(null) disconnects; both update localStorage and window.__freighterPublicKey and notify every other subscriber.
To sign and submit a transaction:
import { Transaction, Networks } from '@stellar/stellar-sdk'
import { signWithFreighter } from '@/lib/freighter'
// Build your XDR transaction, then:
const signed = await signWithFreighter(txXdr, {
networkPassphrase: Networks.TESTNET,
})All contract IDs are validated against the Soroban address format before saving:
import { isValidContractId } from '@/lib/stellar'
isValidContractId('CAAAA...') // true — starts with C, 56 base-32 chars
isValidContractId('GAAAA...') // false — that's a Stellar account addressAlert history rows link directly to Stellar Expert for transaction inspection:
import { explorerTxUrl, explorerContractUrl } from '@/lib/stellar'
explorerTxUrl('testnet', txHash) // https://stellar.expert/explorer/testnet/tx/<hash>
explorerContractUrl('mainnet', contractId) // https://stellar.expert/explorer/public/contract/<id>Alert rules are defined in types/index.ts and must stay in sync with the Rust structs in stellar-txwatch-core. To add a new rule type:
- Add the variant to
AlertRuleTypeintypes/index.ts - Add a label and colour to
AlertRuleBadge.tsx - Add conditional input fields in
RuleBuilder.tsx - Add the matching variant to the Rust
AlertRuleenum in the core engine
When a rule fires, the core engine POSTs this JSON to the registered webhook URL:
{
"label": "My DEX Contract",
"contract_id": "CAAAA...",
"network": "testnet",
"rule_triggered": "LargeTransfer",
"transaction_hash": "abc123...",
"function_name": "transfer",
"amount": 50000,
"timestamp": 1718000000000,
"horizon_link": "https://horizon-testnet.stellar.org/transactions/abc123"
}The Test button on the Add Contract form sends a mock payload to your endpoint so you can verify delivery before going live.
Requests carry an X-TxWatch-Signature header of the form t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw request body>, keyed with your signing secret (generate one on the Add Contract form; it is shown once). Because the timestamp is signed, a captured delivery can't be replayed later: reject any signature older than your tolerance window (5 minutes is a good default). Verify against the raw body, not re-serialised JSON. During secret rotation the header may carry more than one v1 entry; accept the request if any of them matches.
import crypto from 'node:crypto'
const TOLERANCE_SECONDS = 300
function verify(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
const parts = (header ?? '').split(',').map((p) => p.trim().split('='))
const t = Number(parts.find(([k]) => k === 't')?.[1])
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v)
if (!Number.isInteger(t) || sigs.length === 0) return false
if (Math.abs(now - t) > TOLERANCE_SECONDS) return false // stale: possible replay
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest()
return sigs.some((sig) => {
const b = Buffer.from(sig, 'hex')
return b.length === expected.length && crypto.timingSafeEqual(b, expected)
})
}import hmac, hashlib, time
TOLERANCE_SECONDS = 300
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), None)
sigs = [v for k, v in parts if k == "v1"]
if t is None or not t.isdigit() or not sigs:
return False
if abs(time.time() - int(t)) > TOLERANCE_SECONDS:
return False # stale: possible replay
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)The dashboard's own helper, verifyWebhookSignature in lib/webhookSignature.ts, implements the same check.
Every push and pull request to main runs these GitHub Actions checks:
- Lint (
npm run lint) - Formatting (
npm run format:check) - Type-check (
npm run typecheck) - Dependency audit (
npm audit --audit-level=high; currently non-blocking) - Tests (
npm test) - Production build (
npm run build)
- Multi-signature contract support
- Real-time WebSocket updates for alert delivery
- Custom alert rule templates
- Batch contract registration
- Mobile app (React Native)
- Alert aggregation across multiple contracts
- Integration with external notification services (Slack, Discord, email)
- stellar-txwatch-core - Rust monitoring engine
- stellar-txwatch-contracts - Soroban smart contracts
The app stores a local cache of contracts, alert history, and preferences in browser localStorage. When NEXT_PUBLIC_API_URL is configured, contract records also sync with the core API; the browser cache remains in use. This means:
- The local cache and preferences are specific to each browser/device
- Contract records sync through the core API when it is configured
- Clearing browser storage removes the local cache and preferences
- Use export/import backups to move local alert history and settings between browsers
Settings → Export contracts / Import contracts can move your data between browsers. Both export and import let you choose what to include:
- Contracts only — just the registered contracts and their alert rules
- Everything — contracts plus alert history, retention days, notification preference, and view preferences
Exports are versioned JSON. The full-backup format is:
{
"version": 2,
"contracts": [],
"alerts": [],
"settings": {}
}The older contracts-only format (no version field, or version: 1) is still accepted on import, so backups made before the full-backup format existed can be restored. Importing a v2 backup restores alerts and settings as well as contracts.
The dashboard currently operates in two modes:
- Contract registration and alert rule configuration are stored locally
- Webhook testing uses mock payloads
- No real-time monitoring occurs without a backend
- Contracts and rules sync with the txwatch-core backend
- Real webhook delivery and monitoring depend on the backend service
- Requires the stellar-txwatch-core API running
Note: The dashboard itself does not monitor transactions. Monitoring is performed by the core engine. The dashboard is a configuration and monitoring UI only.
The landing page introduces the dashboard and provides quick access to wallet connection and contract registration.
The dashboard displays registered contracts in a grid layout with summary cards showing contract ID, network, and alert rule count.
The contract detail page shows:
- Contract metadata (ID, network, registration date)
- Configured alert rules with type badges
- Webhook delivery history with transaction links to Stellar Expert
The form allows users to:
- Enter a contract ID (validated against Soroban address format)
- Select the network (Mainnet, Testnet, Futurenet)
- Configure alert rules (large transfers, function calls, admin actions, failed transactions)
- Test webhook delivery before going live
The settings page lets users:
- Export contracts only, or everything (contracts, alert history, and settings) as versioned JSON
- Import a contracts-only (v1) or full (v2) backup, choosing whether to restore contracts only or everything
- Configure alert retention days, notification preference, and view preferences
When reporting issues or requesting features, please include:
- Steps to reproduce the issue
- Expected vs. actual behavior
- Browser and OS information
- Screenshots or error messages if applicable
- Whether the issue occurs in mock mode or with a backend API
- Clear description of the desired functionality
- Use case or problem it solves
- Suggested implementation approach (if any)
- Whether it's a UI/UX enhancement or backend integration
Please check existing issues before opening a new one to avoid duplicates.
See CONTRIBUTING.md.
Apache License 2.0. See LICENSE for the full text.
Please report vulnerabilities using the private process in SECURITY.md.