Pure WebSocket API

While developing our WebSocket service, we used Socket.IO and prepared an SDK for it. However, if you are going to use our service with a language other than TypeScript, you should find a Socket.IO interface for the language you will use, or you can learn how to connect to our service via standard WebSocket based on the TypeScript example below.

import TronSocket from './pure-ws'

const token = 'your-token-here'

const socket = new TronSocket(token)

socket
    .connect()
    .then(() => {
        console.log('Connected')
        socket.on('block', (block) => {
            console.log(
                'New block',
                {
                    hash: block.blockHash,
                    number: block.blockNumber
                }
            )
        })
    })
    .catch((error) => {
        console.error('Failed to connect', error)
    })
import WebSocket from 'ws'
import { EventFilters, EventTypes } from '@tronsocket/types'

type EmitError = {
    message: string
    code: number
    key?: string
}

type Events = keyof EventTypes
type Filters<E extends Events> = EventFilters[E]
type Callback<E extends Events> = (data: EventTypes[E]) => void

export default class TronSocket {
    private token: string

    private testnet: boolean

    private socket: WebSocket | null = null

    private socketUrl: string = 'wss://ws.tronsocket.com'

    private connectionUrl: string

    private listeners: { [key: string]: Callback<any>[] } = {}

    constructor(token: string, testnet?: boolean) {
        const api = new URL(this.socketUrl + '/socket.io/?EIO=4&transport=websocket')
        api.searchParams.set('testnet', testnet ? 'true' : 'false')
        api.searchParams.set('token', token)
        this.connectionUrl = api.toString()
        this.testnet = testnet || false
        this.token = token
    }

    private parseData(data: WebSocket.Data): { type: string; payload: any } {
        let message: string

        // Convert different data types to string
        if (typeof data === 'string') {
            message = data
        } else if (data instanceof ArrayBuffer) {
            message = new TextDecoder().decode(data)
        } else if (Array.isArray(data)) {
            message = Buffer.concat(data).toString('utf-8')
        } else if (data instanceof Buffer) {
            message = data.toString('utf-8')
        } else {
            return { type: 'error', payload: { message: 'Unsupported data type', code: 500 } }
        }

        // Socket.IO message format: <packet type>[JSON data]
        const match = message.match(/^(\d+)(.*)$/)

        if (!match) {
            return {
                type: 'error',
                payload: { message: 'Invalid message format', code: 500, rawData: message }
            }
        }

        const [, packetType, packetData] = match

        try {
            let parsedData: any

            // Parse the JSON data if it exists
            if (packetData) {
                parsedData = JSON.parse(packetData)
            }

            // Map Socket.IO packet types to our event types
            switch (packetType) {
                case '0': // Connect
                    return { type: 'connect', payload: parsedData }
                case '1': // Disconnect
                    return { type: 'disconnect', payload: parsedData }
                case '2': // Event
                    if (Array.isArray(parsedData) && parsedData.length > 0) {
                        return { type: parsedData[0], payload: parsedData.slice(1) }
                    }
                    return { type: 'event', payload: parsedData }
                case '3': // Ack
                    return { type: 'ack', payload: parsedData }
                case '4': // Error
                    return { type: 'error', payload: parsedData }
                case '5': // Binary Event
                    return { type: 'binary_event', payload: parsedData }
                case '6': // Binary Ack
                    return { type: 'binary_ack', payload: parsedData }
                default:
                    return { type: 'unknown', payload: { packetType, data: parsedData } }
            }
        } catch (error) {
            console.error('Failed to parse Socket.IO message:', error)
            return {
                type: 'error',
                payload: {
                    message: 'Failed to parse Socket.IO message',
                    code: 500,
                    rawData: message
                }
            }
        }
    }

    private createWsFormat(key: string, event: string, filter?: object): string {
        if (!filter) {
            return `42["${key}","${event}"]`
        } else {
            return `42["${key}","${event}",${JSON.stringify(filter)}]`
        }
    }

    private simpleHash(str: string): string {
        let hash = 0
        for (let i = 0; i < str?.length; i++) {
            const char = str.charCodeAt(i)
            hash = (hash << 5) - hash + char
            hash = hash & hash // Convert to 32-bit integer
        }
        return Math.abs(hash).toString(16)
    }

    private createEventKey<E extends Events>(
        event: E,
        callback: Callback<E>,
        filter?: Filters<E>
    ): string {
        const stringFilter = this.simpleHash(JSON.stringify(filter))
        const stringFunction = this.simpleHash(callback.toString())
        const definer = stringFilter + stringFunction
        return event + `-${definer}`
    }

    private startListener(): void {
        this.socket!.onmessage = (event) => {
            const data = this.parseData(event.data)
            if (data.payload?.packetType === '42') {
                if (data.payload.data[0] === 'error') {
                    this.listeners['error']?.forEach((callback) => {
                        callback(data.payload.data[1])
                    })
                } else {
                    const eventKey = data.payload.data[0]
                    const eventData = data.payload.data[1]
                    this.listeners[eventKey]?.forEach((callback) => {
                        callback(eventData)
                    })
                }
            }
        }
    }

    public async connect(): Promise<boolean> {
        return new Promise((resolve, reject) => {
            this.socket = new WebSocket(this.connectionUrl)

            this.socket.onmessage = (event) => {
                const data = this.parseData(event.data)
                if (data.type === 'error') {
                    reject(data.payload)
                } else {
                    if (data.payload?.packetType === '42') {
                        if (data.payload.data[0] === 'error') {
                            reject(data.payload.data[1])
                        } else if (data.payload.data[0] === 'ready') {
                            this.startListener()
                            resolve(true)
                        } else if (data.payload?.packetType === '41') {
                            this.socket?.close()
                        }
                    }
                }
            }

            this.socket.onopen = () => {
                this.socket?.send('40') // for socket.io connection
            }

            this.socket.onerror = (error) => {
                reject(error.message)
            }

            this.socket.onclose = (error) => {
                reject(error.reason || error.code)
            }
        })
    }

    public async disconnect(): Promise<boolean> {
        return new Promise((resolve, reject) => {
            if (this.socket) {
                this.socket.send('41') // for socket.io disconnection
                this.socket.onclose = () => {
                    this.socket = null
                    resolve(true)
                }
                this.socket.close()
            } else {
                reject('Socket not connected')
            }
        })
    }

    public subscribe<E extends Events>(event: E, callback: Callback<E>, filter?: Filters<E>): void {
        if (this.socket) {
            const eventKey = this.createEventKey(event, callback, filter)
            this.socket.send(this.createWsFormat('subscribe', eventKey, filter))
            this.listeners[eventKey] = this.listeners[eventKey] || []
            this.listeners[eventKey].push(callback)
        } else {
            throw new Error('Socket not connected')
        }
    }

    public on<E extends Events>(event: E, callback: Callback<E>, filter?: Filters<E>): void {
        this.subscribe(event, callback, filter)
    }

    public unsubscribe<E extends Events>(event: E, callback: Callback<E>, filter?: Filters<E>): void {
        if (this.socket) {
            const eventKey = this.createEventKey(event, callback, filter)
            this.socket.send(this.createWsFormat('unsubscribe', eventKey, filter))
            delete this.listeners[eventKey]
        } else {
            throw new Error('Socket not connected')
        }
    }

    public off<E extends Events>(event: E, callback: Callback<E>, filter?: Filters<E>): void {
        this.unsubscribe(event, callback, filter)
    }

    public error(callback: (error: EmitError) => void): void {
        if (this.socket) {
            this.listeners['error'] = this.listeners['error'] || []
            this.listeners['error'].push(callback)
        } else {
            throw new Error('Socket not connected')
        }
    }
}

Last updated