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,
},