|
| 1 | +import { rpc as StellarRpc } from '@stellar/stellar-sdk'; |
| 2 | +import logger from '../config/logger'; |
| 3 | +import { sorobanRpcClient, stellarConfig } from '../config/stellar'; |
| 4 | + |
| 5 | +/** |
| 6 | + * Result returned by a successful connectivity check. |
| 7 | + */ |
| 8 | +export interface ConnectivityCheckResult { |
| 9 | + /** Whether the RPC node is reachable and healthy. */ |
| 10 | + connected: boolean; |
| 11 | + /** Human-readable network alias. */ |
| 12 | + network: string; |
| 13 | + /** Network passphrase used. */ |
| 14 | + networkPassphrase: string; |
| 15 | + /** RPC endpoint that was queried. */ |
| 16 | + rpcUrl: string; |
| 17 | + /** Health status string returned by the node (e.g. "healthy"). */ |
| 18 | + status: string; |
| 19 | + /** Latest ledger number at time of check. */ |
| 20 | + latestLedger: number; |
| 21 | + /** ISO timestamp of when the check was performed. */ |
| 22 | + checkedAt: string; |
| 23 | + /** Round-trip latency in milliseconds. */ |
| 24 | + latencyMs: number; |
| 25 | +} |
| 26 | + |
| 27 | +/** |
| 28 | + * Result returned when the connectivity check fails. |
| 29 | + */ |
| 30 | +export interface ConnectivityCheckError { |
| 31 | + connected: false; |
| 32 | + network: string; |
| 33 | + rpcUrl: string; |
| 34 | + checkedAt: string; |
| 35 | + error: string; |
| 36 | +} |
| 37 | + |
| 38 | +/** |
| 39 | + * SorobanService provides the business-logic layer for all Stellar / Soroban |
| 40 | + * RPC interactions. |
| 41 | + * |
| 42 | + * Responsibilities: |
| 43 | + * - Perform a live connectivity check against the configured RPC node. |
| 44 | + * - Surface health, network, and ledger data for API responses. |
| 45 | + * - Abstract the raw SDK client behind a typed interface so higher layers |
| 46 | + * (controllers, other services) are decoupled from the SDK. |
| 47 | + */ |
| 48 | +export class SorobanService { |
| 49 | + private readonly client: StellarRpc.Server; |
| 50 | + |
| 51 | + constructor(client: StellarRpc.Server = sorobanRpcClient) { |
| 52 | + this.client = client; |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Perform a connectivity check against the Soroban RPC node. |
| 57 | + * |
| 58 | + * Calls `getHealth()` and `getLatestLedger()` in parallel. Both must |
| 59 | + * succeed for the check to be considered healthy. |
| 60 | + * |
| 61 | + * @returns A `ConnectivityCheckResult` on success, or a |
| 62 | + * `ConnectivityCheckError` on failure. |
| 63 | + */ |
| 64 | + public async checkConnectivity(): Promise< |
| 65 | + ConnectivityCheckResult | ConnectivityCheckError |
| 66 | + > { |
| 67 | + const checkedAt = new Date().toISOString(); |
| 68 | + const start = Date.now(); |
| 69 | + |
| 70 | + logger.debug( |
| 71 | + `[Soroban] Connectivity check — network=${stellarConfig.network} url=${stellarConfig.rpcUrl}`, |
| 72 | + ); |
| 73 | + |
| 74 | + try { |
| 75 | + const [health, ledger] = await Promise.all([ |
| 76 | + this.client.getHealth(), |
| 77 | + this.client.getLatestLedger(), |
| 78 | + ]); |
| 79 | + |
| 80 | + const latencyMs = Date.now() - start; |
| 81 | + |
| 82 | + const result: ConnectivityCheckResult = { |
| 83 | + connected: true, |
| 84 | + network: stellarConfig.network, |
| 85 | + networkPassphrase: stellarConfig.networkPassphrase, |
| 86 | + rpcUrl: stellarConfig.rpcUrl, |
| 87 | + status: health.status, |
| 88 | + latestLedger: ledger.sequence, |
| 89 | + checkedAt, |
| 90 | + latencyMs, |
| 91 | + }; |
| 92 | + |
| 93 | + logger.info( |
| 94 | + `[Soroban] Connectivity OK — network=${stellarConfig.network} ` + |
| 95 | + `ledger=${ledger.sequence} latency=${latencyMs}ms`, |
| 96 | + ); |
| 97 | + |
| 98 | + return result; |
| 99 | + } catch (err) { |
| 100 | + const latencyMs = Date.now() - start; |
| 101 | + const message = err instanceof Error ? err.message : 'Unknown error'; |
| 102 | + |
| 103 | + logger.error( |
| 104 | + `[Soroban] Connectivity FAILED — network=${stellarConfig.network} ` + |
| 105 | + `latency=${latencyMs}ms error="${message}"`, |
| 106 | + ); |
| 107 | + |
| 108 | + const errorResult: ConnectivityCheckError = { |
| 109 | + connected: false, |
| 110 | + network: stellarConfig.network, |
| 111 | + rpcUrl: stellarConfig.rpcUrl, |
| 112 | + checkedAt, |
| 113 | + error: message, |
| 114 | + }; |
| 115 | + |
| 116 | + return errorResult; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Fetch the latest ledger sequence number from the RPC node. |
| 122 | + * |
| 123 | + * @returns The ledger sequence number. |
| 124 | + * @throws If the RPC call fails. |
| 125 | + */ |
| 126 | + public async getLatestLedger(): Promise<number> { |
| 127 | + const ledger = await this.client.getLatestLedger(); |
| 128 | + return ledger.sequence; |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Fetch network information (passphrase, protocol version) from the RPC node. |
| 133 | + * |
| 134 | + * @returns The raw `getNetwork` response from the SDK. |
| 135 | + */ |
| 136 | + public async getNetworkInfo(): Promise<StellarRpc.Api.GetNetworkResponse> { |
| 137 | + return this.client.getNetwork(); |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +/** Singleton instance for use across the application. */ |
| 142 | +export const sorobanService = new SorobanService(); |
0 commit comments