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.