Indexing staking positions
Positions are readable directly, so a simple integration needs no indexer at all.
Reading a wallet's positions
const ids = await client.readContract({ address: staking, abi: gravityStakingAbi, functionName: 'positionsOf', args: [owner],}); const data = await client.multicall({ contracts: ids.flatMap((id) => [ {address: staking, abi: gravityStakingAbi, functionName: 'getPosition', args: [id]}, {address: staking, abi: gravityStakingAbi, functionName: 'pendingRewards', args: [id]}, ]),});struct Position { address owner; uint256 amount; uint256 weight; uint64 createdAt; uint64 unlockAt; uint256 rewardDebt;}Events
event Staked(address indexed owner, uint256 indexed positionId, uint256 amount, uint256 weight, uint64 unlockAt);event RewardsClaimed(address indexed owner, uint256 indexed positionId, uint256 amount);event Withdrawn(address indexed owner, uint256 indexed positionId, uint256 amount, uint256 rewards);event EmergencyWithdrawn(address indexed owner, uint256 indexed positionId, uint256 amount, uint256 forfeited);event RewardsNotified(uint256 amount, uint256 accRewardPerWeight, uint256 heldUndistributed);Both owner and positionId are indexed, so filtering by wallet is cheap.
Computing pending rewards off chain
If you need pending amounts without an RPC round-trip per position:
pending = (weight × accRewardPerWeight) / 1e27 − rewardDebtTrack accRewardPerWeight from RewardsNotified, and rewardDebt from the position. Remember that rewardDebt is rewritten on every claim, so an index must apply RewardsClaimed events too.
The safest approach is to read pendingRewards(id) from chain; the formula above is for building a leaderboard, not for showing a user what they can withdraw.
Lifecycle state machine
Staked ──────► open │ │ │ ├─ RewardsClaimed (repeatable, position stays open) │ │ │ ├─ Withdrawn ──► closed (matured; principal + rewards) │ │ │ └─ EmergencyWithdrawn ──► closed (principal only; rewards forfeited)There is no partial withdrawal and no transfer. A position belongs to its opener until it closes.
Suggested schema
CREATE TABLE position ( position_id NUMERIC(78,0) PRIMARY KEY, owner BYTEA NOT NULL, amount_wei NUMERIC(78,0) NOT NULL, weight_wei NUMERIC(78,0) NOT NULL, created_at TIMESTAMPTZ NOT NULL, unlock_at TIMESTAMPTZ NOT NULL, closed_at TIMESTAMPTZ, close_kind TEXT CHECK (close_kind IN ('withdraw','emergency'))); CREATE INDEX ON position (owner) WHERE closed_at IS NULL;Reconciling with protocol totals
Σ open position.amount == totalStakedΣ open position.weight == totalWeightBoth are asserted as invariants in the test suite. If your index disagrees with the contract, trust the contract.