From 8afc3789c9942282ad1ff529860c76efdcf64ba6 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 28 Oct 2024 09:00:38 +0200 Subject: [PATCH] started on background cases --- src/App.tsx | 166 +- src/background-cases.ts | 233 ++ src/background-old.ts | 4554 ++++++++++++++++++++++++ src/background.ts | 1417 +------- src/messaging/messagesToBackground.tsx | 66 + 5 files changed, 4998 insertions(+), 1438 deletions(-) create mode 100644 src/background-cases.ts create mode 100644 src/background-old.ts create mode 100644 src/messaging/messagesToBackground.tsx diff --git a/src/App.tsx b/src/App.tsx index a0e9c2f..b410141 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -101,10 +101,11 @@ import { Settings } from "./components/Group/Settings"; import { MainAvatar } from "./components/MainAvatar"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; -import { useRecoilState, useResetRecoilState } from "recoil"; +import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; import { canSaveSettingToQdnAtom, fullScreenAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; import { useAppFullScreen } from "./useAppFullscreen"; import { NotAuthenticated } from "./ExtStates/NotAuthenticated"; +import { sendMessageBackground, sendMessageState } from "./messaging/messagesToBackground"; type extStates = | "not-authenticated" @@ -343,6 +344,9 @@ function App() { const { toggleFullScreen } = useAppFullScreen(setFullScreen); + + + useEffect(() => { // Attach a global event listener for double-click const handleDoubleClick = () => { @@ -543,21 +547,33 @@ function App() { const getBalanceFunc = () => { setQortBalanceLoading(true); - chrome?.runtime?.sendMessage({ action: "balance" }, (response) => { + window.sendMessage("balance") + .then((response) => { if (!response?.error && !isNaN(+response)) { setBalance(response); } setQortBalanceLoading(false); + }) + .catch((error) => { + console.error("Failed to get balance:", error); + setQortBalanceLoading(false); }); + }; const getLtcBalanceFunc = () => { setLtcBalanceLoading(true); - chrome?.runtime?.sendMessage({ action: "ltcBalance" }, (response) => { - if (!response?.error && !isNaN(+response)) { - setLtcBalance(response); - } - setLtcBalanceLoading(false); - }); + window.sendMessage("ltcBalance") + .then((response) => { + if (!response?.error && !isNaN(+response)) { + setLtcBalance(response); + } + setLtcBalanceLoading(false); + }) + .catch((error) => { + console.error("Failed to get LTC balance:", error); + setLtcBalanceLoading(false); + }); + }; const sendCoinFunc = () => { setSendPaymentError(""); @@ -575,27 +591,25 @@ function App() { return; } setIsLoading(true); - chrome?.runtime?.sendMessage( - { - action: "sendCoin", - payload: { - amount: Number(paymentAmount), - receiver: paymentTo.trim(), - password: paymentPassword, - }, - }, - (response) => { + window.sendMessage("sendCoin", { + amount: Number(paymentAmount), + receiver: paymentTo.trim(), + password: paymentPassword, + }) + .then((response) => { if (response?.error) { setSendPaymentError(response.error); } else { setIsOpenSendQort(false); setIsOpenSendQortSuccess(true); - // setExtstate("transfer-success-regular"); - // setSendPaymentSuccess("Payment successfully sent"); } setIsLoading(false); - } - ); + }) + .catch((error) => { + console.error("Failed to send coin:", error); + setIsLoading(false); + }); + }; const clearAllStates = () => { @@ -883,11 +897,16 @@ function App() { }, 10000); }); } - chrome?.runtime?.sendMessage({ action: "userInfo" }, (response) => { + window.sendMessage("userInfo") + .then((response) => { if (response && !response.error) { setUserInfo(response); } + }) + .catch((error) => { + console.error("Failed to get user info:", error); }); + getBalanceFunc(); } catch (error) {} }, []); @@ -974,37 +993,41 @@ function App() { crypto.kdfThreads, () => {} ); - chrome?.runtime?.sendMessage( - { - action: "decryptWallet", - payload: { - password: walletToBeDownloadedPassword, - wallet, - }, - }, - (response) => { - if (response && !response?.error) { + window.sendMessage("decryptWallet", { + password: walletToBeDownloadedPassword, + wallet, + }) + .then((response) => { + if (response && !response.error) { setRawWallet(wallet); setWalletToBeDownloaded({ wallet, qortAddress: wallet.address0, }); - chrome?.runtime?.sendMessage( - { action: "userInfo" }, - (response2) => { + + window.sendMessage("userInfo") + .then((response2) => { setIsLoading(false); if (response2 && !response2.error) { - setUserInfo(response); + setUserInfo(response2); } - } - ); + }) + .catch((error) => { + setIsLoading(false); + console.error("Failed to get user info:", error); + }); + getBalanceFunc(); } else if (response?.error) { setIsLoading(false); setWalletToBeDecryptedError(response.error); } - } - ); + }) + .catch((error) => { + setIsLoading(false); + console.error("Failed to decrypt wallet:", error); + }); + } catch (error: any) { setWalletToBeDownloadedError(error?.message); setIsLoading(false); @@ -1082,40 +1105,49 @@ function App() { res(); }, 250); }); - chrome?.runtime?.sendMessage( - { - action: "decryptWallet", - payload: { - password: authenticatePassword, - wallet: rawWallet, - }, - }, - (response) => { - if (response && !response?.error) { + window.sendMessage("decryptWallet", { + password: authenticatePassword, + wallet: rawWallet, + }) + .then((response) => { + if (response && !response.error) { setAuthenticatePassword(""); setExtstate("authenticated"); setWalletToBeDecryptedError(""); - chrome?.runtime?.sendMessage({ action: "userInfo" }, (response) => { - setIsLoading(false); - if (response && !response.error) { - setUserInfo(response); - } - }); - getBalanceFunc(); - chrome?.runtime?.sendMessage( - { action: "getWalletInfo" }, - (response) => { - if (response && response?.walletInfo) { - setRawWallet(response?.walletInfo); + + window.sendMessage("userInfo") + .then((response) => { + setIsLoading(false); + if (response && !response.error) { + setUserInfo(response); } - } - ); + }) + .catch((error) => { + setIsLoading(false); + console.error("Failed to get user info:", error); + }); + + getBalanceFunc(); + + window.sendMessage("getWalletInfo") + .then((response) => { + if (response && response.walletInfo) { + setRawWallet(response.walletInfo); + } + }) + .catch((error) => { + console.error("Failed to get wallet info:", error); + }); } else if (response?.error) { setIsLoading(false); setWalletToBeDecryptedError(response.error); } - } - ); + }) + .catch((error) => { + setIsLoading(false); + console.error("Failed to decrypt wallet:", error); + }); + } catch (error) { setWalletToBeDecryptedError("Unable to authenticate. Wrong password"); } diff --git a/src/background-cases.ts b/src/background-cases.ts new file mode 100644 index 0000000..9777fac --- /dev/null +++ b/src/background-cases.ts @@ -0,0 +1,233 @@ +import { + decryptWallet, + findUsableApi, + getBalanceInfo, + getKeyPair, + getLTCBalance, + getNameInfo, + getUserInfo, + sendCoin, + walletVersion, +} from "./background"; + +export function versionCase(request, event) { + event.source.postMessage( + { + requestId: request.requestId, + action: "version", + payload: { version: "1.0" }, + type: "backgroundMessageResponse", + }, + event.origin + ); +} + +export async function getWalletInfoCase(request, event) { + try { + const response = await getKeyPair(); + + event.source.postMessage( + { + requestId: request.requestId, + action: "getWalletInfo", + payload: { walletInfo: response }, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "getWalletInfo", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + +export async function validApiCase(request, event) { + try { + const usableApi = await findUsableApi(); + + event.source.postMessage( + { + requestId: request.requestId, + action: "validApi", + payload: usableApi, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "validApi", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + +export async function nameCase(request, event) { + try { + const response = await getNameInfo(); + + event.source.postMessage( + { + requestId: request.requestId, + action: "name", + payload: response, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "name", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + +export async function userInfoCase(request, event) { + try { + const response = await getUserInfo(); + + event.source.postMessage( + { + requestId: request.requestId, + action: "userInfo", + payload: response, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "userInfo", + error: "User not authenticated", + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + +export async function decryptWalletCase(request, event) { + try { + const { password, wallet } = request.payload; + const response = await decryptWallet(password, wallet, walletVersion); + + event.source.postMessage( + { + requestId: request.requestId, + action: "decryptWallet", + payload: response, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "decryptWallet", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + +export async function balanceCase(request, event) { + try { + const response = await getBalanceInfo(); + + event.source.postMessage( + { requestId: request.requestId, action: "balance", payload: response, type: "backgroundMessageResponse" }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "balance", + error: error?.message, + type: "backgroundMessageResponse" + }, + event.origin + ); + } + } + export async function ltcBalanceCase(request, event) { + try { + const response = await getLTCBalance(); + + event.source.postMessage( + { requestId: request.requestId, action: "ltcBalance", payload: response, type: "backgroundMessageResponse" }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "ltcBalance", + error: error?.message, + type: "backgroundMessageResponse" + }, + event.origin + ); + } + } + + export async function sendCoinCase(request, event) { + try { + const { receiver, password, amount } = request.payload; + const { res } = await sendCoin({receiver, password, amount}); + if (!res?.success) { + event.source.postMessage( + { + requestId: request.requestId, + action: "sendCoin", + error: res?.data?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + return; + } + event.source.postMessage( + { + requestId: request.requestId, + action: "sendCoin", + payload: true, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "sendCoin", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } + } \ No newline at end of file diff --git a/src/background-old.ts b/src/background-old.ts new file mode 100644 index 0000000..414a382 --- /dev/null +++ b/src/background-old.ts @@ -0,0 +1,4554 @@ +// @ts-nocheck + +import './qortalRequests' +import { isArray } from "lodash"; +import { + decryptGroupEncryption, + encryptAndPublishSymmetricKeyGroupChat, + publishGroupEncryptedResource, + publishOnQDN, + uint8ArrayToObject, +} from "./backgroundFunctions/encryption"; +import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes"; +import Base58 from "./deps/Base58"; +import { + base64ToUint8Array, + decryptSingle, + encryptSingle, + objectToBase64, +} from "./qdn/encryption/group-encryption"; +import { reusableGet } from "./qdn/publish/pubish"; +import { signChat } from "./transactions/signChat"; +import { createTransaction } from "./transactions/transactions"; +import { decryptChatMessage } from "./utils/decryptChatMessage"; +import { decryptStoredWallet } from "./utils/decryptWallet"; +import PhraseWallet from "./utils/generateWallet/phrase-wallet"; +import { RequestQueueWithPromise } from "./utils/queue/queue"; +import { validateAddress } from "./utils/validateAddress"; +import { Sha256 } from "asmcrypto.js"; +import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; +import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; + +export function cleanUrl(url) { + return url?.replace(/^(https?:\/\/)?(www\.)?/, ''); +} +export function getProtocol(url) { + if (url?.startsWith('https://')) { + return 'https'; + } else if (url?.startsWith('http://')) { + return 'http'; + } else { + return 'unknown'; // If neither protocol is present + } +} + +let lastGroupNotification; +export const groupApi = "https://ext-node.qortal.link"; +export const groupApiSocket = "wss://ext-node.qortal.link"; +export const groupApiLocal = "http://127.0.0.1:12391"; +export const groupApiSocketLocal = "ws://127.0.0.1:12391"; +const timeDifferenceForNotificationChatsBackground = 600000; +const requestQueueAnnouncements = new RequestQueueWithPromise(1); +let isMobile = true; + + +const isMobileDevice = () => { + const userAgent = navigator.userAgent || navigator.vendor || window.opera; + + if (/android/i.test(userAgent)) { + return true; // Android device + } + + if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + return true; // iOS device + } + + return false; +}; + +if (isMobileDevice()) { + isMobile = true; + console.log("Running on a mobile device"); +} else { + console.log("Running on a desktop"); +} +const allQueues = { + requestQueueAnnouncements: requestQueueAnnouncements, +}; + +const controlAllQueues = (action) => { + Object.keys(allQueues).forEach((key) => { + const val = allQueues[key]; + try { + if (typeof val[action] === "function") { + val[action](); + } + } catch (error) { + console.error(error); + } + }); +}; + +export const clearAllQueues = () => { + Object.keys(allQueues).forEach((key) => { + const val = allQueues[key]; + try { + val.clear(); + } catch (error) { + console.error(error); + } + }); +}; + +const pauseAllQueues = () => controlAllQueues("pause"); +const resumeAllQueues = () => controlAllQueues("resume"); +const checkDifference = (createdTimestamp) => { + return ( + Date.now() - createdTimestamp < timeDifferenceForNotificationChatsBackground + ); +}; +const getApiKeyFromStorage = async () => { + return new Promise((resolve, reject) => { + chrome.storage.local.get("apiKey", (result) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(result.apiKey || null); // Return null if apiKey isn't found + }); + }); +}; + +const getCustomNodesFromStorage = async () => { + return new Promise((resolve, reject) => { + chrome.storage.local.get("customNodes", (result) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(result.customNodes || null); // Return null if apiKey isn't found + }); + }); +}; + + +const getArbitraryEndpoint = async () => { + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + if (apiKey) { + return `/arbitrary/resources/searchsimple`; + } else { + return `/arbitrary/resources/searchsimple`; + } +}; + +export const getBaseApi = async (customApi?: string) => { + if (customApi) { + return customApi; + } + + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + if (apiKey) { + return apiKey?.url; + } else { + return groupApi; + } +}; +export const isUsingLocal = async () => { + + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + if (apiKey) { + return true + } else { + return false; + } +}; + + + +export const createEndpoint = async (endpoint, customApi?: string) => { + if (customApi) { + return `${customApi}${endpoint}`; + } + + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + + if (apiKey) { + // Check if the endpoint already contains a query string + const separator = endpoint.includes("?") ? "&" : "?"; + return `${apiKey?.url}${endpoint}${separator}apiKey=${apiKey?.apikey}`; + } else { + return `${groupApi}${endpoint}`; + } +}; + +export const walletVersion = 2; +// List of your API endpoints +const apiEndpoints = [ + "https://api.qortal.org", + "https://api2.qortal.org", + "https://appnode.qortal.org", + "https://apinode.qortalnodes.live", + "https://apinode1.qortalnodes.live", + "https://apinode2.qortalnodes.live", + "https://apinode3.qortalnodes.live", + "https://apinode4.qortalnodes.live", +]; + +const buyTradeNodeBaseUrl = "https://appnode.qortal.org"; +const proxyAccountAddress = "QXPejUe5Za1KD3zCMViWCX35AreMQ9H7ku"; +const proxyAccountPublicKey = "5hP6stDWybojoDw5t8z9D51nV945oMPX7qBd29rhX1G7"; +const pendingResponses = new Map(); +let groups = null; + +let socket; +let timeoutId; +let groupSocketTimeout; +let socketTimeout: any; +let interval; +let intervalThreads; +// Function to check each API endpoint +export async function findUsableApi() { + for (const endpoint of apiEndpoints) { + try { + const response = await fetch(`${endpoint}/admin/status`); + if (!response.ok) throw new Error("Failed to fetch"); + + const data = await response.json(); + if (data.isSynchronizing === false && data.syncPercent === 100) { + console.log(`Usable API found: ${endpoint}`); + return endpoint; + } else { + console.log(`API not ready: ${endpoint}`); + } + } catch (error) { + console.error(`Error checking API ${endpoint}:`, error); + } + } + + throw new Error("No usable API found"); +} + +export function isExtMsg(data) { + let isMsgFromExtensionGroup = true; + try { + const decode1 = atob(data); + const decode2 = atob(decode1); + const keyStr = decode2.slice(0, 10); + + // Convert the key string back to a number + const highestKey = parseInt(keyStr, 10); + if (isNaN(highestKey)) { + isMsgFromExtensionGroup = false; + } + } catch (error) { + isMsgFromExtensionGroup = false; + } + + return isMsgFromExtensionGroup; +} + +export function isUpdateMsg(data) { + let isUpdateMessage = true; + try { + const decode1 = atob(data); + const decode2 = atob(decode1); + const keyStr = decode2.slice(10, 13); + + // Convert the key string back to a number + const numberKey = parseInt(keyStr, 10); + if (isNaN(numberKey)) { + isUpdateMessage = false; + } else if(numberKey !== RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS){ + isUpdateMessage = false; + } + } catch (error) { + isUpdateMessage = false; + } + + return isUpdateMessage; +} + +async function checkWebviewFocus() { + return new Promise((resolve) => { + // Set a timeout for 1 second + const timeout = setTimeout(() => { + resolve(false); // No response within 1 second, assume not focused + }, 1000); + + // Send message to the content script to check focus + chrome.runtime.sendMessage({ action: "CHECK_FOCUS" }, (response) => { + clearTimeout(timeout); // Clear the timeout if we get a response + + if (chrome.runtime.lastError) { + resolve(false); // Error occurred, assume not focused + } else { + resolve(response); // Resolve based on the response + } + }); + }); +} + +function playNotificationSound() { + // chrome.runtime.sendMessage({ action: "PLAY_NOTIFICATION_SOUND" }); +} + +const handleNotificationDirect = async (directs) => { + let isFocused; + const wallet = await getSaveWallet(); + const address = wallet.address0; + let isDisableNotifications = await getUserSettings({key: 'disable-push-notifications'}) || false + const dataDirects = directs.filter((direct) => direct?.sender !== address); + try { + if(isDisableNotifications) return + if (!dataDirects || dataDirects?.length === 0) return; + isFocused = await checkWebviewFocus(); + + if (isFocused) { + throw new Error("isFocused"); + } + const newActiveChats = dataDirects; + const oldActiveChats = await getChatHeadsDirect(); + + if (newActiveChats?.length === 0) return; + + let newestLatestTimestamp; + let oldestLatestTimestamp; + // Find the latest timestamp from newActiveChats + newActiveChats?.forEach((newChat) => { + if ( + !newestLatestTimestamp || + newChat?.timestamp > newestLatestTimestamp?.timestamp + ) { + newestLatestTimestamp = newChat; + } + }); + + // Find the latest timestamp from oldActiveChats + oldActiveChats?.forEach((oldChat) => { + if ( + !oldestLatestTimestamp || + oldChat?.timestamp > oldestLatestTimestamp?.timestamp + ) { + oldestLatestTimestamp = oldChat; + } + }); + + if ( + (checkDifference(newestLatestTimestamp.timestamp) && + !oldestLatestTimestamp) || + (newestLatestTimestamp && + newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp) + ) { + const notificationId = + "chat_notification_" + + Date.now() + + "_type=direct" + + `_from=${newestLatestTimestamp.address}`; + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New Direct message! ${ + newestLatestTimestamp?.name && `from ${newestLatestTimestamp.name}` + }`, + message: "You have received a new direct message", + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + + // chrome.runtime.sendMessage( + // { + // action: "notification", + // payload: { + // }, + // } + // ) + // audio.play(); + playNotificationSound(); + } + } catch (error) { + if (!isFocused) { + chrome.runtime.sendMessage( + { + action: "notification", + payload: {}, + }, + (response) => { + if (!response?.error) { + } + } + ); + const notificationId = "chat_notification_" + Date.now(); + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New Direct message!`, + message: "You have received a new direct message", + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); + // audio.play(); + // } + } + } finally { + setChatHeadsDirect(dataDirects); + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); + } +}; +async function getThreadActivity() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `threadactivity-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return null; + } +} + +async function updateThreadActivity({ threadId, qortalName, groupId, thread }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; // One week in milliseconds + let lastResetTime = 0; + // Retrieve the last reset timestamp from storage + const key = `threadactivity-${address}`; + + chrome.storage.local.get([key], (data) => { + let threads; + + if (!data[key] || Object.keys(data?.[key]?.length === 0)) { + threads = { + createdThreads: [], + mostVisitedThreads: [], + recentThreads: [], + }; + } else { + threads = JSON.parse(data[key]); + } + if (threads?.lastResetTime) { + lastResetTime = threads.lastResetTime; + } + + const currentTime = Date.now(); + + // Check if a week has passed since the last reset + if (!lastResetTime || currentTime - lastResetTime > ONE_WEEK_IN_MS) { + // Reset the visit counts for all most visited threads + threads.mostVisitedThreads.forEach((thread) => (thread.visitCount = 0)); + lastResetTime = currentTime; // Update the last reset time + threads.lastResetTime = lastResetTime; + } + + // Update the recent threads list + threads.recentThreads = threads.recentThreads.filter( + (t) => t.threadId !== threadId + ); + threads.recentThreads.unshift({ + threadId, + qortalName, + groupId, + thread, + visitCount: 1, + lastVisited: Date.now(), + }); + + // Sort the recent threads by lastVisited time (descending) + threads.recentThreads.sort((a, b) => b.lastVisited - a.lastVisited); + // Limit the recent threads list to 2 items + threads.recentThreads = threads.recentThreads.slice(0, 2); + + // Update the most visited threads list + const existingThread = threads.mostVisitedThreads.find( + (t) => t.threadId === threadId + ); + if (existingThread) { + existingThread.visitCount += 1; + existingThread.lastVisited = Date.now(); // Update the last visited time as well + } else { + threads.mostVisitedThreads.push({ + threadId, + qortalName, + groupId, + thread, + visitCount: 1, + lastVisited: Date.now(), + }); + } + + // Sort the most visited threads by visitCount (descending) + threads.mostVisitedThreads.sort((a, b) => b.visitCount - a.visitCount); + // Limit the most visited threads list to 2 items + threads.mostVisitedThreads = threads.mostVisitedThreads.slice(0, 2); + + // Store the updated thread information and last reset time + // chrome.storage.local.set({ threads, lastResetTime }); + + const dataString = JSON.stringify(threads); + chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }); + }); +} + +const handleNotification = async (groups) => { + const wallet = await getSaveWallet(); + const address = wallet.address0; + let isDisableNotifications = await getUserSettings({key: 'disable-push-notifications'}) || false + + let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] + if(!isArray(mutedGroups)) mutedGroups = [] + + let isFocused; + const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !isUpdateMsg(group?.data)); + const dataWithUpdates = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId)); + + try { + if(isDisableNotifications) return + if (!data || data?.length === 0) return; + isFocused = await checkWebviewFocus(); + + if (isFocused) { + throw new Error("isFocused"); + } + const newActiveChats = data; + const oldActiveChats = await getChatHeads(); + + let results = []; + let newestLatestTimestamp; + let oldestLatestTimestamp; + // Find the latest timestamp from newActiveChats + newActiveChats?.forEach((newChat) => { + if ( + !newestLatestTimestamp || + newChat?.timestamp > newestLatestTimestamp?.timestamp + ) { + newestLatestTimestamp = newChat; + } + }); + + // Find the latest timestamp from oldActiveChats + oldActiveChats?.forEach((oldChat) => { + if ( + !oldestLatestTimestamp || + oldChat?.timestamp > oldestLatestTimestamp?.timestamp + ) { + oldestLatestTimestamp = oldChat; + } + }); + + if ( + (checkDifference(newestLatestTimestamp.timestamp) && + !oldestLatestTimestamp) || + (newestLatestTimestamp && + newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp) + ) { + if ( + !lastGroupNotification || + Date.now() - lastGroupNotification >= 120000 + ) { + if ( + !newestLatestTimestamp?.data || + !isExtMsg(newestLatestTimestamp?.data) + ) + return; + + const notificationId = + "chat_notification_" + + Date.now() + + "_type=group" + + `_from=${newestLatestTimestamp.groupId}`; + + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: "New Group Message!", + message: `You have received a new message from ${newestLatestTimestamp?.groupName}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + // chrome.runtime.sendMessage( + // { + // action: "notification", + // payload: { + // }, + // } + // ) + // audio.play(); + playNotificationSound(); + lastGroupNotification = Date.now(); + } + } + } catch (error) { + if (!isFocused) { + chrome.runtime.sendMessage( + { + action: "notification", + payload: {}, + }, + (response) => { + if (!response?.error) { + } + } + ); + const notificationId = "chat_notification_" + Date.now(); + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: "New Group Message!", + message: "You have received a new message from one of your groups", + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); + // audio.play(); + lastGroupNotification = Date.now(); + // } + } + } finally { + if (!data || data?.length === 0) return; + setChatHeads(dataWithUpdates); + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); + } +}; + +const checkThreads = async (bringBack) => { + try { + let myName = ""; + const userData = await getUserInfo(); + if (userData?.name) { + myName = userData.name; + } + let newAnnouncements = []; + let dataToBringBack = []; + const threadActivity = await getThreadActivity(); + if (!threadActivity) return null; + + const selectedThreads = [ + ...threadActivity.createdThreads.slice(0, 2), + ...threadActivity.mostVisitedThreads.slice(0, 2), + ...threadActivity.recentThreads.slice(0, 2), + ]; + + if (selectedThreads?.length === 0) return null; + const tempData = {}; + for (const thread of selectedThreads) { + try { + const identifier = `thmsg-${thread?.threadId}`; + const name = thread?.qortalName; + const endpoint = await getArbitraryEndpoint(); + const url = await createEndpoint( + `${endpoint}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true` + ); + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + const latestMessage = responseData.filter( + (pub) => pub?.name !== myName + )[0]; + // const latestMessage = responseData[0] + + if (!latestMessage) { + continue; + } + + if ( + checkDifference(latestMessage.created) && + latestMessage.created > thread?.lastVisited && + (!thread?.lastNotified || thread?.lastNotified < thread?.created) + ) { + tempData[thread.threadId] = latestMessage.created; + newAnnouncements.push(thread); + } + if (latestMessage.created > thread?.lastVisited) { + dataToBringBack.push(thread); + } + } catch (error) { + conosle.log({ error }); + } + } + + if (bringBack) { + return dataToBringBack; + } + + const updateThreadWithLastNotified = { + ...threadActivity, + createdThreads: (threadActivity?.createdThreads || [])?.map((item) => { + if (tempData[item.threadId]) { + return { + ...item, + lastNotified: tempData[item.threadId], + }; + } else { + return item; + } + }), + mostVisitedThreads: (threadActivity?.mostVisitedThreads || [])?.map( + (item) => { + if (tempData[item.threadId]) { + return { + ...item, + lastNotified: tempData[item.threadId], + }; + } else { + return item; + } + } + ), + recentThreads: (threadActivity?.recentThreads || [])?.map((item) => { + if (tempData[item.threadId]) { + return { + ...item, + lastNotified: tempData[item.threadId], + }; + } else { + return item; + } + }), + }; + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(updateThreadWithLastNotified); + chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }); + + if (newAnnouncements.length > 0) { + const notificationId = + "chat_notification_" + + Date.now() + + "_type=thread-post" + + `_data=${JSON.stringify(newAnnouncements[0])}`; + let isDisableNotifications = await getUserSettings({key: 'disable-push-notifications'}) || false + if(!isDisableNotifications){ + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New thread post!`, + message: `New post in ${newAnnouncements[0]?.thread?.threadData?.title}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); + } + + } + const savedtimestampAfter = await getTimestampGroupAnnouncement(); + chrome.runtime.sendMessage({ + action: "SET_GROUP_ANNOUNCEMENTS", + payload: savedtimestampAfter, + }); + } catch (error) { + } finally { + } +}; +const checkNewMessages = async () => { + try { + let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] + if(!isArray(mutedGroups)) mutedGroups = [] + let myName = ""; + const userData = await getUserInfo(); + if (userData?.name) { + myName = userData.name; + } + + let newAnnouncements = []; + const activeData = (await getStoredData("active-groups-directs")) || { + groups: [], + directs: [], + }; + const groups = activeData?.groups; + if (!groups || groups?.length === 0) return; + const savedtimestamp = await getTimestampGroupAnnouncement(); + + await Promise.all( + groups.map(async (group) => { + try { + const identifier = `grp-${group.groupId}-anc-`; + const endpoint = await getArbitraryEndpoint(); + const url = await createEndpoint( + `${endpoint}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=0&reverse=true&prefix=true` + ); + const response = await requestQueueAnnouncements.enqueue(() => { + return fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + }); + const responseData = await response.json(); + + const latestMessage = responseData.filter( + (pub) => pub?.name !== myName + )[0]; + if (!latestMessage) { + return; // continue to the next group + } + + if ( + checkDifference(latestMessage.created) && + (!savedtimestamp[group.groupId] || + latestMessage.created > + savedtimestamp?.[group.groupId]?.notification) + ) { + newAnnouncements.push(group); + await addTimestampGroupAnnouncement({ + groupId: group.groupId, + timestamp: Date.now(), + }); + // save new timestamp + } + } catch (error) { + console.error(error); // Handle error if needed + } + }) + ); + let isDisableNotifications = await getUserSettings({key: 'disable-push-notifications'}) || false + + if (newAnnouncements.length > 0 && !mutedGroups.includes(newAnnouncements[0]?.groupId) && !isDisableNotifications) { + const notificationId = + "chat_notification_" + + Date.now() + + "_type=group-announcement" + + `_from=${newAnnouncements[0]?.groupId}`; + + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New group announcement!`, + message: `You have received a new announcement from ${newAnnouncements[0]?.groupName}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); + } + const savedtimestampAfter = await getTimestampGroupAnnouncement(); + chrome.runtime.sendMessage({ + action: "SET_GROUP_ANNOUNCEMENTS", + payload: savedtimestampAfter, + }); + } catch (error) { + } finally { + } +}; + +const listenForNewGroupAnnouncements = async () => { + try { + setTimeout(() => { + checkNewMessages(); + }, 500); + if (interval) { + clearInterval(interval); + } + + let isCalling = false; + interval = setInterval(async () => { + if (isCalling) return; + isCalling = true; + const res = await checkNewMessages(); + isCalling = false; + }, 180000); + } catch (error) {} +}; +const listenForThreadUpdates = async () => { + try { + setTimeout(() => { + checkThreads(); + }, 500); + if (intervalThreads) { + clearInterval(intervalThreads); + } + + let isCalling = false; + intervalThreads = setInterval(async () => { + if (isCalling) return; + isCalling = true; + const res = await checkThreads(); + isCalling = false; + }, 60000); + } catch (error) {} +}; + +const forceCloseWebSocket = () => { + if (socket) { + clearTimeout(timeoutId); + clearTimeout(groupSocketTimeout); + clearTimeout(socketTimeout); + timeoutId = null; + groupSocketTimeout = null; + socket.close(1000, "forced"); + socket = null; + } +}; + +async function getNameInfo() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const validApi = await getBaseApi(); + const response = await fetch(validApi + "/names/address/" + address); + const nameData = await response.json(); + if (nameData?.length > 0) { + return nameData[0].name; + } else { + return ""; + } +} +async function getAddressInfo(address) { + const validApi = await getBaseApi(); + const response = await fetch(validApi + "/addresses/" + address); + const data = await response.json(); + + if (!response?.ok && data?.error !== 124) + throw new Error("Cannot fetch address info"); + if (data?.error === 124) { + return { + address, + }; + } + return data; +} + +export async function getKeyPair() { + const res = await chrome.storage.local.get(["keyPair"]); + if (res?.keyPair) { + return res.keyPair; + } else { + throw new Error("Wallet not authenticated"); + } +} + +export async function getSaveWallet() { + const res = await chrome.storage.local.get(["walletInfo"]); + if (res?.walletInfo) { + return res.walletInfo; + } else { + throw new Error("No wallet saved"); + } +} + +async function clearAllNotifications() { + const notifications = await chrome.notifications.getAll(); + for (const notificationId of Object.keys(notifications)) { + await chrome.notifications.clear(notificationId); + } +} + +async function getUserInfo() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const addressInfo = await getAddressInfo(address); + const name = await getNameInfo(); + return { + name, + publicKey: wallet.publicKey, + ...addressInfo, + }; +} + +async function connection(hostname) { + const isConnected = chrome.storage.local.get([hostname]); + return isConnected; +} + +async function getTradeInfo(qortalAtAddress) { + const response = await fetch( + buyTradeNodeBaseUrl + "/crosschain/trade/" + qortalAtAddress + ); + if (!response?.ok) throw new Error("Cannot crosschain trade information"); + const data = await response.json(); + return data; +} +async function getTradesInfo(qortalAtAddresses) { + // Use Promise.all to fetch data for all addresses concurrently + const trades = await Promise.all( + qortalAtAddresses.map((address) => getTradeInfo(address)) + ); + return trades; // Return the array of trade info objects +} + +export async function getBalanceInfo() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const validApi = await getBaseApi(); + const response = await fetch(validApi + "/addresses/balance/" + address); + + if (!response?.ok) throw new Error("Cannot fetch balance"); + const data = await response.json(); + return data; +} +async function getLTCBalance() { + const wallet = await getSaveWallet(); + let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`; + const keyPair = await getKeyPair(); + const parsedKeyPair = JSON.parse(keyPair); + let _body = parsedKeyPair.ltcPublicKey; + const response = await fetch(_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: _body, + }); + if (response?.ok) { + const data = await response.text(); + const dataLTCBalance = (Number(data) / 1e8).toFixed(8); + return +dataLTCBalance; + } else throw new Error("Onable to get LTC balance"); +} + +const processTransactionVersion2Chat = async (body: any, customApi) => { + // const validApi = await findUsableApi(); + const url = await createEndpoint( + "/transactions/process?apiVersion=2", + customApi + ); + return fetch(url, { + method: "POST", + headers: {}, + body: Base58.encode(body), + }).then(async (response) => { + try { + const json = await response.clone().json(); + return json; + } catch (e) { + return await response.text(); + } + }); +}; + +export const processTransactionVersion2 = async (body: any) => { + const url = await createEndpoint(`/transactions/process?apiVersion=2`); + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", // Ensure the body is correctly parsed + }, + body, // Convert body to JSON string + }); + + // if (!response.ok) { + // // If the response is not successful (status code is not 2xx) + // throw new Error(`HTTP error! Status: ${response.status}`); + // } + + try { + const json = await response.clone().json(); + return json; + } catch (jsonError) { + try { + const text = await response.text(); + return text; + } catch (textError) { + throw new Error(`Failed to parse response as both JSON and text.`); + } + } + } catch (error) { + console.error("Error processing transaction:", error); + throw error; // Re-throw the error after logging it + } +}; + +const transaction = async ( + { type, params, apiVersion, keyPair }: any, + validApi +) => { + const tx = createTransaction(type, keyPair, params); + let res; + + if (apiVersion && apiVersion === 2) { + const signedBytes = Base58.encode(tx.signedBytes); + res = await processTransactionVersion2(signedBytes, validApi); + } + let success = true; + if (res?.error) { + success = false; + } + + return { + success, + data: res, + }; +}; +const makeTransactionRequest = async ( + receiver, + lastRef, + amount, + fee, + keyPair, + validApi +) => { + const myTxnrequest = await transaction( + { + nonce: 0, + type: 2, + params: { + recipient: receiver, + // recipientName: recipientName, + amount: amount, + lastReference: lastRef, + fee: fee, + }, + apiVersion: 2, + keyPair, + }, + validApi + ); + return myTxnrequest; +}; + +export const getLastRef = async () => { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const validApi = await getBaseApi(); + const response = await fetch( + validApi + "/addresses/lastreference/" + address + ); + if (!response?.ok) throw new Error("Cannot fetch balance"); + const data = await response.text(); + return data; +}; +export const sendQortFee = async (): Promise => { + const validApi = await getBaseApi(); + const response = await fetch( + validApi + "/transactions/unitfee?txType=PAYMENT" + ); + + if (!response.ok) { + throw new Error("Error when fetching join fee"); + } + + const data = await response.json(); + const qortFee = (Number(data) / 1e8).toFixed(8); + return qortFee; +}; + +async function getNameOrAddress(receiver) { + try { + const isAddress = validateAddress(receiver); + if (isAddress) { + return receiver; + } + const validApi = await getBaseApi(); + + const response = await fetch(validApi + "/names/" + receiver); + const data = await response.json(); + if (data?.owner) return data.owner; + if (data?.error) { + throw new Error("Name does not exist"); + } + if (!response?.ok) throw new Error("Cannot fetch name"); + return { error: "cannot validate address or name" }; + } catch (error) { + throw new Error(error?.message || "cannot validate address or name"); + } +} + +export async function getPublicKey(receiver) { + try { + const validApi = await getBaseApi(); + + const response = await fetch(validApi + "/addresses/publickey/" + receiver); + if (!response?.ok) throw new Error("Cannot fetch recipient's public key"); + + const data = await response.text(); + if (!data?.error && data !== "false") return data; + if (data?.error) { + throw new Error("Cannot fetch recipient's public key"); + } + throw new Error("Cannot fetch recipient's public key"); + } catch (error) { + throw new Error(error?.message || "cannot validate address or name"); + } +} + +const MAX_STORAGE_SIZE = 3 * 1024 * 1024; // 3MB in bytes + +async function getDataPublishes(groupId, type) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + return new Promise((resolve) => { + chrome.storage.local.get([`${address}-publishData`], (result) => { + if (chrome.runtime.lastError) { + console.error("Error retrieving data:", chrome.runtime.lastError); + resolve(null); // Return null in case of an error + return; + } + + let storedData = result[`${address}-publishData`] || {}; // Get the stored data or initialize an empty object + let groupData = storedData[groupId] || {}; // Get data by groupId + let typeData = groupData[type] || {}; // Get data by type + + resolve(typeData); // Resolve with the data inside the specific type + }); + }); +} + +async function addDataPublishes(newData, groupId, type) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const nameIdentifier = `${newData.name}-${newData.identifier}`; + + // Prevent adding data larger than 50KB + if (newData?.size > 50000) return false; + + return new Promise((res) => { + chrome.storage.local.get([`${address}-publishData`], (result) => { + let storedData = result[`${address}-publishData`] || {}; // Get existing data or initialize + let groupData = storedData[groupId] || {}; // Get or initialize group by groupId + let typeData = groupData[type] || {}; // Get or initialize the type within the group + + let totalSize = 0; + + // Calculate the current size of all stored data + Object.values(storedData).forEach((group) => { + Object.values(group).forEach((type) => { + Object.values(type).forEach((data) => { + totalSize += data.size; // Accumulate the sizes of actual data + }); + }); + }); + + // Check if adding the new data exceeds 3MB + if (totalSize + newData.size > MAX_STORAGE_SIZE) { + // Sort and remove older data within the group and type + let dataEntries = Object.entries(typeData); + dataEntries.sort((a, b) => a[1].timestampSaved - b[1].timestampSaved); + + // Remove old data until there's enough space + while ( + totalSize + newData.size > MAX_STORAGE_SIZE && + dataEntries.length > 0 + ) { + const removedEntry = dataEntries.shift(); + totalSize -= removedEntry[1].size; + delete typeData[removedEntry[0]]; // Remove from the typeData + } + } + + // Add or update the new data within the group and type + if (totalSize + newData.size <= MAX_STORAGE_SIZE) { + typeData[`${nameIdentifier}`] = newData; // Add new data under name-identifier + groupData[type] = typeData; // Update type data within the group + storedData[groupId] = groupData; // Update group data within the stored data + + // Save the updated structure back to chrome.storage.local + chrome.storage.local.set( + { [`${address}-publishData`]: storedData }, + () => { + res(true); // Data successfully added + } + ); + } else { + console.error("Failed to add data, still exceeds storage limit."); + res(false); // Failure due to storage limit + } + }); + }); +} + +// Fetch user settings based on the key +async function getUserSettings({ key }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + return new Promise((resolve) => { + chrome.storage.local.get([`${address}-userSettings`], (result) => { + if (chrome.runtime.lastError) { + console.error("Error retrieving data:", chrome.runtime.lastError); + resolve(null); // Return null in case of an error + return; + } + + const storedData = result[`${address}-userSettings`] || {}; // Get the stored data or initialize an empty object + const value = storedData[key] || null; // Get data by key + + resolve(value); // Resolve with the data for the specific key + }); + }); +} + +// Add or update user settings +async function addUserSettings({ keyValue }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const { key, value } = keyValue; + + // No need to check size here, unless value is a large object. For simple settings, size checks aren't necessary. + + return new Promise((res) => { + chrome.storage.local.get([`${address}-userSettings`], (result) => { + let storedData = result[`${address}-userSettings`] || {}; // Get existing data or initialize + + storedData[key] = value; // Update the key-value pair within the stored data + + // Save the updated structure back to chrome.storage.local + chrome.storage.local.set( + { [`${address}-userSettings`]: storedData }, + () => { + res(true); // Data successfully added + } + ); + }); + }); +} + +async function decryptWallet({ password, wallet, walletVersion }) { + try { + const response = await decryptStoredWallet(password, wallet); + const wallet2 = new PhraseWallet(response, walletVersion); + const keyPair = wallet2._addresses[0].keyPair; + const ltcPrivateKey = + wallet2._addresses[0].ltcWallet.derivedMasterPrivateKey; + const ltcPublicKey = wallet2._addresses[0].ltcWallet.derivedMasterPublicKey; + const ltcAddress = wallet2._addresses[0].ltcWallet.address; + const toSave = { + privateKey: Base58.encode(keyPair.privateKey), + publicKey: Base58.encode(keyPair.publicKey), + ltcPrivateKey: ltcPrivateKey, + ltcPublicKey: ltcPublicKey, + arrrSeed58: wallet2._addresses[0].arrrWallet.seed58, + btcAddress: wallet2._addresses[0].btcWallet.address, + btcPublicKey: wallet2._addresses[0].btcWallet.derivedMasterPublicKey, + btcPrivateKey: wallet2._addresses[0].btcWallet.derivedMasterPrivateKey, + + ltcAddress: wallet2._addresses[0].ltcWallet.address, + + dogeAddress: wallet2._addresses[0].dogeWallet.address, + dogePublicKey: wallet2._addresses[0].dogeWallet.derivedMasterPublicKey, + dogePrivateKey: wallet2._addresses[0].dogeWallet.derivedMasterPrivateKey, + + dgbAddress: wallet2._addresses[0].dgbWallet.address, + dgbPublicKey: wallet2._addresses[0].dgbWallet.derivedMasterPublicKey, + dgbPrivateKey: wallet2._addresses[0].dgbWallet.derivedMasterPrivateKey, + + rvnAddress: wallet2._addresses[0].rvnWallet.address, + rvnPublicKey: wallet2._addresses[0].rvnWallet.derivedMasterPublicKey, + rvnPrivateKey: wallet2._addresses[0].rvnWallet.derivedMasterPrivateKey + }; + const dataString = JSON.stringify(toSave); + await new Promise((resolve, reject) => { + chrome.storage.local.set({ keyPair: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); + const newWallet = { + ...wallet, + publicKey: Base58.encode(keyPair.publicKey), + ltcAddress: ltcAddress, + }; + await new Promise((resolve, reject) => { + chrome.storage.local.set({ walletInfo: newWallet }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); + + return true; + } catch (error) { + throw new Error(error.message); + } +} + +export async function signChatFunc(chatBytesArray, chatNonce, customApi, keyPair) { + let response; + try { + const signedChatBytes = signChat(chatBytesArray, chatNonce, keyPair); + + const res = await processTransactionVersion2Chat( + signedChatBytes, + customApi + ); + response = res; + } catch (e) { + console.error(e); + console.error(e.message); + response = false; + } + return response; +} +function sbrk(size, heap) { + let brk = 512 * 1024; // stack top + let old = brk; + brk += size; + if (brk > heap.length) throw new Error("heap exhausted"); + return old; +} + +export const computePow = async ({ chatBytes, path, difficulty }) => { + let response = null; + await new Promise((resolve, reject) => { + const _chatBytesArray = Object.keys(chatBytes).map(function (key) { + return chatBytes[key]; + }); + const chatBytesArray = new Uint8Array(_chatBytesArray); + const chatBytesHash = new Sha256().process(chatBytesArray).finish().result; + const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }); + const heap = new Uint8Array(memory.buffer); + + const hashPtr = sbrk(32, heap); + const hashAry = new Uint8Array(memory.buffer, hashPtr, 32); + hashAry.set(chatBytesHash); + const workBufferLength = 8 * 1024 * 1024; + const workBufferPtr = sbrk(workBufferLength, heap); + const importObject = { + env: { + memory: memory, + }, + }; + function loadWebAssembly(filename, imports) { + // Fetch the file and compile it + return fetch(filename) + .then((response) => response.arrayBuffer()) + .then((buffer) => WebAssembly.compile(buffer)) + .then((module) => { + // Create the instance. + return new WebAssembly.Instance(module, importObject); + }); + } + loadWebAssembly(path).then((wasmModule) => { + response = { + nonce: wasmModule.exports.compute2( + hashPtr, + workBufferPtr, + workBufferLength, + difficulty + ), + chatBytesArray, + }; + resolve(); + }); + }); + return response; +}; + +const getStoredData = async (key) => { + return new Promise((resolve, reject) => { + chrome.storage.local.get(key, (result) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(result[key]); + }); + }); +}; + +async function handleActiveGroupDataFromSocket({ groups, directs }) { + try { + chrome.runtime.sendMessage({ + action: "SET_GROUPS", + payload: groups, + }); + chrome.runtime.sendMessage({ + action: "SET_DIRECTS", + payload: directs, + }); + groups = groups; + directs = directs; + const activeData = { + groups: groups || [], // Your groups data here + directs: directs || [], // Your directs data here + }; + + // Save the active data to localStorage + chrome.storage.local.set({ "active-groups-directs": activeData }); + try { + handleNotification(groups); + handleNotificationDirect(directs); + } catch (error) {} + } catch (error) {} +} + +async function sendChat({ qortAddress, recipientPublicKey, message }) { + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let sendTimestamp = Date.now(); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + const difficulty = 8; + const jsonData = { + addresses: message.addresses, + foreignKey: message.foreignKey, + receivingAddress: message.receivingAddress, + }; + const finalJson = { + callRequest: jsonData, + extra: "whatever additional data goes here", + }; + const messageStringified = JSON.stringify(finalJson); + + const tx = await createTransaction(18, keyPair, { + timestamp: sendTimestamp, + recipient: qortAddress, + recipientPublicKey: recipientPublicKey, + hasChatReference: 0, + message: messageStringified, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + }); + if (!hasEnoughBalance) { + const _encryptedMessage = tx._encryptedMessage; + const encryptedMessageToBase58 = Base58.encode(_encryptedMessage); + return { + encryptedMessageToBase58, + signature: "id-" + Date.now() + "-" + Math.floor(Math.random() * 1000), + reference, + }; + } + const path = chrome.runtime.getURL("memory-pow.wasm.full"); + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + let _response = await signChatFunc( + chatBytesArray, + nonce, + "https://appnode.qortal.org", + keyPair + ); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + +async function sendChatGroup({ + groupId, + typeMessage, + chatReference, + messageText, +}) { + + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + // const balance = await getBalanceInfo(); + // const hasEnoughBalance = +balance < 4 ? false : true; + const difficulty = 8; + + const txBody = { + timestamp: Date.now(), + groupID: Number(groupId), + hasReceipient: 0, + hasChatReference: chatReference ? 1 : 0, + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, // Set default to not encrypted for groups + isText: 1, + } + + if(chatReference){ + txBody['chatReference'] = chatReference + } + + const tx = await createTransaction(181, keyPair, txBody); + + // if (!hasEnoughBalance) { + // throw new Error("Must have at least 4 QORT to send a chat message"); + // } + const path = chrome.runtime.getURL("memory-pow.wasm.full"); + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + let _response = await signChatFunc(chatBytesArray, nonce, null, keyPair); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + +async function sendChatDirect({ + address, + directTo, + typeMessage, + chatReference, + messageText, + publicKeyOfRecipient, + otherData +}) { + let recipientPublicKey; + let recipientAddress = address; + if (publicKeyOfRecipient) { + recipientPublicKey = publicKeyOfRecipient; + } else { + recipientAddress = await getNameOrAddress(directTo); + recipientPublicKey = await getPublicKey(recipientAddress); + } + if (!recipientAddress) { + recipientAddress = await getNameOrAddress(directTo); + } + + if (!recipientPublicKey) throw new Error("Cannot retrieve publickey"); + + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + // const balance = await getBalanceInfo(); + // const hasEnoughBalance = +balance < 4 ? false : true; + + const difficulty = 8; + + const finalJson = { + message: messageText, + version: 2, + ...(otherData || {}) + }; + const messageStringified = JSON.stringify(finalJson); + + const txBody = { + timestamp: Date.now(), + recipient: recipientAddress, + recipientPublicKey: recipientPublicKey, + hasChatReference: chatReference ? 1 : 0, + message: messageStringified, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + } + if(chatReference){ + txBody['chatReference'] = chatReference + } + const tx = await createTransaction(18, keyPair, txBody); + + // if (!hasEnoughBalance) { + // throw new Error("Must have at least 4 QORT to send a chat message"); + // } + const path = chrome.runtime.getURL("memory-pow.wasm.full"); + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + + let _response = await signChatFunc(chatBytesArray, nonce, null, keyPair); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + +async function decryptSingleFunc({ + messages, + secretKeyObject, + skipDecodeBase64, +}) { + let holdMessages = []; + + for (const message of messages) { + try { + const res = await decryptSingle({ + data64: message.data, + secretKeyObject, + skipDecodeBase64, + }); + + const decryptToUnit8Array = base64ToUint8Array(res); + const responseData = uint8ArrayToObject(decryptToUnit8Array); + holdMessages.push({ ...message, decryptedData: responseData }); + } catch (error) {} + } + return holdMessages; +} +async function decryptSingleForPublishes({ + messages, + secretKeyObject, + skipDecodeBase64, +}) { + let holdMessages = []; + + for (const message of messages) { + try { + const res = await decryptSingle({ + data64: message.data, + secretKeyObject, + skipDecodeBase64, + }); + + const decryptToUnit8Array = base64ToUint8Array(res); + const responseData = uint8ArrayToObject(decryptToUnit8Array); + holdMessages.push({ ...message, decryptedData: responseData }); + } catch (error) {} + } + return holdMessages; +} + +async function decryptDirectFunc({ messages, involvingAddress }) { + const senderPublicKey = await getPublicKey(involvingAddress); + let holdMessages = []; + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + for (const message of messages) { + try { + const decodedMessage = decryptChatMessage( + message.data, + keyPair.privateKey, + senderPublicKey, + message.reference + ); + const parsedMessage = JSON.parse(decodedMessage); + holdMessages.push({ ...message, ...parsedMessage }); + } catch (error) {} + } + return holdMessages; +} + +async function createBuyOrderTx({ crosschainAtInfo, useLocal }) { + try { + if(useLocal){ + const wallet = await getSaveWallet(); + + const address = wallet.address0; + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const message = { + addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + foreignKey: parsedData.ltcPrivateKey, + receivingAddress: address, + }; + let responseVar + const txn = new TradeBotRespondMultipleRequest().createTransaction(message) + const apiKey = await getApiKeyFromStorage(); + const responseFetch = await fetch(`${apiKey?.url}/crosschain/tradebot/respondmultiple?apiKey=${apiKey?.apikey}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(txn), + }); + + const res = await responseFetch.json(); + + if(res === false){ + responseVar = { response: "Unable to execute buy order", success: false }; + } else { + responseVar = { response: res, success: true }; + } + const { response, success } = responseVar + let responseMessage; + if (success) { + responseMessage = { + callResponse: response, + extra: { + message: 'Transaction processed successfully!', + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + + } + }; + } else { + responseMessage = { + callResponse: 'ERROR', + extra: { + message: response, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + + } + }; + } + + setTimeout(() => { + chrome.tabs.query({}, function (tabs) { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { + type: "RESPONSE_FOR_TRADES", + message: responseMessage, + }); + }); + }); + }, 5000); + + return + } + const wallet = await getSaveWallet(); + const address = wallet.address0; + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const message = { + addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + foreignKey: parsedData.ltcPrivateKey, + receivingAddress: address, + }; + const res = await sendChat({ + qortAddress: proxyAccountAddress, + recipientPublicKey: proxyAccountPublicKey, + message, + }); + if (res?.signature) { + listenForChatMessageForBuyOrder({ + nodeBaseUrl: buyTradeNodeBaseUrl, + senderAddress: proxyAccountAddress, + senderPublicKey: proxyAccountPublicKey, + signature: res?.signature, + }); + if (res?.encryptedMessageToBase58) { + return { + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + encryptedMessageToBase58: res?.encryptedMessageToBase58, + node: buyTradeNodeBaseUrl, + qortAddress: address, + chatSignature: res?.signature, + senderPublicKey: parsedData.publicKey, + sender: address, + reference: res?.reference, + }; + } + return { + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + chatSignature: res?.signature, + node: buyTradeNodeBaseUrl, + qortAddress: address, + }; + } else { + throw new Error("Unable to send buy order message"); + } + } catch (error) { + throw new Error(error.message); + } +} + +async function sendChatNotification( + res, + groupId, + secretKeyObject, + numberOfMembers +) { + try { + const data = await objectToBase64({ + type: "notification", + subType: "new-group-encryption", + data: { + timestamp: res.timestamp, + name: res.name, + message: `${res.name} has updated the encryption key`, + numberOfMembers, + }, + }); + + encryptSingle({ + data64: data, + secretKeyObject: secretKeyObject, + }) + .then((res2) => { + pauseAllQueues(); + sendChatGroup({ + groupId, + typeMessage: undefined, + chatReference: undefined, + messageText: res2, + }) + .then(() => {}) + .catch((error) => { + console.error("1", error.message); + }) + .finally(() => { + resumeAllQueues(); + }); + }) + .catch((error) => { + console.error("2", error.message); + }); + } catch (error) {} +} + +export const getFee = async (txType) => { + const timestamp = Date.now(); + const data = await reusableGet( + `/transactions/unitfee?txType=${txType}×tamp=${timestamp}` + ); + const arbitraryFee = (Number(data) / 1e8).toFixed(8); + + return { + timestamp, + fee: arbitraryFee, + }; +}; + +async function leaveGroup({ groupId }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("LEAVE_GROUP"); + + const tx = await createTransaction(32, keyPair, { + fee: feeres.fee, + registrantAddress: address, + rGroupId: groupId, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +export async function joinGroup({ groupId }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("JOIN_GROUP"); + + const tx = await createTransaction(31, keyPair, { + fee: feeres.fee, + registrantAddress: address, + rGroupId: groupId, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error(res?.message || "Transaction was not able to be processed"); + return res; +} + +async function cancelInvitationToGroup({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("CANCEL_GROUP_INVITE"); + + const tx = await createTransaction(30, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +async function cancelBan({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("CANCEL_GROUP_BAN"); + + const tx = await createTransaction(27, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} +async function registerName({ name }) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("REGISTER_NAME"); + + const tx = await createTransaction(3, keyPair, { + fee: feeres.fee, + name, + value: "", + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} +async function makeAdmin({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("ADD_GROUP_ADMIN"); + + const tx = await createTransaction(24, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +async function removeAdmin({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("REMOVE_GROUP_ADMIN"); + + const tx = await createTransaction(25, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +async function banFromGroup({ + groupId, + qortalAddress, + rBanReason = "", + rBanTime, +}) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("GROUP_BAN"); + + const tx = await createTransaction(26, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + rBanReason: rBanReason, + rBanTime, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +async function kickFromGroup({ groupId, qortalAddress, rBanReason = "" }) { + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const feeres = await getFee("GROUP_KICK"); + + const tx = await createTransaction(28, keyPair, { + fee: feeres.fee, + recipient: qortalAddress, + rGroupId: groupId, + rBanReason: rBanReason, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +async function createGroup({ + groupName, + groupDescription, + groupType, + groupApprovalThreshold, + minBlock, + maxBlock, +}) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + if (!address) throw new Error("Cannot find user"); + const lastReference = await getLastRef(); + const feeres = await getFee("CREATE_GROUP"); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const tx = await createTransaction(22, keyPair, { + fee: feeres.fee, + registrantAddress: address, + rGroupName: groupName, + rGroupDesc: groupDescription, + rGroupType: groupType, + rGroupApprovalThreshold: groupApprovalThreshold, + rGroupMinimumBlockDelay: minBlock, + rGroupMaximumBlockDelay: maxBlock, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} +async function inviteToGroup({ groupId, qortalAddress, inviteTime }) { + const address = await getNameOrAddress(qortalAddress); + if (!address) throw new Error("Cannot find user"); + const lastReference = await getLastRef(); + const feeres = await getFee("GROUP_INVITE"); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const tx = await createTransaction(29, keyPair, { + fee: feeres.fee, + recipient: address, + rGroupId: groupId, + rInviteTime: inviteTime, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; +} + +export async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { + try { + const confirmReceiver = await getNameOrAddress(receiver); + if (confirmReceiver.error) + throw new Error("Invalid receiver address or name"); + const wallet = await getSaveWallet(); + let keyPair = ""; + if (skipConfirmPassword) { + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + } else { + const response = await decryptStoredWallet(password, wallet); + const wallet2 = new PhraseWallet(response, walletVersion); + + keyPair = wallet2._addresses[0].keyPair; + } + + const lastRef = await getLastRef(); + const fee = await sendQortFee(); + const validApi = await findUsableApi(); + + const res = await makeTransactionRequest( + confirmReceiver, + lastRef, + amount, + fee, + keyPair, + validApi + ); + + return { res, validApi }; + } catch (error) { + throw new Error(error.message); + } +} + +function fetchMessages(apiCall) { + let retryDelay = 2000; // Start with a 2-second delay + const maxDuration = 360000 * 2; // Maximum duration set to 12 minutes + const startTime = Date.now(); // Record the start time + + // Promise to handle polling logic + return new Promise((resolve, reject) => { + const attemptFetch = async () => { + if (Date.now() - startTime > maxDuration) { + return reject(new Error("Maximum polling time exceeded")); + } + + try { + const response = await fetch(apiCall); + const data = await response.json(); + if (data && data.length > 0) { + resolve(data[0]); // Resolve the promise when data is found + } else { + setTimeout(attemptFetch, retryDelay); + retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes + } + } catch (error) { + reject(error); // Reject the promise on error + } + }; + + attemptFetch(); // Initial call to start the polling + }); +} + +async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { + let retryDelay = 2000; // Start with a 2-second delay + const maxDuration = 360000 * 2; // Maximum duration set to 12 minutes + const startTime = Date.now(); // Record the start time + let triedChatMessage = []; + // Promise to handle polling logic + await new Promise((res) => { + setTimeout(() => { + res(); + }, 40000); + }); + return new Promise((resolve, reject) => { + const attemptFetch = async () => { + if (Date.now() - startTime > maxDuration) { + return reject(new Error("Maximum polling time exceeded")); + } + + try { + const response = await fetch(apiCall); + let data = await response.json(); + + data = data.filter( + (item) => !triedChatMessage.includes(item.signature) + ); + if (data && data.length > 0) { + const encodedMessageObj = data[0]; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const decodedMessage = decryptChatMessage( + encodedMessageObj.data, + keyPair.privateKey, + senderPublicKey, + encodedMessageObj.reference + ); + const parsedMessage = JSON.parse(decodedMessage); + if (parsedMessage?.extra?.chatRequestSignature === signature) { + resolve(parsedMessage); + } else { + triedChatMessage.push(encodedMessageObj.signature); + setTimeout(attemptFetch, retryDelay); + retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes + } + // Resolve the promise when data is found + } else { + setTimeout(attemptFetch, retryDelay); + retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes + } + } catch (error) { + reject(error); // Reject the promise on error + } + }; + + attemptFetch(); // Initial call to start the polling + }); +} + +async function listenForChatMessage({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + timestamp, +}) { + try { + let validApi = ""; + const checkIfNodeBaseUrlIsAcceptable = apiEndpoints.find( + (item) => item === nodeBaseUrl + ); + if (checkIfNodeBaseUrlIsAcceptable) { + validApi = checkIfNodeBaseUrlIsAcceptable; + } else { + validApi = await findUsableApi(); + } + const wallet = await getSaveWallet(); + const address = wallet.address0; + const before = timestamp + 5000; + const after = timestamp - 5000; + const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}&encoding=BASE64`; + const encodedMessageObj = await fetchMessages(apiCall); + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const decodedMessage = decryptChatMessage( + encodedMessageObj.data, + keyPair.privateKey, + senderPublicKey, + encodedMessageObj.reference + ); + return { secretCode: decodedMessage }; + } catch (error) { + console.error(error); + throw new Error(error.message); + } +} + +async function listenForChatMessageForBuyOrder({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + signature, +}) { + try { + let validApi = ""; + const checkIfNodeBaseUrlIsAcceptable = apiEndpoints.find( + (item) => item === nodeBaseUrl + ); + if (checkIfNodeBaseUrlIsAcceptable) { + validApi = checkIfNodeBaseUrlIsAcceptable; + } else { + validApi = await findUsableApi(); + } + const wallet = await getSaveWallet(); + const address = wallet.address0; + const before = Date.now() + 1200000; + const after = Date.now(); + const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}&encoding=BASE64`; + const parsedMessageObj = await fetchMessagesForBuyOrders( + apiCall, + signature, + senderPublicKey + ); + + chrome.tabs.query({}, function (tabs) { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { + type: "RESPONSE_FOR_TRADES", + message: parsedMessageObj, + }); + }); + }); + } catch (error) { + console.error(error); + throw new Error(error.message); + } +} + +export function removeDuplicateWindow(popupUrl) { + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Filter to find popups matching the specific URL + const existingPopupsPending = windows.filter( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.pendingUrl && tab.pendingUrl.startsWith(popupUrl) + ) + ); + const existingPopups = windows.filter( + (w) => + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + + if (existingPopupsPending.length > 1) { + chrome.windows.remove( + existingPopupsPending?.[0]?.tabs?.[0]?.windowId, + () => {} + ); + } else if ( + existingPopupsPending.length > 0 && + existingPopups.length > 0 + ) { + chrome.windows.remove( + existingPopupsPending?.[0]?.tabs?.[0]?.windowId, + () => {} + ); + } + } + ); +} + +async function setChatHeads(data) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`chatheads-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); +} + +async function checkLocalFunc(){ + const apiKey = await getApiKeyFromStorage() + return !!apiKey + +} + +async function getTempPublish() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `tempPublish-${address}`; + const res = await chrome.storage.local.get([key]); + const SIX_MINUTES = 6 * 60 * 1000; // 6 minutes in milliseconds + + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + const currentTime = Date.now(); + + // Filter through each top-level key (e.g., "announcement") and then through its nested entries + const filteredData = Object.fromEntries( + Object.entries(parsedData).map(([category, entries]) => { + // Filter out entries inside each category that are older than 6 minutes + const filteredEntries = Object.fromEntries( + Object.entries(entries).filter(([entryKey, entryValue]) => { + return currentTime - entryValue.timestampSaved < SIX_MINUTES; + }) + ); + return [category, filteredEntries]; + }) + ); + + if (JSON.stringify(filteredData) !== JSON.stringify(parsedData)) { + const dataString = JSON.stringify(filteredData); + await chrome.storage.local.set({ [key]: dataString }); + } + return filteredData; + } else { + return {}; + } +} + +async function saveTempPublish({ data, key }) { + const existingTemp = await getTempPublish(); + const wallet = await getSaveWallet(); + const address = wallet.address0; + const newTemp = { + ...existingTemp, + [key]: { + ...(existingTemp[key] || {}), + [data.identifier]: { + data, + timestampSaved: Date.now(), + }, + }, + }; + + const dataString = JSON.stringify(newTemp); + + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`tempPublish-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(newTemp[key]); + } + }); + }); +} + +async function setChatHeadsDirect(data) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set( + { [`chatheads-direct-${address}`]: dataString }, + () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + } + ); + }); +} + +async function getTimestampEnterChat() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `enter-chat-timestamp-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return {}; + } +} +async function getTimestampGroupAnnouncement() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `group-announcement-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return {}; + } +} + +async function addTimestampGroupAnnouncement({ + groupId, + timestamp, + seenTimestamp, +}) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = (await getTimestampGroupAnnouncement()) || {}; + data[groupId] = { + notification: timestamp, + seentimestamp: seenTimestamp ? true : false, + }; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set( + { [`group-announcement-${address}`]: dataString }, + () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + } + ); + }); +} + +async function getGroupData() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `group-data-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return {}; + } +} +async function getGroupDataSingle(groupId) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `group-data-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData[groupId] || null; + } else { + return null; + } +} + +async function setGroupData({ + groupId, + secretKeyData, + secretKeyResource, + admins, +}) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = (await getGroupData()) || {}; + data[groupId] = { + timestampLastSet: Date.now(), + admins, + secretKeyData, + secretKeyResource, + }; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`group-data-${address}`]: dataString }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); + }); +} + +async function addTimestampEnterChat({ groupId, timestamp }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = await getTimestampEnterChat(); + data[groupId] = timestamp; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set( + { [`enter-chat-timestamp-${address}`]: dataString }, + () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + } + ); + }); +} + +async function notifyAdminRegenerateSecretKey({ groupName, adminAddress }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const name = await getNameInfo(address); + const nameOrAddress = name || address; + await sendChatDirect({ + directTo: adminAddress, + typeMessage: undefined, + chatReference: undefined, + messageText: `

Member ${nameOrAddress} has requested that you regenerate the group's secret key. Group: ${groupName}

`, + }); + return true; +} + +async function getChatHeads() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `chatheads-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + throw new Error("No Chatheads saved"); + } +} + +async function getChatHeadsDirect() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `chatheads-direct-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + throw new Error("No Chatheads saved"); + } +} +chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { + if (request) { + + console.log('REQUEST MESSAGE', request) + + switch (request.action) { + case "version": + // Example: respond with the version + sendResponse({ version: "1.0" }); + break; + case "storeWalletInfo": + chrome.storage.local.set({ walletInfo: request.wallet }, () => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else { + sendResponse({ result: "Data saved successfully" }); + } + }); + break; + case "getWalletInfo": + getKeyPair() + .then(() => { + chrome.storage.local.get(["walletInfo"], (result) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else if (result.walletInfo) { + sendResponse({ walletInfo: result.walletInfo }); + } else { + sendResponse({ error: "No wallet info found" }); + } + }); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + case "validApi": + findUsableApi() + .then((usableApi) => { + console.log("Usable API:", usableApi); + }) + .catch((error) => { + console.error(error.message); + }); + case "name": + getNameInfo() + .then((name) => { + sendResponse(name); + }) + .catch((error) => { + console.error(error.message); + }); + break; + case "userInfo": + getUserInfo() + .then((name) => { + sendResponse(name); + }) + .catch((error) => { + sendResponse({ error: "User not authenticated" }); + console.error(error.message); + }); + break; + case "decryptWallet": + { + const { password, wallet } = request.payload; + + decryptWallet({ + password, + wallet, + walletVersion, + }) + .then((hasDecrypted) => { + sendResponse(hasDecrypted); + }) + .catch((error) => { + sendResponse({ error: error?.message }); + console.error(error.message); + }); + } + + break; + case "balance": + getBalanceInfo() + .then((balance) => { + sendResponse(balance); + }) + .catch((error) => { + console.error(error.message); + }); + break; + case "ltcBalance": + { + getLTCBalance() + .then((balance) => { + sendResponse(balance); + }) + .catch((error) => { + console.error(error.message); + }); + } + break; + case "sendCoin": + { + const { receiver, password, amount } = request.payload; + sendCoin({ receiver, password, amount }) + .then(({ res }) => { + if (!res?.success) { + sendResponse({ error: res?.data?.message }); + return; + } + sendResponse(true); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "inviteToGroup": + { + const { groupId, qortalAddress, inviteTime } = request.payload; + inviteToGroup({ groupId, qortalAddress, inviteTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "saveTempPublish": + { + const { data, key } = request.payload; + saveTempPublish({ data, key }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true; + } + break; + case "getTempPublish": + { + getTempPublish() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "createGroup": + { + const { + groupName, + groupDescription, + groupType, + groupApprovalThreshold, + minBlock, + maxBlock, + } = request.payload; + createGroup({ + groupName, + groupDescription, + groupType, + groupApprovalThreshold, + minBlock, + maxBlock, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "cancelInvitationToGroup": + { + const { groupId, qortalAddress } = request.payload; + cancelInvitationToGroup({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "leaveGroup": + { + const { groupId } = request.payload; + leaveGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "joinGroup": + { + const { groupId } = request.payload; + joinGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "kickFromGroup": + { + const { groupId, qortalAddress, rBanReason } = request.payload; + kickFromGroup({ groupId, qortalAddress, rBanReason }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "banFromGroup": + { + const { groupId, qortalAddress, rBanReason, rBanTime } = + request.payload; + banFromGroup({ groupId, qortalAddress, rBanReason, rBanTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "addDataPublishes": + { + const { data, groupId, type } = request.payload; + addDataPublishes(data, groupId, type) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "getDataPublishes": + { + const { groupId, type } = request.payload; + getDataPublishes(groupId, type) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "addUserSettings": + { + const { keyValue } = request.payload; + addUserSettings({keyValue}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "getUserSettings": + { + const { key } = request.payload; + getUserSettings({key}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "cancelBan": + { + const { groupId, qortalAddress } = request.payload; + cancelBan({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "registerName": + { + const { name } = request.payload; + registerName({ name }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "makeAdmin": + { + const { groupId, qortalAddress } = request.payload; + makeAdmin({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "removeAdmin": + { + const { groupId, qortalAddress } = request.payload; + removeAdmin({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "oauth": { + const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = + request.payload; + + listenForChatMessage({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + timestamp, + }) + .then(({ secretCode }) => { + sendResponse(secretCode); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "setChatHeads": { + const { data } = request.payload; + + setChatHeads({ + data, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "getChatHeads": { + getChatHeads() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "notification": { + const notificationId = "chat_notification_" + Date.now(); // Create a unique ID + + const {} = request.payload; + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: "New Group Message!", + message: "You have received a new message from one of your groups", + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + // Set a timeout to clear the notification after 'timeout' milliseconds + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 3000); + sendResponse(true); + break; + } + case "addTimestampEnterChat": { + const { groupId, timestamp } = request.payload; + addTimestampEnterChat({ groupId, timestamp }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + + case "setApiKey": { + const { payload } = request; + + // Save the apiKey in chrome.storage.local for persistence + chrome.storage.local.set({ apiKey: payload }, () => { + sendResponse(true); + }); + return true; + break; + } + case "setCustomNodes": { + const { nodes } = request; + + // Save the customNodes in chrome.storage.local for persistence + chrome.storage.local.set({ customNodes: nodes }, () => { + sendResponse(true); + }); + return true; + break; + } + case "getApiKey": { + getApiKeyFromStorage() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true; + break; + } + case "getCustomNodesFromStorage": { + getCustomNodesFromStorage() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true; + break; + } + + case "notifyAdminRegenerateSecretKey": { + const { groupName, adminAddress } = request.payload; + notifyAdminRegenerateSecretKey({ groupName, adminAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + + case "addGroupNotificationTimestamp": { + const { groupId, timestamp } = request.payload; + addTimestampGroupAnnouncement({ + groupId, + timestamp, + seenTimestamp: true, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "clearAllNotifications": { + clearAllNotifications() + .then((res) => {}) + .catch((error) => {}); + break; + } + case "setGroupData": { + const { groupId, secretKeyData, secretKeyResource, admins } = + request.payload; + setGroupData({ + groupId, + secretKeyData, + secretKeyResource, + admins, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getGroupDataSingle": { + const { groupId } = request.payload; + getGroupDataSingle(groupId) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true; + break; + } + case "getTimestampEnterChat": { + getTimestampEnterChat() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getGroupNotificationTimestamp": { + getTimestampGroupAnnouncement() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "authentication": + { + getSaveWallet() + .then(() => { + sendResponse(true); + }) + .catch((error) => { + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ) + ); + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } else { + // No existing popup found, create a new one + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create( + { + url: chrome.runtime.getURL( + "index.html?secondary=true" + ), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: request.timeout ? 0.75 * request.timeout : 60, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_REQUEST_AUTHENTICATION", + payload: { + hostname, + interactionId, + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + let intervalId = null; + const startTime = Date.now(); + const checkInterval = 3000; // Check every 3 seconds + const timeout = request.timeout + ? 0.75 * (request.timeout * 1000) + : 60000; // Stop after 15 seconds + + const checkFunction = () => { + getSaveWallet() + .then(() => { + clearInterval(intervalId); // Stop checking + sendResponse(true); // Perform the success action + chrome.runtime.sendMessage({ + action: "closePopup", + }); + }) + .catch((error) => { + // Handle error if needed + }); + + if (Date.now() - startTime > timeout) { + sendResponse({ + error: "User has not authenticated, try again.", + }); + clearInterval(intervalId); // Stop checking due to timeout + + // Handle timeout situation if needed + } + }; + + intervalId = setInterval(checkFunction, checkInterval); + } + ); + }); + } + break; + case "buyOrder": + { + const { qortalAtAddresses, hostname, useLocal } = request.payload; + getTradesInfo(qortalAtAddresses) + .then((crosschainAtInfo) => { + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ) + ); + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } else { + // No existing popup found, create a new one + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create( + { + url: chrome.runtime.getURL( + "index.html?secondary=true" + ), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: request.timeout ? 0.9 * request.timeout : 20, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_REQUEST_BUY_ORDER", + payload: { + hostname, + crosschainAtInfo, + interactionId, + useLocal + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); + }) + .catch((error) => { + console.error(error.message); + }); + } + + break; + case "connection": + { + const { hostname } = request.payload; + + connection(hostname) + .then((isConnected) => { + if ( + Object.keys(isConnected)?.length > 0 && + isConnected[hostname] + ) { + sendResponse(true); + } else { + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ) + ); + + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } else { + // No existing popup found, create a new one + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create( + { + url: popupUrl, + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: request.timeout ? 0.9 * request.timeout : 20, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_REQUEST_CONNECTION", + payload: { + hostname, + interactionId, + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); + } + }) + .catch((error) => { + console.error(error.message); + }); + } + + break; + case "sendQort": + { + const { amount, hostname, address, description } = request.payload; + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } else { + // No existing popup found, create a new one + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create( + { + url: chrome.runtime.getURL("index.html?secondary=true"), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: (request.timeout ? request.timeout : 60) - 6, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_CONFIRM_SEND_QORT", + payload: { + amount, + address, + hostname, + description, + interactionId, + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); + } + + break; + case "responseToConnectionRequest": + { + const { hostname, isOkay } = request.payload; + const interactionId3 = request.payload.interactionId; + if (!isOkay) { + const originalSendResponse = pendingResponses.get(interactionId3); + if (originalSendResponse) { + originalSendResponse(false); + sendResponse(false); + } + } else { + const originalSendResponse = pendingResponses.get(interactionId3); + if (originalSendResponse) { + // Example of setting domain permission + chrome.storage.local.set({ [hostname]: true }); + + originalSendResponse(true); + sendResponse(true); + } + } + + pendingResponses.delete(interactionId3); + } + + break; + case "sendQortConfirmation": + const { password, amount, receiver, isDecline } = request.payload; + const interactionId2 = request.payload.interactionId; + // Retrieve the stored sendResponse callback + const originalSendResponse = pendingResponses.get(interactionId2); + + if (originalSendResponse) { + if (isDecline) { + originalSendResponse({ error: "User has declined" }); + sendResponse(false); + pendingResponses.delete(interactionId2); + return; + } + sendCoin({ password, amount, receiver }, true) + .then((res) => { + sendResponse(true); + // Use the sendResponse callback to respond to the original message + originalSendResponse(res); + // Remove the callback from the Map as it's no longer needed + pendingResponses.delete(interactionId2); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + originalSendResponse({ error: error.message }); + }); + } + + break; + case "buyOrderConfirmation": + { + const { crosschainAtInfo, isDecline, useLocal } = request.payload; + const interactionId2 = request.payload.interactionId; + // Retrieve the stored sendResponse callback + const originalSendResponse = pendingResponses.get(interactionId2); + + if (originalSendResponse) { + if (isDecline) { + originalSendResponse({ error: "User has declined" }); + sendResponse(false); + pendingResponses.delete(interactionId2); + return; + } + createBuyOrderTx({ crosschainAtInfo, useLocal }) + .then((res) => { + sendResponse(true); + originalSendResponse(res); + pendingResponses.delete(interactionId2); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + // originalSendResponse({ error: error.message }); + }); + } + } + + break; + case "encryptAndPublishSymmetricKeyGroupChat": { + const { groupId, previousData, previousNumber } = request.payload; + + encryptAndPublishSymmetricKeyGroupChat({ + groupId, + previousData, + previousNumber, + }) + .then(({ data, numberOfMembers }) => { + sendResponse(data); + + if (!previousData) { + // first secret key of the group + sendChatGroup({ + groupId, + typeMessage: undefined, + chatReference: undefined, + messageText: PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY, + }) + .then(() => {}) + .catch((error) => { + console.error("1", error.message); + }); + return; + } + sendChatNotification(data, groupId, previousData, numberOfMembers); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "publishGroupEncryptedResource": { + const { encryptedData, identifier } = request.payload; + + publishGroupEncryptedResource({ + encryptedData, + identifier, + }) + .then((data) => { + sendResponse(data); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + return true; + break; + } + case "publishOnQDN": { + const { data, identifier, service, title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, uploadType } = request.payload; + + publishOnQDN({ + data, + identifier, + service, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + uploadType + }) + .then((data) => { + sendResponse(data); + }) + .catch((error) => { + console.error(error?.message); + sendResponse({ error: error?.message || 'Unable to publish' }); + }); + return true; + break; + } + case "handleActiveGroupDataFromSocket": { + const { groups, directs } = request.payload; + handleActiveGroupDataFromSocket({ + groups, + directs, + }) + .then((data) => { + sendResponse(true); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "getThreadActivity": { + checkThreads(true) + .then((data) => { + sendResponse(data); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "updateThreadActivity": { + const { threadId, qortalName, groupId, thread } = request.payload; + + updateThreadActivity({ threadId, qortalName, groupId, thread }) + .then(() => { + sendResponse(true); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "decryptGroupEncryption": { + const { data } = request.payload; + + decryptGroupEncryption({ data }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "encryptSingle": { + const { data, secretKeyObject, typeNumber } = request.payload; + + encryptSingle({ data64: data, secretKeyObject: secretKeyObject, typeNumber }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "decryptSingle": { + const { data, secretKeyObject, skipDecodeBase64 } = request.payload; + + decryptSingleFunc({ messages: data, secretKeyObject, skipDecodeBase64 }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "pauseAllQueues": { + pauseAllQueues(); + sendResponse(true); + + break; + + } + case "resumeAllQueues": { + resumeAllQueues(); + sendResponse(true); + + break; + } + case "checkLocal": { + checkLocalFunc() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + + break; + } + case "decryptSingleForPublishes": { + const { data, secretKeyObject, skipDecodeBase64 } = request.payload; + + decryptSingleForPublishes({ + messages: data, + secretKeyObject, + skipDecodeBase64, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "decryptDirect": { + const { data, involvingAddress } = request.payload; + + decryptDirectFunc({ messages: data, involvingAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "sendChatGroup": { + const { + groupId, + typeMessage = undefined, + chatReference = undefined, + messageText, + } = request.payload; + + sendChatGroup({ groupId, typeMessage, chatReference, messageText }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "sendChatDirect": { + const { + directTo, + typeMessage = undefined, + chatReference = undefined, + messageText, + publicKeyOfRecipient, + address, + otherData + } = request.payload; + + sendChatDirect({ + directTo, + chatReference, + messageText, + typeMessage, + publicKeyOfRecipient, + address, + otherData + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "setupGroupWebsocket": { + checkNewMessages(); + checkThreads(); + sendResponse(true); + + break; + } + + case "logout": + { + try { + const logoutFunc = async () => { + forceCloseWebSocket(); + clearAllQueues(); + if (interval) { + // for announcement notification + clearInterval(interval); + } + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key1 = `tempPublish-${address}`; + const key2 = `group-data-${address}`; + const key3 = `${address}-publishData`; + chrome.storage.local.remove( + [ + "keyPair", + "walletInfo", + "active-groups-directs", + key1, + key2, + key3, + ], + () => { + if (chrome.runtime.lastError) { + // Handle error + console.error(chrome.runtime.lastError.message); + } else { + chrome.tabs.query({}, function (tabs) { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); + }); + }); + // Data removed successfully + sendResponse(true); + } + } + ); + }; + logoutFunc(); + } catch (error) {} + } + + break; + } + } + return true; +}); + +// Function to save window position and size +const saveWindowBounds = (windowId) => { + chrome.windows.get(windowId, (window) => { + const { top, left, width, height } = window; + chrome.storage.local.set( + { + windowBounds: { top, left, width, height }, + }, + () => { + console.log("Window bounds saved:", { top, left, width, height }); + } + ); + }); +}; + +// Function to restore window position and size +const restoreWindowBounds = (callback) => { + chrome.storage.local.get("windowBounds", (data) => { + if (data.windowBounds) { + callback(data.windowBounds); + } else { + callback(null); // No saved bounds, use default size/position + } + }); +}; + +chrome.action?.onClicked?.addListener((tab) => { + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find((w) => { + return ( + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + }); + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + + if (isMobile) { + const correctTab = existingPopup.tabs.find( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ); + if (correctTab) { + chrome.tabs.update(correctTab.id, { active: true }); + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } + } else { + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } + } else { + // No existing popup found, restore the saved bounds or create a new one + restoreWindowBounds((savedBounds) => { + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const screenHeight = primaryDisplay.bounds.height; + + // Create a new window that uses the saved bounds if available + chrome.windows.create( + { + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, + (newWindow) => { + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); + + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + } + ); + }); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "INITIATE_MAIN", + payload: {}, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); +}); + +const checkGroupList = async () => { + try { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const url = await createEndpoint(`/chat/active/${address}`); + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + + const filteredGroups = + data.groups?.filter((item) => item?.groupId !== 0) || []; + const sortedGroups = filteredGroups.sort( + (a, b) => (b.timestamp || 0) - (a.timestamp || 0) + ); + const sortedDirects = (data?.direct || []) + .filter( + (item) => + item?.name !== "extension-proxy" && + item?.address !== "QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH" + ) + .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + + handleActiveGroupDataFromSocket({ + groups: sortedGroups, + directs: sortedDirects, + }); + } catch (error) { + console.error(error); + } finally { + } +}; + +const checkActiveChatsForNotifications = async () => { + try { + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find((w) => { + return ( + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + }); + + if (existingPopup) { + } else { + checkGroupList(); + } + } + ); + } catch (error) {} +}; +chrome.notifications?.onClicked?.addListener((notificationId) => { + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + const isDirect = notificationId.includes("_type=direct_"); + const isGroup = notificationId.includes("_type=group_"); + const isGroupAnnouncement = notificationId.includes( + "_type=group-announcement_" + ); + const isNewThreadPost = notificationId.includes("_type=thread-post_"); + + let isExisting = false; + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + async (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find((w) => { + return ( + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + }); + + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + isExisting = true; + } else { + // No existing popup found, restore saved bounds or create a new one + restoreWindowBounds((savedBounds) => { + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const screenHeight = primaryDisplay.bounds.height; + + // Create a new window that takes up the full screen or uses saved bounds + chrome.windows.create( + { + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, + (newWindow) => { + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); + + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + } + ); + }); + }); + } + const activeData = (await getStoredData("active-groups-directs")) || { + groups: [], + directs: [], + }; + setTimeout( + () => { + chrome.runtime.sendMessage({ + action: "SET_GROUPS", + payload: activeData?.groups || [], + }); + chrome.runtime.sendMessage({ + action: "SET_DIRECTS", + payload: activeData?.directs || [], + }); + }, + isExisting ? 100 : 1000 + ); + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout( + () => { + chrome.runtime.sendMessage({ + action: "INITIATE_MAIN", + payload: {}, + }); + + // Handle different types of notifications + if (isDirect) { + const fromValue = notificationId.split("_from=")[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_DIRECT", + payload: { from: fromValue }, + }); + } else if (isGroup) { + const fromValue = notificationId.split("_from=")[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_GROUP", + payload: { from: fromValue }, + }); + } else if (isGroupAnnouncement) { + const fromValue = notificationId.split("_from=")[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP", + payload: { from: fromValue }, + }); + } else if (isNewThreadPost) { + const dataValue = notificationId.split("_data=")[1]; + const dataParsed = JSON.parse(dataValue); + + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_THREAD_NEW_POST", + payload: { data: dataParsed }, + }); + } + }, + isExisting ? 400 : 3000 + ); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); +}); + +// Reconnect when service worker wakes up +chrome.runtime?.onStartup.addListener(() => { + console.log("Service worker started up, reconnecting WebSocket..."); +}); + +chrome.runtime?.onInstalled.addListener((details) => { + if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { + console.log("Extension Installed"); + // Perform tasks that should only happen on extension installation + // Example: Initialize WebSocket, set default settings, etc. + } else if (details.reason === chrome.runtime.OnInstalledReason.UPDATE) { + console.log("Extension Updated"); + // Handle the update logic here (e.g., migrate settings) + } else if ( + details.reason === chrome.runtime.OnInstalledReason.CHROME_UPDATE + ) { + console.log("Chrome updated"); + // Optional: Handle Chrome-specific updates if necessary + } + + +}); + +// Check if the alarm already exists before creating it +chrome.alarms?.get("checkForNotifications", (existingAlarm) => { + if (!existingAlarm) { + // If the alarm does not exist, create it + chrome.alarms.create("checkForNotifications", { periodInMinutes: 10 }); + } +}); + +chrome.alarms?.onAlarm.addListener(async (alarm) => { + try { + if (alarm.name === "checkForNotifications") { + const wallet = await getSaveWallet(); + const address = wallet.address0; + if (!address) return; + checkActiveChatsForNotifications(); + checkNewMessages(); + checkThreads(); + } + } catch (error) {} +}); diff --git a/src/background.ts b/src/background.ts index 414a382..c9bbdb6 100644 --- a/src/background.ts +++ b/src/background.ts @@ -28,6 +28,7 @@ import { validateAddress } from "./utils/validateAddress"; import { Sha256 } from "asmcrypto.js"; import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; +import { balanceCase, decryptWalletCase, getWalletInfoCase, ltcBalanceCase, nameCase, sendCoinCase, userInfoCase, validApiCase, versionCase } from './background-cases'; export function cleanUrl(url) { return url?.replace(/^(https?:\/\/)?(www\.)?/, ''); @@ -941,7 +942,7 @@ const forceCloseWebSocket = () => { } }; -async function getNameInfo() { +export async function getNameInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; const validApi = await getBaseApi(); @@ -993,7 +994,7 @@ async function clearAllNotifications() { } } -async function getUserInfo() { +export async function getUserInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; const addressInfo = await getAddressInfo(address); @@ -1036,7 +1037,7 @@ export async function getBalanceInfo() { const data = await response.json(); return data; } -async function getLTCBalance() { +export async function getLTCBalance() { const wallet = await getSaveWallet(); let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`; const keyPair = await getKeyPair(); @@ -1355,7 +1356,7 @@ async function addUserSettings({ keyValue }) { }); } -async function decryptWallet({ password, wallet, walletVersion }) { +export async function decryptWallet({ password, wallet, walletVersion }) { try { const response = await decryptStoredWallet(password, wallet); const wallet2 = new PhraseWallet(response, walletVersion); @@ -2824,1389 +2825,63 @@ async function getChatHeadsDirect() { throw new Error("No Chatheads saved"); } } -chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { - if (request) { +// TODO: listener - console.log('REQUEST MESSAGE', request) + +function setupMessageListener() { + window.addEventListener("message", async (event) => { + const request = event.data; + + // Check if the message is intended for this listener + if (request?.type !== "backgroundMessage") return; // Only process messages of type 'backgroundMessage' + + + console.log("REQUEST MESSAGE", request); switch (request.action) { case "version": - // Example: respond with the version - sendResponse({ version: "1.0" }); + versionCase(request, event); break; - case "storeWalletInfo": - chrome.storage.local.set({ walletInfo: request.wallet }, () => { - if (chrome.runtime.lastError) { - sendResponse({ error: chrome.runtime.lastError.message }); - } else { - sendResponse({ result: "Data saved successfully" }); - } - }); - break; - case "getWalletInfo": - getKeyPair() - .then(() => { - chrome.storage.local.get(["walletInfo"], (result) => { - if (chrome.runtime.lastError) { - sendResponse({ error: chrome.runtime.lastError.message }); - } else if (result.walletInfo) { - sendResponse({ walletInfo: result.walletInfo }); - } else { - sendResponse({ error: "No wallet info found" }); - } - }); - }) - .catch((error) => { - sendResponse({ error: error.message }); - }); + + // case "storeWalletInfo": + // storeWalletInfoCase(request, event); + // break; + case "getWalletInfo": + getWalletInfoCase(request, event); break; + case "validApi": - findUsableApi() - .then((usableApi) => { - console.log("Usable API:", usableApi); - }) - .catch((error) => { - console.error(error.message); - }); + validApiCase(request, event) + break; + case "name": - getNameInfo() - .then((name) => { - sendResponse(name); - }) - .catch((error) => { - console.error(error.message); - }); + nameCase(request, event) break; case "userInfo": - getUserInfo() - .then((name) => { - sendResponse(name); - }) - .catch((error) => { - sendResponse({ error: "User not authenticated" }); - console.error(error.message); - }); + userInfoCase(request, event) break; case "decryptWallet": - { - const { password, wallet } = request.payload; - - decryptWallet({ - password, - wallet, - walletVersion, - }) - .then((hasDecrypted) => { - sendResponse(hasDecrypted); - }) - .catch((error) => { - sendResponse({ error: error?.message }); - console.error(error.message); - }); - } - - break; + decryptWalletCase(request, event) + break; case "balance": - getBalanceInfo() - .then((balance) => { - sendResponse(balance); - }) - .catch((error) => { - console.error(error.message); - }); - break; + balanceCase(request, event) + break; case "ltcBalance": - { - getLTCBalance() - .then((balance) => { - sendResponse(balance); - }) - .catch((error) => { - console.error(error.message); - }); - } - break; - case "sendCoin": - { - const { receiver, password, amount } = request.payload; - sendCoin({ receiver, password, amount }) - .then(({ res }) => { - if (!res?.success) { - sendResponse({ error: res?.data?.message }); - return; - } - sendResponse(true); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "inviteToGroup": - { - const { groupId, qortalAddress, inviteTime } = request.payload; - inviteToGroup({ groupId, qortalAddress, inviteTime }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "saveTempPublish": - { - const { data, key } = request.payload; - saveTempPublish({ data, key }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - return true; - } - break; - case "getTempPublish": - { - getTempPublish() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - - case "createGroup": - { - const { - groupName, - groupDescription, - groupType, - groupApprovalThreshold, - minBlock, - maxBlock, - } = request.payload; - createGroup({ - groupName, - groupDescription, - groupType, - groupApprovalThreshold, - minBlock, - maxBlock, - }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "cancelInvitationToGroup": - { - const { groupId, qortalAddress } = request.payload; - cancelInvitationToGroup({ groupId, qortalAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "leaveGroup": - { - const { groupId } = request.payload; - leaveGroup({ groupId }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "joinGroup": - { - const { groupId } = request.payload; - joinGroup({ groupId }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - - case "kickFromGroup": - { - const { groupId, qortalAddress, rBanReason } = request.payload; - kickFromGroup({ groupId, qortalAddress, rBanReason }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "banFromGroup": - { - const { groupId, qortalAddress, rBanReason, rBanTime } = - request.payload; - banFromGroup({ groupId, qortalAddress, rBanReason, rBanTime }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "addDataPublishes": - { - const { data, groupId, type } = request.payload; - addDataPublishes(data, groupId, type) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - break; - case "getDataPublishes": - { - const { groupId, type } = request.payload; - getDataPublishes(groupId, type) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - break; - case "addUserSettings": - { - const { keyValue } = request.payload; - addUserSettings({keyValue}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - break; - case "getUserSettings": - { - const { key } = request.payload; - getUserSettings({key}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - break; - case "cancelBan": - { - const { groupId, qortalAddress } = request.payload; - cancelBan({ groupId, qortalAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "registerName": - { - const { name } = request.payload; - registerName({ name }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "makeAdmin": - { - const { groupId, qortalAddress } = request.payload; - makeAdmin({ groupId, qortalAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "removeAdmin": - { - const { groupId, qortalAddress } = request.payload; - removeAdmin({ groupId, qortalAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - - case "oauth": { - const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = - request.payload; - - listenForChatMessage({ - nodeBaseUrl, - senderAddress, - senderPublicKey, - timestamp, - }) - .then(({ secretCode }) => { - sendResponse(secretCode); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - - break; - } - case "setChatHeads": { - const { data } = request.payload; - - setChatHeads({ - data, - }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - - break; - } - case "getChatHeads": { - getChatHeads() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - - break; - } - case "notification": { - const notificationId = "chat_notification_" + Date.now(); // Create a unique ID - - const {} = request.payload; - chrome.notifications.create(notificationId, { - type: "basic", - iconUrl: "qort.png", // Add an appropriate icon for chat notifications - title: "New Group Message!", - message: "You have received a new message from one of your groups", - priority: 2, // Use the maximum priority to ensure it's noticeable - // buttons: [ - // { title: 'Go to group' } - // ] - }); - // Set a timeout to clear the notification after 'timeout' milliseconds - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 3000); - sendResponse(true); - break; - } - case "addTimestampEnterChat": { - const { groupId, timestamp } = request.payload; - addTimestampEnterChat({ groupId, timestamp }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - - case "setApiKey": { - const { payload } = request; - - // Save the apiKey in chrome.storage.local for persistence - chrome.storage.local.set({ apiKey: payload }, () => { - sendResponse(true); - }); - return true; - break; - } - case "setCustomNodes": { - const { nodes } = request; - - // Save the customNodes in chrome.storage.local for persistence - chrome.storage.local.set({ customNodes: nodes }, () => { - sendResponse(true); - }); - return true; - break; - } - case "getApiKey": { - getApiKeyFromStorage() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - return true; - break; - } - case "getCustomNodesFromStorage": { - getCustomNodesFromStorage() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - return true; - break; - } - - case "notifyAdminRegenerateSecretKey": { - const { groupName, adminAddress } = request.payload; - notifyAdminRegenerateSecretKey({ groupName, adminAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - - case "addGroupNotificationTimestamp": { - const { groupId, timestamp } = request.payload; - addTimestampGroupAnnouncement({ - groupId, - timestamp, - seenTimestamp: true, - }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - case "clearAllNotifications": { - clearAllNotifications() - .then((res) => {}) - .catch((error) => {}); - break; - } - case "setGroupData": { - const { groupId, secretKeyData, secretKeyResource, admins } = - request.payload; - setGroupData({ - groupId, - secretKeyData, - secretKeyResource, - admins, - }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - case "getGroupDataSingle": { - const { groupId } = request.payload; - getGroupDataSingle(groupId) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - return true; - break; - } - case "getTimestampEnterChat": { - getTimestampEnterChat() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - case "getGroupNotificationTimestamp": { - getTimestampGroupAnnouncement() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - case "authentication": - { - getSaveWallet() - .then(() => { - sendResponse(true); - }) - .catch((error) => { - const popupUrl = chrome.runtime.getURL( - "index.html?secondary=true" - ); - - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - w.tabs && - w.tabs.some( - (tab) => tab.url && tab.url.startsWith(popupUrl) - ) - ); - if (existingPopup) { - // If the popup exists but is minimized or not focused, focus it - chrome.windows.update(existingPopup.id, { - focused: true, - state: "normal", - }); - } else { - // No existing popup found, create a new one - chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) - const primaryDisplay = displays[0]; - const screenWidth = primaryDisplay.bounds.width; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width - - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; - - // Calculate top position for the window, adjust as desired - const topPosition = - (primaryDisplay.bounds.height - windowHeight) / 2; - - chrome.windows.create( - { - url: chrome.runtime.getURL( - "index.html?secondary=true" - ), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, - () => { - removeDuplicateWindow(popupUrl); - } - ); - }); - } - - const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: request.timeout ? 0.75 * request.timeout : 60, - }); - chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_AUTHENTICATION", - payload: { - hostname, - interactionId, - }, - }); - }, 500); - - // Store sendResponse callback with the interaction ID - pendingResponses.set(interactionId, sendResponse); - let intervalId = null; - const startTime = Date.now(); - const checkInterval = 3000; // Check every 3 seconds - const timeout = request.timeout - ? 0.75 * (request.timeout * 1000) - : 60000; // Stop after 15 seconds - - const checkFunction = () => { - getSaveWallet() - .then(() => { - clearInterval(intervalId); // Stop checking - sendResponse(true); // Perform the success action - chrome.runtime.sendMessage({ - action: "closePopup", - }); - }) - .catch((error) => { - // Handle error if needed - }); - - if (Date.now() - startTime > timeout) { - sendResponse({ - error: "User has not authenticated, try again.", - }); - clearInterval(intervalId); // Stop checking due to timeout - - // Handle timeout situation if needed - } - }; - - intervalId = setInterval(checkFunction, checkInterval); - } - ); - }); - } - break; - case "buyOrder": - { - const { qortalAtAddresses, hostname, useLocal } = request.payload; - getTradesInfo(qortalAtAddresses) - .then((crosschainAtInfo) => { - const popupUrl = chrome.runtime.getURL( - "index.html?secondary=true" - ); - - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - w.tabs && - w.tabs.some( - (tab) => tab.url && tab.url.startsWith(popupUrl) - ) - ); - if (existingPopup) { - // If the popup exists but is minimized or not focused, focus it - chrome.windows.update(existingPopup.id, { - focused: true, - state: "normal", - }); - } else { - // No existing popup found, create a new one - chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) - const primaryDisplay = displays[0]; - const screenWidth = primaryDisplay.bounds.width; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width - - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; - - // Calculate top position for the window, adjust as desired - const topPosition = - (primaryDisplay.bounds.height - windowHeight) / 2; - - chrome.windows.create( - { - url: chrome.runtime.getURL( - "index.html?secondary=true" - ), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, - () => { - removeDuplicateWindow(popupUrl); - } - ); - }); - } - - const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: request.timeout ? 0.9 * request.timeout : 20, - }); - chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_BUY_ORDER", - payload: { - hostname, - crosschainAtInfo, - interactionId, - useLocal - }, - }); - }, 500); - - // Store sendResponse callback with the interaction ID - pendingResponses.set(interactionId, sendResponse); - } - ); - }) - .catch((error) => { - console.error(error.message); - }); - } - - break; - case "connection": - { - const { hostname } = request.payload; - - connection(hostname) - .then((isConnected) => { - if ( - Object.keys(isConnected)?.length > 0 && - isConnected[hostname] - ) { - sendResponse(true); - } else { - const popupUrl = chrome.runtime.getURL( - "index.html?secondary=true" - ); - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - w.tabs && - w.tabs.some( - (tab) => tab.url && tab.url.startsWith(popupUrl) - ) - ); - - if (existingPopup) { - // If the popup exists but is minimized or not focused, focus it - chrome.windows.update(existingPopup.id, { - focused: true, - state: "normal", - }); - } else { - // No existing popup found, create a new one - chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) - const primaryDisplay = displays[0]; - const screenWidth = primaryDisplay.bounds.width; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width - - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; - - // Calculate top position for the window, adjust as desired - const topPosition = - (primaryDisplay.bounds.height - windowHeight) / 2; - - chrome.windows.create( - { - url: popupUrl, - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, - () => { - removeDuplicateWindow(popupUrl); - } - ); - }); - } - - const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: request.timeout ? 0.9 * request.timeout : 20, - }); - chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_CONNECTION", - payload: { - hostname, - interactionId, - }, - }); - }, 500); - - // Store sendResponse callback with the interaction ID - pendingResponses.set(interactionId, sendResponse); - } - ); - } - }) - .catch((error) => { - console.error(error.message); - }); - } - - break; - case "sendQort": - { - const { amount, hostname, address, description } = request.payload; - const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); - - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - w.tabs && - w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) - ); - if (existingPopup) { - // If the popup exists but is minimized or not focused, focus it - chrome.windows.update(existingPopup.id, { - focused: true, - state: "normal", - }); - } else { - // No existing popup found, create a new one - chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) - const primaryDisplay = displays[0]; - const screenWidth = primaryDisplay.bounds.width; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width - - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; - - // Calculate top position for the window, adjust as desired - const topPosition = - (primaryDisplay.bounds.height - windowHeight) / 2; - - chrome.windows.create( - { - url: chrome.runtime.getURL("index.html?secondary=true"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, - () => { - removeDuplicateWindow(popupUrl); - } - ); - }); - } - - const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: (request.timeout ? request.timeout : 60) - 6, - }); - chrome.runtime.sendMessage({ - action: "UPDATE_STATE_CONFIRM_SEND_QORT", - payload: { - amount, - address, - hostname, - description, - interactionId, - }, - }); - }, 500); - - // Store sendResponse callback with the interaction ID - pendingResponses.set(interactionId, sendResponse); - } - ); - } - - break; - case "responseToConnectionRequest": - { - const { hostname, isOkay } = request.payload; - const interactionId3 = request.payload.interactionId; - if (!isOkay) { - const originalSendResponse = pendingResponses.get(interactionId3); - if (originalSendResponse) { - originalSendResponse(false); - sendResponse(false); - } - } else { - const originalSendResponse = pendingResponses.get(interactionId3); - if (originalSendResponse) { - // Example of setting domain permission - chrome.storage.local.set({ [hostname]: true }); - - originalSendResponse(true); - sendResponse(true); - } - } - - pendingResponses.delete(interactionId3); - } - - break; - case "sendQortConfirmation": - const { password, amount, receiver, isDecline } = request.payload; - const interactionId2 = request.payload.interactionId; - // Retrieve the stored sendResponse callback - const originalSendResponse = pendingResponses.get(interactionId2); - - if (originalSendResponse) { - if (isDecline) { - originalSendResponse({ error: "User has declined" }); - sendResponse(false); - pendingResponses.delete(interactionId2); - return; - } - sendCoin({ password, amount, receiver }, true) - .then((res) => { - sendResponse(true); - // Use the sendResponse callback to respond to the original message - originalSendResponse(res); - // Remove the callback from the Map as it's no longer needed - pendingResponses.delete(interactionId2); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - originalSendResponse({ error: error.message }); - }); - } - - break; - case "buyOrderConfirmation": - { - const { crosschainAtInfo, isDecline, useLocal } = request.payload; - const interactionId2 = request.payload.interactionId; - // Retrieve the stored sendResponse callback - const originalSendResponse = pendingResponses.get(interactionId2); - - if (originalSendResponse) { - if (isDecline) { - originalSendResponse({ error: "User has declined" }); - sendResponse(false); - pendingResponses.delete(interactionId2); - return; - } - createBuyOrderTx({ crosschainAtInfo, useLocal }) - .then((res) => { - sendResponse(true); - originalSendResponse(res); - pendingResponses.delete(interactionId2); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - // originalSendResponse({ error: error.message }); - }); - } - } - - break; - case "encryptAndPublishSymmetricKeyGroupChat": { - const { groupId, previousData, previousNumber } = request.payload; - - encryptAndPublishSymmetricKeyGroupChat({ - groupId, - previousData, - previousNumber, - }) - .then(({ data, numberOfMembers }) => { - sendResponse(data); - - if (!previousData) { - // first secret key of the group - sendChatGroup({ - groupId, - typeMessage: undefined, - chatReference: undefined, - messageText: PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY, - }) - .then(() => {}) - .catch((error) => { - console.error("1", error.message); - }); - return; - } - sendChatNotification(data, groupId, previousData, numberOfMembers); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "publishGroupEncryptedResource": { - const { encryptedData, identifier } = request.payload; - - publishGroupEncryptedResource({ - encryptedData, - identifier, - }) - .then((data) => { - sendResponse(data); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - return true; - break; - } - case "publishOnQDN": { - const { data, identifier, service, title, - description, - category, - tag1, - tag2, - tag3, - tag4, - tag5, uploadType } = request.payload; - - publishOnQDN({ - data, - identifier, - service, - title, - description, - category, - tag1, - tag2, - tag3, - tag4, - tag5, - uploadType - }) - .then((data) => { - sendResponse(data); - }) - .catch((error) => { - console.error(error?.message); - sendResponse({ error: error?.message || 'Unable to publish' }); - }); - return true; - break; - } - case "handleActiveGroupDataFromSocket": { - const { groups, directs } = request.payload; - handleActiveGroupDataFromSocket({ - groups, - directs, - }) - .then((data) => { - sendResponse(true); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "getThreadActivity": { - checkThreads(true) - .then((data) => { - sendResponse(data); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - - case "updateThreadActivity": { - const { threadId, qortalName, groupId, thread } = request.payload; - - updateThreadActivity({ threadId, qortalName, groupId, thread }) - .then(() => { - sendResponse(true); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "decryptGroupEncryption": { - const { data } = request.payload; - - decryptGroupEncryption({ data }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "encryptSingle": { - const { data, secretKeyObject, typeNumber } = request.payload; - - encryptSingle({ data64: data, secretKeyObject: secretKeyObject, typeNumber }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "decryptSingle": { - const { data, secretKeyObject, skipDecodeBase64 } = request.payload; - - decryptSingleFunc({ messages: data, secretKeyObject, skipDecodeBase64 }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "pauseAllQueues": { - pauseAllQueues(); - sendResponse(true); - - break; - - } - case "resumeAllQueues": { - resumeAllQueues(); - sendResponse(true); - - break; - } - case "checkLocal": { - checkLocalFunc() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - - break; - } - case "decryptSingleForPublishes": { - const { data, secretKeyObject, skipDecodeBase64 } = request.payload; - - decryptSingleForPublishes({ - messages: data, - secretKeyObject, - skipDecodeBase64, - }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - - case "decryptDirect": { - const { data, involvingAddress } = request.payload; - - decryptDirectFunc({ messages: data, involvingAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - - case "sendChatGroup": { - const { - groupId, - typeMessage = undefined, - chatReference = undefined, - messageText, - } = request.payload; - - sendChatGroup({ groupId, typeMessage, chatReference, messageText }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "sendChatDirect": { - const { - directTo, - typeMessage = undefined, - chatReference = undefined, - messageText, - publicKeyOfRecipient, - address, - otherData - } = request.payload; - - sendChatDirect({ - directTo, - chatReference, - messageText, - typeMessage, - publicKeyOfRecipient, - address, - otherData - }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - }); - - break; - } - case "setupGroupWebsocket": { - checkNewMessages(); - checkThreads(); - sendResponse(true); - - break; - } - - case "logout": - { - try { - const logoutFunc = async () => { - forceCloseWebSocket(); - clearAllQueues(); - if (interval) { - // for announcement notification - clearInterval(interval); - } - - const wallet = await getSaveWallet(); - const address = wallet.address0; - const key1 = `tempPublish-${address}`; - const key2 = `group-data-${address}`; - const key3 = `${address}-publishData`; - chrome.storage.local.remove( - [ - "keyPair", - "walletInfo", - "active-groups-directs", - key1, - key2, - key3, - ], - () => { - if (chrome.runtime.lastError) { - // Handle error - console.error(chrome.runtime.lastError.message); - } else { - chrome.tabs.query({}, function (tabs) { - tabs.forEach((tab) => { - chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); - }); - }); - // Data removed successfully - sendResponse(true); - } - } - ); - }; - logoutFunc(); - } catch (error) {} - } - - break; + ltcBalanceCase(request, event) + break; + case "sendCoin": + sendCoinCase(request, event) + break; + + default: + console.error("Unknown action:", request.action); } - } - return true; -}); + }); +} + +setupMessageListener() + // Function to save window position and size const saveWindowBounds = (windowId) => { diff --git a/src/messaging/messagesToBackground.tsx b/src/messaging/messagesToBackground.tsx new file mode 100644 index 0000000..a374992 --- /dev/null +++ b/src/messaging/messagesToBackground.tsx @@ -0,0 +1,66 @@ + + +// Utility to generate unique request IDs +function generateRequestId() { + return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} + +// Map to store callbacks by requestId +const callbackMap = new Map(); + +// Global listener for handling message responses +window.addEventListener("message", (event) => { + const { type, requestId, payload, error, message } = event.data; + + // Only process messages of type `backgroundMessageResponse` + if (type !== "backgroundMessageResponse") return; + + // Check if there’s a callback stored for this requestId + if (callbackMap.has(requestId)) { + const { resolve, reject } = callbackMap.get(requestId); + callbackMap.delete(requestId); // Remove callback after use + + if (error) { + reject({ error, message }); + } else { + resolve({ payload }); + } + } +}); + +// Define the sendMessage function +export const sendMessageBackground = (action, data = {}, timeout = 60000) => { + return new Promise((resolve) => { + const requestId = generateRequestId(); // Unique ID for each request + callbackMap.set(requestId, { resolve }); // Store resolve callback only + + // Send the message with `backgroundMessage` type + window.postMessage({ type: "backgroundMessage", action, requestId, payload: data }, "*"); + + // Set up a timeout to automatically resolve with an error if no response is received + const timeoutId = setTimeout(() => { + // Remove the callback to prevent memory leaks + callbackMap.delete(requestId); + resolve({ error: "timeout", message: `Request timed out after ${timeout} ms` }); + }, timeout); + + // Adjust resolve to clear the timeout when a response arrives + callbackMap.set(requestId, { + resolve: (response) => { + clearTimeout(timeoutId); // Clear the timeout if the response is received in time + resolve(response); + } + }); + }).then((response) => { + // Return payload or error based on response content + if (response?.payload) { + return response.payload; + } else if (response?.error) { + return { error: response.error, message: response?.message || "An error occurred" }; + } + }); +}; + + + +window.sendMessage = sendMessageBackground; \ No newline at end of file