Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Track unbonding completions

When an unbonding period completes, the protocol emits an UnbondingCompleted event through the StableSystem precompile (0x0000000000000000000000000000000000009999) via a system transaction. This lets dApps notify users and update balances in real time without running custom indexers or polling REST endpoints.

Prerequisites

  • Understanding of System transactions.
  • Familiarity with Staking, specifically undelegate and the unbonding process.
  • Experience with contract event subscription and filtering using a standard web3 library (e.g. ethers.js v6).

Overview

  • Set up the contract instance: create a contract instance for the StableSystem precompile.
  • Handle events in your application: subscribe to real-time events or query historical data depending on your application logic.
  • Handle connection issues: implement reconnection logic for persistent WebSocket subscriptions.

Step 1: Set up the contract instance

Create a contract instance for the StableSystem precompile using the UnbondingCompleted event ABI.

// config.ts
import { ethers } from "ethers";
 
export const STABLE_SYSTEM_ADDRESS =
  "0x0000000000000000000000000000000000009999";
 
export const STABLE_SYSTEM_ABI = [
  "event UnbondingCompleted(address indexed delegator, address indexed validator, uint64 indexed originBlockHeight, uint256 amount)",
];
 
export const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
export const stableSystem = new ethers.Contract(
  STABLE_SYSTEM_ADDRESS,
  STABLE_SYSTEM_ABI,
  provider
);
No output. The StableSystem contract instance is ready to query or subscribe.

Step 2: Handle events in your application

Subscribe to real-time events, query historical data, or both depending on your application logic.

Real-time subscription

Subscribe to UnbondingCompleted events for real-time notifications when any unbonding completes. Useful for triggering balance updates, sending notifications, or refreshing dashboard statistics.

// subscribeBasic.ts
import { ethers } from "ethers";
import { stableSystem } from "./config";
 
stableSystem.on("UnbondingCompleted", (delegator, validator, originBlockHeight, amount, event) => {
  console.log("Unbonding completed:");
  console.log("  Delegator:", delegator);
  console.log("  Validator:", validator);
  console.log("  Origin block:", originBlockHeight.toString());
  console.log("  Amount:", ethers.formatEther(amount), "tokens");
  console.log("  Block:", event.log.blockNumber);
  console.log("  Tx Hash:", event.log.transactionHash);
});
Unbonding completed:
  Delegator: 0xabcd...
  Validator: 0x1234...
  Origin block: 36975999
  Amount: 100.0 tokens
  Block: 36976000
  Tx Hash: 0x12ab...

Filter by user

To only receive events for a particular delegator address, use the indexed event parameters to create a filter.

// subscribeByUser.ts
import { ethers } from "ethers";
import { stableSystem } from "./config";
 
const userAddress = "0xabcd...";
const filter = stableSystem.filters.UnbondingCompleted(userAddress);
 
stableSystem.on(filter, (delegator, validator, originBlockHeight, amount) => {
  console.log("User unbonding completed:", {
    delegator,
    validator,
    originBlockHeight,
    amount: ethers.formatEther(amount),
  });
});
User unbonding completed: {
  delegator: "0xabcd...",
  validator: "0x1234...",
  originBlockHeight: 36975999n,
  amount: "100.0"
}

Filter by validator

// subscribeByValidator.ts
import { stableSystem } from "./config";
 
const validatorAddress = "0x1234...";
const validatorFilter = stableSystem.filters.UnbondingCompleted(
  null,
  validatorAddress
);
 
stableSystem.on(validatorFilter, (delegator, validator, originBlockHeight, amount) => {
  console.log("Validator unbonding completed:", {
    delegator,
    validator,
    originBlockHeight,
    amount,
  });
});
Validator unbonding completed: {
  delegator: "0xabcd...",
  validator: "0x1234...",
  originBlockHeight: 36975999n,
  amount: 100000000000000000000n
}

Historical query

If your dApp needs to show a history of past unbonding completions, query historical events using event filters with block ranges.

// queryHistory.ts
import { ethers } from "ethers";
import { provider, stableSystem } from "./config";
 
async function getUnbondingHistory(
  userAddress: string,
  fromBlock: number,
  toBlock: number
) {
  const filter = stableSystem.filters.UnbondingCompleted(userAddress);
  const events = await stableSystem.queryFilter(filter, fromBlock, toBlock);
 
  return events.map((event) => ({
    delegator: event.args.delegator,
    validator: event.args.validator,
    originBlockHeight: event.args.originBlockHeight,
    amount: ethers.formatEther(event.args.amount),
    blockNumber: event.blockNumber,
    txHash: event.transactionHash,
  }));
}
 
const currentBlock = await provider.getBlockNumber();
const history = await getUnbondingHistory(
  "0xabcd...",
  currentBlock - 1000,
  currentBlock
);
 
console.log(history);
[
  {
    delegator: "0xabcd...",
    validator: "0x1234...",
    originBlockHeight: 36975999n,
    amount: "100.0",
    blockNumber: 36976000,
    txHash: "0x12ab..."
  }
]

Step 3: Handle connection issues

Event subscriptions rely on persistent WebSocket connections. Implement reconnection logic for production dApps.

// subscribeWithReconnection.ts
import { ethers } from "ethers";
import { STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI } from "./config";
 
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
 
function handleUnbonding(
  delegator: string,
  validator: string,
  originBlockHeight: bigint,
  amount: bigint
) {
  console.log("Unbonding completed:", {
    delegator,
    validator,
    originBlockHeight,
    amount,
  });
}
 
function setupEventListener() {
  const wsProvider = new ethers.WebSocketProvider("wss://rpc.testnet.stable.xyz");
 
  wsProvider.on("error", (error) => {
    console.error("Provider error:", error);
    if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      reconnectAttempts++;
      setTimeout(() => setupEventListener(), 5000);
    }
  });
 
  const stableSystem = new ethers.Contract(
    STABLE_SYSTEM_ADDRESS,
    STABLE_SYSTEM_ABI,
    wsProvider
  );
 
  stableSystem.on("UnbondingCompleted", handleUnbonding);
}
 
setupEventListener();
No output until an unbonding completes or the provider reports an error.

Where to go next