Soroban smart contracts for on-chain alert configuration storage and watcher registry.
Part of the Tx-wat organization.
| Contract | Description |
|---|---|
| Alert Registry | Stores alert configs on-chain keyed by contract address |
| Watcher Registry | Stores authorized watcher node addresses |
# Install Rust + Soroban target
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
# Build
cargo build --release --target wasm32-unknown-unknown
# Test
cargo testflowchart TD
subgraph Stellar["Stellar Network (on-chain)"]
AR["AlertRegistry\n─────────────\nstores alert configs\nkeyed by contract address"]
WR["WatcherRegistry\n─────────────\nstores authorized\nwatcher addresses"]
end
subgraph OffChain["Off-chain (stellar-txwatch-core)"]
W["Watcher Node\n─────────────\npolls Horizon\nmatches rules\nfires webhooks"]
end
Owner["Owner"] -->|"register_alert / update_alert"| AR
Admin["Admin"] -->|"register_watcher / remove_watcher"| WR
W -->|"is_authorized(watcher)"| WR
W -->|"get_alerts_for_contract(target)"| AR
Horizon["Horizon API"] -->|"GET /accounts/{id}/transactions"| W
W -->|"POST webhook URL"| Endpoint["Downstream\nIntegration"]
Data flow:
- An owner registers an alert in
AlertRegistry— specifying the target contract, rules, and a hashed webhook URL. - Authorized watcher nodes are recorded in
WatcherRegistryby an admin. - A watcher node polls Horizon for transaction activity, fetches matching alert configs from
AlertRegistry, and checks whether any rule matches. - On a match the watcher fires the configured webhook so downstream integrations can react.
The system is centered around three data-flow steps:
- An owner registers an alert in
AlertRegistrywith the contract address, labels, webhook hash, and rules. - Authorized watcher nodes poll Horizon for transaction activity, then check the stored alert definitions in
AlertRegistryto determine whether a watched contract event matches. - When a match is found, the watcher fires the configured webhook so downstream integrations can react.
This keeps alert configuration on-chain while letting watcher nodes perform off-chain polling and delivery.
# Testnet
rpc_url = "https://soroban-testnet.stellar.org"
passphrase = "Test SDF Network ; September 2015"
horizon_url = "https://horizon-testnet.stellar.org"
# Mainnet
rpc_url = "https://mainnet.stellar.validationcloud.io/v1/<API_KEY>"
passphrase = "Public Global Stellar Network ; September 2015"
horizon_url = "https://horizon.stellar.org"# Register a watcher (admin only)
stellar contract invoke \
--id <WATCHER_REGISTRY_CONTRACT_ID> \
--source <ADMIN_IDENTITY> \
--network testnet \
-- register_watcher \
--admin <ADMIN_ADDRESS> \
--watcher <WATCHER_ADDRESS>
# Register an alert config
stellar contract invoke \
--id <ALERT_REGISTRY_CONTRACT_ID> \
--source <OWNER_IDENTITY> \
--network testnet \
-- register_alert \
--owner <OWNER_ADDRESS> \
--target_contract <WATCHED_CONTRACT_ADDRESS> \
--label "My Alert" \
--webhook_hash "<sha256-of-webhook-url>" \
--rules '["rule:transfer","rule:mint"]'
# Query alerts for a contract
stellar contract invoke \
--id <ALERT_REGISTRY_CONTRACT_ID> \
--network testnet \
-- get_alerts_for_contract \
--target_contract <WATCHED_CONTRACT_ADDRESS>import {
Contract,
SorobanRpc,
TransactionBuilder,
Networks,
BASE_FEE,
nativeToScVal,
Address,
} from "@stellar/stellar-sdk";
const server = new SorobanRpc.Server("https://soroban-testnet.stellar.org");
const contract = new Contract("<ALERT_REGISTRY_CONTRACT_ID>");
// Build a register_alert transaction
const account = await server.getAccount(ownerKeypair.publicKey());
const tx = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: Networks.TESTNET,
})
.addOperation(
contract.call(
"register_alert",
new Address(ownerKeypair.publicKey()).toScVal(), // owner
new Address("<WATCHED_CONTRACT_ADDRESS>").toScVal(), // target_contract
nativeToScVal("My Alert", { type: "string" }), // label
nativeToScVal("<sha256-of-webhook-url>", { type: "string" }), // webhook_hash
nativeToScVal(["rule:transfer", "rule:mint"], { type: "array", element: { type: "string" } }), // rules
)
)
.setTimeout(30)
.build();
const preparedTx = await server.prepareTransaction(tx);
preparedTx.sign(ownerKeypair);
const result = await server.sendTransaction(preparedTx);
console.log("Transaction hash:", result.hash);use soroban_sdk::{Address, Env, String, Vec};
// In a cross-contract call context:
let alert_registry = AlertRegistryClient::new(&env, &alert_registry_id);
let config_id = alert_registry.register_alert(
&owner,
&target_contract,
&String::from_str(&env, "My Alert"),
&String::from_str(&env, "<webhook-hash>"),
&rules,
);Re-entrancy safety: Soroban executes contract calls atomically and prevents classic callback-based re-entrancy within the same transaction. The registry contracts only mutate local storage after
require_auth()succeeds, and they do not invoke external contracts during state updates.
All mutating functions require Stellar auth signatures:
Owner signs → register_alert / update_alert / remove_alert
Admin signs → register_watcher / remove_watcher / transfer_admin
Stellar's require_auth() enforces this at the protocol level — no custom signature verification needed.
Contracts emit no custom events yet. Watchers poll via Horizon's transaction endpoint:
GET https://horizon-testnet.stellar.org/accounts/<CONTRACT_ID>/transactions
Future versions will emit soroban_sdk::events for real-time indexing.
See DEPLOYMENTS.md.
See CONTRIBUTING.md.
- Core engine: https://github.com/Tx-wat/stellar-txwatch-core
- Web dashboard: https://github.com/Tx-wat/stellar-txwatch-web
MIT