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.