Skip to content

Fix ProtobufJS Mapping for Spot Account Orders #50

@tienryo-2

Description

@tienryo-2

I’m integrating MEXC’s WebSocket stream for Spot Account Orders in a NestJS project. I'm using protobufjs to decode messages from the spot@private.orders.v3.api.pb topic.

Below is the relevant NestJS code I’m using to connect and decode messages:

import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { MexcService } from 'src/services/mexc.service';
import * as WebSocket from 'ws';
import * as protobuf from 'protobufjs';

let PrivateOrdersV3Api: protobuf.Type;

protobuf.load('proto/PrivateOrdersV3Api.proto').then((root) => {
    PrivateOrdersV3Api = root.lookupType('PrivateOrdersV3Api');
});

@Injectable()
export class StreamService implements OnModuleInit, OnModuleDestroy {
    private readonly logger = new Logger(StreamService.name);
    private listenKey: string;
    private ws: WebSocket;
    private renewInterval: NodeJS.Timeout;

    constructor(
        private readonly mexcService: MexcService,
    ) {
        this.mexcService = mexcService
    }

    async onModuleInit() {
        this.logger.log('Initializing MEXC WebSocket...');
        await this.connectWebSocket();
    }

    async onModuleDestroy() {
        this.logger.warn('Cleaning up MEXC WebSocket...');
        if (this.ws) this.ws.terminate();
        clearInterval(this.renewInterval);
        await this.mexcService.closeListenKey()
    }

    private async connectWebSocket() {
        try {
            const { listenKey } = await this.mexcService.createListenKey();
            this.listenKey = listenKey
            this.logger.log(`🔑 listenKey: ${this.listenKey}`);

            this.ws = new WebSocket(`wss://wbs-api.mexc.com/ws?listenKey=${this.listenKey}`);

            this.ws.on('open', async () => {
                this.logger.log('✅ WebSocket connected');

                const payload = {
                    method: 'SUBSCRIPTION',
                    "params": [
                        "spot@private.orders.v3.api.pb"
                    ]
                };
                this.ws.send(JSON.stringify(payload));

                await this.mexcService.keepListenKey()
            });

            this.ws.on('message', (data) => {
                try {
                    const decode = PrivateOrdersV3Api.decode(data);
                    const decodeObj = PrivateOrdersV3Api.toObject(decode);

                    console.log('🌐 Decode:',decode,  decodeObj);
                } catch (err) {
                    console.error(data)
                    this.logger.error('❌ Message error:', err.message);
                }
            });

            this.ws.on('error', (err) => {
                this.logger.error('❌ WebSocket error:', err.message);
                this.reconnect();
            });

            this.ws.on('close', () => {
                this.logger.warn('🔌 WebSocket closed');
                this.reconnect();
            });


            // Renew listenKey every 30 minutes
            clearInterval(this.renewInterval);
            this.renewInterval = setInterval(() => this.mexcService.keepListenKey(), 1000 * 60 * 60);
        } catch (err) {
            this.logger.error('❌ Failed to connect WebSocket', err.message);
            setTimeout(() => this.connectWebSocket(), 5000);
        }
    }

    private async reconnect() {
        this.logger.warn('🔁 Reconnecting WebSocket...');
        if (this.ws) {
            this.ws.removeAllListeners();
            this.ws.terminate();
            this.ws = null;
        }
        clearInterval(this.renewInterval);
        await this.mexcService.closeListenKey()
        await this.connectWebSocket();
    }
}

The .proto file I'm using is named PrivateOrdersV3Api.proto and contains the following definition:

// spot@private.orders.v3.api.pb

syntax = "proto3";

option java_package = "com.mxc.push.common.protobuf";
option optimize_for = SPEED;
option java_multiple_files = true;
option java_outer_classname = "PrivateOrdersV3ApiProto";

message PrivateOrdersV3Api {

  string id = 1;
  string clientId = 2;

  string price = 3;
  string quantity = 4;
  string amount = 5;
  string avgPrice = 6;

  int32 orderType = 7;
  int32 tradeType = 8;
  bool isMaker = 9;

  string remainAmount = 10;
  string remainQuantity= 11;
  optional string lastDealQuantity = 12;
  string cumulativeQuantity = 13;
  string cumulativeAmount = 14;

  int32 status = 15;
  int64 createTime = 16;

  optional string market = 17;
  optional int32 triggerType = 18;
  optional string triggerPrice= 19;
  optional int32 state = 20;

  optional string ocoId = 21;
  optional string routeFactor = 22;

  optional string symbolId = 23;
  optional string marketId = 24;

  optional string marketCurrencyId = 25;
  optional string currencyId = 26;
}

❗️Issues Encountered

Image

WebSocket connection closes after a few minutes :
→ Possibly due to listenKey expiration or incorrect keep-alive handling.

Protobuf decoding returns invalid or malformed data
Example decoded output:

🌐 Decode: PrivateOrdersV3Api {
  status: 34,
  cumulativeQuantity: '":0,"code":0,"msg":"spot@private.orders.v3.api.pb"}'
} {
  cumulativeQuantity: '":0,"code":0,"msg":"spot@private.orders.v3.api.pb"}',
  status: 34
}

→ It seems the incoming message is not a valid protobuf buffer, or perhaps there is an outer wrapper message structure that I’m missing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions