gRPC Example

Below is an example of how to connect to a gRPC endpoint.

You can use this to confirm that you can connect to your node properly and start building your application.

Running the demo

  1. Save all files into a directory
  2. Set your dedicated node's gRPC endpoint and x-token in the .env file.
  3. Install required packages with npm install
  4. Run the script with npm run yellowstone.

You will see output from the script similar to the screenshot below



import YellowstoneGrpc, { CommitmentLevel } from '@triton-one/yellowstone-grpc';
const Client = YellowstoneGrpc.default || YellowstoneGrpc;
import dotenv from 'dotenv';

dotenv.config();

// gRPC Configuration
// Please set GRPC_ENDPOINT environment variable with your gRPC endpoint
const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT || '';
// Please set GRPC_XTOKEN environment variable with your gRPC authentication token if required
const GRPC_XTOKEN = process.env.GRPC_XTOKEN || '';

class SolanaYellowstoneClient {
  constructor() {
    this.client = null;
    this.stream = null;
    this.pingInterval = null;
    this.pingId = 0;
  }

  async initialize() {
    console.log('🚀 Initializing Solana Yellowstone gRPC Client...\n');
    console.log(`📡 Endpoint: ${GRPC_ENDPOINT}`);
    console.log(`🔑 X-Token: ${GRPC_XTOKEN.substring(0, 10)}...`);
    console.log('');

    try {
      // Create the Yellowstone client with configuration
      this.client = new Client(GRPC_ENDPOINT, GRPC_XTOKEN, {
        "grpc.max_receive_message_length": 64 * 1024 * 1024, // 64MiB
        "grpc.max_send_message_length": 64 * 1024 * 1024 // 64MiB
      });
      
      console.log('✅ Yellowstone gRPC client initialized successfully\n');
      
    } catch (error) {
      console.error('❌ Failed to initialize Yellowstone gRPC client:', error.message);
      throw error;
    }
  }

  async getVersion() {
    console.log('📋 Getting server version...');
    
    try {
      const version = await this.client.getVersion();
      console.log('✅ Server version:', version);
      return version;
    } catch (error) {
      console.error('❌ Failed to get version:', error.message);
      throw error;
    }
  }

  async startSubscription() {
    console.log('📊 Starting subscription stream via Yellowstone gRPC...\n');
    
    // Example accounts to monitor (System Program and Token Program)
    const accountsToMonitor = [
      '11111111111111111111111111111111',
      'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
    ];

    // Create subscription request for slots only (simplified to start)
    const request = {
      slots: {
        'client': {}
      },
      commitment: CommitmentLevel.CONFIRMED
    };

    console.log('📤 Setting up subscription with:');
    console.log(`  - Slots: monitoring all slots`);
    console.log(`  - Accounts: ${accountsToMonitor.join(', ')}`);
    console.log(`  - Transactions: non-vote, successful only`);
    console.log(`  - Commitment: CONFIRMED`);
    console.log('');

    try {
      // Use subscribeOnce with explicit parameters
      this.stream = await this.client.subscribeOnce(
        {}, // accounts
        { 'client': {} }, // slots
        {}, // transactions
        {}, // transactionsStatus
        {}, // entry
        {}, // blocks
        {}, // blocksMeta
        CommitmentLevel.CONFIRMED, // commitment
        [] // accountsDataSlice
      );

      // Set up event handlers
      this.stream.on('data', (data) => {
        this.handleStreamData(data);
      });

      this.stream.on('error', (error) => {
        console.error('❌ Yellowstone stream error:', error.message);
        if (error.message.includes('Unauthenticated') || error.message.includes('401')) {
          console.error('Authentication failed. Check your X-Token.');
        } else if (error.message.includes('Internal')) {
          console.error('Internal error details:', error);
        } else if (error.message.includes('Unavailable')) {
          console.error('Service unavailable. Check endpoint and network connection.');
        }
      });

      this.stream.on('end', () => {
        console.log('⚠️  Stream ended by server');
        this.stopPingInterval();
      });

      this.stream.on('close', () => {
        console.log('⚠️  Stream closed');
        this.stopPingInterval();
      });

      this.stream.on('status', (status) => {
        console.log('Stream status update:', status);
      });

      console.log('✅ Subscription request sent\n');
      console.log('👀 Listening for events... (Press Ctrl+C to stop)\n');
      
    } catch (error) {
      console.error('❌ Failed to start subscription:', error);
      throw error;
    }
  }

  handleStreamData(data) {
    // Get the filter label if available
    const filterLabel = data.filters && data.filters.length > 0 ? data.filters[0] : '';

    if (data.slot) {
      console.log(`[Slot Update${filterLabel ? ' - ' + filterLabel : ''}]`);
      console.log(`  Slot: ${data.slot.slot}`);
      console.log(`  Parent: ${data.slot.parent || 'N/A'}`);
      console.log(`  Status: ${this.getCommitmentName(data.slot.status)}`);
      console.log('');
    }
    
    if (data.account) {
      const account = data.account;
      console.log(`[Account Update${filterLabel ? ' - ' + filterLabel : ''}]`);
      console.log(`  Pubkey: ${account.account.pubkey}`);
      console.log(`  Lamports: ${account.account.lamports}`);
      console.log(`  Owner: ${account.account.owner}`);
      console.log(`  Slot: ${account.slot}`);
      console.log(`  Executable: ${account.account.executable}`);
      console.log(`  Data Length: ${account.account.data ? account.account.data.length : 0} bytes`);
      console.log('');
    }
    
    if (data.transaction) {
      const tx = data.transaction;
      console.log(`[Transaction${filterLabel ? ' - ' + filterLabel : ''}]`);
      console.log(`  Signature: ${tx.transaction.signature}`);
      console.log(`  Slot: ${tx.slot}`);
      console.log(`  Is Vote: ${tx.transaction.isVote}`);
      if (tx.transaction.meta) {
        console.log(`  Success: ${!tx.transaction.meta.err}`);
        console.log(`  Fee: ${tx.transaction.meta.fee} lamports`);
        const logCount = tx.transaction.meta.logMessages ? tx.transaction.meta.logMessages.length : 0;
        console.log(`  Log Messages: ${logCount}`);
      }
      console.log('');
    }
    
    if (data.block) {
      const block = data.block;
      console.log(`[Block${filterLabel ? ' - ' + filterLabel : ''}]`);
      console.log(`  Slot: ${block.slot}`);
      console.log(`  Blockhash: ${block.blockhash}`);
      console.log(`  Parent Slot: ${block.parentSlot}`);
      console.log(`  Transactions Count: ${block.transactions ? block.transactions.length : 0}`);
      console.log('');
    }

    if (data.blockMeta) {
      const blockMeta = data.blockMeta;
      console.log(`[Block Meta${filterLabel ? ' - ' + filterLabel : ''}]`);
      console.log(`  Slot: ${blockMeta.slot}`);
      console.log(`  Blockhash: ${blockMeta.blockhash}`);
      console.log(`  Parent Slot: ${blockMeta.parentSlot}`);
      console.log(`  Executed Transactions: ${blockMeta.executedTransactionCount}`);
      console.log('');
    }

    if (data.ping) {
      console.log(`[Ping] Keepalive ping sent`);
    }

    if (data.pong) {
      console.log(`[Pong] Keepalive response received (id: ${data.pong.id})`);
    }
  }

  startPingInterval() {
    // Send ping every 30 seconds to keep connection alive
    this.pingInterval = setInterval(() => {
      if (this.stream && !this.stream.destroyed) {
        const pingRequest = {
          ping: {
            id: this.pingId++
          }
        };
        
        try {
          this.stream.write(pingRequest, (err) => {
            if (!err) {
              console.log(`[Keepalive] Ping sent (id: ${this.pingId - 1})`);
            } else {
              console.error('Failed to send ping:', err.message);
            }
          });
        } catch (error) {
          console.error('Failed to send ping:', error.message);
        }
      }
    }, 30000); // 30 seconds
  }

  stopPingInterval() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }

  getCommitmentName(value) {
    // Handle string values and numeric values
    if (typeof value === 'string') {
      return value.toUpperCase();
    }
    
    const commitments = {
      0: 'PROCESSED',
      1: 'CONFIRMED', 
      2: 'FINALIZED'
    };
    return commitments[value] || `UNKNOWN(${value})`;
  }

  async shutdown() {
    console.log('\n🛑 Shutting down Yellowstone gRPC client...');
    
    this.stopPingInterval();
    
    if (this.stream) {
      try {
        this.stream.end();
        this.stream.destroy();
      } catch (error) {
        console.error('Error closing stream:', error.message);
      }
    }
    
    // Yellowstone client doesn't need explicit closure like raw gRPC
    console.log('✅ Yellowstone gRPC client shutdown complete');
  }
}

// Main execution
async function main() {
  const client = new SolanaYellowstoneClient();
  
  try {
    await client.initialize();
    
    // Test connection with version check
    await client.getVersion();
    
    console.log('\n' + '='.repeat(60));
    console.log('Starting Yellowstone gRPC subscription stream...');
    console.log('='.repeat(60) + '\n');
    
    // Start the subscription stream
    await client.startSubscription();
    
    // Start keepalive ping
    client.startPingInterval();
    
    // Handle graceful shutdown
    process.on('SIGINT', async () => {
      await client.shutdown();
      process.exit(0);
    });
    
    // Keep process running
    await new Promise(() => {});
    
  } catch (error) {
    console.error('Fatal error:', error);
    process.exit(1);
  }
}

main();
{
  "name": "solana-rpc-demo",
  "version": "1.0.0",
  "type": "module",
  "description": "Demo script for connecting to Solana RPC",
  "main": "yellowstone-grpc-demo.js",
  "scripts": {
    "yellowstone": "node yellowstone-grpc-demo.js",
  },
  "dependencies": {
    "@grpc/grpc-js": "^1.9.14",
    "@grpc/proto-loader": "^0.7.10",
    "@solana/web3.js": "^1.98.4",
    "@triton-one/yellowstone-grpc": "^0.6.0",
    "@witches-brew/spore": "0.1.0",
    "dotenv": "^17.2.1",
    "node-fetch": "^3.3.2"
  },
  "devDependencies": {
    "bs58": "^6.0.0"
  }
}
# These are available in your developer dashboard at https://www.hellomoon.io/dashboard
GRPC_ENDPOINT=<your node>
GRPC_XTOKEN=<your x-token>