Skip to content

feat(websockets): add real-time payment notifications via Socket.IO (fix #430) - #441

Open
RedheadedProgrammer wants to merge 4 commits into
Abdulazeem-code:mainfrom
RedheadedProgrammer:fix/430-websockets
Open

feat(websockets): add real-time payment notifications via Socket.IO (fix #430)#441
RedheadedProgrammer wants to merge 4 commits into
Abdulazeem-code:mainfrom
RedheadedProgrammer:fix/430-websockets

Conversation

@RedheadedProgrammer

Copy link
Copy Markdown
Contributor

Closes #430

Short description:

  • Adds Socket.IO-based real-time payment notifications.
  • Introduces stellar-payment-platform/src/socketManager.js to manage socket rooms per Stellar address.
  • Initializes Socket.IO in server.js so API and WS share the same port.
  • Updates horizonListener.js to connect as a socket.io client and emit 'payment' events when incoming payments are detected.

How to test locally:

  1. cd stellar-payment-platform
  2. npm install
  3. npm start # starts the API server (default port 5000)
  4. In another terminal: npm run listener # starts horizonListener which connects to the API
  5. Connect a Socket.IO client to http://localhost:5000, then emit:
    socket.emit('authenticate', { address: '<TEST_STELLAR_ADDRESS>' })
    socket.on('payment', (data) => console.log('payment received', data))
  6. Trigger a payment on testnet for the watched address or simulate by emitting from the listener process; confirm the client receives the 'payment' event.

Files changed:

  • stellar-payment-platform/src/socketManager.js (new)
  • stellar-payment-platform/server.js (initialize Socket.IO)
  • stellar-payment-platform/horizonListener.js (connects as socket.io client and forwards payments)
  • stellar-payment-platform/package.json (added socket.io deps)

Notes:

  • No database or schema changes required.
  • This is a minimal, backwards-compatible integration: existing HTTP API remains unchanged.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@RedheadedProgrammer is attempting to deploy a commit to the Abdulazeem's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@RedheadedProgrammer Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Abdulazeem-code

Copy link
Copy Markdown
Owner

Fix backend build error

@Abdulazeem-code

Copy link
Copy Markdown
Owner

Fix backend build error

@daveades

Copy link
Copy Markdown
Contributor

The backend build is failing on one test, tests/socket.test.js. The other 349 pass.

The test disconnects the publisher on the line right after emit:

publisherSocket.emit('payment', { ... });
publisherSocket.disconnect();

Socket.IO opens on HTTP long-polling and upgrades to WebSocket a moment later, so that disconnect() closes the transport before the packet is flushed. The server never receives it, the subscriber never gets payment, done() is never called, and the test hits its 30s timeout. It fails about three runs in four, locally too.

The setTimeout(..., 200) waiting for the room join is a second race waiting to happen. Both go away if authenticate acknowledges, which is worth having anyway — right now a client can't tell when its subscription is live.

--- a/stellar-payment-platform/src/socketManager.js
+++ b/stellar-payment-platform/src/socketManager.js
@@ -18,17 +18,25 @@ function initSocketServer(httpServer, corsOptions = {}) {
   io.on('connection', (socket) => {
     logger.info(`Socket connected: ${socket.id}`);
 
-    socket.on('authenticate', (payload) => {
+    // The optional ack lets a client wait until it is actually subscribed.
+    // Without it a client that emits `authenticate` and immediately expects
+    // events can miss any payment that arrives before the room join lands.
+    socket.on('authenticate', async (payload, ack) => {
+      let subscribed = false;
       try {
         const address = payload && typeof payload.address === 'string' ? payload.address : null;
         if (address) {
-          socket.join(address);
+          await socket.join(address);
           socket.address = address;
+          subscribed = true;
           logger.info(`Socket ${socket.id} joined room for ${address}`);
         }
       } catch (err) {
         logger.error('Socket authenticate error', err);
       }
+      if (typeof ack === 'function') {
+        ack({ subscribed });
+      }
     });
 
     socket.on('payment', (payload) => {
--- a/stellar-payment-platform/tests/socket.test.js
+++ b/stellar-payment-platform/tests/socket.test.js
@@ -19,6 +19,7 @@ jest.mock('../src/logger', () => ({
 describe('Socket.IO real-time notification system', () => {
   let server;
   let clientSocket;
+  let publisherSocket;
   let port;
 
   beforeAll((done) => {
@@ -31,9 +32,6 @@ describe('Socket.IO real-time notification system', () => {
   });
 
   afterAll((done) => {
-    if (clientSocket && clientSocket.connected) {
-      clientSocket.disconnect();
-    }
     closeSocketServer();
     server.close(done);
   });
@@ -44,27 +42,54 @@ describe('Socket.IO real-time notification system', () => {
   });
 
   afterEach(() => {
-    clientSocket.disconnect();
+    for (const socket of [clientSocket, publisherSocket]) {
+      if (socket && socket.connected) socket.disconnect();
+    }
+    publisherSocket = null;
   });
 
   test('should authenticate and join the address room and receive payment events', (done) => {
     const testAddress = 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ';
-    clientSocket.emit('authenticate', { address: testAddress });
 
-    setTimeout(() => {
-      clientSocket.on('payment', (paymentData) => {
-        expect(paymentData).toEqual({ amount: '100', asset: 'XLM' });
-        done();
-      });
+    clientSocket.on('payment', (paymentData) => {
+      expect(paymentData).toEqual({ amount: '100', asset: 'XLM' });
+      done();
+    });
 
-      const publisherSocket = ioClient(`http://localhost:${port}`);
+    // The ack fires after the server has joined the room, so the publish below
+    // cannot outrun the subscription.
+    clientSocket.emit('authenticate', { address: testAddress }, () => {
+      publisherSocket = ioClient(`http://localhost:${port}`);
       publisherSocket.on('connect', () => {
         publisherSocket.emit('payment', {
           address: testAddress,
           payment: { amount: '100', asset: 'XLM' },
         });
-        publisherSocket.disconnect();
+        // Disconnecting here would close the transport before the packet is
+        // flushed and drop the event; afterEach tears the socket down instead.
       });
-    }, 200);
+    });
+  });
+
+  test('does not deliver payments for an address the client did not subscribe to', (done) => {
+    const subscribed = 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ';
+    const other = 'GBDQD3WTQ6W2VQ2W4V74UZ5WYF6B72GZ6EHD7I3L3WYH357Y4K5H3E4W';
+
+    clientSocket.on('payment', () => {
+      done(new Error('received a payment addressed to another account'));
+    });
+
+    clientSocket.emit('authenticate', { address: subscribed }, () => {
+      publisherSocket = ioClient(`http://localhost:${port}`);
+      publisherSocket.on('connect', () => {
+        publisherSocket.emit('payment', {
+          address: other,
+          payment: { amount: '100', asset: 'XLM' },
+        });
+        // Round-trip through the same socket: once this ack returns the server
+        // has already handled the payment above, so nothing is still in flight.
+        publisherSocket.emit('authenticate', { address: other }, () => done());
+      });
+    });
   });
 });

With these the suite is 351/351 across repeated runs. The ack argument is optional, so clients that call authenticate without a callback are unaffected.

@Abdulazeem-code

Copy link
Copy Markdown
Owner

Still the same thing

@Abdulazeem-code

Copy link
Copy Markdown
Owner

fix backend build error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backend: Add WebSockets for Real-Time Payment Notifications

3 participants