diff --git a/public/content-script.js b/public/content-script.js index 7cb53f7..145bcc9 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -53,6 +53,7 @@ const fileToBase64 = (file) => new Promise(async (resolve, reject) => { } }) +const fileReferences = {} @@ -542,6 +543,86 @@ async function handleGetFileFromIndexedDB(fileId, sendResponse) { } } +async function reusablePostStream(endpoint, _body) { + + const headers = {}; + + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: _body, + }); + + return response; +} + +async function uploadChunkWithRetry(endpoint, formData, index, maxRetries = 3) { + let attempt = 0; + while (attempt < maxRetries) { + try { + const response = await reusablePostStream(endpoint, formData); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText); + } + return; // Success + } catch (err) { + attempt++; + console.warn( + `Chunk ${index} failed (attempt ${attempt}): ${err.message}` + ); + if (attempt >= maxRetries) { + throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`); + } + // Wait 10 seconds before next retry + await new Promise((res) => setTimeout(res, 10_000)); + } + } +} + +async function handleSendDataChunksToCore(fileId, chunkUrl, sendResponse){ + try { + if(!fileReferences[fileId]) throw new Error('No file reference found') + const chunkSize = 5 * 1024 * 1024; // 5MB + + const file = fileReferences[fileId] + const totalChunks = Math.ceil(file.size / chunkSize); + + for (let index = 0; index < totalChunks; index++) { + const start = index * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + + const formData = new FormData(); + formData.append('chunk', chunk, file.name); // Optional: include filename + formData.append('index', index); + + await uploadChunkWithRetry(chunkUrl, formData, index); + } + sendResponse({ result: true }); + } catch (error) { + sendResponse({ result: null, error: error?.message || "Could not save chunks to the core" }); + } finally { + if(fileReferences[fileId]){ + delete fileReferences[fileId] + } + } +} + +async function handleGetFileBase64(fileId, sendResponse){ + try { + if(!fileReferences[fileId]) throw new Error('No file reference found') + const base64 = await fileToBase64(fileReferences[fileId]); + sendResponse({ result: base64 }); + } catch (error) { + sendResponse({ result: null, error: error?.message || "Could not save chunks to the core" }); + } finally { + if(fileReferences[fileId]){ + delete fileReferences[fileId] + } + } +} + const testAsync = async (sendResponse)=> { await new Promise((res)=> { setTimeout(() => { @@ -573,6 +654,22 @@ const saveFile = (blob, filename) => { const showSaveFilePicker = async (data) => { + + if(data?.locationEndpoint){ + try { + const a = document.createElement('a'); + + a.href = data?.locationEndpoint; + a.download = data.filename; + document.body.appendChild(a); + a.click(); + a.remove(); + + } catch (error) { + console.error(error) + } + return + } let blob let fileName try { @@ -599,7 +696,10 @@ const showSaveFilePicker = async (data) => { } } +if (!window.hasAddedChromeMessageListener) { + chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { + window.hasAddedChromeMessageListener = true; if (message.type === "LOGOUT") { // Notify the web page window.postMessage( @@ -621,14 +721,18 @@ chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) ); } else if(message.action === "SHOW_SAVE_FILE_PICKER"){ showSaveFilePicker(message?.data) - } - - else if (message.action === "getFileFromIndexedDB") { + } else if (message.action === "getFileFromIndexedDB") { handleGetFileFromIndexedDB(message.fileId, sendResponse); return true; // Keep the message channel open for async response + } else if (message.action === "sendDataChunksToCore") { + handleSendDataChunksToCore(message.fileId, message.chunkUrl, sendResponse); + return true; // Keep the message channel open for async response + } else if(message.action === "getFileBase64"){ + handleGetFileBase64(message.fileId, sendResponse); + return true } }); - +} function openIndexedDB() { return new Promise((resolve, reject) => { const request = indexedDB.open("fileStorageDB", 1); @@ -795,6 +899,33 @@ async function storeFilesInIndexedDB(obj) { } +function saveFileReferences(obj) { + + if (obj.file instanceof File) { + const fileId = "objFile_qortalfile_" + Date.now(); + + fileReferences[fileId] = obj.file + obj.fileId = fileId; + } + if (obj.blob instanceof Blob) { + const fileId = "objFile_qortalfile_" + Date.now(); + + fileReferences[fileId] = obj.blob + obj.fileId = fileId + } + + // Iterate through resources to find files and save them to IndexedDB + for (let resource of (obj?.resources || [])) { + if (resource.file instanceof File) { + const fileId = resource.identifier + "_qortalfile_" + Date.now(); + + fileReferences[fileId] = resource.file + resource.fileId = fileId + } + } + return obj +} + const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO', 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY'] @@ -853,7 +984,7 @@ if (!window.hasAddedQortalListener) { { action: event.data.action, type: 'qortalRequest', payload: event.data }, event.ports[0] ); - } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' || event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE') { + } else if (event?.data?.action === 'ENCRYPT_DATA') { let data; try { data = await storeFilesInIndexedDB(event.data); @@ -877,6 +1008,52 @@ if (!window.hasAddedQortalListener) { error: 'Failed to prepare data for publishing', }); } + } else if (event?.data?.action === 'SAVE_FILE') { + let data; + try { + console.log('event', event?.data) + if(!event?.data?.location){ + data = await storeFilesInIndexedDB(event.data); + } else { + data = event.data + } + } catch (error) { + console.error('Error storing files in IndexedDB:', error); + event.ports[0].postMessage({ + result: null, + error: 'Failed to store files in IndexedDB', + }); + return; + } + + if (data) { + sendMessageToRuntime( + { action: event.data.action, type: 'qortalRequest', payload: data }, + event.ports[0] + ); + } else { + event.ports[0].postMessage({ + result: null, + error: 'Failed to prepare data for publishing', + }); + } + } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' ) { + let data; + try { + data = saveFileReferences(event.data); + } catch (error) { + console.error('Failed to store file references::', error); + event.ports[0].postMessage({ + result: null, + error: 'Failed to store file references', + }); + return; + } + + sendMessageToRuntime( + { action: event.data.action, type: 'qortalRequest', payload: data }, + event.ports[0] + ); } }; @@ -907,3 +1084,5 @@ window.addEventListener("message", (event) => { }); }); + + diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts index 1e3bdf1..a2bc160 100644 --- a/src/backgroundFunctions/encryption.ts +++ b/src/backgroundFunctions/encryption.ts @@ -58,6 +58,16 @@ export async function getNameInfo() { return ""; } } + + export async function getAllUserNames() { + 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(); + return nameData.map((item) => item.name); + } + // async function getKeyPair() { // const res = await chrome.storage.local.get(["keyPair"]); // if (res?.keyPair) { @@ -148,7 +158,7 @@ export const encryptAndPublishSymmetricKeyGroupChatForAdmins = async ({groupId, if(encryptedData){ const registeredName = await getNameInfo() const data = await publishData({ - registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true + registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true }) return { data, @@ -198,7 +208,7 @@ export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousD if(encryptedData){ const registeredName = await getNameInfo() const data = await publishData({ - registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true + registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true }) return { data, @@ -219,7 +229,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier}) const registeredName = await getNameInfo() if(!registeredName) throw new Error('You need a name to publish') const data = await publishData({ - registeredName, file: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'file', isBase64: true, withFee: true + registeredName, data: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'base64', withFee: true }) return data @@ -230,7 +240,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier}) throw new Error(error.message); } } -export const publishOnQDN = async ({data, identifier, service, title, +export const publishOnQDN = async ({data, name = "", identifier, service, title, description, category, tag1, @@ -238,15 +248,15 @@ export const publishOnQDN = async ({data, identifier, service, title, tag3, tag4, tag5, - uploadType = 'file' + uploadType }) => { if(data && service){ - const registeredName = await getNameInfo() + const registeredName = name || await getNameInfo() if(!registeredName) throw new Error('You need a name to publish') const res = await publishData({ - registeredName, file: data, service, identifier, uploadType, isBase64: true, withFee: true, title, + registeredName, data: data, service, identifier, uploadType, withFee: true, title, description, category, tag1, @@ -254,7 +264,6 @@ export const publishOnQDN = async ({data, identifier, service, title, tag3, tag4, tag5 - }) return res diff --git a/src/components/Apps/AppPublish.tsx b/src/components/Apps/AppPublish.tsx index 9736bfe..4f4b75d 100644 --- a/src/components/Apps/AppPublish.tsx +++ b/src/components/Apps/AppPublish.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { AppCircle, AppCircleContainer, @@ -49,6 +49,8 @@ import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { getFee } from "../../background"; import { fileToBase64 } from "../../utils/fileReading"; +import { publishData } from "../../qdn/publish/pubish"; +import { useSortedMyNames } from "../../hooks/useSortedMyNames"; const CustomSelect = styled(Select)({ border: "0.5px solid var(--50-white, #FFFFFF80)", @@ -82,7 +84,8 @@ const CustomMenuItem = styled(MenuItem)({ }, }); -export const AppPublish = ({ names, categories }) => { +export const AppPublish = ({ categories, myAddress, myName }) => { + const [names, setNames] = useState([]); const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); @@ -98,6 +101,8 @@ export const AppPublish = ({ names, categories }) => { const [tag5, setTag5] = useState(""); const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); + const mySortedNames = useSortedMyNames(names, myName); + const [isLoading, setIsLoading] = useState(""); const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB const { getRootProps, getInputProps } = useDropzone({ @@ -126,6 +131,25 @@ export const AppPublish = ({ names, categories }) => { }, }); + const getNames = useCallback(async () => { + if (!myAddress) return; + try { + setIsLoading('Loading names'); + const res = await fetch( + `${getBaseApiReact()}/names/address/${myAddress}?limit=0` + ); + const data = await res.json(); + setNames(data?.map((item) => item.name)); + } catch (error) { + console.error(error); + } finally { + setIsLoading(''); + } + }, [myAddress]); + useEffect(() => { + getNames(); + }, [getNames]); + const getQapp = React.useCallback(async (name, appType) => { try { setIsLoading("Loading app information"); @@ -199,34 +223,16 @@ export const AppPublish = ({ names, categories }) => { publishFee: fee.fee + " QORT", }); setIsLoading("Publishing... Please wait."); - const fileBase64 = await fileToBase64(file); - await new Promise((res, rej) => { - chrome?.runtime?.sendMessage( - { - action: "publishOnQDN", - payload: { - data: fileBase64, - service: appType, - title, - description, - category, - tag1, - tag2, - tag3, - tag4, - tag5, - uploadType: 'zip' - }, - }, - (response) => { - if (!response?.error) { - res(response); - return; - } - rej(response.error); - } - ); - }); + await publishData({ + registeredName: name, data: file, service: appType, identifier: null, uploadType: 'zip', withFee: true, title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5 + }) setInfoSnack({ type: "success", message: @@ -288,7 +294,7 @@ export const AppPublish = ({ names, categories }) => { {" "} {/* This is the placeholder item */} - {names.map((name) => { + {mySortedNames.map((name) => { return {name}; })} diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx index 0e25621..84424e6 100644 --- a/src/components/Apps/AppsDesktop.tsx +++ b/src/components/Apps/AppsDesktop.tsx @@ -25,7 +25,7 @@ import { AppsIcon } from "../../assets/Icons/AppsIcon"; const uid = new ShortUniqueId({ length: 8 }); -export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects, setDesktopViewMode, isApps, desktopViewMode}) => { +export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects, setDesktopViewMode, isApps, desktopViewMode, myAddress}) => { const [availableQapps, setAvailableQapps] = useState([]); const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [selectedCategory, setSelectedCategory] = useState(null) @@ -395,7 +395,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop }}> - + )} @@ -412,7 +412,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop {mode === "appInfo" && !selectedTab && } {mode === "appInfo-from-category" && !selectedTab && } - {mode === "publish" && !selectedTab && } + {mode === "publish" && !selectedTab && } {tabs.map((tab) => { if (!iframeRefs.current[tab.tabId]) { iframeRefs.current[tab.tabId] = React.createRef(); @@ -440,7 +440,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop }}> - + )} diff --git a/src/components/Apps/AppsHomeDesktop.tsx b/src/components/Apps/AppsHomeDesktop.tsx index 0fa3ffb..61e1aa6 100644 --- a/src/components/Apps/AppsHomeDesktop.tsx +++ b/src/components/Apps/AppsHomeDesktop.tsx @@ -23,7 +23,8 @@ export const AppsHomeDesktop = ({ myApp, myWebsite, availableQapps, - myName + myName, + myAddress }) => { const [qortalUrl, setQortalUrl] = useState('') @@ -138,7 +139,7 @@ export const AppsHomeDesktop = ({ Library - + { +export const AppsPrivate = ({myName, myAddress}) => { + const [names, setNames] = useState([]); + const [name, setName] = useState(0); const { openApp } = useHandlePrivateApps(); const [file, setFile] = useState(null); const [logo, setLogo] = useState(null); @@ -49,6 +52,9 @@ export const AppsPrivate = ({myName}) => { myGroupsWhereIAmAdminAtom ); + const mySortedNames = useSortedMyNames(names, myName); + + const myGroupsWhereIAmAdmin = useMemo(()=> { return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false) }, [myGroupsWhereIAmAdminFromGlobal, groupsProperties]) @@ -180,6 +186,8 @@ export const AppsPrivate = ({myName}) => { data: decryptedData, identifier: newPrivateAppValues?.identifier, service: newPrivateAppValues?.service, + uploadType: 'base64', + name }, }, (response) => { @@ -195,7 +203,7 @@ export const AppsPrivate = ({myName}) => { { identifier: newPrivateAppValues?.identifier, service: newPrivateAppValues?.service, - name: myName, + name: name, groupId: selectedGroup, }, true @@ -210,6 +218,22 @@ export const AppsPrivate = ({myName}) => { } }; + const getNames = useCallback(async () => { + if (!myAddress) return; + try { + const res = await fetch( + `${getBaseApiReact()}/names/address/${myAddress}?limit=0` + ); + const data = await res.json(); + setNames(data?.map((item) => item.name)); + } catch (error) { + console.error(error); + } + }, [myAddress]); + useEffect(() => { + getNames(); + }, [getNames]); + const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValueTabPrivateApp(newValue); }; @@ -446,6 +470,33 @@ export const AppsPrivate = ({myName}) => { {file ? "Change" : "Choose"} File + + + + + + { }; +const fileReferences = {} + + +function saveFileReferences(obj) { + if (obj.file) { + const fileId = "objFile_qortalfile_" + Date.now(); + + fileReferences[fileId] = obj.file + obj.fileId = fileId; + } + if (obj.blob) { + const fileId = "objFile_qortalfile_" + Date.now(); + + fileReferences[fileId] = obj.blob + obj.fileId = fileId + } + + // Iterate through resources to find files and save them to IndexedDB + for (let resource of (obj?.resources || [])) { + if (resource.file) { + const fileId = resource.identifier + "_qortalfile_" + Date.now(); + + fileReferences[fileId] = resource.file + resource.fileId = fileId + } + } + return obj +} + class Semaphore { constructor(count) { this.count = count @@ -235,7 +264,85 @@ async function handleGetFileFromIndexedDB(fileId, sendResponse) { } + async function reusablePostStream(endpoint, _body) { + + const headers = {}; + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: _body, + }); + + return response; + } + + async function uploadChunkWithRetry(endpoint, formData, index, maxRetries = 3) { + let attempt = 0; + while (attempt < maxRetries) { + try { + const response = await reusablePostStream(endpoint, formData); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText); + } + return; // Success + } catch (err) { + attempt++; + console.warn( + `Chunk ${index} failed (attempt ${attempt}): ${err.message}` + ); + if (attempt >= maxRetries) { + throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`); + } + // Wait 10 seconds before next retry + await new Promise((res) => setTimeout(res, 10_000)); + } + } + } + + async function handleSendDataChunksToCore(fileId, chunkUrl, sendResponse){ + try { + if(!fileReferences[fileId]) throw new Error('No file reference found') + const chunkSize = 5 * 1024 * 1024; // 5MB + + const file = fileReferences[fileId] + const totalChunks = Math.ceil(file.size / chunkSize); + + for (let index = 0; index < totalChunks; index++) { + const start = index * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + + const formData = new FormData(); + formData.append('chunk', chunk, file.name); // Optional: include filename + formData.append('index', index); + + await uploadChunkWithRetry(chunkUrl, formData, index); + } + sendResponse({ result: true }); + } catch (error) { + sendResponse({ result: null, error: error?.message || "Could not save chunks to the core" }); + } finally { + if(fileReferences[fileId]){ + delete fileReferences[fileId] + } + } + } + + async function handleGetFileBase64(fileId, sendResponse){ + try { + if(!fileReferences[fileId]) throw new Error('No file reference found') + const base64 = await fileToBase64(fileReferences[fileId]); + sendResponse({ result: base64 }); + } catch (error) { + sendResponse({ result: null, error: error?.message || "Could not save chunks to the core" }); + } finally { + if(fileReferences[fileId]){ + delete fileReferences[fileId] + } + } + } const UIQortalRequests = [ @@ -329,6 +436,20 @@ const UIQortalRequests = [ } const showSaveFilePicker = async (data) => { + if(data?.locationEndpoint){ + try { + const a = document.createElement('a'); + + a.href = data?.locationEndpoint; + a.download = data.filename; + document.body.appendChild(a); + a.click(); + a.remove(); + } catch (error) { + console.error(error) + } + return + } let blob let fileName try { @@ -539,9 +660,7 @@ isDOMContentLoaded: false event.ports[0] ); } else if ( - event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || - event?.data?.action === 'PUBLISH_QDN_RESOURCE' || - event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA' + event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA' ) { let data; @@ -566,6 +685,54 @@ isDOMContentLoaded: false error: 'Failed to prepare data for publishing', }); } + } else if ( + event?.data?.action === 'SAVE_FILE' + + ) { + let data; + try { + if(!event?.data?.location){ + data = await storeFilesInIndexedDB(event.data); + } else { + data = event?.data + } + + } catch (error) { + console.error('Error storing files in IndexedDB:', error); + event.ports[0].postMessage({ + result: null, + error: 'Failed to store files in IndexedDB', + }); + return; + } + if (data) { + sendMessageToRuntime( + { action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true }, + event.ports[0] + ); + } else { + event.ports[0].postMessage({ + result: null, + error: 'Failed to prepare data for publishing', + }); + } + } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' ) { + let data; + try { + data = saveFileReferences(event.data); + } catch (error) { + console.error('Failed to store file references:', error); + event.ports[0].postMessage({ + result: null, + error: 'Failed to store file references', + }); + return; + } + + sendMessageToRuntime( + { action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true }, + event.ports[0] + ); } else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' || event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null @@ -679,16 +846,29 @@ isDOMContentLoaded: false }, [appName, appService]); // Empty dependency array to run once when the component mounts - chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { - if(message.action === "SHOW_SAVE_FILE_PICKER"){ - showSaveFilePicker(message?.data) - } - - else if (message.action === "getFileFromIndexedDB") { - handleGetFileFromIndexedDB(message.fileId, sendResponse); - return true; // Keep the message channel open for async response - } - }); + useEffect(() => { + const listener = (message, sender, sendResponse) => { + if (message.action === 'SHOW_SAVE_FILE_PICKER') { + showSaveFilePicker(message?.data); + } else if (message.action === 'getFileFromIndexedDB') { + handleGetFileFromIndexedDB(message.fileId, sendResponse); + return true; // Keep channel open for async + } else if (message.action === 'sendDataChunksToCore') { + handleSendDataChunksToCore(message.fileId, message.chunkUrl, sendResponse); + return true; // Keep channel open for async + } else if (message.action === 'getFileBase64') { + handleGetFileBase64(message.fileId, sendResponse); + return true; // Keep channel open for async + } + }; + + chrome.runtime?.onMessage.addListener(listener); + + // ✅ Cleanup on unmount + return () => { + chrome.runtime?.onMessage.removeListener(listener); + }; + }, []); return {path, history, resetHistory, changeCurrentIndex} }; diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 38507d6..5cbbf73 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -638,6 +638,7 @@ const sendMessage = async ()=> { data: 'RA==', identifier: onEditMessage?.images[0]?.identifier, service: onEditMessage?.images[0]?.service, + uploadType: 'base64', }, }, (response) => { diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 58286e2..377d2a3 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -2844,7 +2844,7 @@ export const Group = ({ {!isMobile && ( + groupsAnnHasUnread} setDesktopViewMode={setDesktopViewMode} isApps={desktopViewMode === 'apps'} desktopViewMode={desktopViewMode} myAddress={userInfo?.address} /> )} diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index bbcd010..bdfad11 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -229,6 +229,7 @@ export const ListOfGroupPromotions = () => { data: data, identifier: identifier, service: "DOCUMENT", + uploadType: 'base64', }, }, (response) => { diff --git a/src/components/MainAvatar.tsx b/src/components/MainAvatar.tsx index c10c4ca..cb3885e 100644 --- a/src/components/MainAvatar.tsx +++ b/src/components/MainAvatar.tsx @@ -71,7 +71,8 @@ const [isLoading, setIsLoading] = useState(false) payload: { data: avatarBase64, identifier: "qortal_avatar", - service: 'THUMBNAIL' + service: 'THUMBNAIL', + uploadType: 'base64', }, }, (response) => { diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index 12cb5e1..eb4b83e 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -154,7 +154,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { payload: { data: encryptData, identifier: "ext_saved_settings", - service: 'DOCUMENT_PRIVATE' + service: 'DOCUMENT_PRIVATE', + uploadType: 'base64', }, }, (response) => { diff --git a/src/hooks/useSortedMyNames.tsx b/src/hooks/useSortedMyNames.tsx new file mode 100644 index 0000000..6fb8917 --- /dev/null +++ b/src/hooks/useSortedMyNames.tsx @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; + +export function useSortedMyNames(names, myName) { + return useMemo(() => { + return [...names].sort((a, b) => { + if (a === myName) return -1; + if (b === myName) return 1; + return 0; + }); + }, [names, myName]); +} diff --git a/src/qdn/publish/pubish.ts b/src/qdn/publish/pubish.ts index f114583..7257607 100644 --- a/src/qdn/publish/pubish.ts +++ b/src/qdn/publish/pubish.ts @@ -1,264 +1,366 @@ // @ts-nocheck -import { Buffer } from "buffer" -import Base58 from "../../deps/Base58" -import nacl from "../../deps/nacl-fast" -import utils from "../../utils/utils" -import { createEndpoint, getBaseApi, getKeyPair } from "../../background"; +import { Buffer } from 'buffer'; +import Base58 from '../../deps/Base58'; +import nacl from '../../deps/nacl-fast'; +import utils from '../../utils/utils'; +import { createEndpoint, getBaseApi, getKeyPair } from '../../background'; +import { sendDataChunksToCore } from '../../qortalRequests/get'; -export async function reusableGet(endpoint){ - const validApi = await getBaseApi(); - - const response = await fetch(validApi + endpoint); - const data = await response.json(); - return data - } - - async function reusablePost(endpoint, _body){ - // const validApi = await findUsableApi(); - const url = await createEndpoint(endpoint) - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: _body +export async function reusableGet(endpoint) { + const validApi = await getBaseApi(); + + const response = await fetch(validApi + endpoint); + const data = await response.json(); + return data; +} + +async function reusablePost(endpoint, _body) { + // const validApi = await findUsableApi(); + const url = await createEndpoint(endpoint); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: _body, }); - let data + let data; try { - data = await response.clone().json() + data = await response.clone().json(); } catch (e) { - data = await response.text() - } - return data + data = await response.text(); } + return data; +} + +async function reusablePostStream(endpoint, _body) { + const url = await createEndpoint(endpoint); + + const headers = {}; + + const response = await fetch(url, { + method: 'POST', + headers, + body: _body, + }); + + return response; // return the actual response so calling code can use response.ok +} + +async function uploadChunkWithRetry(endpoint, formData, index, maxRetries = 3) { + let attempt = 0; + while (attempt < maxRetries) { + try { + const response = await reusablePostStream(endpoint, formData); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText); + } + return; // Success + } catch (err) { + attempt++; + console.warn( + `Chunk ${index} failed (attempt ${attempt}): ${err.message}` + ); + if (attempt >= maxRetries) { + throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`); + } + // Wait 10 seconds before next retry + await new Promise((res) => setTimeout(res, 10_000)); + } + } +} + -// 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 const publishData = async ({ - registeredName, - file, - service, - identifier, - uploadType, - isBase64, - filename, - withFee, - title, - description, - category, - tag1, - tag2, - tag3, - tag4, - tag5, - feeAmount + registeredName, + data, + service, + identifier, + uploadType, + filename, + withFee, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + feeAmount, + sender }: any) => { - - const validateName = async (receiverName: string) => { - return await reusableGet(`/names/${receiverName}`) - } + const validateName = async (receiverName: string) => { + return await reusableGet(`/names/${receiverName}`); + }; - const convertBytesForSigning = async (transactionBytesBase58: string) => { - return await reusablePost('/transactions/convert', transactionBytesBase58) - } + const convertBytesForSigning = async (transactionBytesBase58: string) => { + return await reusablePost('/transactions/convert', transactionBytesBase58); + }; - const getArbitraryFee = async () => { - const timestamp = Date.now() + const getArbitraryFee = async () => { + const timestamp = Date.now(); - let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}`) + let fee = await reusableGet( + `/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}` + ); - return { - timestamp, - fee: Number(fee), - feeToShow: (Number(fee) / 1e8).toFixed(8) - } - } + return { + timestamp, + fee: Number(fee), + feeToShow: (Number(fee) / 1e8).toFixed(8), + }; + }; - const signArbitraryWithFee = (arbitraryBytesBase58, arbitraryBytesForSigningBase58, keyPair) => { - if (!arbitraryBytesBase58) { - throw new Error('ArbitraryBytesBase58 not defined') - } - - if (!keyPair) { - throw new Error('keyPair not defined') - } - - const arbitraryBytes = Base58.decode(arbitraryBytesBase58) - const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map(function (key) { return arbitraryBytes[key]; }) - const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer) - const arbitraryBytesForSigning = Base58.decode(arbitraryBytesForSigningBase58) - const _arbitraryBytesForSigningBuffer = Object.keys(arbitraryBytesForSigning).map(function (key) { return arbitraryBytesForSigning[key]; }) - const arbitraryBytesForSigningBuffer = new Uint8Array(_arbitraryBytesForSigningBuffer) - const signature = nacl.sign.detached(arbitraryBytesForSigningBuffer, keyPair.privateKey) - - return utils.appendBuffer(arbitraryBytesBuffer, signature) + const signArbitraryWithFee = ( + arbitraryBytesBase58, + arbitraryBytesForSigningBase58, + keyPair + ) => { + if (!arbitraryBytesBase58) { + throw new Error('ArbitraryBytesBase58 not defined'); } - const processTransactionVersion2 = async (bytes) => { + if (!keyPair) { + throw new Error('keyPair not defined'); + } - return await reusablePost('/transactions/process?apiVersion=2', Base58.encode(bytes)) - } + const arbitraryBytes = Base58.decode(arbitraryBytesBase58); + const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map( + function (key) { + return arbitraryBytes[key]; + } + ); + const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer); + const arbitraryBytesForSigning = Base58.decode( + arbitraryBytesForSigningBase58 + ); + const _arbitraryBytesForSigningBuffer = Object.keys( + arbitraryBytesForSigning + ).map(function (key) { + return arbitraryBytesForSigning[key]; + }); + const arbitraryBytesForSigningBuffer = new Uint8Array( + _arbitraryBytesForSigningBuffer + ); + const signature = nacl.sign.detached( + arbitraryBytesForSigningBuffer, + keyPair.privateKey + ); - const signAndProcessWithFee = async (transactionBytesBase58: string) => { - let convertedBytesBase58 = await convertBytesForSigning( - transactionBytesBase58 - ) + return utils.appendBuffer(arbitraryBytesBuffer, signature); + }; - if (convertedBytesBase58.error) { - throw new Error('Error when signing') - } + const processTransactionVersion2 = async (bytes) => { + return await reusablePost( + '/transactions/process?apiVersion=2', + Base58.encode(bytes) + ); + }; + const signAndProcessWithFee = async (transactionBytesBase58: string) => { + let convertedBytesBase58 = await convertBytesForSigning( + transactionBytesBase58 + ); - 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 - }; + if (convertedBytesBase58.error) { + throw new Error('Error when signing'); + } - let signedArbitraryBytes = signArbitraryWithFee(transactionBytesBase58, convertedBytesBase58, keyPair) - const response = await processTransactionVersion2(signedArbitraryBytes) + 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, + }; - let myResponse = { error: '' } + let signedArbitraryBytes = signArbitraryWithFee( + transactionBytesBase58, + convertedBytesBase58, + keyPair + ); + const response = await processTransactionVersion2(signedArbitraryBytes); - if (response === false) { - throw new Error('Error when signing') - } else { - myResponse = response - } + let myResponse = { error: '' }; - return myResponse - } + if (response === false) { + throw new Error('Error when signing'); + } else { + myResponse = response; + } - const validate = async () => { - let validNameRes = await validateName(registeredName) + return myResponse; + }; - if (validNameRes.error) { - throw new Error('Name not found') - } + const validate = async () => { + let validNameRes = await validateName(registeredName); - let fee = null + if (validNameRes.error) { + throw new Error('Name not found'); + } - if (withFee && feeAmount) { - fee = feeAmount - } else if (withFee) { - const res = await getArbitraryFee() - if (res.fee) { - fee = res.fee - } else { - throw new Error('unable to get fee') - } - } - - let transactionBytes = await uploadData(registeredName, file, fee) - if (!transactionBytes || transactionBytes.error) { - throw new Error(transactionBytes?.message || 'Error when uploading') - } else if (transactionBytes.includes('Error 500 Internal Server Error')) { - throw new Error('Error when uploading') - } + let fee = null; - let signAndProcessRes + if (withFee && feeAmount) { + fee = feeAmount; + } else if (withFee) { + const res = await getArbitraryFee(); + if (res.fee) { + fee = res.fee; + } else { + throw new Error('unable to get fee'); + } + } - if (withFee) { - signAndProcessRes = await signAndProcessWithFee(transactionBytes) - } + let transactionBytes = await uploadData(registeredName, data, fee); + if (!transactionBytes || transactionBytes.error) { + throw new Error(transactionBytes?.message || 'Error when uploading'); + } else if (transactionBytes.includes('Error 500 Internal Server Error')) { + throw new Error('Error when uploading'); + } - if (signAndProcessRes?.error) { - throw new Error('Error when signing') - } + let signAndProcessRes; - return signAndProcessRes - } + if (withFee) { + signAndProcessRes = await signAndProcessWithFee(transactionBytes); + } - const uploadData = async (registeredName: string, file:any, fee: number) => { + if (signAndProcessRes?.error) { + throw new Error('Error when signing'); + } - let postBody = '' - let urlSuffix = '' + return signAndProcessRes; + }; - if (file != null) { - // If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API - if (uploadType === 'zip') { - urlSuffix = '/zip' - } + const uploadData = async (registeredName: string, data: any, fee: number) => { + let postBody = ''; + let urlSuffix = ''; - // If we're sending file data, use the /base64 version of the POST /arbitrary/* API - else if (uploadType === 'file') { - urlSuffix = '/base64' - } + if (data != null) { + if (uploadType === 'base64') { + urlSuffix = '/base64'; + } - // Base64 encode the file to work around compatibility issues between javascript and java byte arrays - if (isBase64) { - postBody = file - } + if (uploadType === 'base64') { + postBody = data; + } + } else { + throw new Error('No data provided'); + } - if (!isBase64) { - let fileBuffer = new Uint8Array(await file.arrayBuffer()) - postBody = Buffer.from(fileBuffer).toString("base64") - } + let uploadDataUrl = `/arbitrary/${service}/${registeredName}`; + let paramQueries = ''; + if (identifier?.trim().length > 0) { + uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`; + } - } - - let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}` - if (identifier?.trim().length > 0) { - uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}` - } - - uploadDataUrl = uploadDataUrl + `?fee=${fee}` - + paramQueries = paramQueries + `?fee=${fee}`; - if (filename != null && filename != 'undefined') { - uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename) - } + if (filename != null && filename != 'undefined') { + paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename); + } - if (title != null && title != 'undefined') { - uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title) - } + if (title != null && title != 'undefined') { + paramQueries = paramQueries + '&title=' + encodeURIComponent(title); + } - if (description != null && description != 'undefined') { - uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description) - } + if (description != null && description != 'undefined') { + paramQueries = + paramQueries + '&description=' + encodeURIComponent(description); + } - if (category != null && category != 'undefined') { - uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category) - } + if (category != null && category != 'undefined') { + paramQueries = paramQueries + '&category=' + encodeURIComponent(category); + } - if (tag1 != null && tag1 != 'undefined') { - uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1) - } + if (tag1 != null && tag1 != 'undefined') { + paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1); + } - if (tag2 != null && tag2 != 'undefined') { - uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2) - } + if (tag2 != null && tag2 != 'undefined') { + paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2); + } - if (tag3 != null && tag3 != 'undefined') { - uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3) - } + if (tag3 != null && tag3 != 'undefined') { + paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3); + } - if (tag4 != null && tag4 != 'undefined') { - uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4) - } + if (tag4 != null && tag4 != 'undefined') { + paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4); + } - if (tag5 != null && tag5 != 'undefined') { - uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5) - } + if (tag5 != null && tag5 != 'undefined') { + paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5); + } + if (uploadType === 'zip') { + paramQueries = paramQueries + '&isZip=' + true; + } - return await reusablePost(uploadDataUrl, postBody) - - } + if (uploadType === 'base64') { + if (urlSuffix) { + uploadDataUrl = uploadDataUrl + urlSuffix; + } + uploadDataUrl = uploadDataUrl + paramQueries; + return await reusablePost(uploadDataUrl, postBody); + } - try { - return await validate() - } catch (error: any) { - throw new Error(error?.message) - } -} \ No newline at end of file + const file = data; + // const urlCheck = `/arbitrary/check-tmp-space?totalSize=${file.size}`; + + // const checkEndpoint = await createEndpoint(urlCheck); + // const checkRes = await fetch(checkEndpoint); + // if (!checkRes.ok) { + // throw new Error('Not enough space on your hard drive'); + // } + + const chunkUrl = uploadDataUrl + `/chunk`; + const createdChunkUrl = await createEndpoint(chunkUrl) + if(sender){ + await sendDataChunksToCore(file, createdChunkUrl, sender) + + } else { + const chunkSize = 5 * 1024 * 1024; // 5MB + + const totalChunks = Math.ceil(file.size / chunkSize); + + for (let index = 0; index < totalChunks; index++) { + const start = index * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + const formData = new FormData(); + formData.append('chunk', chunk, file.name); // Optional: include filename + formData.append('index', index); + + await uploadChunkWithRetry(chunkUrl, formData, index); + } + } + + const finalizeUrl = uploadDataUrl + `/finalize` + paramQueries; + + const finalizeEndpoint = await createEndpoint(finalizeUrl); + + const response = await fetch(finalizeEndpoint, { + method: 'POST', + headers: {}, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Finalize failed: ${errorText}`); + } + + const result = await response.text(); // Base58-encoded unsigned transaction + return result; + }; + + try { + return await validate(); + } catch (error: any) { + throw new Error(error?.message); + } +}; \ No newline at end of file diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index f456a5d..6989094 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -38,7 +38,7 @@ import { getNameOrAddress, getAssetBalanceInfo } from "../background"; -import { decryptGroupEncryption, getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; +import { decryptGroupEncryption, getAllUserNames, getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; import { QORT_DECIMALS } from "../constants/constants"; import Base58 from "../deps/Base58"; @@ -424,6 +424,39 @@ function getFileFromContentScript(fileId, sender) { ); }); } + +export function sendDataChunksToCore(fileId, chunkUrl, sender) { + return new Promise((resolve, reject) => { + chrome.tabs.sendMessage( + sender.tab.id, + { action: "sendDataChunksToCore", fileId: fileId, chunkUrl }, + (response) => { + if (response && response.result) { + resolve(response.result); + } else { + reject(response?.error || "Failed to retrieve file"); + } + } + ); + }); +} + +export function getFileBase64(fileId, sender) { + return new Promise((resolve, reject) => { + chrome.tabs.sendMessage( + sender.tab.id, + { action: "getFileBase64", fileId: fileId }, + (response) => { + if (response && response.result) { + resolve(response.result); + } else { + reject(response?.error || "Failed to retrieve file"); + } + } + ); + }); +} + function sendToSaveFilePicker(data, sender) { chrome.tabs.sendMessage(sender.tab.id, { @@ -883,7 +916,7 @@ export const publishQDNResource = async (data: any, sender, isFromExtension) => if(appFee && appFee > 0 && appFeeRecipient){ hasAppFee = true } - const registeredName = await getNameInfo(); + const registeredName = data?.name || await getNameInfo(); const name = registeredName; if(!name){ throw new Error('User has no Qortal name') @@ -917,15 +950,16 @@ const { tag1, tag2, tag3, tag4, tag5 } = result; throw new Error("Encrypting data requires public keys"); } - if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId, sender); - } + if (data.encrypt) { try { const resKeyPair = await getKeyPair() const parsedData = JSON.parse(resKeyPair) const privateKey = parsedData.privateKey const userPublicKey = parsedData.publicKey + if (data?.fileId) { + data64 = await getFileBase64(data?.fileId, sender); + } const encryptDataResponse = encryptDataGroup({ data64, publicKeys: data.publicKeys, @@ -973,11 +1007,10 @@ const { tag1, tag2, tag3, tag4, tag5 } = result; try { const resPublish = await publishData({ registeredName: encodeURIComponent(name), - file: data64, + data: data64 ? data64 : data?.fileId, service: service, identifier: encodeURIComponent(identifier), - uploadType: "file", - isBase64: true, + uploadType: data64 ? "base64" : "file", filename: filename, title, description, @@ -989,6 +1022,7 @@ const { tag1, tag2, tag3, tag4, tag5 } = result; tag5, apiVersion: 2, withFee: true, + sender }); if(resPublish?.signature && hasAppFee && checkbox1){ sendCoinFunc({ @@ -1082,6 +1116,14 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten if(!name){ throw new Error('You need a Qortal name to publish.') } + + const userNames = await getAllUserNames(); + data.resources?.forEach((item) => { + if (item?.name && !userNames?.includes(item.name)) + throw new Error( + `The name ${item.name}, does not belong to the publisher.` + ); + }); const appFee = data?.appFee ? +data.appFee : undefined const appFeeRecipient = data?.appFeeRecipient let hasAppFee = false @@ -1208,7 +1250,8 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten } const service = resource.service; let identifier = resource.identifier; - let data64 = resource?.data64 || resource?.base64; + // let data64 = resource?.data64 || resource?.base64; + let rawData = resource?.data64 || resource?.base64; const filename = resource.filename; const title = resource.title; const description = resource.description; @@ -1232,26 +1275,34 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten failedPublishesIdentifiers.push({ reason: errorMsg, identifier: resource.identifier, + service: resource.service, + name: resource?.name || name, }); continue; } if (resource.fileId) { - data64 = await getFileFromContentScript(resource.fileId, sender); + rawData = resource.fileId; } + // if (resource.fileId) { + // data64 = await sendDataChunksToCore(resource.fileId, sender); + // } if (resourceEncrypt) { try { + if (resource?.fileId) { + rawData = await getFileBase64(resource.fileId, sender); + } const resKeyPair = await getKeyPair() const parsedData = JSON.parse(resKeyPair) const privateKey = parsedData.privateKey const userPublicKey = parsedData.publicKey const encryptDataResponse = encryptDataGroup({ - data64, + data64: rawData, publicKeys: data.publicKeys, privateKey, userPublicKey }); if (encryptDataResponse) { - data64 = encryptDataResponse; + rawData = encryptDataResponse; } } catch (error) { const errorMsg = @@ -1260,20 +1311,24 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten reason: errorMsg, identifier: resource.identifier, service: resource.service, + name: resource?.name || name, }); continue; } } try { + const dataType = + (resource?.base64 || resource?.data64 || resourceEncrypt) + ? 'base64' + : 'file'; await retryTransaction(publishData, [ { registeredName: encodeURIComponent(name), - file: data64, + data: rawData, service: service, identifier: encodeURIComponent(identifier), - uploadType: "file", - isBase64: true, + uploadType: dataType, filename: filename, title, description, @@ -1285,6 +1340,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten tag5, apiVersion: 2, withFee: true, + sender }, ], true); await new Promise((res) => { @@ -1298,6 +1354,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten reason: errorMsg, identifier: resource.identifier, service: resource.service, + name: resource?.name || name, }); } } catch (error) { @@ -1305,6 +1362,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten reason: error?.message || "Unknown error", identifier: resource.identifier, service: resource.service, + name: resource?.name || name, }); } } @@ -1683,6 +1741,46 @@ export const joinGroup = async (data, isFromExtension) => { export const saveFile = async (data, sender, isFromExtension) => { try { + if (!data?.filename) throw new Error('Missing filename'); + if (data?.location) { + const requiredFieldsLocation = ['service', 'name']; + const missingFieldsLocation: string[] = []; + requiredFieldsLocation.forEach((field) => { + if (!data?.location[field]) { + missingFieldsLocation.push(field); + } + }); + if (missingFieldsLocation.length > 0) { + const missingFieldsString = missingFieldsLocation.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const resPermission = await getUserPermission( + { + text1: 'Would you like to download:', + highlightedText: `${data?.filename}`, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (!accepted) throw new Error('User declined to save file'); + let locationUrl = `/arbitrary/${data.location.service}/${data.location.name}`; + if (data.location.identifier) { + locationUrl = locationUrl + `/${data.location.identifier}`; + } + const endpoint = await createEndpoint( + locationUrl + `?attachment=true&attachmentFilename=${data?.filename}` + ); + + sendToSaveFilePicker( + { + locationEndpoint: endpoint, + filename: data.filename + }, + sender + ); + return true; + } const requiredFields = ["filename", "fileId"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -4084,9 +4182,10 @@ export const updateNameRequest = async (data, isFromExtension) => { const fee = await getFee("UPDATE_NAME"); const resPermission = await getUserPermission( { - text1: `Do you give this application permission to register this name?`, - highlightedText: data.newName, - text2: data?.description, + text1: `Do you give this application permission to update this name?`, + text2: `previous name: ${oldName}`, + text3: `new name: ${newName}`, + text4: data?.description, fee: fee.fee, }, isFromExtension @@ -4807,7 +4906,7 @@ export const updateGroupRequest = async (data, isFromExtension) => { const requiredFields = ["groupId", "newOwner", "type", "approvalThreshold", "minBlock", "maxBlock"]; const missingFields: string[] = []; requiredFields.forEach((field) => { - if (data[field] !== undefined && data[field] !== null) { + if (data[field] === undefined || data[field] === null) { missingFields.push(field); } }); @@ -4872,7 +4971,7 @@ export const sellNameRequest = async (data, isFromExtension) => { const requiredFields = ["salePrice", "nameForSale"]; const missingFields: string[] = []; requiredFields.forEach((field) => { - if (data[field] !== undefined && data[field] !== null) { + if (data[field] === undefined || data[field] === null) { missingFields.push(field); } }); @@ -4917,7 +5016,7 @@ export const cancelSellNameRequest = async (data, isFromExtension) => { const requiredFields = ["nameForSale"]; const missingFields: string[] = []; requiredFields.forEach((field) => { - if (data[field] !== undefined && data[field] !== null) { + if (data[field] === undefined || data[field] === null) { missingFields.push(field); } }); @@ -4958,7 +5057,7 @@ export const buyNameRequest = async (data, isFromExtension) => { const requiredFields = ["nameForSale"]; const missingFields: string[] = []; requiredFields.forEach((field) => { - if (data[field] !== undefined && data[field] !== null) { + if (data[field] === undefined || data[field] === null) { missingFields.push(field); } }); @@ -5261,12 +5360,11 @@ const assetBalance = await getAssetBalanceInfo(assetId) const resPublish = await retryTransaction(publishData, [ { registeredName: encodeURIComponent(name), - file: encryptDataResponse, + data: encryptDataResponse, service: transaction.service, identifier: encodeURIComponent(transaction.identifier), - uploadType: "file", + uploadType: "base64", description: transaction?.description, - isBase64: true, apiVersion: 2, withFee: true, }, @@ -5302,12 +5400,11 @@ const assetBalance = await getAssetBalanceInfo(assetId) const resPublish = await retryTransaction(publishData, [ { registeredName: encodeURIComponent(name), - file: encryptDataResponse, + data: encryptDataResponse, service: transaction.service, identifier: encodeURIComponent(transaction.identifier), - uploadType: "file", + uploadType: "base64", description: transaction?.description, - isBase64: true, apiVersion: 2, withFee: true, },