Documentation Index Fetch the complete documentation index at: https://docs.stable.xyz/llms.txt
Use this file to discover all available pages before exploring further.
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.
Concept: For how system transactions bridge SDK-layer events to the EVM and why it matters, see System transactions .
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, 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
);
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 { stableSystem } from "./config" ;
stableSystem . on ( "UnbondingCompleted" , ( delegator , validator , amount , event ) => {
console . log ( "Unbonding completed:" );
console . log ( " Delegator:" , delegator );
console . log ( " Validator:" , validator );
console . log ( " Amount:" , ethers . formatEther ( amount ), "tokens" );
console . log ( " Block:" , event . log . blockNumber );
console . log ( " Tx Hash:" , event . log . transactionHash );
});
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 , amount , event ) => {
refreshUserBalance ( userAddress );
showNotification (
`Your unbonding of ${ ethers . formatEther ( amount ) } tokens completed!`
);
});
Filter by validator
// subscribeByValidator.ts
import { stableSystem } from "./config" ;
const validatorAddress = "0x1234..." ;
const validatorFilter = stableSystem . filters . UnbondingCompleted (
null ,
validatorAddress
);
stableSystem . on ( validatorFilter , ( delegator , validator , amount ) => {
updateValidatorStats ( validator , amount );
});
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 ,
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
);
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 , amount : bigint ) {
console . log ( "Unbonding completed:" , { delegator , validator , 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 ();
Next recommended
System transactions concept Understand how protocol-level events reach the EVM.
Staking module concept Review the delegation and unbonding flow.
Staking precompile reference Look up the methods that trigger the events tracked here.