Examples: Subscriptions

Real-time data updates using GraphQL subscriptions over WebSockets.

Table of Contents

Setup

Apollo Client with Subscriptions

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { getMainDefinition } from '@apollo/client/utilities'
import { createClient } from 'graphql-ws'

const httpLink = new HttpLink({
  uri: 'https://sai-keeper.testnet-2.nibiru.fi/graphql'
})

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'wss://sai-keeper.testnet-2.nibiru.fi/graphql',
    connectionParams: {
      // Add auth headers if needed
    },
    retryAttempts: 5,
    shouldRetry: () => true
  })
)

// Split link based on operation type
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query)
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    )
  },
  wsLink,
  httpLink
)

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
})

urql with Subscriptions

import { createClient, subscriptionExchange, fetchExchange } from 'urql'
import { createClient as createWSClient } from 'graphql-ws'

const wsClient = createWSClient({
  url: 'wss://sai-keeper.testnet-2.nibiru.fi/graphql'
})

const client = createClient({
  url: 'https://sai-keeper.testnet-2.nibiru.fi/graphql',
  exchanges: [
    fetchExchange,
    subscriptionExchange({
      forwardSubscription: (operation) => ({
        subscribe: (sink) => ({
          unsubscribe: wsClient.subscribe(operation, sink)
        })
      })
    })
  ]
})

Perp Subscriptions

Example 1: Watch User Trades

Subscribe to all trade updates for a specific user.

import { gql } from '@apollo/client'

const WATCH_TRADES = gql`
  subscription WatchTrades($trader: String!) {
    perpTrades(where: { trader: $trader }) {
      id
      isOpen
      isLong
      leverage
      collateralAmount
      openPrice
      closePrice
      sl
      tp
      perpBorrowing {
        baseToken {
          symbol
          logoUrl
        }
        marketId
      }
      state {
        pnlCollateral
        pnlPct
        liquidationPrice
        positionValue
        borrowingFeeCollateral
      }
      openBlock {
        block_ts
      }
    }
  }
`

// Apollo Client usage
const subscription = client.subscribe({
  query: WATCH_TRADES,
  variables: { trader: 'nibi1abc...' }
}).subscribe({
  next: ({ data }) => {
    console.log('Trades updated:', data.perpTrades)
    
    data.perpTrades.forEach(trade => {
      const pnlPct = (trade.state.pnlPct * 100).toFixed(2)
      const status = trade.isOpen ? 'OPEN' : 'CLOSED'
      
      console.log(`[${status}] ${trade.perpBorrowing.baseToken.symbol} ${trade.isLong ? 'LONG' : 'SHORT'} ${trade.leverage}x`)
      console.log(`  PnL: ${pnlPct}%`)
      console.log(`  Liquidation: $${trade.state.liquidationPrice.toFixed(2)}`)
    })
  },
  error: (error) => {
    console.error('Subscription error:', error)
  }
})

// Cleanup
// subscription.unsubscribe()

Example 2: Watch Trade Events

Subscribe to trade history events (opens, closes, liquidations, etc).

const WATCH_TRADE_EVENTS = gql`
  subscription WatchTradeEvents($trader: String!) {
    perpTradeHistory(where: { trader: $trader }) {
      id
      tradeChangeType
      realizedPnlCollateral
      realizedPnlPct
      block {
        block
        block_ts
      }
      trade {
        id
        perpBorrowing {
          baseToken {
            symbol
          }
          collateralToken {
            symbol
            decimals
          }
        }
        isLong
        leverage
        openPrice
        closePrice
      }
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_TRADE_EVENTS,
  variables: { trader: 'nibi1abc...' }
}).subscribe({
  next: ({ data }) => {
    const events = data.perpTradeHistory
    
    events.forEach(event => {
      const timestamp = new Date(event.block.block_ts).toLocaleTimeString()
      const token = event.trade?.perpBorrowing.baseToken.symbol
      
      console.log(`[${timestamp}] ${event.tradeChangeType}`)
      
      if (event.realizedPnlPct !== null) {
        const pnlPct = (event.realizedPnlPct * 100).toFixed(2)
        const decimals = event.trade.perpBorrowing.collateralToken.decimals
        const pnl = event.realizedPnlCollateral / (10 ** decimals)
        
        console.log(`  ${token} - Realized PnL: ${pnl.toFixed(2)} (${pnlPct}%)`)
      }
    })
    
    // Show notification for important events
    events.forEach(event => {
      if (event.tradeChangeType === 'position_liquidated') {
        showNotification('⚠️ Position Liquidated!', {
          body: `Your ${event.trade?.perpBorrowing.baseToken.symbol} position was liquidated`
        })
      }
      
      if (event.tradeChangeType === 'position_closed_tp') {
        showNotification('✅ Take Profit Hit!', {
          body: `TP triggered on ${event.trade?.perpBorrowing.baseToken.symbol}`
        })
      }
    })
  }
})

Example 3: Watch Market Borrowing Rates

Subscribe to real-time borrowing rate updates for a specific market.

const WATCH_MARKET = gql`
  subscription WatchMarket($collateralId: Int!, $marketId: Int!) {
    perpBorrowing(collateralId: $collateralId, marketId: $marketId) {
      marketId
      baseToken {
        symbol
      }
      price
      oiLong
      oiShort
      oiMax
      feesPerHourLong
      feesPerHourShort
      openFeePct
      closeFeePct
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_MARKET,
  variables: { collateralId: 1, marketId: 1 }
}).subscribe({
  next: ({ data }) => {
    const market = data.perpBorrowing
    
    // Calculate funding rate APR
    const fundingAprLong = market.feesPerHourLong * 24 * 365 * 100
    const fundingAprShort = market.feesPerHourShort * 24 * 365 * 100
    
    // Calculate OI utilization
    const oiTotal = market.oiLong + market.oiShort
    const utilization = (oiTotal / market.oiMax) * 100
    
    console.log(`${market.baseToken.symbol} Market Update`)
    console.log(`Price: $${market.price.toFixed(2)}`)
    console.log(`OI: ${market.oiLong} L / ${market.oiShort} S`)
    console.log(`Utilization: ${utilization.toFixed(1)}%`)
    console.log(`Funding APR - Long: ${fundingAprLong.toFixed(2)}% | Short: ${fundingAprShort.toFixed(2)}%`)
    
    // Alert on high utilization
    if (utilization > 90) {
      console.warn('⚠️  High OI utilization!')
    }
  }
})

Example 4: Watch All Markets

Subscribe to updates for all available markets.

const WATCH_ALL_MARKETS = gql`
  subscription WatchAllMarkets {
    perpBorrowings {
      marketId
      baseToken {
        symbol
        name
      }
      collateralToken {
        symbol
      }
      visible
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_ALL_MARKETS
}).subscribe({
  next: ({ data }) => {
    const markets = data.perpBorrowings.filter(m => m.visible)
    console.log(`Active markets: ${markets.length}`)
    
    // Update market selector UI
    updateMarketList(markets)
  }
})

LP Subscriptions

Example 5: Watch Vault Metrics

Subscribe to real-time vault updates (TVL, APY, share price).

const WATCH_VAULTS = gql`
  subscription WatchVaults {
    lpVaults {
      address
      collateralToken {
        symbol
        name
        decimals
      }
      tvl
      sharePrice
      apy
      availableAssets
      currentEpoch
      revenueInfo {
        NetProfit
        TraderLosses
        Liabilities
      }
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_VAULTS
}).subscribe({
  next: ({ data }) => {
    data.lpVaults.forEach(vault => {
      const decimals = vault.collateralToken.decimals
      const tvl = vault.tvl / (10 ** decimals)
      const available = vault.availableAssets / (10 ** decimals)
      const utilization = ((tvl - available) / tvl) * 100
      
      console.log(`${vault.collateralToken.symbol} Vault`)
      console.log(`  TVL: ${tvl.toLocaleString()}`)
      console.log(`  APY: ${vault.apy?.toFixed(2) || 'N/A'}%`)
      console.log(`  Share Price: ${vault.sharePrice.toFixed(6)}`)
      console.log(`  Utilization: ${utilization.toFixed(1)}%`)
      console.log(`  Net Profit: ${vault.revenueInfo.NetProfit}`)
    })
    
    // Update dashboard
    updateVaultDashboard(data.lpVaults)
  }
})

Example 6: Watch User LP Positions

Subscribe to user's LP deposit updates.

const WATCH_USER_LP = gql`
  subscription WatchUserLP($user: String!) {
    lpDeposits(where: { depositor: $user }) {
      depositor
      shares
      vault {
        address
        collateralToken {
          symbol
          decimals
        }
        sharePrice
        apy
      }
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_USER_LP,
  variables: { user: 'nibi1abc...' }
}).subscribe({
  next: ({ data }) => {
    let totalValue = 0
    
    data.lpDeposits.forEach(deposit => {
      const decimals = deposit.vault.collateralToken.decimals
      const value = (deposit.shares * deposit.vault.sharePrice) / (10 ** decimals)
      totalValue += value
      
      console.log(`${deposit.vault.collateralToken.symbol}: ${value.toFixed(2)}`)
    })
    
    console.log(`Total LP Value: ${totalValue.toFixed(2)}`)
    
    // Update portfolio UI
    updatePortfolioValue(totalValue)
  }
})

Example 7: Watch Deposit Events

Subscribe to deposit/withdrawal events.

const WATCH_DEPOSIT_EVENTS = gql`
  subscription WatchDepositEvents($user: String!, $vault: String!) {
    lpDepositHistory(where: { depositor: $user, vault: $vault }) {
      id
      amount
      shares
      isWithdraw
      block {
        block_ts
      }
      vault {
        collateralToken {
          symbol
          decimals
        }
      }
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_DEPOSIT_EVENTS,
  variables: { 
    user: 'nibi1abc...',
    vault: 'nibi1vault...'
  }
}).subscribe({
  next: ({ data }) => {
    data.lpDepositHistory.forEach(event => {
      const decimals = event.vault.collateralToken.decimals
      const amount = event.amount / (10 ** decimals)
      const action = event.isWithdraw ? 'Withdrew' : 'Deposited'
      const timestamp = new Date(event.block.block_ts).toLocaleString()
      
      console.log(`[${timestamp}] ${action} ${amount.toFixed(2)} ${event.vault.collateralToken.symbol}`)
      
      // Show notification
      showNotification(`LP ${action}`, {
        body: `${amount.toFixed(2)} ${event.vault.collateralToken.symbol}`,
        timestamp: event.block.block_ts
      })
    })
  }
})

Example 8: Watch Withdrawal Requests

Subscribe to withdrawal request updates.

const WATCH_WITHDRAWALS = gql`
  subscription WatchWithdrawals($user: String!, $vault: String!) {
    lpWithdrawRequests(where: { depositor: $user, vault: $vault }) {
      shares
      status
      unlockEpoch
      autoRedeem
      vault {
        currentEpoch
        sharePrice
        collateralToken {
          symbol
          decimals
        }
      }
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_WITHDRAWALS,
  variables: {
    user: 'nibi1abc...',
    vault: 'nibi1vault...'
  }
}).subscribe({
  next: ({ data }) => {
    data.lpWithdrawRequests.forEach(request => {
      const isReady = request.vault.currentEpoch >= request.unlockEpoch
      const epochsRemaining = Math.max(0, request.unlockEpoch - request.vault.currentEpoch)
      
      const decimals = request.vault.collateralToken.decimals
      const estimatedValue = (request.shares * request.vault.sharePrice) / (10 ** decimals)
      
      console.log(`Withdrawal Request`)
      console.log(`  Status: ${isReady ? 'Ready ✅' : `${epochsRemaining} epochs remaining`}`)
      console.log(`  Estimated Value: ${estimatedValue.toFixed(2)} ${request.vault.collateralToken.symbol}`)
      console.log(`  Auto-redeem: ${request.autoRedeem}`)
      
      // Notify when ready
      if (isReady && !notifiedAlready) {
        showNotification('✅ Withdrawal Ready!', {
          body: `Your withdrawal of ${estimatedValue.toFixed(2)} ${request.vault.collateralToken.symbol} is ready`
        })
        notifiedAlready = true
      }
    })
  }
})

Oracle Subscriptions

Example 9: Watch Token Prices

Subscribe to real-time price updates.

const WATCH_PRICES = gql`
  subscription WatchPrices {
    tokenPricesUsd {
      token {
        id
        symbol
        name
      }
      priceUsd
      lastUpdatedBlock {
        block
        block_ts
      }
    }
  }
`

// Usage
const priceCache = new Map()

const subscription = client.subscribe({
  query: WATCH_PRICES
}).subscribe({
  next: ({ data }) => {
    data.tokenPricesUsd.forEach(item => {
      const oldPrice = priceCache.get(item.token.symbol)
      const newPrice = item.priceUsd
      
      // Calculate price change
      if (oldPrice) {
        const change = ((newPrice - oldPrice) / oldPrice) * 100
        const arrow = change > 0 ? '↑' : '↓'
        console.log(`${item.token.symbol}: $${newPrice.toFixed(2)} ${arrow} ${Math.abs(change).toFixed(2)}%`)
      } else {
        console.log(`${item.token.symbol}: $${newPrice.toFixed(2)}`)
      }
      
      priceCache.set(item.token.symbol, newPrice)
    })
    
    // Update price ticker UI
    updatePriceTicker(data.tokenPricesUsd)
  }
})

Example 10: Watch Specific Token Price

const WATCH_TOKEN_PRICE = gql`
  subscription WatchTokenPrice($tokenId: Int!) {
    tokenPricesUsd(where: { tokenId: $tokenId }) {
      token {
        symbol
      }
      priceUsd
      lastUpdatedBlock {
        block_ts
      }
    }
  }
`

// Usage with price alerts
const subscription = client.subscribe({
  query: WATCH_TOKEN_PRICE,
  variables: { tokenId: 1 } // BTC
}).subscribe({
  next: ({ data }) => {
    const priceData = data.tokenPricesUsd[0]
    const price = priceData.priceUsd
    
    console.log(`BTC: $${price.toFixed(2)}`)
    
    // Price alerts
    if (price > 50000) {
      showNotification('🚀 BTC Above $50k!', {
        body: `Current price: $${price.toFixed(2)}`
      })
    }
    
    if (price < 45000) {
      showNotification('📉 BTC Below $45k', {
        body: `Current price: $${price.toFixed(2)}`
      })
    }
  }
})

Example 11: Watch User Balances

const WATCH_BALANCES = gql`
  subscription WatchBalances($user: String!) {
    userBalances(where: { user: $user }) {
      amount
      token_info {
        symbol
        name
        decimals
        type
        logo
      }
    }
  }
`

// Usage
const subscription = client.subscribe({
  query: WATCH_BALANCES,
  variables: { user: 'nibi1abc...' }
}).subscribe({
  next: ({ data }) => {
    console.log('Balance Update:')
    
    data.userBalances.forEach(balance => {
      const amount = parseFloat(balance.amount) / (10 ** balance.token_info.decimals)
      console.log(`  ${balance.token_info.symbol}: ${amount.toFixed(4)}`)
    })
    
    // Update wallet UI
    updateWalletBalances(data.userBalances)
  }
})

Advanced Patterns

Example 12: Multiple Subscriptions Manager

class SubscriptionManager {
  constructor(client) {
    this.client = client
    this.subscriptions = new Map()
  }
  
  subscribe(name, query, variables, callback) {
    // Unsubscribe existing if any
    this.unsubscribe(name)
    
    const subscription = this.client.subscribe({
      query,
      variables
    }).subscribe({
      next: callback,
      error: (error) => {
        console.error(`Subscription ${name} error:`, error)
        // Attempt reconnection
        setTimeout(() => {
          this.subscribe(name, query, variables, callback)
        }, 5000)
      }
    })
    
    this.subscriptions.set(name, subscription)
  }
  
  unsubscribe(name) {
    const sub = this.subscriptions.get(name)
    if (sub) {
      sub.unsubscribe()
      this.subscriptions.delete(name)
    }
  }
  
  unsubscribeAll() {
    this.subscriptions.forEach(sub => sub.unsubscribe())
    this.subscriptions.clear()
  }
}

// Usage
const manager = new SubscriptionManager(client)

manager.subscribe('trades', WATCH_TRADES, { trader: 'nibi1abc...' }, (data) => {
  updateTradesUI(data.perpTrades)
})

manager.subscribe('prices', WATCH_PRICES, {}, (data) => {
  updatePricesUI(data.tokenPricesUsd)
})

// Cleanup on component unmount
// manager.unsubscribeAll()

Example 13: Subscription with Reconnection Logic

function createResilientSubscription(query, variables, onData) {
  let subscription = null
  let reconnectAttempts = 0
  const maxReconnectAttempts = 10
  
  function connect() {
    subscription = client.subscribe({
      query,
      variables
    }).subscribe({
      next: (data) => {
        reconnectAttempts = 0 // Reset on successful data
        onData(data)
      },
      error: (error) => {
        console.error('Subscription error:', error)
        
        if (reconnectAttempts < maxReconnectAttempts) {
          reconnectAttempts++
          const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
          console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...`)
          
          setTimeout(connect, delay)
        } else {
          console.error('Max reconnection attempts reached')
        }
      },
      complete: () => {
        console.log('Subscription completed')
      }
    })
  }
  
  connect()
  
  return {
    unsubscribe: () => {
      if (subscription) {
        subscription.unsubscribe()
      }
    }
  }
}

// Usage
const sub = createResilientSubscription(
  WATCH_TRADES,
  { trader: 'nibi1abc...' },
  (data) => {
    console.log('Received trade update:', data.perpTrades)
  }
)

// Cleanup
// sub.unsubscribe()

Best Practices

1. Always Clean Up Subscriptions

useEffect(() => {
  const subscription = client.subscribe({
    query: WATCH_TRADES,
    variables: { trader }
  }).subscribe({
    next: (data) => setTrades(data.perpTrades)
  })
  
  // Cleanup function
  return () => {
    subscription.unsubscribe()
  }
}, [trader])

2. Handle Connection States

const [connectionState, setConnectionState] = useState('connecting')

const subscription = client.subscribe({
  query: WATCH_PRICES
}).subscribe({
  next: (data) => {
    setConnectionState('connected')
    updatePrices(data)
  },
  error: (error) => {
    setConnectionState('error')
    console.error(error)
  }
})

// Show connection status in UI
if (connectionState === 'connecting') {
  return <div>Connecting to real-time feed...</div>
}

3. Debounce Rapid Updates

import { debounce } from 'lodash'

const debouncedUpdate = debounce((data) => {
  updateUI(data)
}, 100)

const subscription = client.subscribe({
  query: WATCH_PRICES
}).subscribe({
  next: (data) => {
    debouncedUpdate(data.tokenPricesUsd)
  }
})

4. Combine with Queries for Initial Data

// Fetch initial data with query
const { data: initialData } = await client.query({
  query: GET_TRADES,
  variables: { trader }
})

setTrades(initialData.perp.trades)

// Then subscribe to updates
const subscription = client.subscribe({
  query: WATCH_TRADES,
  variables: { trader }
}).subscribe({
  next: (data) => {
    setTrades(data.perpTrades)
  }
})

Next: Client Setup Guide | Previous | Back to Introduction

Last updated