From 3024c5b2fc7ce6c6f7aa243551e02da4e19ff568 Mon Sep 17 00:00:00 2001
From: duriancrepe <94508990+duriancrepe@users.noreply.github.com>
Date: Wed, 9 Mar 2022 04:43:30 -0800
Subject: [PATCH] Add API functions to support trade chat (#75)
---
 src/HavenoDaemon.test.ts | 81 ++++++++++++++++++++++++++++++++++++++++
 src/HavenoDaemon.ts      | 39 ++++++++++++++++++-
 2 files changed, 118 insertions(+), 2 deletions(-)
diff --git a/src/HavenoDaemon.test.ts b/src/HavenoDaemon.test.ts
index 78d8318f..f5d3d362 100644
--- a/src/HavenoDaemon.test.ts
+++ b/src/HavenoDaemon.test.ts
@@ -879,6 +879,9 @@ test("Can complete a trade", async () => {
   // alice can get trade
   fetchedTrade = await alice.getTrade(trade.getTradeId());
   expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_PUBLISHED");
+
+  // test trader chat
+  await testTradeChat(trade.getTradeId(), alice, bob);
   
   // mine until deposit txs unlock
   HavenoUtils.log(1, "Mining to unlock deposit txs");
@@ -1779,4 +1782,82 @@ function testOffer(offer: OfferInfo, config?: any) {
     expect(offer.getSellerSecurityDeposit() / offer.getAmount()).toEqual(config.buyerSecurityDeposit); // TODO: use same config.securityDeposit for buyer and seller? 
   }
   // TODO: test rest of offer
+}
+
+/**
+ * Tests trade chat functionality. Must be called during an open trade.
+ */
+async function testTradeChat(tradeId: string, alice: HavenoDaemon, bob: HavenoDaemon) {
+  HavenoUtils.log(1, "Testing trade chat");
+
+  // invalid trade should throw error
+  try {
+    await alice.getChatMessages("invalid");
+    throw new Error("get chat messages with invalid id should fail");
+  } catch (err) {
+    assert.equal(err.message, "trade with id 'invalid' not found");
+  }
+
+  // trade chat should be in initial state
+  let messages = await alice.getChatMessages(tradeId);
+  assert(messages.length == 0);
+  messages = await bob.getChatMessages(tradeId);
+  assert(messages.length == 0);
+
+  // add notification handlers and send some messages
+  let aliceNotifications: NotificationMessage[] = [];
+  let bobNotifications: NotificationMessage[] = [];
+  await alice.addNotificationListener(notification => { aliceNotifications.push(notification); });
+  await bob.addNotificationListener(notification => { bobNotifications.push(notification); });
+
+  // send simple conversation and verify the list of messages
+  let aliceMsg = "Hi I'm Alice";
+  await alice.sendChatMessage(tradeId, aliceMsg);
+  await wait(TestConfig.maxTimePeerNoticeMs);
+  messages = await bob.getChatMessages(tradeId);
+  expect(messages.length).toEqual(2);
+  expect(messages[0].getIsSystemMessage()).toEqual(true); // first message is system
+  expect(messages[1].getMessage()).toEqual(aliceMsg);
+
+  let bobMsg = "Hello I'm Bob";
+  await bob.sendChatMessage(tradeId, bobMsg);
+  await wait(TestConfig.maxTimePeerNoticeMs);
+  messages = await alice.getChatMessages(tradeId);
+  expect(messages.length).toEqual(3);
+  expect(messages[0].getIsSystemMessage()).toEqual(true);
+  expect(messages[1].getMessage()).toEqual(aliceMsg);
+  expect(messages[2].getMessage()).toEqual(bobMsg);
+
+  // verify notifications
+  let chatNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE);
+  expect(chatNotifications.length).toBe(1);
+  expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(bobMsg);
+  chatNotifications = getNotifications(bobNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE);
+  expect(chatNotifications.length).toBe(1);
+  expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(aliceMsg);
+
+  // additional msgs
+  let msgs = ["", "  ", "", "さようなら"];
+  for(let msg of msgs) {
+    await alice.sendChatMessage(tradeId, msg);
+    await wait(1000); // the async operation can result in out of order messages
+  }
+  await wait(TestConfig.maxTimePeerNoticeMs);
+  messages = await bob.getChatMessages(tradeId);
+  let offset = 3; // 3 existing messages
+  expect(messages.length).toEqual(offset + msgs.length);
+  expect(messages[0].getIsSystemMessage()).toEqual(true);
+  expect(messages[1].getMessage()).toEqual(aliceMsg);
+  expect(messages[2].getMessage()).toEqual(bobMsg);
+  for (var i = 0; i < msgs.length; i++) {
+    expect(messages[i+offset].getMessage()).toEqual(msgs[i]);
+  }
+
+  chatNotifications = getNotifications(bobNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE);
+  offset = 1; // 1 existing notification
+  expect(chatNotifications.length).toBe(offset + msgs.length);
+  expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(aliceMsg);
+  for (var i = 0; i < msgs.length; i++) {
+    expect(chatNotifications[i + offset].getChatMessage()?.getMessage()).toEqual(msgs[i]);
+  }
 }
\ No newline at end of file
diff --git a/src/HavenoDaemon.ts b/src/HavenoDaemon.ts
index 22a527c9..d7de8230 100644
--- a/src/HavenoDaemon.ts
+++ b/src/HavenoDaemon.ts
@@ -2,8 +2,8 @@ import {HavenoUtils} from "./utils/HavenoUtils";
 import {TaskLooper} from "./utils/TaskLooper";
 import * as grpcWeb from 'grpc-web';
 import {GetVersionClient, AccountClient, MoneroConnectionsClient, DisputesClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient} from './protobuf/GrpcServiceClientPb';
-import {GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest} from './protobuf/grpc_pb';
-import {PaymentMethod, PaymentAccount, AvailabilityResult, Attachment, DisputeResult, Dispute} from './protobuf/pb_pb';
+import {GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply} from './protobuf/grpc_pb';
+import {PaymentMethod, PaymentAccount, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage} from './protobuf/pb_pb';
 
 const console = require('console');
 
@@ -1026,6 +1026,41 @@ class HavenoDaemon {
       });
     });
   }
+
+  /**
+   * Get all chat messages for a trade.
+   *
+   * @param {string} tradeId - the id of the trade
+   */
+  async getChatMessages(tradeId: string): Promise {
+    let that = this;
+    return new Promise(function(resolve, reject) {
+      let request = new GetChatMessagesRequest().setTradeId(tradeId);
+      that._tradesClient.getChatMessages(request, {password: that._password}, function(err: grpcWeb.RpcError, response: GetChatMessagesReply) {
+        if (err) reject(err);
+        else resolve(response.getMessageList());
+      });
+    });
+  }
+
+  /**
+   * Send a trade chat message.
+   *
+   * @param {string} tradeId - the id of the trade
+   * @param {string} message - the message
+   */
+  async sendChatMessage(tradeId: string, message: string): Promise {
+    let that = this;
+    return new Promise(function(resolve, reject) {
+      let request = new SendChatMessageRequest()
+            .setTradeId(tradeId)
+            .setMessage(message);
+      that._tradesClient.sendChatMessage(request, {password: that._password}, function(err: grpcWeb.RpcError) {
+        if (err) reject(err);
+        else resolve();
+      });
+    });
+  }
   
   /**
    * Get a dispute by trade id.