diff --git a/src/core/agent/agent.test.ts b/src/core/agent/agent.test.ts index 996d28c76..e04090494 100644 --- a/src/core/agent/agent.test.ts +++ b/src/core/agent/agent.test.ts @@ -46,10 +46,13 @@ const mockCredentialService = { syncKeriaCredentials: jest.fn(), removeCredentialsPendingDeletion: jest.fn(), }; +const mockKeriaNotificationService = { + syncIPEXReplyOperations: jest.fn(), +}; const mockEntropy = "00000000000000000000000000000000"; -describe("test cases of bootAndConnect function", () => { +describe("KERIA connectivity", () => { let agent: Agent; let mockAgentUrls: AgentUrls; let mockSignifyClient: any; @@ -238,7 +241,7 @@ describe("test cases of bootAndConnect function", () => { }); }); -describe("test cases of recoverKeriaAgent function", () => { +describe("Recovery of DB from cloud sync", () => { let agent: Agent; let mockSeedPhrase: string[]; let mockConnectUrl: string; @@ -254,6 +257,7 @@ describe("test cases of recoverKeriaAgent function", () => { (agent as any).basicStorageService = mockBasicStorageService; (agent as any).agentServicesProps = mockAgentServicesProps; (agent as any).connectionService = mockConnectionService; + (agent as any).keriaNotificationService = mockKeriaNotificationService; mockSeedPhrase = [ "abandon", @@ -306,6 +310,7 @@ describe("test cases of recoverKeriaAgent function", () => { expect(mockConnectionService.syncKeriaContacts).toHaveBeenCalled(); expect(mockIdentifierService.syncKeriaIdentifiers).toHaveBeenCalled(); expect(mockCredentialService.syncKeriaCredentials).toHaveBeenCalled(); + expect(mockKeriaNotificationService.syncIPEXReplyOperations).toHaveBeenCalled(); expect(mockSignifyClient.connect).toHaveBeenCalled(); expect(mockBasicStorageService.createOrUpdateBasicRecord).toHaveBeenCalledWith({ _tags: {}, @@ -319,13 +324,6 @@ describe("test cases of recoverKeriaAgent function", () => { KeyStoreKeys.SIGNIFY_BRAN, expectedBran ); - expect(Agent.isOnline).toBe(true); - expect(mockAgentServicesProps.eventEmitter.emit).toBeCalledWith({ - type: EventTypes.KeriaStatusChanged, - payload: { - isOnline: true, - }, - }); }); test("should throw an error for invalid mnemonic", async () => { diff --git a/src/core/agent/agent.ts b/src/core/agent/agent.ts index 29e269819..adca18f74 100644 --- a/src/core/agent/agent.ts +++ b/src/core/agent/agent.ts @@ -314,6 +314,7 @@ class Agent { await this.connections.syncKeriaContacts(); await this.identifiers.syncKeriaIdentifiers(); await this.credentials.syncKeriaCredentials(); + await this.keriaNotifications.syncIPEXReplyOperations(); await this.basicStorage.createOrUpdateBasicRecord( new BasicRecord({ @@ -321,8 +322,6 @@ class Agent { content: { syncing: false }, }) ); - - this.markAgentStatus(true); } private async connectSignifyClient(): Promise { @@ -349,6 +348,7 @@ class Agent { }); } + // For now this is called by UI/AppWrapper to not prematurely mark online while mid-recovery markAgentStatus(online: boolean) { Agent.isOnline = online; diff --git a/src/core/agent/event.types.ts b/src/core/agent/event.types.ts index 3c08218b7..5188aa9a6 100644 --- a/src/core/agent/event.types.ts +++ b/src/core/agent/event.types.ts @@ -29,7 +29,7 @@ enum EventTypes { interface NotificationAddedEvent extends BaseEventEmitter { type: typeof EventTypes.NotificationAdded; payload: { - keriaNotif: KeriaNotification; + note: KeriaNotification; }; } diff --git a/src/core/agent/records/notificationRecord.ts b/src/core/agent/records/notificationRecord.ts index a855674ea..686c20116 100644 --- a/src/core/agent/records/notificationRecord.ts +++ b/src/core/agent/records/notificationRecord.ts @@ -1,6 +1,6 @@ import { BaseRecord, Tags } from "../../storage/storage.types"; import { NotificationRoute } from "../agent.types"; -import { LinkedGroupRequest } from "./notificationRecord.types"; +import { LinkedRequest } from "./notificationRecord.types"; import { randomSalt } from "../services/utils"; interface NotificationRecordStorageProps { @@ -13,7 +13,7 @@ interface NotificationRecordStorageProps { multisigId?: string; connectionId: string; credentialId?: string; - linkedGroupRequest?: LinkedGroupRequest; + linkedRequest?: LinkedRequest; groupReplied?: boolean, initiatorAid?: string, groupInitiator?: boolean, @@ -25,11 +25,12 @@ class NotificationRecord extends BaseRecord { read!: boolean; multisigId?: string; connectionId!: string; - linkedGroupRequest!: LinkedGroupRequest; + linkedRequest!: LinkedRequest; credentialId?: string; groupReplied?: boolean; initiatorAid?: string; groupInitiator?: boolean; + hidden = false; // Hide from UI but don't delete (used for reliability while recovering IPEX long running operations) static readonly type = "NotificationRecord"; readonly type = NotificationRecord.type; @@ -45,7 +46,7 @@ class NotificationRecord extends BaseRecord { this.multisigId = props.multisigId; this.connectionId = props.connectionId; this._tags = props.tags ?? {}; - this.linkedGroupRequest = props.linkedGroupRequest ?? { accepted: false }; + this.linkedRequest = props.linkedRequest ?? { accepted: false }; this.credentialId = props.credentialId; this.groupReplied = props.groupReplied; this.initiatorAid = props.initiatorAid; @@ -55,6 +56,7 @@ class NotificationRecord extends BaseRecord { getTags() { return { + ...this._tags, route: this.route, read: this.read, multisigId: this.multisigId, @@ -63,7 +65,8 @@ class NotificationRecord extends BaseRecord { groupReplied: this.groupReplied, initiatorAid: this.initiatorAid, groupInitiator: this.groupInitiator, - ...this._tags, + currentLinkedRequest: this.linkedRequest.current, + hidden: this.hidden, }; } } diff --git a/src/core/agent/records/notificationRecord.types.ts b/src/core/agent/records/notificationRecord.types.ts index 5b3f11d10..169a6eed9 100644 --- a/src/core/agent/records/notificationRecord.types.ts +++ b/src/core/agent/records/notificationRecord.types.ts @@ -1,6 +1,6 @@ import { Notification } from "../services/credentialService.types"; -interface LinkedGroupRequest { +interface LinkedRequest { accepted: boolean; current?: string; previous?: string; @@ -12,4 +12,4 @@ interface NotificationAttempts { notification: Notification; } -export type { LinkedGroupRequest, NotificationAttempts }; +export type { LinkedRequest, NotificationAttempts }; diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index aedf46baf..63603bdec 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -502,18 +502,9 @@ class IdentifierService extends AgentService { const op: Operation = await this.props.signifyClient .operations() - .get(`witness.${identifier.prefix}`) - .catch(async (error) => { - const status = error.message.split(" - ")[1]; - if (/404/gi.test(status)) { - return await this.props.signifyClient - .operations() - .get(`done.${identifier.prefix}`); - } - throw error; - }); + .get(`witness.${identifier.prefix}`); + const isPending = !op.done; - if (isPending) { const pendingOperation = await this.operationPendingStorage.save({ id: op.name, @@ -531,6 +522,7 @@ class IdentifierService extends AgentService { const identifierDetail = (await this.props.signifyClient .identifiers() .get(identifier.prefix)) as HabState & { icp_dt: string }; + if (isMultiSig) { const groupId = identifier.name.split(":")[1]; const groupInitiator = groupId.split("-")[0] === "1"; @@ -563,19 +555,21 @@ class IdentifierService extends AgentService { if (identifier.name.startsWith("XX")) { continue; } + + const identifierDetail = (await this.props.signifyClient + .identifiers() + .get(identifier.prefix)) as HabState & { icp_dt: string }; const multisigManageAid = identifier.group.mhab.prefix; const groupId = identifier.group.mhab.name.split(":")[1]; const theme = parseInt(identifier.name.split(":")[0], 10); const groupInitiator = groupId.split("-")[0] === "1"; + const op = await this.props.signifyClient .operations() .get(`group.${identifier.prefix}`); + const isPending = !op.done; - const identifierDetail = (await this.props.signifyClient - .identifiers() - .get(identifier.prefix)) as HabState & { icp_dt: string }; - if (isPending) { const pendingOperation = await this.operationPendingStorage.save({ id: op.name, diff --git a/src/core/agent/services/ipexCommunicationService.test.ts b/src/core/agent/services/ipexCommunicationService.test.ts index 3e186c4bf..188bce660 100644 --- a/src/core/agent/services/ipexCommunicationService.test.ts +++ b/src/core/agent/services/ipexCommunicationService.test.ts @@ -1,4 +1,5 @@ import { Saider, Serder } from "signify-ts"; +import { current } from "@reduxjs/toolkit"; import { CoreEventEmitter } from "../event"; import { IpexCommunicationService } from "./ipexCommunicationService"; import { Agent } from "../agent"; @@ -315,7 +316,7 @@ describe("Receive individual ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -332,7 +333,7 @@ describe("Receive individual ACDC actions", () => { id: "opName", recordType: OperationPendingRecordType.ExchangeReceiveCredential, }); - ipexAdmitMock.mockResolvedValue(["admit", "sigs", "aend"]); + ipexAdmitMock.mockResolvedValue([{ ked: { d: "admit-said" } }, "sigs", "aend"]); const connectionNote = { id: "note:id", @@ -379,7 +380,7 @@ describe("Receive individual ACDC actions", () => { }); expect(ipexSubmitAdmitMock).toBeCalledWith( "identifierId", - "admit", + { ked: { d: "admit-said" } }, "sigs", "aend", ["EC9bQGHShmp2Juayqp0C5XcheBiHyc1p54pZ_Op-B95x"] @@ -397,7 +398,15 @@ describe("Receive individual ACDC actions", () => { }, }, }); - expect(notificationStorage.deleteById).toBeCalledWith(id); + expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ + id, + route: NotificationRoute.ExnIpexGrant, + linkedRequest: { + accepted: true, + current: "admit-said", + }, + hidden: true, + })); }); test("Cannot accept ACDC if the notification is missing in the DB", async () => { @@ -425,7 +434,9 @@ describe("Receive individual ACDC actions", () => { a: { d: "saidForUuid", }, - linkedGroupRequest: { accepted: false }, + linkedRequest: { + current: "EL3A2jk9gvmVe4ROISB2iWmM8yPSNwQlmar6-SFVWSPW", + accepted: false }, }); identifierStorage.getIdentifierMetadata = jest .fn() @@ -462,7 +473,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", @@ -526,7 +537,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { "EDm8iNyZ9I3P93jb0lFtL6DJD-4Mtd2zw1ADFOoEQAqw": false, }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", @@ -580,7 +591,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { "accepted": true, "current": "EL3A2jk9gvmVe4ROISB2iWmM8yPSNwQlmar6-SFVWSPW", }, @@ -617,7 +628,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR" }, @@ -646,7 +657,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "EL3A2jk9gvmVe4ROISB2iWmM8yPSNwQlmar6-SFVWSPW", }, @@ -749,7 +760,7 @@ describe("Receive group ACDC actions", () => { expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ id: "id", route: NotificationRoute.ExnIpexGrant, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "EL3A2jk9gvmVe4ROISB2iWmM8yPSNwQlmar6-SFVWSPW", }, @@ -800,7 +811,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-admit-said" }, @@ -832,7 +843,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", @@ -863,7 +874,7 @@ describe("Receive group ACDC actions", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "EL3A2jk9gvmVe4ROISB2iWmM8yPSNwQlmar6-SFVWSPW", }, @@ -891,7 +902,7 @@ describe("Receive group ACDC progress", () => { await new ConfigurationService().start(); }); - test("Cannot get linkedGroupRequest from ipex/grant if the notification is missing in the DB", async () => { + test("Cannot get linkedRequest from ipex/grant if the notification is missing in the DB", async () => { const id = "uuid"; const date = DATETIME.toISOString(); const notification = { @@ -914,7 +925,7 @@ describe("Receive group ACDC progress", () => { test("Should return the current progress of an admit linked to a grant", async () => { const grantNoteRecord = { - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "currentsaid" }, @@ -959,7 +970,7 @@ describe("Receive group ACDC progress", () => { members: ["memberA", "memberB", "memberC"], threshold: "2", othersJoined: ["memberB", "memberC"], - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "currentsaid" } @@ -968,7 +979,7 @@ describe("Receive group ACDC progress", () => { test("Should return the defaults when there is no admit linked to a grant", async () => { const grantNoteRecord = { - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, a: { d: "d" }, }; @@ -1005,7 +1016,7 @@ describe("Receive group ACDC progress", () => { members: ["memberA", "memberB", "memberC"], threshold: "2", othersJoined: [], - linkedGroupRequest: { + linkedRequest: { accepted: false, } }); @@ -1031,7 +1042,7 @@ describe("Offer ACDC individual actions", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { current: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1047,7 +1058,7 @@ describe("Offer ACDC individual actions", () => { identifierStorage.getIdentifierMetadata = jest.fn().mockReturnValue({ id: "abc123", }); - ipexOfferMock.mockResolvedValue(["offer", "sigs", "gend"]); + ipexOfferMock.mockResolvedValue([{ ked: { d: "offer-said" } }, "sigs", "gend"]); ipexSubmitOfferMock.mockResolvedValue({ name: "opName", done: true }); saveOperationPendingMock.mockResolvedValueOnce({ id: "opName", @@ -1057,6 +1068,10 @@ describe("Offer ACDC individual actions", () => { await ipexCommunicationService.offerAcdcFromApply(id, grantForIssuanceExnMessage.exn.e.acdc); + expect(operationPendingStorage.save).toBeCalledWith({ + id: "opName", + recordType: OperationPendingRecordType.ExchangeOfferCredential, + }); expect(ipexOfferMock).toBeCalledWith({ senderName: "abc123", recipient: "i", @@ -1065,12 +1080,11 @@ describe("Offer ACDC individual actions", () => { }); expect(ipexSubmitOfferMock).toBeCalledWith( "abc123", - "offer", + { ked: { d: "offer-said" } }, "sigs", "gend", ["i"] ); - expect(markNotificationMock).toBeCalledWith(id); expect(operationPendingStorage.save).toBeCalledWith({ id: "opName", recordType: OperationPendingRecordType.ExchangeOfferCredential, @@ -1084,7 +1098,15 @@ describe("Offer ACDC individual actions", () => { }, }, }); - expect(notificationStorage.deleteById).toBeCalledWith(id); + expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ + id, + route: NotificationRoute.ExnIpexApply, + linkedRequest: { + accepted: true, + current: "offer-said", + }, + hidden: true, + })); }); test("Cannot offer ACDC if the apply notification is missing in the DB", async () => { @@ -1127,7 +1149,7 @@ describe("Offer ACDC group actions", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1173,7 +1195,7 @@ describe("Offer ACDC group actions", () => { expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ id, route: NotificationRoute.ExnIpexApply, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "EARi8kQ1PkSSRyFEIPOFPdnsnv7P2QZYEQqnmr1Eo2N8", }, @@ -1190,8 +1212,7 @@ describe("Offer ACDC group actions", () => { recordType: OperationPendingRecordType.ExchangeOfferCredential, }, }, - }); - expect(markNotificationMock).not.toBeCalled(); + }); expect(markNotificationMock).not.toBeCalled(); expect(notificationStorage.deleteById).not.toBeCalled(); }); @@ -1199,7 +1220,7 @@ describe("Offer ACDC group actions", () => { Agent.agent.getKeriaOnlineStatus = jest.fn().mockReturnValueOnce(true); eventEmitter.emit = jest.fn(); const applyNoteRecord = { - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "currentsaid" }, @@ -1226,7 +1247,7 @@ describe("Offer ACDC group actions", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "current-offer-said" }, @@ -1296,7 +1317,7 @@ describe("Offer ACDC group actions", () => { expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ id: "id", route: NotificationRoute.ExnIpexApply, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-offer-said", }, @@ -1326,7 +1347,7 @@ describe("Offer ACDC group actions", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-offer-said" }, @@ -1354,7 +1375,7 @@ describe("Offer ACDC group actions", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", @@ -1398,7 +1419,7 @@ describe("Offer ACDC group progress", () => { test("Should return the current progress of a group offer", async () => { const applyNoteRecord = { - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-offer-said" }, @@ -1438,7 +1459,7 @@ describe("Offer ACDC group progress", () => { expect(result).toEqual({ members: ["memberA", "memberB", "memberC"], threshold: "2", - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-offer-said", }, @@ -1448,7 +1469,7 @@ describe("Offer ACDC group progress", () => { test("Should return the defaults when there is no offer linked to the apply", async () => { const applyNoteRecord = { - linkedGroupRequest: { + linkedRequest: { accepted: false, }, a: { d: "d" }, @@ -1480,7 +1501,7 @@ describe("Offer ACDC group progress", () => { expect(result).toEqual({ members: ["memberA", "memberB"], threshold: "2", - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, othersJoined: [], }); }); @@ -1504,7 +1525,7 @@ describe("Grant ACDC individual actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1517,7 +1538,7 @@ describe("Grant ACDC individual actions", () => { id: "opName", recordType: OperationPendingRecordType.ExchangePresentCredential, }); - ipexGrantMock.mockResolvedValue(["grant", "sigs", "gend"]); + ipexGrantMock.mockResolvedValue([{ ked: { d: "grant-said" } }, "sigs", "gend"]); markNotificationMock.mockResolvedValueOnce({status: "done"}); await ipexCommunicationService.grantAcdcFromAgree("agree-note-id"); @@ -1533,7 +1554,15 @@ describe("Grant ACDC individual actions", () => { senderName: "abc123", agreeSaid: "EJ1jbI8vTFCEloTfSsZkBpV0bUJnhGVyak5q-5IFIglL", }); - expect(ipexSubmitGrantMock).toBeCalledWith("abc123", "grant", "sigs", "gend", ["EC9bQGHShmp2Juayqp0C5XcheBiHyc1p54pZ_Op-B95x"]); + expect(ipexSubmitGrantMock).toBeCalledWith("abc123", { ked: { d: "grant-said" } }, "sigs", "gend", ["EC9bQGHShmp2Juayqp0C5XcheBiHyc1p54pZ_Op-B95x"]); + expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ + id: "note-id", + route: NotificationRoute.ExnIpexAgree, + linkedRequest: { + accepted: true, + current: "grant-said", + }, + })); expect(operationPendingStorage.save).toBeCalledWith({ id: "opName", recordType: OperationPendingRecordType.ExchangePresentCredential, @@ -1547,7 +1576,12 @@ describe("Grant ACDC individual actions", () => { }, }, }); - expect(notificationStorage.deleteById).toBeCalledWith("agree-note-id"); + expect(eventEmitter.emit).toBeCalledTimes(1); + expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ + id: "note-id", + hidden: true, + linkedRequest: { accepted: true, current: "grant-said" } + })); }); test("Cannot present ACDC if the notification is missing in the DB", async () => { @@ -1569,7 +1603,7 @@ describe("Grant ACDC individual actions", () => { test("Cannot present non existing ACDC", async () => { Agent.agent.getKeriaOnlineStatus = jest.fn().mockReturnValue(true); eventEmitter.emit = jest.fn(); - notificationStorage.findById = jest.fn().mockResolvedValue({ + notificationStorage.findById = jest.fn().mockResolvedValueOnce({ type: "NotificationRecord", id: "note-id", createdAt: DATETIME, @@ -1579,7 +1613,7 @@ describe("Grant ACDC individual actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false, current: "current-grant-said" }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1594,7 +1628,7 @@ describe("Grant ACDC individual actions", () => { await expect(ipexCommunicationService.grantAcdcFromAgree("id")).rejects.toThrowError( IpexCommunicationService.CREDENTIAL_NOT_FOUND ); - + expect(ipexGrantMock).not.toBeCalled(); expect(ipexSubmitGrantMock).not.toBeCalled(); expect(notificationStorage.save).not.toBeCalled(); @@ -1615,7 +1649,7 @@ describe("Grant ACDC individual actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1646,7 +1680,7 @@ describe("Grant ACDC group actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1704,7 +1738,7 @@ describe("Grant ACDC group actions", () => { expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ id: "note-id", route: NotificationRoute.ExnIpexAgree, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "EEpfEHR6EedLnEzleK7mM3AKJSoPWuSQeREC8xjyq3pa", }, @@ -1737,7 +1771,7 @@ describe("Grant ACDC group actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: true, current: "current-grant-said" }, + linkedRequest: { accepted: true, current: "current-grant-said" }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }); @@ -1790,7 +1824,7 @@ describe("Grant ACDC group actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "current-grant-said" }, @@ -1827,7 +1861,7 @@ describe("Grant ACDC group actions", () => { expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ id: "note-id", route: NotificationRoute.ExnIpexAgree, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-grant-said", }, @@ -1845,7 +1879,7 @@ describe("Grant ACDC group actions", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "current-grant-said" }, @@ -1869,7 +1903,7 @@ describe("Grant ACDC group actions", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { + linkedRequest: { accepted: false, }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index fa66cd254..197a2f177 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -94,7 +94,7 @@ class IpexCommunicationService extends AgentService { } // For groups only - if (grantNoteRecord.linkedGroupRequest.accepted) { + if (grantNoteRecord.linkedRequest.accepted) { throw new Error(`${IpexCommunicationService.IPEX_ALREADY_REPLIED} ${notificationId}`); } @@ -148,41 +148,45 @@ class IpexCommunicationService extends AgentService { grantExn, allSchemaSaids ); + op = opMultisigAdmit; - grantNoteRecord.linkedGroupRequest = { - ...grantNoteRecord.linkedGroupRequest, + grantNoteRecord.linkedRequest = { + ...grantNoteRecord.linkedRequest, accepted: true, current: exnSaid, }; - await this.notificationStorage.update(grantNoteRecord); } else { - op = await this.admitIpex( + const { + op: opAdmit, + exnSaid + } = await this.admitIpex( grantNoteRecord.a.d as string, holder.id, grantExn.exn.i, allSchemaSaids ); + + op = opAdmit; + grantNoteRecord.linkedRequest = { + ...grantNoteRecord.linkedRequest, + accepted: true, + current: exnSaid, + }; + grantNoteRecord.hidden = true; } + const pendingOperation = await this.operationPendingStorage.save({ id: op.name, recordType: OperationPendingRecordType.ExchangeReceiveCredential, }); - this.props.eventEmitter.emit({ type: EventTypes.OperationAdded, payload: { operation: pendingOperation }, }); - - if (!holder.multisigManageAid) { - await deleteNotificationRecordById( - this.props.signifyClient, - this.notificationStorage, - notificationId, - grantNoteRecord.a.r as NotificationRoute - ); - } + + await this.notificationStorage.update(grantNoteRecord); } - + @OnlineOnly async offerAcdcFromApply(notificationId: string, acdc: any) { const applyNoteRecord = await this.notificationStorage.findById(notificationId); @@ -191,9 +195,9 @@ class IpexCommunicationService extends AgentService { `${IpexCommunicationService.NOTIFICATION_NOT_FOUND} ${notificationId}` ); } - + // For groups only - if (applyNoteRecord.linkedGroupRequest.accepted) { + if (applyNoteRecord.linkedRequest.accepted) { throw new Error(`${IpexCommunicationService.IPEX_ALREADY_REPLIED} ${notificationId}`); } @@ -214,13 +218,13 @@ class IpexCommunicationService extends AgentService { acdc, applyExn.exn.i ); + op = opMultisigOffer; - applyNoteRecord.linkedGroupRequest = { - ...applyNoteRecord.linkedGroupRequest, + applyNoteRecord.linkedRequest = { + ...applyNoteRecord.linkedRequest, accepted: true, current: exnSaid, }; - await this.notificationStorage.update(applyNoteRecord); } else { const [offer, sigs, end] = await this.props.signifyClient.ipex().offer({ senderName: discloser.id, @@ -231,26 +235,25 @@ class IpexCommunicationService extends AgentService { op = await this.props.signifyClient .ipex() .submitOffer(discloser.id, offer, sigs, end, [applyExn.exn.i]); + + applyNoteRecord.linkedRequest = { + ...applyNoteRecord.linkedRequest, + accepted: true, + current: offer.ked.d, + }; + applyNoteRecord.hidden = true; } const pendingOperation = await this.operationPendingStorage.save({ id: op.name, recordType: OperationPendingRecordType.ExchangeOfferCredential, }); - this.props.eventEmitter.emit({ type: EventTypes.OperationAdded, payload: { operation: pendingOperation }, }); - if (!discloser.multisigManageAid) { - await deleteNotificationRecordById( - this.props.signifyClient, - this.notificationStorage, - notificationId, - applyNoteRecord.a.r as NotificationRoute - ); - } + await this.notificationStorage.update(applyNoteRecord); } @OnlineOnly @@ -263,7 +266,7 @@ class IpexCommunicationService extends AgentService { } // For groups only - if (agreeNoteRecord.linkedGroupRequest.accepted) { + if (agreeNoteRecord.linkedRequest.accepted) { throw new Error(`${IpexCommunicationService.IPEX_ALREADY_REPLIED} ${notificationId}`); } @@ -307,12 +310,11 @@ class IpexCommunicationService extends AgentService { ); op = opMultisigGrant; - agreeNoteRecord.linkedGroupRequest = { - ...agreeNoteRecord.linkedGroupRequest, + agreeNoteRecord.linkedRequest = { + ...agreeNoteRecord.linkedRequest, accepted: true, current: exnSaid, }; - await this.notificationStorage.update(agreeNoteRecord); } else { const [grant, sigs, end] = await this.props.signifyClient.ipex().grant({ senderName: discloser.id, @@ -328,26 +330,25 @@ class IpexCommunicationService extends AgentService { op = await this.props.signifyClient .ipex() .submitGrant(discloser.id, grant, sigs, end, [agreeExn.exn.i]); + + agreeNoteRecord.linkedRequest = { + ...agreeNoteRecord.linkedRequest, + accepted: true, + current: grant.ked.d, + }; + agreeNoteRecord.hidden = true; } const pendingOperation = await this.operationPendingStorage.save({ id: op.name, recordType: OperationPendingRecordType.ExchangePresentCredential, }); - this.props.eventEmitter.emit({ type: EventTypes.OperationAdded, payload: { operation: pendingOperation }, }); - if (!discloser.multisigManageAid) { - await deleteNotificationRecordById( - this.props.signifyClient, - this.notificationStorage, - notificationId, - agreeNoteRecord.a.r as NotificationRoute - ); - } + await this.notificationStorage.update(agreeNoteRecord); } @OnlineOnly @@ -450,7 +451,7 @@ class IpexCommunicationService extends AgentService { holderAid: string, issuerAid: string, schemaSaids: string[] - ): Promise { + ): Promise<{ op: Operation, exnSaid: string }> { // @TODO - foconnor: For now this will only work with our test server, we need to find a better way to handle this in production. for (const schemaSaid of schemaSaids) { if (schemaSaid) { @@ -468,10 +469,11 @@ class IpexCommunicationService extends AgentService { recipient: issuerAid, datetime: dt, }); + const op = await this.props.signifyClient .ipex() .submitAdmit(holderAid, admit, sigs, aend, [issuerAid]); - return op; + return { op, exnSaid: admit.ked.d }; } async createLinkedIpexMessageRecord( @@ -541,11 +543,11 @@ class IpexCommunicationService extends AgentService { ); } - if (grantNoteRecord.linkedGroupRequest.accepted) { + if (grantNoteRecord.linkedRequest.accepted) { throw new Error(IpexCommunicationService.IPEX_ALREADY_REPLIED); } - const multiSigExnSaid = grantNoteRecord.linkedGroupRequest.current; + const multiSigExnSaid = grantNoteRecord.linkedRequest.current; if (!multiSigExnSaid) { throw new Error(IpexCommunicationService.NO_CURRENT_IPEX_MSG_TO_JOIN); } @@ -600,14 +602,13 @@ class IpexCommunicationService extends AgentService { id: op.name, recordType: OperationPendingRecordType.ExchangeReceiveCredential, }); - this.props.eventEmitter.emit({ type: EventTypes.OperationAdded, payload: { operation: pendingOperation }, }); - grantNoteRecord.linkedGroupRequest = { - ...grantNoteRecord.linkedGroupRequest, + grantNoteRecord.linkedRequest = { + ...grantNoteRecord.linkedRequest, accepted: true, }; await this.notificationStorage.update(grantNoteRecord); @@ -621,11 +622,11 @@ class IpexCommunicationService extends AgentService { ); } - if (applyNoteRecord.linkedGroupRequest.accepted) { + if (applyNoteRecord.linkedRequest.accepted) { throw new Error(IpexCommunicationService.IPEX_ALREADY_REPLIED); } - const multiSigExnSaid = applyNoteRecord.linkedGroupRequest.current; + const multiSigExnSaid = applyNoteRecord.linkedRequest.current; if (!multiSigExnSaid) { throw new Error(IpexCommunicationService.NO_CURRENT_IPEX_MSG_TO_JOIN); } @@ -645,25 +646,24 @@ class IpexCommunicationService extends AgentService { id: op.name, recordType: OperationPendingRecordType.ExchangeOfferCredential, }); - this.props.eventEmitter.emit({ type: EventTypes.OperationAdded, payload: { operation: pendingOperation }, }); - applyNoteRecord.linkedGroupRequest = { - ...applyNoteRecord.linkedGroupRequest, + applyNoteRecord.linkedRequest = { + ...applyNoteRecord.linkedRequest, accepted: true, }; await this.notificationStorage.update(applyNoteRecord); } async joinMultisigGrant(multiSigExn: ExnMessage, agreeNoteRecord: NotificationRecord): Promise { - if (agreeNoteRecord.linkedGroupRequest.accepted) { + if (agreeNoteRecord.linkedRequest.accepted) { throw new Error(IpexCommunicationService.IPEX_ALREADY_REPLIED); } - if (!agreeNoteRecord.linkedGroupRequest.current) { + if (!agreeNoteRecord.linkedRequest.current) { throw new Error(IpexCommunicationService.NO_CURRENT_IPEX_MSG_TO_JOIN); } @@ -683,14 +683,13 @@ class IpexCommunicationService extends AgentService { id: op.name, recordType: OperationPendingRecordType.ExchangePresentCredential, }); - this.props.eventEmitter.emit({ type: EventTypes.OperationAdded, payload: { operation: pendingOperation }, }); - agreeNoteRecord.linkedGroupRequest = { - ...agreeNoteRecord.linkedGroupRequest, + agreeNoteRecord.linkedRequest = { + ...agreeNoteRecord.linkedRequest, accepted: true, }; await this.notificationStorage.update(agreeNoteRecord); @@ -1071,8 +1070,8 @@ class IpexCommunicationService extends AgentService { const memberAids = members.signing.map((member: any) => member.aid); const othersJoined: string[] = []; - if (grantNoteRecord.linkedGroupRequest.current) { - for (const signal of (await this.props.signifyClient.groups().getRequest(grantNoteRecord.linkedGroupRequest.current))) { + if (grantNoteRecord.linkedRequest.current) { + for (const signal of (await this.props.signifyClient.groups().getRequest(grantNoteRecord.linkedRequest.current))) { othersJoined.push(signal.exn.i); } } @@ -1081,7 +1080,7 @@ class IpexCommunicationService extends AgentService { threshold: multisigAid.state.kt, members: memberAids, othersJoined: othersJoined, - linkedGroupRequest: grantNoteRecord.linkedGroupRequest, + linkedRequest: grantNoteRecord.linkedRequest, } } @@ -1106,8 +1105,8 @@ class IpexCommunicationService extends AgentService { const memberAids = members.signing.map((member: any) => member.aid); const othersJoined: string[] = []; - if (applyNoteRecord.linkedGroupRequest.current) { - for (const signal of (await this.props.signifyClient.groups().getRequest(applyNoteRecord.linkedGroupRequest.current))) { + if (applyNoteRecord.linkedRequest.current) { + for (const signal of (await this.props.signifyClient.groups().getRequest(applyNoteRecord.linkedRequest.current))) { othersJoined.push(signal.exn.i); } } @@ -1116,7 +1115,7 @@ class IpexCommunicationService extends AgentService { threshold: multisigAid.state.kt, members: memberAids, othersJoined: othersJoined, - linkedGroupRequest: applyNoteRecord.linkedGroupRequest, + linkedRequest: applyNoteRecord.linkedRequest, } } diff --git a/src/core/agent/services/ipexCommunicationService.types.ts b/src/core/agent/services/ipexCommunicationService.types.ts index fa1088c29..1cc131081 100644 --- a/src/core/agent/services/ipexCommunicationService.types.ts +++ b/src/core/agent/services/ipexCommunicationService.types.ts @@ -1,4 +1,4 @@ -import { LinkedGroupRequest } from "../records/notificationRecord.types"; +import { LinkedRequest } from "../records/notificationRecord.types"; interface CredentialsMatchingApply { schema: { @@ -17,7 +17,7 @@ interface LinkedGroupInfo { threshold: string | string[]; members: string[]; othersJoined: string[]; - linkedGroupRequest: LinkedGroupRequest; + linkedRequest: LinkedRequest; } export type { CredentialsMatchingApply, LinkedGroupInfo }; diff --git a/src/core/agent/services/keriaNotificationService.test.ts b/src/core/agent/services/keriaNotificationService.test.ts index ecf7310ab..76e0c97a0 100644 --- a/src/core/agent/services/keriaNotificationService.test.ts +++ b/src/core/agent/services/keriaNotificationService.test.ts @@ -1,4 +1,3 @@ -import { create } from "domain"; import { Agent } from "../agent"; import { ConnectionStatus, @@ -39,6 +38,7 @@ import { } from "../../__fixtures__/agent/keriaNotificationFixtures"; import { ConnectionHistoryType } from "./connectionService.types"; import { StorageMessage } from "../../storage/storage.types"; +import { OperationPendingRecordType } from "../records/operationPendingRecord.type"; const identifiersListMock = jest.fn(); const identifiersGetMock = jest.fn(); @@ -260,6 +260,7 @@ const DATETIME = new Date(); describe("Signify notification service of agent", () => { beforeEach(() => { jest.resetAllMocks(); + markNotificationMock.mockResolvedValue({ status: "done" }); }); test("emit new event for new notification", async () => { @@ -300,7 +301,7 @@ describe("Signify notification service of agent", () => { }, connectionId: "ED_3K5-VPI8N3iRrV7o75fIMOnJfoSmEJy679HTkWsFQ", read: false, - linkedGroupRequest: { accepted: false } + linkedRequest: { accepted: false } }); groupGetRequestMock.mockResolvedValue([{ exn: { a: { gid: "id" } } }]); @@ -313,7 +314,7 @@ describe("Signify notification service of agent", () => { expect(eventEmitter.emit).toHaveBeenCalledWith({ type: EventTypes.NotificationAdded, payload: { - keriaNotif: { + note: { a: { d: "EBcuMc13wJx0wbmxdWqqjoD5V_c532dg2sO-fvISrrMH", m: "", @@ -338,6 +339,8 @@ describe("Signify notification service of agent", () => { et: "rev", }); notificationStorage.findAllByQuery = jest.fn().mockResolvedValue([]); + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); + const notes = [notificationIpexGrantProp]; credentialStorage.getCredentialMetadata.mockResolvedValue( credentialMetadataMock @@ -406,7 +409,6 @@ describe("Signify notification service of agent", () => { test("should call delete keri notification when trigger deleteNotificationRecordById", async () => { const id = "uuid"; - markNotificationMock.mockResolvedValueOnce({status: "done"}); await keriaNotificationService.deleteNotificationRecordById( id, @@ -422,7 +424,6 @@ describe("Signify notification service of agent", () => { const notificationStorage = new NotificationStorage( agentServicesProps.signifyClient ); - markNotificationMock.mockResolvedValueOnce({status: "done"}); notificationStorage.deleteById = jest.fn(); await deleteNotificationRecordById( @@ -632,7 +633,8 @@ describe("Signify notification service of agent", () => { ); notificationStorage.save = jest .fn() - .mockReturnValue({ id: "id", createdAt: new Date(), linkedGroupRequest: { accepted: false } }); + .mockReturnValue({ id: "id", createdAt: new Date(), linkedRequest: { accepted: false } }); + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); await keriaNotificationService.processNotification( notificationIpexApplyProp @@ -672,6 +674,7 @@ describe("Signify notification service of agent", () => { done: true, }); notificationStorage.findAllByQuery = jest.fn().mockResolvedValue([]); + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); await keriaNotificationService.processNotification( notificationIpexGrantProp @@ -690,7 +693,7 @@ describe("Signify notification service of agent", () => { }); notificationStorage.save = jest .fn() - .mockReturnValue({ id: "id", createdAt: new Date(), linkedGroupRequest: { accepted: false } }); + .mockReturnValue({ id: "id", createdAt: new Date(), linkedRequest: { accepted: false } }); credentialStorage.getCredentialMetadata.mockResolvedValue( credentialMetadataMock ); @@ -719,12 +722,12 @@ describe("Signify notification service of agent", () => { }, route: NotificationRoute.ExnIpexGrant, read: false, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), }; notificationStorage.findAllByQuery = jest.fn().mockResolvedValue([notification]); - markNotificationMock.mockResolvedValueOnce({status: "done"}); + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); await keriaNotificationService.processNotification( notificationIpexGrantProp @@ -742,7 +745,7 @@ describe("Signify notification service of agent", () => { expect(eventEmitter.emit).toHaveBeenCalledWith({ type: EventTypes.NotificationAdded, payload: { - keriaNotif: { + note: { id: "id", createdAt: expect.stringMatching( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/ @@ -769,7 +772,7 @@ describe("Signify notification service of agent", () => { }); notificationStorage.save = jest .fn() - .mockReturnValue({ id: "id", createdAt: new Date(), linkedGroupRequest: { accepted: false } }); + .mockReturnValue({ id: "id", createdAt: new Date(), linkedRequest: { accepted: false } }); credentialStorage.getCredentialMetadata.mockResolvedValue(undefined); const notification = { type: "NotificationRecord", @@ -781,7 +784,7 @@ describe("Signify notification service of agent", () => { }, route: NotificationRoute.ExnIpexGrant, read: false, - linkedGroupRequest: { accepted: false }, + linkedGlinkedRequestroupRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), }; @@ -797,7 +800,7 @@ describe("Signify notification service of agent", () => { .mockResolvedValue({ id: "id", }); - markNotificationMock.mockResolvedValueOnce({status: "done"}); + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); await keriaNotificationService.processNotification( notificationIpexGrantProp @@ -836,7 +839,8 @@ describe("Signify notification service of agent", () => { .mockResolvedValue({ id: "id", }); - + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); + await expect(keriaNotificationService.processNotification( notificationIpexGrantProp )).rejects.toThrowError(KeriaNotificationService.DUPLICATE_ISSUANCE); @@ -927,7 +931,7 @@ describe("Signify notification service of agent", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }, @@ -942,7 +946,7 @@ describe("Signify notification service of agent", () => { id: "id", read: false, route: NotificationRoute.ExnIpexGrant, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "ELW97_QXT2MWtsmWLCSR8RBzH-dcyF2gTJvt72I0wEFO", }, @@ -959,7 +963,7 @@ describe("Signify notification service of agent", () => { expect(eventEmitter.emit).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: EventTypes.NotificationAdded, payload: expect.objectContaining({ - keriaNotif: expect.objectContaining({ + note: expect.objectContaining({ id: "id" }), }), @@ -967,7 +971,7 @@ describe("Signify notification service of agent", () => { expect(eventEmitter.emit).not.toHaveBeenNthCalledWith(2, expect.objectContaining({ type: EventTypes.NotificationAdded, payload: expect.objectContaining({ - keriaNotif: expect.objectContaining({ + note: expect.objectContaining({ createdAt: DATETIME, }), }), @@ -1026,7 +1030,7 @@ describe("Signify notification service of agent", () => { expect(notificationStorage.save).not.toBeCalled(); }); - test("Should return all notifications except those with notification route is /exn/ipex/agree", async () => { + test("Should return all notifications", async () => { const mockNotifications = [ { id: "0AC0W27tnnd2WyHWUh-368EI", @@ -1035,7 +1039,8 @@ describe("Signify notification service of agent", () => { multisigId: "multisig1", read: false, connectionId: "ED_3K5-VPI8N3iRrV7o75fIMOnJfoSmEJy679HTkWsFQ", - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, + hidden: false, }, { id: "0AC0W34tnnd2WyUCOy-790AY", @@ -1044,7 +1049,8 @@ describe("Signify notification service of agent", () => { multisigId: "multisig2", read: false, connectionId: "ED_5C2-UOA8N3iRrV7o75fIMOnJfoSmYAe829YCiSaVB", - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, + hidden: false, }, ]; @@ -1058,6 +1064,7 @@ describe("Signify notification service of agent", () => { $not: { route: NotificationRoute.ExnIpexAgree, }, + hidden: false, }); expect(result).toEqual([ { @@ -1090,7 +1097,7 @@ describe("Signify notification service of agent", () => { multisigId: "multisig1", read: false, connectionId: "ED_3K5-VPI8N3iRrV7o75fIMOnJfoSmEJy679HTkWsFQ", - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, }, { id: "0AC0W34tnnd2WyUCOy-790AY", @@ -1099,7 +1106,7 @@ describe("Signify notification service of agent", () => { multisigId: "multisig2", read: false, connectionId: "ED_5C2-UOA8N3iRrV7o75fIMOnJfoSmYAe829YCiSaVB", - linkedGroupRequest: { accepted: false, current: "current-admit-said" }, + linkedRequest: { accepted: false, current: "current-admit-said" }, }, ]; @@ -1113,6 +1120,7 @@ describe("Signify notification service of agent", () => { $not: { route: NotificationRoute.ExnIpexAgree, }, + hidden: false, }); expect(result).toEqual([ { @@ -1282,7 +1290,8 @@ describe("Signify notification service of agent", () => { .mockRejectedValueOnce( new Error(IdentifierStorage.IDENTIFIER_METADATA_RECORD_MISSING) ); - + notificationStorage.findById = jest.fn().mockResolvedValueOnce({linkedRequest: {current: "current_id"}}); + jest.useRealTimers(); await keriaNotificationService.processNotification( notificationIpexGrantProp @@ -1334,7 +1343,7 @@ describe("Signify notification service of agent", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), }, @@ -1365,7 +1374,7 @@ describe("Signify notification service of agent", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), }, @@ -1439,7 +1448,7 @@ describe("Signify notification service of agent", () => { }); notificationStorage.save = jest .fn() - .mockReturnValue({ id: "id", createdAt: new Date(), linkedGroupRequest: { accepted: false } }); + .mockReturnValue({ id: "id", createdAt: new Date(), linkedRequest: { accepted: false } }); const notes = [notificationIpexAgreeProp]; for (const notif of notes) { @@ -1538,7 +1547,7 @@ describe("Group IPEX presentation", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: DATETIME, }, @@ -1553,7 +1562,7 @@ describe("Group IPEX presentation", () => { expect(notificationStorage.update).toBeCalledWith(expect.objectContaining({ id: "id", route: NotificationRoute.ExnIpexApply, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "ELW97_QXT2MWtsmWLCSR8RBzH-dcyF2gTJvt72I0wEFO", }, @@ -1571,7 +1580,7 @@ describe("Group IPEX presentation", () => { expect(eventEmitter.emit).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: EventTypes.NotificationAdded, payload: expect.objectContaining({ - keriaNotif: expect.objectContaining({ + note: expect.objectContaining({ id: "id", read: false, }), @@ -1580,7 +1589,7 @@ describe("Group IPEX presentation", () => { expect(eventEmitter.emit).not.toHaveBeenNthCalledWith(2, expect.objectContaining({ type: EventTypes.NotificationAdded, payload: expect.objectContaining({ - keriaNotif: expect.objectContaining({ + note: expect.objectContaining({ createdAt: DATETIME, }), }), @@ -1630,7 +1639,7 @@ describe("Group IPEX presentation", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date("2024-04-29T11:01:04.903Z"), }, @@ -1643,7 +1652,7 @@ describe("Group IPEX presentation", () => { const updatedAgree = { id: "id", route: NotificationRoute.ExnIpexAgree, - linkedGroupRequest: { + linkedRequest: { accepted: false, current: "ELW97_QXT2MWtsmWLCSR8RBzH-dcyF2gTJvt72I0wEFO", }, @@ -1683,7 +1692,7 @@ describe("Group IPEX presentation", () => { .mockResolvedValue(groupIdentifierMetadataRecord); notificationStorage.save = jest .fn() - .mockReturnValue({ id: "id", createdAt: new Date(), linkedGroupRequest: { accepted: false } }); + .mockReturnValue({ id: "id", createdAt: new Date(), linkedRequest: { accepted: false } }); identifiersMemberMock.mockResolvedValue({ signing: [ { @@ -1727,7 +1736,7 @@ describe("Group IPEX presentation", () => { .mockResolvedValue(groupIdentifierMetadataRecord); notificationStorage.save = jest .fn() - .mockReturnValue({ id: "id", createdAt: new Date(), linkedGroupRequest: { accepted: false } }); + .mockReturnValue({ id: "id", createdAt: new Date(), linkedRequest: { accepted: false } }); identifiersMemberMock.mockResolvedValue({ signing: [ { @@ -2039,6 +2048,7 @@ describe("Long running operation tracker", () => { }, }; operationsGetMock.mockResolvedValue(operationMock); + markNotificationMock.mockResolvedValue({ status: "done" }); }); test("Should handle long operations with type group", async () => { @@ -2052,7 +2062,9 @@ describe("Long running operation tracker", () => { identifierStorage.getIdentifierMetadata.mockResolvedValueOnce({ id: "id", }); + await keriaNotificationService.processOperation(operationRecord); + expect(multiSigs.endRoleAuthorization).toBeCalledWith("id"); expect(eventEmitter.emit).toHaveBeenCalledWith({ type: EventTypes.OperationComplete, @@ -2061,7 +2073,7 @@ describe("Long running operation tracker", () => { oid: "AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA", }, }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("group.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should handle long operations with type witness", async () => { @@ -2085,7 +2097,6 @@ describe("Long running operation tracker", () => { updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - await keriaNotificationService.processOperation(operationRecord); expect(identifierStorage.updateIdentifierMetadata).toBeCalledWith( @@ -2101,7 +2112,7 @@ describe("Long running operation tracker", () => { oid: "AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA", }, }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("witness.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should handle long operations with type oobi", async () => { @@ -2160,7 +2171,7 @@ describe("Long running operation tracker", () => { oid: "AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA", }, }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("oobi.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should delele Keria connection if the connection metadata record exists but pendingDeletion is true", async () => { @@ -2192,10 +2203,10 @@ describe("Long running operation tracker", () => { recordType: "oobi", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - contactGetMock.mockResolvedValueOnce(null); await keriaNotificationService.processOperation(operationRecord); + expect(contactDeleteMock).toBeCalledWith("id"); expect(eventEmitter.emit).toHaveBeenCalledWith({ type: EventTypes.OperationComplete, @@ -2204,7 +2215,7 @@ describe("Long running operation tracker", () => { oid: "AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA", }, }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("oobi.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should delete Keria connection if local connection metadata record does not exist", async () => { @@ -2241,7 +2252,7 @@ describe("Long running operation tracker", () => { oid: "AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA", }, }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("oobi.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test.skip("Cannot create connection if the connection is already created", async () => { @@ -2273,14 +2284,15 @@ describe("Long running operation tracker", () => { recordType: "oobi", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - contactGetMock.mockResolvedValueOnce({ alias: "alias", oobi: "oobi", id: "id", createdAt: new Date(), }); + await keriaNotificationService.processOperation(operationRecord); + expect(connectionStorage.update).toBeCalledTimes(0); expect(contactsUpdateMock).toBeCalledTimes(0); expect(eventEmitter.emit).toHaveBeenCalledWith({ @@ -2290,10 +2302,10 @@ describe("Long running operation tracker", () => { oid: "AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA", }, }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("oobi.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); - test("Should handle long operations with type exchange.receivecredential", async () => { + test("Should handle long operations with type exchange.receivecredential and delete original notification", async () => { const credentialIdMock = "credentialId"; signifyClient .exchanges() @@ -2321,7 +2333,6 @@ describe("Long running operation tracker", () => { recordType: "exchange.receivecredential", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - identifierStorage.getIdentifierMetadata = jest.fn().mockResolvedValue({ type: "IdentifierMetadataRecord", id: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", @@ -2329,6 +2340,20 @@ describe("Long running operation tracker", () => { createdAt: new Date(), updatedAt: new Date(), }); + notificationStorage.findAllByQuery.mockResolvedValue([{ + type: "NotificationRecord", + id: "id", + createdAt: new Date("2024-08-01T10:36:17.814Z"), + a: { + r: NotificationRoute.ExnIpexGrant, + d: "EIDUavcmyHBseNZAdAHR3SF8QMfX1kSJ3Ct0OqS0-HCW", + }, + route: NotificationRoute.ExnIpexGrant, + read: true, + linkedRequest: { accepted: false }, + connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", + updatedAt: new Date(), + }]); await keriaNotificationService.processOperation(operationRecord); @@ -2345,21 +2370,16 @@ describe("Long running operation tracker", () => { }, ConnectionHistoryType.CREDENTIAL_ISSUANCE ); - expect(notificationStorage.save).not.toBeCalled(); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(notificationStorage.deleteById).toBeCalledWith("id"); + expect(markNotificationMock).toBeCalledWith("id"); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.receivecredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); - test("Should handle long operations with type exchange.offercredential", async () => { + test("Should handle long operations with type exchange.offercredential and delete original notification", async () => { const credentialIdMock = "credentialId"; signifyClient .exchanges() .get.mockResolvedValueOnce({ - exn: { - r: ExchangeRoute.IpexApply, - p: "p", - }, - }) - .mockResolvedValueOnce({ exn: { r: ExchangeRoute.IpexOffer, d: "d", @@ -2369,6 +2389,12 @@ describe("Long running operation tracker", () => { }, }, }, + }) + .mockResolvedValueOnce({ + exn: { + r: ExchangeRoute.IpexApply, + p: "p", + }, }); const operationRecord = { type: "OperationPendingRecord", @@ -2377,7 +2403,6 @@ describe("Long running operation tracker", () => { recordType: "exchange.offercredential", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - identifierStorage.getIdentifierMetadata = jest.fn().mockResolvedValue({ type: "IdentifierMetadataRecord", id: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", @@ -2385,13 +2410,29 @@ describe("Long running operation tracker", () => { createdAt: new Date(), updatedAt: new Date(), }); + notificationStorage.findAllByQuery.mockResolvedValue([{ + type: "NotificationRecord", + id: "id", + createdAt: new Date("2024-08-01T10:36:17.814Z"), + a: { + r: NotificationRoute.ExnIpexApply, + d: "EIDUavcmyHBseNZAdAHR3SF8QMfX1kSJ3Ct0OqS0-HCW", + }, + route: NotificationRoute.ExnIpexApply, + read: true, + linkedRequest: { accepted: false }, + connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", + updatedAt: new Date(), + }]); await keriaNotificationService.processOperation(operationRecord); - expect(notificationStorage.save).not.toBeCalled(); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + + expect(notificationStorage.deleteById).toBeCalledWith("id"); + expect(markNotificationMock).toBeCalledWith("id"); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.offercredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); - test("Should handle long operations with type exchange.presentcredential", async () => { + test("Should handle long operations with type exchange.presentcredential and delete original notification", async () => { const credentialIdMock = "credentialId"; const grantExchangeMock = { exn: { @@ -2421,7 +2462,6 @@ describe("Long running operation tracker", () => { recordType: "exchange.presentcredential", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - identifierStorage.getIdentifierMetadata = jest.fn().mockResolvedValue({ type: "IdentifierMetadataRecord", id: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", @@ -2429,14 +2469,30 @@ describe("Long running operation tracker", () => { createdAt: new Date(), updatedAt: new Date(), }); + notificationStorage.findAllByQuery.mockResolvedValue([{ + type: "NotificationRecord", + id: "id", + createdAt: new Date("2024-08-01T10:36:17.814Z"), + a: { + r: NotificationRoute.ExnIpexAgree, + d: "EIDUavcmyHBseNZAdAHR3SF8QMfX1kSJ3Ct0OqS0-HCW", + }, + route: NotificationRoute.ExnIpexAgree, + read: true, + linkedRequest: { accepted: false }, + connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", + updatedAt: new Date(), + }]); await keriaNotificationService.processOperation(operationRecord); + expect(ipexCommunications.createLinkedIpexMessageRecord).toBeCalledWith( grantExchangeMock, ConnectionHistoryType.CREDENTIAL_PRESENTED ); - expect(notificationStorage.save).not.toBeCalled(); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(notificationStorage.deleteById).toBeCalledWith("id"); + expect(markNotificationMock).toBeCalledWith("id"); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.presentcredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should handle long operations with default case", async () => { @@ -2447,6 +2503,7 @@ describe("Long running operation tracker", () => { recordType: "unknown", updatedAt: new Date("2024-08-01T10:36:17.814Z"), }; + await keriaNotificationService.processOperation( operationRecord as OperationPendingRecord ); @@ -2484,7 +2541,6 @@ describe("Long running operation tracker", () => { recordType: "exchange.receivecredential", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - identifierStorage.getIdentifierMetadata = jest.fn().mockResolvedValue({ type: "IdentifierMetadataRecord", id: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", @@ -2493,7 +2549,6 @@ describe("Long running operation tracker", () => { createdAt: new Date("2024-08-01T10:36:17.814Z"), updatedAt: new Date(), }); - notificationStorage.findAllByQuery = jest.fn().mockResolvedValue([ { type: "NotificationRecord", @@ -2505,12 +2560,11 @@ describe("Long running operation tracker", () => { }, route: NotificationRoute.ExnIpexGrant, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), }, ]); - markNotificationMock.mockResolvedValueOnce({status: "done"}); await keriaNotificationService.processOperation(operationRecord); @@ -2519,14 +2573,14 @@ describe("Long running operation tracker", () => { CredentialStatus.CONFIRMED ); expect(notificationStorage.deleteById).toBeCalledWith("id"); + expect(markNotificationMock).toBeCalledWith("id"); expect(eventEmitter.emit).toHaveBeenNthCalledWith(1, { type: EventTypes.NotificationRemoved, payload: { id: "id" } }); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); - expect(markNotificationMock).toBeCalledWith("id"); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.receivecredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should refresh original apply notification when multi-sig offer operation completes", async () => { @@ -2577,7 +2631,7 @@ describe("Long running operation tracker", () => { }, route: NotificationRoute.ExnIpexApply, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), groupReplied: true, @@ -2628,7 +2682,7 @@ describe("Long running operation tracker", () => { expect(eventEmitter.emit).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: EventTypes.NotificationAdded, payload: expect.objectContaining({ - keriaNotif: expect.objectContaining({ + note: expect.objectContaining({ id: "id" }), }), @@ -2636,12 +2690,12 @@ describe("Long running operation tracker", () => { expect(eventEmitter.emit).not.toHaveBeenNthCalledWith(2, expect.objectContaining({ type: EventTypes.NotificationAdded, payload: expect.objectContaining({ - keriaNotif: expect.objectContaining({ + note: expect.objectContaining({ createdAt: DATETIME, }), }), })); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.offercredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should delete original agree notification when multi-sig grant operation completes", async () => { @@ -2672,7 +2726,6 @@ describe("Long running operation tracker", () => { recordType: "exchange.presentcredential", updatedAt: new Date("2024-08-01T10:36:17.814Z"), } as OperationPendingRecord; - identifierStorage.getIdentifierMetadata = jest.fn().mockResolvedValue({ type: "IdentifierMetadataRecord", id: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", @@ -2681,7 +2734,6 @@ describe("Long running operation tracker", () => { createdAt: new Date(), updatedAt: new Date(), }); - notificationStorage.findAllByQuery = jest.fn().mockResolvedValue([ { type: "NotificationRecord", @@ -2693,18 +2745,17 @@ describe("Long running operation tracker", () => { }, route: NotificationRoute.ExnIpexAgree, read: true, - linkedGroupRequest: { accepted: false }, + linkedRequest: { accepted: false }, connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E", updatedAt: new Date(), }, ]); - markNotificationMock.mockResolvedValueOnce({status: "done"}); await keriaNotificationService.processOperation(operationRecord); expect(notificationStorage.deleteById).toBeCalledWith("id"); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); expect(markNotificationMock).toBeCalledWith("id"); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.presentcredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("ExchangeReceiveCredential operations must have an exchange route of /ipex/admit", async () => { @@ -2743,7 +2794,7 @@ describe("Long running operation tracker", () => { expect(operationsGetMock).toBeCalledTimes(1); expect(credentialService.markAcdc).toBeCalledTimes(0); - expect(operationPendingStorage.deleteById).toBeCalledTimes(1); + expect(operationPendingStorage.deleteById).toBeCalledWith("exchange.receivecredential.AOCUvGbpidkplC7gAoJOxLgXX1P2j4xlWMbzk3gM8JzA"); }); test("Should call setTimeout listening for pending operations if Keria is offline", async () => { @@ -2905,4 +2956,34 @@ describe("Long running operation tracker", () => { keriaNotificationService.processOperation(operationRecord) ).rejects.toThrow(errorMessage); }); + + test("Can recover on-going long running operations related to IPEX", async () => { + notificationStorage.findAllByQuery.mockResolvedValue([ + { route: NotificationRoute.ExnIpexApply, linkedRequest: { current: "offer-said" }}, + { route: NotificationRoute.MultiSigExn, linkedRequest: { current: "should-not-happen-skip-me" }}, + { route: NotificationRoute.ExnIpexGrant, linkedRequest: { current: "admit-said" }}, + { route: NotificationRoute.ExnIpexAgree, linkedRequest: { current: "grant-said" }}, + ]); + + await keriaNotificationService.syncIPEXReplyOperations(); + + expect(notificationStorage.findAllByQuery).toBeCalledWith({ + $not: { + currentLinkedRequest: undefined, + }, + }); + expect(operationPendingStorage.save).toBeCalledTimes(3); + expect(operationPendingStorage.save).toHaveBeenNthCalledWith(1, { + id: "exchange.offer-said", + recordType: OperationPendingRecordType.ExchangeOfferCredential, + }); + expect(operationPendingStorage.save).toHaveBeenNthCalledWith(2, { + id: "exchange.admit-said", + recordType: OperationPendingRecordType.ExchangeReceiveCredential, + }); + expect(operationPendingStorage.save).toHaveBeenNthCalledWith(3, { + id: "exchange.grant-said", + recordType: OperationPendingRecordType.ExchangePresentCredential, + }); + }); }); diff --git a/src/core/agent/services/keriaNotificationService.ts b/src/core/agent/services/keriaNotificationService.ts index fcfb2fda1..a787ad140 100644 --- a/src/core/agent/services/keriaNotificationService.ts +++ b/src/core/agent/services/keriaNotificationService.ts @@ -308,13 +308,13 @@ class KeriaNotificationService extends AgentService { } try { - const keriaNotif = await this.createNotificationRecord(notif); + const note = await this.createNotificationRecord(notif); if (notif.a.r !== NotificationRoute.ExnIpexAgree) { // Hidden from UI, so don't emit this.props.eventEmitter.emit({ type: EventTypes.NotificationAdded, payload: { - keriaNotif, + note, }, }); } @@ -520,7 +520,7 @@ class KeriaNotificationService extends AgentService { this.props.eventEmitter.emit({ type: EventTypes.NotificationAdded, payload: { - keriaNotif: { + note: { id: notificationRecord.id, createdAt: new Date().toISOString(), a: { @@ -651,8 +651,8 @@ class KeriaNotificationService extends AgentService { // Refresh the date and read status for UI, and link const notificationRecord = grantNotificationRecords[0]; - notificationRecord.linkedGroupRequest = { - ...notificationRecord.linkedGroupRequest, + notificationRecord.linkedRequest = { + ...notificationRecord.linkedRequest, current: exchange.exn.d, }; notificationRecord.createdAt = new Date(); @@ -669,7 +669,7 @@ class KeriaNotificationService extends AgentService { this.props.eventEmitter.emit({ type: EventTypes.NotificationAdded, payload: { - keriaNotif: { + note: { id: notificationRecord.id, createdAt: notificationRecord.createdAt.toISOString(), a: notificationRecord.a, @@ -701,8 +701,8 @@ class KeriaNotificationService extends AgentService { // Refresh the date and read status for UI, and link const notificationRecord = applyNotificationRecords[0]; - notificationRecord.linkedGroupRequest = { - ...notificationRecord.linkedGroupRequest, + notificationRecord.linkedRequest = { + ...notificationRecord.linkedRequest, current: exchange.exn.d, }; notificationRecord.createdAt = new Date(); @@ -732,7 +732,7 @@ class KeriaNotificationService extends AgentService { this.props.eventEmitter.emit({ type: EventTypes.NotificationAdded, payload: { - keriaNotif: { + note: { id: notificationRecord.id, createdAt: notificationRecord.createdAt.toISOString(), a: notificationRecord.a, @@ -766,8 +766,8 @@ class KeriaNotificationService extends AgentService { // @TODO - foconnor: Could be optimised to only update record once but deviates from the other IPEX messages - OK for now. const notificationRecord = agreeNotificationRecords[0]; - notificationRecord.linkedGroupRequest = { - ...notificationRecord.linkedGroupRequest, + notificationRecord.linkedRequest = { + ...notificationRecord.linkedRequest, current: exchange.exn.d, }; @@ -828,7 +828,7 @@ class KeriaNotificationService extends AgentService { multisigId: result.multisigId, connectionId: result.connectionId, read: result.read, - groupReplied: result.linkedGroupRequest.current !== undefined, + groupReplied: result.linkedRequest.current !== undefined, }; } @@ -859,7 +859,9 @@ class KeriaNotificationService extends AgentService { $not: { route: NotificationRoute.ExnIpexAgree, }, + hidden: false, }); + return notifications.map((notification) => { return { id: notification.id, @@ -868,7 +870,7 @@ class KeriaNotificationService extends AgentService { multisigId: notification.multisigId, connectionId: notification.connectionId, read: notification.read, - groupReplied: notification.linkedGroupRequest.current !== undefined, + groupReplied: notification.linkedRequest.current !== undefined, groupInitiator: notification.groupInitiator, initiatorAid: notification.initiatorAid, }; @@ -886,6 +888,38 @@ class KeriaNotificationService extends AgentService { return notificationRecord; } + async syncIPEXReplyOperations(): Promise { + const records = await this.notificationStorage.findAllByQuery({ + $not: { + currentLinkedRequest: undefined, + }, + }); + + for (const record of records) { + if (!record.linkedRequest.current) continue; + + let recordType; + switch (record.route) { + case NotificationRoute.ExnIpexApply: + recordType = OperationPendingRecordType.ExchangeOfferCredential; + break; + case NotificationRoute.ExnIpexAgree: + recordType = OperationPendingRecordType.ExchangePresentCredential; + break; + case NotificationRoute.ExnIpexGrant: + recordType = OperationPendingRecordType.ExchangeReceiveCredential; + break; + default: + continue; + } + + await this.operationPendingStorage.save({ + id: `exchange.${record.linkedRequest.current}`, + recordType, + }); + } + } + async pollLongOperations() { try { await this._pollLongOperations(); @@ -1044,29 +1078,24 @@ class KeriaNotificationService extends AgentService { .get(admitExchange.exn.p); const credentialId = grantExchange.exn.e.acdc.d; - const holder = await this.identifierStorage.getIdentifierMetadata( - admitExchange.exn.i - ); - if (holder.multisigManageAid) { - const notifications = - await this.notificationStorage.findAllByQuery({ - exnSaid: grantExchange.exn.d, - }); - for (const notification of notifications) { - await deleteNotificationRecordById( - this.props.signifyClient, - this.notificationStorage, - notification.id, - notification.a.r as NotificationRoute - ); - - this.props.eventEmitter.emit({ - type: EventTypes.NotificationRemoved, - payload: { - id: notification.id, - }, + const notifications = + await this.notificationStorage.findAllByQuery({ + exnSaid: grantExchange.exn.d, }); - } + for (const notification of notifications) { + await deleteNotificationRecordById( + this.props.signifyClient, + this.notificationStorage, + notification.id, + notification.a.r as NotificationRoute + ); + + this.props.eventEmitter.emit({ + type: EventTypes.NotificationRemoved, + payload: { + id: notification.id, + }, + }); } await this.credentialService.markAcdc( @@ -1094,56 +1123,56 @@ class KeriaNotificationService extends AgentService { const holder = await this.identifierStorage.getIdentifierMetadata( offerExchange.exn.i ); - if (holder.multisigManageAid) { - const notifications = - await this.notificationStorage.findAllByQuery({ - exnSaid: applyExchange.exn.d, - }); - for (const notification of notifications) { - // "Refresh" the notification so user is aware offer is successfully sent - notification.createdAt = new Date(); - notification.read = false; - - const { multisigMembers, ourIdentifier } = - await this.multiSigs.getMultisigParticipants( - applyExchange.exn.rp - ); - - const initiatorAid = multisigMembers.map( - (member: any) => member.aid - )[0]; - - notification.groupReplied = true; - notification.initiatorAid = initiatorAid; - notification.groupInitiator = - ourIdentifier.groupMetadata?.groupInitiator; - - await this.notificationStorage.update(notification); - - this.props.eventEmitter.emit({ - type: EventTypes.NotificationRemoved, - payload: { - id: notification.id, - }, + const notifications = + await this.notificationStorage.findAllByQuery({ + exnSaid: applyExchange.exn.d, }); + + for (const notification of notifications) { + if (!holder.multisigManageAid) { + await deleteNotificationRecordById(this.props.signifyClient, this.notificationStorage, notification.id, notification.a.r as NotificationRoute); + continue; + } + + // "Refresh" the notification so user is aware offer is successfully sent + notification.createdAt = new Date(); + notification.read = false; + + const { multisigMembers, ourIdentifier } = + await this.multiSigs.getMultisigParticipants( + applyExchange.exn.rp + ); + + notification.groupReplied = true; + notification.initiatorAid = multisigMembers[0].aid; + notification.groupInitiator = + ourIdentifier.groupMetadata?.groupInitiator; + + await this.notificationStorage.update(notification); + + this.props.eventEmitter.emit({ + type: EventTypes.NotificationRemoved, + payload: { + id: notification.id, + }, + }); - this.props.eventEmitter.emit({ - type: EventTypes.NotificationAdded, - payload: { - keriaNotif: { - id: notification.id, - createdAt: notification.createdAt.toISOString(), - a: notification.a, - multisigId: notification.multisigId, - connectionId: notification.connectionId, - read: notification.read, - groupReplied: notification.groupReplied, - initiatorAid, - groupInitiator: notification.groupInitiator, - }, + this.props.eventEmitter.emit({ + type: EventTypes.NotificationAdded, + payload: { + note: { + id: notification.id, + createdAt: notification.createdAt.toISOString(), + a: notification.a, + multisigId: notification.multisigId, + connectionId: notification.connectionId, + read: notification.read, + groupReplied: notification.groupReplied, + initiatorAid: notification.initiatorAid, + groupInitiator: notification.groupInitiator, }, - }); - } + }, + }); } } break; @@ -1157,23 +1186,19 @@ class KeriaNotificationService extends AgentService { .exchanges() .get(grantExchange.exn.p); - const holder = await this.identifierStorage.getIdentifierMetadata( - grantExchange.exn.i - ); - if (holder.multisigManageAid) { - const notifications = - await this.notificationStorage.findAllByQuery({ - exnSaid: agreeExchange.exn.d, - }); - for (const notification of notifications) { - await deleteNotificationRecordById( - this.props.signifyClient, - this.notificationStorage, - notification.id, - notification.a.r as NotificationRoute - ); - } + const notifications = + await this.notificationStorage.findAllByQuery({ + exnSaid: agreeExchange.exn.d, + }); + for (const notification of notifications) { + await deleteNotificationRecordById( + this.props.signifyClient, + this.notificationStorage, + notification.id, + notification.a.r as NotificationRoute + ); } + await this.ipexCommunications.createLinkedIpexMessageRecord( grantExchange, ConnectionHistoryType.CREDENTIAL_PRESENTED diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index dc5499a91..ad3d4ec92 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -539,7 +539,6 @@ const AppWrapper = (props: { children: ReactNode }) => { await Agent.agent.syncWithKeria(); } } - Agent.agent.markAgentStatus(true); } catch (e) { const errorMessage = (e as Error).message; // If the error is failed to fetch with signify, we retry until the connection is secured @@ -553,6 +552,7 @@ const AppWrapper = (props: { children: ReactNode }) => { } } finally { await loadDatabase(); + Agent.agent.markAgentStatus(true); } } diff --git a/src/ui/components/AppWrapper/coreEventListeners.ts b/src/ui/components/AppWrapper/coreEventListeners.ts index bad556046..0687aca56 100644 --- a/src/ui/components/AppWrapper/coreEventListeners.ts +++ b/src/ui/components/AppWrapper/coreEventListeners.ts @@ -23,7 +23,7 @@ const notificationStateChanged = ( ) => { switch (event.type) { case EventTypes.NotificationAdded: - dispatch(addNotification(event.payload.keriaNotif)); + dispatch(addNotification(event.payload.note)); break; case EventTypes.NotificationRemoved: dispatch(deleteNotificationById(event.payload.id)); diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.test.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.test.tsx index e5de4f2c0..c8213ca19 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.test.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.test.tsx @@ -219,7 +219,7 @@ describe("Credential request: Multisig", () => { beforeEach(() => { getLinkedGroupFromIpexApplyMock.mockImplementation(() => Promise.resolve({ - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "", previous: undefined, diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx index 2187e815a..098525f4f 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx @@ -36,7 +36,7 @@ const CredentialRequest = ({ const reachThreshold = linkedGroup && - linkedGroup.othersJoined.length + (linkedGroup.linkedGroupRequest.accepted ? 1 : 0) >= Number(linkedGroup.threshold); + linkedGroup.othersJoined.length + (linkedGroup.linkedRequest.accepted ? 1 : 0) >= Number(linkedGroup.threshold); const userAID = useMemo(() => { if(!credentialRequest) return null; @@ -58,7 +58,7 @@ const CredentialRequest = ({ return { aid: member, name: userName, - joined: linkedGroup.linkedGroupRequest.accepted, + joined: linkedGroup.linkedRequest.accepted, } } diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.test.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.test.tsx index ba1249f3d..adde6db47 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.test.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.test.tsx @@ -111,7 +111,7 @@ describe("Credential request information", () => { describe("Credential request information: multisig", () => { const linkedGroup = { - linkedGroupRequest:{ + linkedRequest:{ accepted: false, current: "", previous: undefined, @@ -184,7 +184,7 @@ describe("Credential request information: multisig", () => { test("Initiator chosen cred", async () => { const linkedGroup = { - linkedGroupRequest:{ + linkedRequest:{ accepted: true, current: "cred-id", previous: undefined, @@ -257,7 +257,7 @@ describe("Credential request information: multisig", () => { test("Member open cred", async () => { const linkedGroup = { - linkedGroupRequest:{ + linkedRequest:{ accepted: false, current: "cred-id", previous: undefined, @@ -344,7 +344,7 @@ describe("Credential request information: multisig", () => { test("Member open cred after initiator chosen cred", async () => { const linkedGroup = { - linkedGroupRequest:{ + linkedRequest:{ accepted: true, current: "cred-id", previous: undefined, diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.tsx index 09627c6f8..14d014c61 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequestInformation/CredentialRequestInformation.tsx @@ -60,15 +60,15 @@ const CredentialRequestInformation = ({ const isGroupInitiatorJoined = !!linkedGroup?.memberInfos.at(0)?.joined; const getCred = useCallback(async () => { - if(!isGroupInitiatorJoined || !linkedGroup?.linkedGroupRequest.current) return; + if(!isGroupInitiatorJoined || !linkedGroup?.linkedRequest.current) return; try { - const id = await Agent.agent.ipexCommunications.getOfferedCredentialSaid(linkedGroup.linkedGroupRequest.current); + const id = await Agent.agent.ipexCommunications.getOfferedCredentialSaid(linkedGroup.linkedRequest.current); setChooseCredId(id); } catch (error) { showError("Unable to get choosen cred", error, dispatch); } - }, [dispatch, isGroupInitiatorJoined, linkedGroup?.linkedGroupRequest]); + }, [dispatch, isGroupInitiatorJoined, linkedGroup?.linkedRequest]); useOnlineStatusEffect(getCred); @@ -114,7 +114,7 @@ const CredentialRequestInformation = ({ const reachThreshold = linkedGroup && - linkedGroup.othersJoined.length + (linkedGroup.linkedGroupRequest.accepted ? 1 : 0) >= Number(linkedGroup.threshold); + linkedGroup.othersJoined.length + (linkedGroup.linkedRequest.accepted ? 1 : 0) >= Number(linkedGroup.threshold); const showProvidedCred = () => { setViewCredId(chooseCredId); @@ -251,7 +251,7 @@ const CredentialRequestInformation = ({ } { - linkedGroup?.linkedGroupRequest.current && { threshold: "2", members: ["member-1", "member-2"], othersJoined: [], - linkedGroupRequest: { + linkedRequest: { accepted: false, } }); @@ -514,7 +514,7 @@ describe("Credential request: Multisig", () => { threshold: "2", members: ["member-1", "member-2"], othersJoined: ["member-1"], - linkedGroupRequest: { + linkedRequest: { accepted: false, } }); @@ -562,7 +562,7 @@ describe("Credential request: Multisig", () => { threshold: "2", members: ["member-1", "member-2", "member-3"], othersJoined: ["member-1", "member-2"], - linkedGroupRequest: { + linkedRequest: { accepted: false, } }); @@ -604,7 +604,7 @@ describe("Credential request: Multisig", () => { threshold: "2", members: ["member-1", "member-2"], othersJoined: ["member-1"], - linkedGroupRequest: { + linkedRequest: { accepted: true, current: "currentadmitsaid" } diff --git a/src/ui/pages/NotificationDetails/components/ReceiveCredential/ReceiveCredential.tsx b/src/ui/pages/NotificationDetails/components/ReceiveCredential/ReceiveCredential.tsx index 200fcc9d7..d14bddfaa 100644 --- a/src/ui/pages/NotificationDetails/components/ReceiveCredential/ReceiveCredential.tsx +++ b/src/ui/pages/NotificationDetails/components/ReceiveCredential/ReceiveCredential.tsx @@ -79,7 +79,7 @@ const ReceiveCredential = ({ threshold: "0", members: [], othersJoined: [], - linkedGroupRequest: { + linkedRequest: { accepted: false, } }); @@ -93,10 +93,10 @@ const ReceiveCredential = ({ const connection = connectionsCache?.[notificationDetails.connectionId]?.label; - const userAccepted = multisigMemberStatus.linkedGroupRequest.accepted; + const userAccepted = multisigMemberStatus.linkedRequest.accepted; const maxThreshold = isMultisig && - (multisigMemberStatus.othersJoined.length + (multisigMemberStatus.linkedGroupRequest.accepted ? 1 : 0)) >= + (multisigMemberStatus.othersJoined.length + (multisigMemberStatus.linkedRequest.accepted ? 1 : 0)) >= Number(multisigMemberStatus.threshold); const identifier = useMemo(() => { @@ -193,7 +193,7 @@ const ReceiveCredential = ({ if(!isMultisig || (isMultisig && isGroupInitiator)) { await Agent.agent.ipexCommunications.admitAcdcFromGrant(notificationDetails.id); - } else if(multisigMemberStatus.linkedGroupRequest.current) { + } else if(multisigMemberStatus.linkedRequest.current) { await Agent.agent.ipexCommunications.joinMultisigAdmit(notificationDetails.id); } @@ -240,13 +240,13 @@ const ReceiveCredential = ({ return MemberAcceptStatus.Accepted; } - if (multisigMemberStatus.linkedGroupRequest.accepted && identifier?.multisigManageAid === member) { + if (multisigMemberStatus.linkedRequest.accepted && identifier?.multisigManageAid === member) { return MemberAcceptStatus.Accepted; } return MemberAcceptStatus.Waiting; }, - [multisigMemberStatus.othersJoined, multisigMemberStatus.linkedGroupRequest, identifier] + [multisigMemberStatus.othersJoined, multisigMemberStatus.linkedRequest, identifier] ); const members = useMemo(() => {