diff --git a/src/customer-server-client.test.ts b/src/customer-server-client.test.ts index 653893d..d770a8a 100644 --- a/src/customer-server-client.test.ts +++ b/src/customer-server-client.test.ts @@ -42,7 +42,7 @@ describe('Customer server client', () => { //@ts-ignore jest.spyOn(customerServerApi, 'messagesStatus').mockImplementation(() => { return { - messages: [], + statuses: [], }; }); @@ -58,4 +58,43 @@ describe('Customer server client', () => { await jest.advanceTimersByTimeAsync(2000); expect(customerServerApi.messagesStatus).toHaveBeenCalledTimes(2); }); + + it(`got same unexpected status from customer server in response to messagesStatus`, async () => { + const requestId = c.guid(); + const requestType = 'KEY_LINK_PROOF_OF_OWNERSHIP_REQUEST' + const responseType = 'KEY_LINK_PROOF_OF_OWNERSHIP_RESPONSE' + const aTxToSignMessage = messageBuilder.aMessagePayload(requestType, { requestId }); + const fbMessage = messageBuilder.fbMessage(aTxToSignMessage); + const msgEnvelop = messageBuilder.aMessageEnvelope(requestId, requestType, fbMessage.payload); + const messageStatus: MessageStatus = { + type: responseType, + status: 'SIGNED', + requestId, + response: {}, + }; + const messageStatus2: MessageStatus = { + type: responseType, + status: 'SIGNED', + requestId: c.guid(), + response: {}, + }; + const messageStatus3: MessageStatus = { + type: responseType, + status: 'INVALID' as any, + requestId: c.guid(), + response: {}, + }; + jest.spyOn(messagesService, 'getPendingMessages').mockReturnValue([{ messageStatus, msgId: c.natural(), request: msgEnvelop }]); + jest.spyOn(messagesService, 'updateStatus').mockResolvedValue(); + // @ts-ignore + jest.spyOn(customerServerApi, 'messagesStatus').mockImplementation(() => { + return { + statuses: [messageStatus2, messageStatus3, messageStatus], + }; + }); + + + await service.pullMessagesStatus(); + expect(messagesService.updateStatus).toHaveBeenCalledWith([{ msgId: expect.any(Number), request: msgEnvelop, messageStatus }]); + }); }); diff --git a/src/customer-server-client.ts b/src/customer-server-client.ts index 0bf3baf..b21a633 100644 --- a/src/customer-server-client.ts +++ b/src/customer-server-client.ts @@ -8,20 +8,24 @@ class CustomerClient { try { const messages = messagesService.getPendingMessages() ?? []; const requestsIds = messages.map((msg) => msg.messageStatus.requestId); - logger.info(`Pulling messages status for ${JSON.stringify(requestsIds)} from customer server`); + logger.info(`Pulling messages status for ${JSON.stringify(requestsIds)}`); const { statuses: serverStatuses } = await customerServerApi.messagesStatus({ requestsIds }); logger.info(`Got ${messages.length} statuses from Customer server`); if (!!serverStatuses.length) { - logger.info(`Got messages status for ${JSON.stringify(serverStatuses.map((status) => { return { requestId: status.requestId, status: status.status } }))}`); + logger.info(`Got from customer server messages status for ${JSON.stringify(serverStatuses.map((status) => { return { requestId: status.requestId, status: status.status } }))}`); await messagesService.updateStatus(serverStatuses.map((messagesStatus): ExtendedMessageStatusCache => { const decodedMsg = messages.find((msg) => msg.messageStatus.requestId === messagesStatus.requestId); + if (!decodedMsg) { + logger.error(`Message with requestId ${messagesStatus.requestId} not in pending cache messages`); + return null; + } return { msgId: decodedMsg.msgId, request: decodedMsg.request, messageStatus: messagesStatus, }; - })); + }).filter((msg) => msg !== null)); } } catch (e) { logger.error(`Got error from customer server: "${e.message}"`); diff --git a/src/services/fireblocks-agent.ts b/src/services/fireblocks-agent.ts index 8f168da..7dc18ed 100644 --- a/src/services/fireblocks-agent.ts +++ b/src/services/fireblocks-agent.ts @@ -42,7 +42,7 @@ class FireblocksAgentImpl implements FireblocksAgent { _runLoopStep = async () => { const start = Date.now(); - logger.info(`Waiting for a message from Fireblocks`); + logger.info(`Waiting for messages from Fireblocks...`); const messages = await fbServerApi.getMessages(); logger.info(`Got ${messages.length} messages from Fireblocks after ${Date.now() - start}ms`); await messageService.handleMessages(messages); diff --git a/src/services/messages.service.test.ts b/src/services/messages.service.test.ts index f902528..ef13d6c 100644 --- a/src/services/messages.service.test.ts +++ b/src/services/messages.service.test.ts @@ -214,4 +214,49 @@ describe('messages service', () => { expect(customerServerApi.messagesToSign).toHaveBeenCalledTimes(1); expect(fbServerApi.ackMessage).toHaveBeenCalledWith(msgId3); }); + + it('got same unexpected status from customer server in response to messagesToSign', async () => { + const requestId = c.guid(); + const msgId = c.natural(); + const requestType = 'KEY_LINK_PROOF_OF_OWNERSHIP_REQUEST'; + const responseType = 'KEY_LINK_PROOF_OF_OWNERSHIP_RESPONSE'; + const aTxToSignMessage = messageBuilder.aMessagePayload(requestType, { requestId }); + const fbMessage = messageBuilder.fbMessage(aTxToSignMessage); + const fbMessageEnvelope = messageBuilder.fbProofOfOwnershipMsgEnvelope({}, fbMessage); + const msgEnvelop = messageBuilder.aMessageEnvelope(requestId, requestType, fbMessage.payload); + + jest.spyOn(messagesUtils, 'decodeAndVerifyMessage').mockReturnValue({ request: msgEnvelop, msgId }); + + const msgStatus: MessageStatus = { + type: responseType, + status: 'SIGNED', + requestId, + response: {}, + }; + + const msgStatus2: MessageStatus = { + type: responseType, + status: 'SIGNED', + requestId: c.guid(), + response: {}, + }; + + const msgStatus3: MessageStatus = { + type: responseType, + status: 'INVALID' as any, + requestId: c.guid(), + response: {}, + }; + + jest.spyOn(customerServerApi, 'messagesToSign').mockResolvedValue([msgStatus2, msgStatus3, msgStatus]); + jest.spyOn(fbServerApi, 'broadcastResponse').mockImplementation(jest.fn(() => Promise.resolve())); + jest.spyOn(fbServerApi, 'ackMessage').mockImplementation(jest.fn(() => Promise.resolve())); + + await service.handleMessages([fbMessageEnvelope]); + + // Verify the agent ignores a message it didn't asked for + expect(fbServerApi.ackMessage).toHaveBeenCalledWith(msgId); + expect(fbServerApi.broadcastResponse).toHaveBeenCalledWith(msgStatus, msgEnvelop); + }); + }); diff --git a/src/services/messages.service.ts b/src/services/messages.service.ts index 60cb231..d846bbc 100644 --- a/src/services/messages.service.ts +++ b/src/services/messages.service.ts @@ -27,7 +27,7 @@ class MessageService implements IMessageService { try { const { msgId, request } = decodeAndVerifyMessage(messageEnvelope, certificates); const { transportMetadata } = request; - logger.info(`Got message id ${msgId} with type ${transportMetadata.type} and request id ${transportMetadata.requestId}`); + logger.info(`Got from Fireblocks message id ${msgId} with type ${transportMetadata.type} and request id ${transportMetadata.requestId}`); return { msgId, request }; } catch (e) { logger.error(`Error decoding message ${e.message}`); @@ -51,7 +51,7 @@ class MessageService implements IMessageService { }); if (!!cachedMessages.length) { - cachedMessages.forEach((msg) => logger.info(`Got cached message id ${msg.msgId} request id ${msg.request.transportMetadata.requestId}`)); + cachedMessages.forEach((msg) => logger.info(`Found cached message id ${msg.msgId} request id ${msg.request.transportMetadata.requestId}`)); const cachedMsgsStatus = cachedMessages.map((msg): ExtendedMessageStatusCache => { return { msgId: msg.msgId, @@ -66,19 +66,24 @@ class MessageService implements IMessageService { if (!!messagesToHandle.length) { const msgStatuses = await customerServerApi.messagesToSign(messagesToHandle.map((msg) => msg.request)); - logger.info(`Got messages status for ${JSON.stringify(msgStatuses.map((status) => { return { requestId: status.requestId, status: status.status } }))}`); + logger.info(`Got from customer server messages status for ${JSON.stringify(msgStatuses.map((status) => { return { requestId: status.requestId, status: status.status } }))}`); await this.updateStatus(msgStatuses.map((messageStatus): ExtendedMessageStatusCache => { const decodedMessage = messagesToHandle.find((msg) => msg.request.transportMetadata.requestId === messageStatus.requestId); + if (!decodedMessage) { + logger.error(`Message with requestId ${messageStatus.requestId} wasn't expected`); + return null; + } + return { msgId: decodedMessage.msgId, request: decodedMessage.request, messageStatus, }; - })); + }).filter((msg) => msg !== null)); } if (!!unknownMessages.length) { - unknownMessages.forEach((msg) => logger.error(`Got unknown message type ${msg.request.transportMetadata.type} and id ${msg.msgId}`)); + unknownMessages.forEach((msg) => logger.error(`Got from Fireblocks unknown message type ${msg.request.transportMetadata.type} and id ${msg.msgId}`)); await this.ackMessages(unknownMessages.map((msg) => msg.msgId)); } } @@ -105,14 +110,13 @@ class MessageService implements IMessageService { } if (status === 'SIGNED' || status === 'FAILED') { - logger.info(`Got message from customer server with status: ${status}, msgId ${msgId}, cacheId: ${requestId}`); - this.msgCache[messageStatus.requestId].messageStatus = messageStatus; - + logger.info(`Got ${isInCache ? "cached" : "from customer server"} message with final status: ${status}, msgId ${msgId}, cacheId: ${requestId}`); await fbServerApi.broadcastResponse(messageStatus, request); await fbServerApi.ackMessage(msgId); + this.msgCache[messageStatus.requestId].messageStatus = messageStatus; } } catch (e) { - throw new Error(`Error updating status to fireblocks ${e.message} for message ${msgStatus.msgId} and status ${msgStatus.messageStatus}`); + throw new Error(`Error updating status for message ${msgStatus.msgId} and status ${JSON.stringify(msgStatus.messageStatus)}. Error: ${e.message}`); } } }