{"version":3,"sources":["../src/NotificationServicesController/services/onchain-notifications.ts"],"sourcesContent":["import { UserStorageController } from '@metamask/profile-sync-controller';\nimport log from 'loglevel';\n\nimport type { TRIGGER_TYPES } from '../constants/notification-schema';\nimport type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification';\nimport type { components } from '../types/on-chain-notification/schema';\nimport type { UserStorage } from '../types/user-storage/user-storage';\nimport {\n  traverseUserStorageTriggers,\n  toggleUserStorageTriggerStatus,\n  makeApiCall,\n} from '../utils/utils';\n\nexport type NotificationTrigger = {\n  id: string;\n  chainId: string;\n  kind: string;\n  address: string;\n};\n\nexport const TRIGGER_API = 'https://trigger.api.cx.metamask.io';\nexport const NOTIFICATION_API = 'https://notification.api.cx.metamask.io';\nexport const TRIGGER_API_BATCH_ENDPOINT = `${TRIGGER_API}/api/v1/triggers/batch`;\nexport const NOTIFICATION_API_LIST_ENDPOINT = `${NOTIFICATION_API}/api/v1/notifications`;\nexport const NOTIFICATION_API_LIST_ENDPOINT_PAGE_QUERY = (page: number) =>\n  `${NOTIFICATION_API_LIST_ENDPOINT}?page=${page}&per_page=100`;\nexport const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = `${NOTIFICATION_API}/api/v1/notifications/mark-as-read`;\n\n/**\n * Creates on-chain triggers based on the provided notification triggers.\n * This method generates a unique token for each trigger using the trigger ID and storage key,\n * proving ownership of the trigger being updated. It then makes an API call to create these triggers.\n * Upon successful creation, it updates the userStorage to reflect the new trigger status.\n *\n * @param userStorage - The user's storage object where triggers and their statuses are stored.\n * @param storageKey - A key used along with the trigger ID to generate a unique token for each trigger.\n * @param bearerToken - The JSON Web Token used for authentication in the API call.\n * @param triggers - An array of notification triggers to be created. Each trigger includes an ID, chain ID, kind, and address.\n * @returns A promise that resolves to void. Throws an error if the API call fails or if there's an issue creating the triggers.\n */\nexport async function createOnChainTriggers(\n  userStorage: UserStorage,\n  storageKey: string,\n  bearerToken: string,\n  triggers: NotificationTrigger[],\n): Promise<void> {\n  type RequestPayloadTrigger = {\n    id: string;\n    // this is the trigger token, generated by using the uuid + storage key. It proves you own the trigger you are updating\n    token: string;\n    config: {\n      kind: string;\n      // eslint-disable-next-line @typescript-eslint/naming-convention\n      chain_id: number;\n      address: string;\n    };\n  };\n  const triggersToCreate: RequestPayloadTrigger[] = triggers.map((t) => ({\n    id: t.id,\n    token: UserStorageController.createSHA256Hash(t.id + storageKey),\n    config: {\n      kind: t.kind,\n      // eslint-disable-next-line @typescript-eslint/naming-convention\n      chain_id: Number(t.chainId),\n      address: t.address,\n    },\n  }));\n\n  if (triggersToCreate.length === 0) {\n    return;\n  }\n\n  const response = await makeApiCall(\n    bearerToken,\n    TRIGGER_API_BATCH_ENDPOINT,\n    'POST',\n    triggersToCreate,\n  );\n\n  if (!response.ok) {\n    const errorData = await response.json().catch(() => undefined);\n    log.error('Error creating triggers:', errorData);\n    throw new Error('OnChain Notifications - unable to create triggers');\n  }\n\n  // If the trigger creation was fine\n  // then update the userStorage\n  for (const trigger of triggersToCreate) {\n    toggleUserStorageTriggerStatus(\n      userStorage,\n      trigger.config.address,\n      String(trigger.config.chain_id),\n      trigger.id,\n      true,\n    );\n  }\n}\n\n/**\n * Deletes on-chain triggers based on the provided UUIDs.\n * This method generates a unique token for each trigger using the UUID and storage key,\n * proving ownership of the trigger being deleted. It then makes an API call to delete these triggers.\n * Upon successful deletion, it updates the userStorage to remove the deleted trigger statuses.\n *\n * @param userStorage - The user's storage object where triggers and their statuses are stored.\n * @param storageKey - A key used along with the UUID to generate a unique token for each trigger.\n * @param bearerToken - The JSON Web Token used for authentication in the API call.\n * @param uuids - An array of UUIDs representing the triggers to be deleted.\n * @returns A promise that resolves to the updated UserStorage object. Throws an error if the API call fails or if there's an issue deleting the triggers.\n */\nexport async function deleteOnChainTriggers(\n  userStorage: UserStorage,\n  storageKey: string,\n  bearerToken: string,\n  uuids: string[],\n): Promise<UserStorage> {\n  const triggersToDelete = uuids.map((uuid) => ({\n    id: uuid,\n    token: UserStorageController.createSHA256Hash(uuid + storageKey),\n  }));\n\n  try {\n    const response = await makeApiCall(\n      bearerToken,\n      TRIGGER_API_BATCH_ENDPOINT,\n      'DELETE',\n      triggersToDelete,\n    );\n\n    if (!response.ok) {\n      throw new Error(\n        `Failed to delete on-chain notifications for uuids ${uuids.join(', ')}`,\n      );\n    }\n\n    // Update the state of the deleted trigger to false\n    for (const uuid of uuids) {\n      for (const address in userStorage) {\n        if (address in userStorage) {\n          for (const chainId in userStorage[address]) {\n            if (userStorage?.[address]?.[chainId]?.[uuid]) {\n              delete userStorage[address][chainId][uuid];\n            }\n          }\n        }\n      }\n    }\n\n    // Follow-up cleanup, if an address had no triggers whatsoever, then we can delete the address\n    const isEmpty = (obj = {}) => Object.keys(obj).length === 0;\n    for (const address in userStorage) {\n      if (address in userStorage) {\n        for (const chainId in userStorage[address]) {\n          // Chain isEmpty Check\n          if (isEmpty(userStorage?.[address]?.[chainId])) {\n            delete userStorage[address][chainId];\n          }\n        }\n\n        // Address isEmpty Check\n        if (isEmpty(userStorage?.[address])) {\n          delete userStorage[address];\n        }\n      }\n    }\n  } catch (err) {\n    log.error(\n      `Error deleting on-chain notifications for uuids ${uuids.join(', ')}:`,\n      err,\n    );\n    throw err;\n  }\n\n  return userStorage;\n}\n\n/**\n * Fetches on-chain notifications for the given user storage and BearerToken.\n * This method iterates through the userStorage to find enabled triggers and fetches notifications for those triggers.\n * It makes paginated API calls to the notifications service, transforming and aggregating the notifications into a single array.\n * The process stops either when all pages have been fetched or when a page has less than 100 notifications, indicating the end of the data.\n *\n * @param userStorage - The user's storage object containing trigger information.\n * @param bearerToken - The JSON Web Token used for authentication in the API call.\n * @returns A promise that resolves to an array of OnChainRawNotification objects. If no triggers are enabled or an error occurs, it may return an empty array.\n */\nexport async function getOnChainNotifications(\n  userStorage: UserStorage,\n  bearerToken: string,\n): Promise<OnChainRawNotification[]> {\n  const triggerIds = traverseUserStorageTriggers(userStorage, {\n    mapTrigger: (t) => {\n      if (!t.enabled) {\n        return undefined;\n      }\n      return t.id;\n    },\n  });\n\n  if (triggerIds.length === 0) {\n    return [];\n  }\n\n  const onChainNotifications: OnChainRawNotification[] = [];\n  const PAGE_LIMIT = 2;\n  for (let page = 1; page <= PAGE_LIMIT; page++) {\n    try {\n      const response = await makeApiCall(\n        bearerToken,\n        NOTIFICATION_API_LIST_ENDPOINT_PAGE_QUERY(page),\n        'POST',\n        // eslint-disable-next-line @typescript-eslint/naming-convention\n        { trigger_ids: triggerIds },\n      );\n\n      const notifications = (await response.json()) as OnChainRawNotification[];\n\n      // Transform and sort notifications\n      const transformedNotifications = notifications\n        .map(\n          (\n            n: components['schemas']['Notification'],\n          ): OnChainRawNotification | undefined => {\n            if (!n.data?.kind) {\n              return undefined;\n            }\n\n            return {\n              ...n,\n              type: n.data.kind as TRIGGER_TYPES,\n            } as OnChainRawNotification;\n          },\n        )\n        .filter((n): n is OnChainRawNotification => Boolean(n));\n\n      onChainNotifications.push(...transformedNotifications);\n\n      // if less than 100 notifications on page, then means we reached end\n      if (notifications.length < 100) {\n        page = PAGE_LIMIT + 1;\n        break;\n      }\n    } catch (err) {\n      log.error(\n        `Error fetching on-chain notifications for trigger IDs ${triggerIds.join(\n          ', ',\n        )}:`,\n        err,\n      );\n      // do nothing\n    }\n  }\n\n  return onChainNotifications;\n}\n\n/**\n * Marks the specified notifications as read.\n * This method sends a POST request to the notifications service to mark the provided notification IDs as read.\n * If the operation is successful, it completes without error. If the operation fails, it throws an error with details.\n *\n * @param bearerToken - The JSON Web Token used for authentication in the API call.\n * @param notificationIds - An array of notification IDs to be marked as read.\n * @returns A promise that resolves to void. The promise will reject if there's an error during the API call or if the response status is not 200.\n */\nexport async function markNotificationsAsRead(\n  bearerToken: string,\n  notificationIds: string[],\n): Promise<void> {\n  if (notificationIds.length === 0) {\n    return;\n  }\n\n  try {\n    const response = await makeApiCall(\n      bearerToken,\n      NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT,\n      'POST',\n      { ids: notificationIds },\n    );\n\n    if (response.status !== 200) {\n      const errorData = await response.json().catch(() => undefined);\n      throw new Error(\n        `Error marking notifications as read: ${errorData?.message as string}`,\n      );\n    }\n  } catch (err) {\n    log.error('Error marking notifications as read:', err);\n    throw err;\n  }\n}\n"],"mappings":";;;;;;;AAAA,SAAS,6BAA6B;AACtC,OAAO,SAAS;AAmBT,IAAM,cAAc;AACpB,IAAM,mBAAmB;AACzB,IAAM,6BAA6B,GAAG,WAAW;AACjD,IAAM,iCAAiC,GAAG,gBAAgB;AAC1D,IAAM,4CAA4C,CAAC,SACxD,GAAG,8BAA8B,SAAS,IAAI;AACzC,IAAM,6CAA6C,GAAG,gBAAgB;AAc7E,eAAsB,sBACpB,aACA,YACA,aACA,UACe;AAYf,QAAM,mBAA4C,SAAS,IAAI,CAAC,OAAO;AAAA,IACrE,IAAI,EAAE;AAAA,IACN,OAAO,sBAAsB,iBAAiB,EAAE,KAAK,UAAU;AAAA,IAC/D,QAAQ;AAAA,MACN,MAAM,EAAE;AAAA;AAAA,MAER,UAAU,OAAO,EAAE,OAAO;AAAA,MAC1B,SAAS,EAAE;AAAA,IACb;AAAA,EACF,EAAE;AAEF,MAAI,iBAAiB,WAAW,GAAG;AACjC;AAAA,EACF;AAEA,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,MAAS;AAC7D,QAAI,MAAM,4BAA4B,SAAS;AAC/C,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAIA,aAAW,WAAW,kBAAkB;AACtC;AAAA,MACE;AAAA,MACA,QAAQ,OAAO;AAAA,MACf,OAAO,QAAQ,OAAO,QAAQ;AAAA,MAC9B,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAcA,eAAsB,sBACpB,aACA,YACA,aACA,OACsB;AACtB,QAAM,mBAAmB,MAAM,IAAI,CAAC,UAAU;AAAA,IAC5C,IAAI;AAAA,IACJ,OAAO,sBAAsB,iBAAiB,OAAO,UAAU;AAAA,EACjE,EAAE;AAEF,MAAI;AACF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,qDAAqD,MAAM,KAAK,IAAI,CAAC;AAAA,MACvE;AAAA,IACF;AAGA,eAAW,QAAQ,OAAO;AACxB,iBAAW,WAAW,aAAa;AACjC,YAAI,WAAW,aAAa;AAC1B,qBAAW,WAAW,YAAY,OAAO,GAAG;AAC1C,gBAAI,cAAc,OAAO,IAAI,OAAO,IAAI,IAAI,GAAG;AAC7C,qBAAO,YAAY,OAAO,EAAE,OAAO,EAAE,IAAI;AAAA,YAC3C;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,UAAU,CAAC,MAAM,CAAC,MAAM,OAAO,KAAK,GAAG,EAAE,WAAW;AAC1D,eAAW,WAAW,aAAa;AACjC,UAAI,WAAW,aAAa;AAC1B,mBAAW,WAAW,YAAY,OAAO,GAAG;AAE1C,cAAI,QAAQ,cAAc,OAAO,IAAI,OAAO,CAAC,GAAG;AAC9C,mBAAO,YAAY,OAAO,EAAE,OAAO;AAAA,UACrC;AAAA,QACF;AAGA,YAAI,QAAQ,cAAc,OAAO,CAAC,GAAG;AACnC,iBAAO,YAAY,OAAO;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,QAAI;AAAA,MACF,mDAAmD,MAAM,KAAK,IAAI,CAAC;AAAA,MACnE;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAEA,SAAO;AACT;AAYA,eAAsB,wBACpB,aACA,aACmC;AACnC,QAAM,aAAa,4BAA4B,aAAa;AAAA,IAC1D,YAAY,CAAC,MAAM;AACjB,UAAI,CAAC,EAAE,SAAS;AACd,eAAO;AAAA,MACT;AACA,aAAO,EAAE;AAAA,IACX;AAAA,EACF,CAAC;AAED,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,uBAAiD,CAAC;AACxD,QAAM,aAAa;AACnB,WAAS,OAAO,GAAG,QAAQ,YAAY,QAAQ;AAC7C,QAAI;AACF,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA,0CAA0C,IAAI;AAAA,QAC9C;AAAA;AAAA,QAEA,EAAE,aAAa,WAAW;AAAA,MAC5B;AAEA,YAAM,gBAAiB,MAAM,SAAS,KAAK;AAG3C,YAAM,2BAA2B,cAC9B;AAAA,QACC,CACE,MACuC;AACvC,cAAI,CAAC,EAAE,MAAM,MAAM;AACjB,mBAAO;AAAA,UACT;AAEA,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,MAAM,EAAE,KAAK;AAAA,UACf;AAAA,QACF;AAAA,MACF,EACC,OAAO,CAAC,MAAmC,QAAQ,CAAC,CAAC;AAExD,2BAAqB,KAAK,GAAG,wBAAwB;AAGrD,UAAI,cAAc,SAAS,KAAK;AAC9B,eAAO,aAAa;AACpB;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI;AAAA,QACF,yDAAyD,WAAW;AAAA,UAClE;AAAA,QACF,CAAC;AAAA,QACD;AAAA,MACF;AAAA,IAEF;AAAA,EACF;AAEA,SAAO;AACT;AAWA,eAAsB,wBACpB,aACA,iBACe;AACf,MAAI,gBAAgB,WAAW,GAAG;AAChC;AAAA,EACF;AAEA,MAAI;AACF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,KAAK,gBAAgB;AAAA,IACzB;AAEA,QAAI,SAAS,WAAW,KAAK;AAC3B,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,MAAS;AAC7D,YAAM,IAAI;AAAA,QACR,wCAAwC,WAAW,OAAiB;AAAA,MACtE;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,MAAM,wCAAwC,GAAG;AACrD,UAAM;AAAA,EACR;AACF;","names":[]}