handle large files and multi-name

This commit is contained in:
PhilReact 2025-05-24 00:00:25 +03:00
parent 28c85cc271
commit be60211365
15 changed files with 949 additions and 309 deletions

View File

@ -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)=> { const testAsync = async (sendResponse)=> {
await new Promise((res)=> { await new Promise((res)=> {
setTimeout(() => { setTimeout(() => {
@ -573,6 +654,22 @@ const saveFile = (blob, filename) => {
const showSaveFilePicker = async (data) => { 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 blob
let fileName let fileName
try { try {
@ -599,7 +696,10 @@ const showSaveFilePicker = async (data) => {
} }
} }
if (!window.hasAddedChromeMessageListener) {
chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) {
window.hasAddedChromeMessageListener = true;
if (message.type === "LOGOUT") { if (message.type === "LOGOUT") {
// Notify the web page // Notify the web page
window.postMessage( window.postMessage(
@ -621,14 +721,18 @@ chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse)
); );
} else if(message.action === "SHOW_SAVE_FILE_PICKER"){ } else if(message.action === "SHOW_SAVE_FILE_PICKER"){
showSaveFilePicker(message?.data) showSaveFilePicker(message?.data)
} } else if (message.action === "getFileFromIndexedDB") {
else if (message.action === "getFileFromIndexedDB") {
handleGetFileFromIndexedDB(message.fileId, sendResponse); handleGetFileFromIndexedDB(message.fileId, sendResponse);
return true; // Keep the message channel open for async response 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() { function openIndexedDB() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open("fileStorageDB", 1); 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'] 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 }, { action: event.data.action, type: 'qortalRequest', payload: event.data },
event.ports[0] 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; let data;
try { try {
data = await storeFilesInIndexedDB(event.data); data = await storeFilesInIndexedDB(event.data);
@ -877,6 +1008,52 @@ if (!window.hasAddedQortalListener) {
error: 'Failed to prepare data for publishing', 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) => {
}); });
}); });

View File

@ -58,6 +58,16 @@ export async function getNameInfo() {
return ""; 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() { // async function getKeyPair() {
// const res = await chrome.storage.local.get(["keyPair"]); // const res = await chrome.storage.local.get(["keyPair"]);
// if (res?.keyPair) { // if (res?.keyPair) {
@ -148,7 +158,7 @@ export const encryptAndPublishSymmetricKeyGroupChatForAdmins = async ({groupId,
if(encryptedData){ if(encryptedData){
const registeredName = await getNameInfo() const registeredName = await getNameInfo()
const data = await publishData({ 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 { return {
data, data,
@ -198,7 +208,7 @@ export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousD
if(encryptedData){ if(encryptedData){
const registeredName = await getNameInfo() const registeredName = await getNameInfo()
const data = await publishData({ 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 { return {
data, data,
@ -219,7 +229,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
const registeredName = await getNameInfo() const registeredName = await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish') if(!registeredName) throw new Error('You need a name to publish')
const data = await publishData({ 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 return data
@ -230,7 +240,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
throw new Error(error.message); throw new Error(error.message);
} }
} }
export const publishOnQDN = async ({data, identifier, service, title, export const publishOnQDN = async ({data, name = "", identifier, service, title,
description, description,
category, category,
tag1, tag1,
@ -238,15 +248,15 @@ export const publishOnQDN = async ({data, identifier, service, title,
tag3, tag3,
tag4, tag4,
tag5, tag5,
uploadType = 'file' uploadType
}) => { }) => {
if(data && service){ if(data && service){
const registeredName = await getNameInfo() const registeredName = name || await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish') if(!registeredName) throw new Error('You need a name to publish')
const res = await publishData({ 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, description,
category, category,
tag1, tag1,
@ -254,7 +264,6 @@ export const publishOnQDN = async ({data, identifier, service, title,
tag3, tag3,
tag4, tag4,
tag5 tag5
}) })
return res return res

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -49,6 +49,8 @@ import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getFee } from "../../background"; import { getFee } from "../../background";
import { fileToBase64 } from "../../utils/fileReading"; import { fileToBase64 } from "../../utils/fileReading";
import { publishData } from "../../qdn/publish/pubish";
import { useSortedMyNames } from "../../hooks/useSortedMyNames";
const CustomSelect = styled(Select)({ const CustomSelect = styled(Select)({
border: "0.5px solid var(--50-white, #FFFFFF80)", 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 [name, setName] = useState("");
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
@ -98,6 +101,8 @@ export const AppPublish = ({ names, categories }) => {
const [tag5, setTag5] = useState(""); const [tag5, setTag5] = useState("");
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const mySortedNames = useSortedMyNames(names, myName);
const [isLoading, setIsLoading] = useState(""); const [isLoading, setIsLoading] = useState("");
const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
const { getRootProps, getInputProps } = useDropzone({ 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) => { const getQapp = React.useCallback(async (name, appType) => {
try { try {
setIsLoading("Loading app information"); setIsLoading("Loading app information");
@ -199,34 +223,16 @@ export const AppPublish = ({ names, categories }) => {
publishFee: fee.fee + " QORT", publishFee: fee.fee + " QORT",
}); });
setIsLoading("Publishing... Please wait."); setIsLoading("Publishing... Please wait.");
const fileBase64 = await fileToBase64(file); await publishData({
await new Promise((res, rej) => { registeredName: name, data: file, service: appType, identifier: null, uploadType: 'zip', withFee: true, title,
chrome?.runtime?.sendMessage( description,
{ category,
action: "publishOnQDN", tag1,
payload: { tag2,
data: fileBase64, tag3,
service: appType, tag4,
title, tag5
description, })
category,
tag1,
tag2,
tag3,
tag4,
tag5,
uploadType: 'zip'
},
},
(response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
}
);
});
setInfoSnack({ setInfoSnack({
type: "success", type: "success",
message: message:
@ -288,7 +294,7 @@ export const AppPublish = ({ names, categories }) => {
</em>{" "} </em>{" "}
{/* This is the placeholder item */} {/* This is the placeholder item */}
</CustomMenuItem> </CustomMenuItem>
{names.map((name) => { {mySortedNames.map((name) => {
return <CustomMenuItem value={name}>{name}</CustomMenuItem>; return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
})} })}
</CustomSelect> </CustomSelect>

View File

@ -25,7 +25,7 @@ import { AppsIcon } from "../../assets/Icons/AppsIcon";
const uid = new ShortUniqueId({ length: 8 }); 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 [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [selectedAppInfo, setSelectedAppInfo] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null) const [selectedCategory, setSelectedCategory] = useState(null)
@ -395,7 +395,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} myAddress={myAddress} />
</Box> </Box>
)} )}
@ -412,7 +412,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
{mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />} {mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
{mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />} {mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
<AppsCategoryDesktop availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} /> <AppsCategoryDesktop availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} />
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />} {mode === "publish" && !selectedTab && <AppPublish categories={categories} myAddress={myAddress} />}
{tabs.map((tab) => { {tabs.map((tab) => {
if (!iframeRefs.current[tab.tabId]) { if (!iframeRefs.current[tab.tabId]) {
iframeRefs.current[tab.tabId] = React.createRef(); iframeRefs.current[tab.tabId] = React.createRef();
@ -440,7 +440,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} myAddress={myAddress} />
</Box> </Box>
</> </>
)} )}

View File

@ -23,7 +23,8 @@ export const AppsHomeDesktop = ({
myApp, myApp,
myWebsite, myWebsite,
availableQapps, availableQapps,
myName myName,
myAddress
}) => { }) => {
const [qortalUrl, setQortalUrl] = useState('') const [qortalUrl, setQortalUrl] = useState('')
@ -138,7 +139,7 @@ export const AppsHomeDesktop = ({
<AppCircleLabel>Library</AppCircleLabel> <AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
<AppsPrivate myName={myName} /> <AppsPrivate myName={myName} myAddress={myAddress} />
<SortablePinnedApps <SortablePinnedApps
isDesktop={true} isDesktop={true}
availableQapps={availableQapps} availableQapps={availableQapps}

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { import {
Avatar, Avatar,
Box, Box,
@ -30,14 +30,17 @@ import {
PublishQAppInfo, PublishQAppInfo,
} from "./Apps-styles"; } from "./Apps-styles";
import ImageUploader from "../../common/ImageUploader"; import ImageUploader from "../../common/ImageUploader";
import { isMobile, MyContext } from "../../App"; import { getBaseApiReact, isMobile, MyContext } from "../../App";
import { fileToBase64 } from "../../utils/fileReading"; import { fileToBase64 } from "../../utils/fileReading";
import { objectToBase64 } from "../../qdn/encryption/group-encryption"; import { objectToBase64 } from "../../qdn/encryption/group-encryption";
import { getFee } from "../../background"; import { getFee } from "../../background";
import { useSortedMyNames } from "../../hooks/useSortedMyNames";
const maxFileSize = 50 * 1024 * 1024; // 50MB const maxFileSize = 50 * 1024 * 1024; // 50MB
export const AppsPrivate = ({myName}) => { export const AppsPrivate = ({myName, myAddress}) => {
const [names, setNames] = useState([]);
const [name, setName] = useState(0);
const { openApp } = useHandlePrivateApps(); const { openApp } = useHandlePrivateApps();
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null); const [logo, setLogo] = useState(null);
@ -49,6 +52,9 @@ export const AppsPrivate = ({myName}) => {
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
const mySortedNames = useSortedMyNames(names, myName);
const myGroupsWhereIAmAdmin = useMemo(()=> { const myGroupsWhereIAmAdmin = useMemo(()=> {
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false) return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties]) }, [myGroupsWhereIAmAdminFromGlobal, groupsProperties])
@ -180,6 +186,8 @@ export const AppsPrivate = ({myName}) => {
data: decryptedData, data: decryptedData,
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
uploadType: 'base64',
name
}, },
}, },
(response) => { (response) => {
@ -195,7 +203,7 @@ export const AppsPrivate = ({myName}) => {
{ {
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
name: myName, name: name,
groupId: selectedGroup, groupId: selectedGroup,
}, },
true 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) => { const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValueTabPrivateApp(newValue); setValueTabPrivateApp(newValue);
}; };
@ -446,6 +470,33 @@ export const AppsPrivate = ({myName}) => {
{file ? "Change" : "Choose"} File {file ? "Change" : "Choose"} File
</PublishQAppChoseFile> </PublishQAppChoseFile>
<Spacer height="20px" /> <Spacer height="20px" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>Select a Qortal name</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={name}
label="Groups where you are an admin"
onChange={(e) => setName(e.target.value)}
>
<MenuItem value={0}>No name selected</MenuItem>
{mySortedNames.map((name) => {
return (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
);
})}
</Select>
</Box>
<Spacer height="20px" />
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",

View File

@ -105,6 +105,35 @@ export const createAndCopyEmbedLink = async (data) => {
}; };
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 { class Semaphore {
constructor(count) { constructor(count) {
this.count = 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 = [ const UIQortalRequests = [
@ -329,6 +436,20 @@ const UIQortalRequests = [
} }
const showSaveFilePicker = async (data) => { 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 blob
let fileName let fileName
try { try {
@ -539,9 +660,7 @@ isDOMContentLoaded: false
event.ports[0] event.ports[0]
); );
} else if ( } else if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA'
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'
) { ) {
let data; let data;
@ -566,6 +685,54 @@ isDOMContentLoaded: false
error: 'Failed to prepare data for publishing', 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' || } else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' ||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null 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 }, [appName, appService]); // Empty dependency array to run once when the component mounts
chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { useEffect(() => {
if(message.action === "SHOW_SAVE_FILE_PICKER"){ const listener = (message, sender, sendResponse) => {
showSaveFilePicker(message?.data) if (message.action === 'SHOW_SAVE_FILE_PICKER') {
} showSaveFilePicker(message?.data);
} else if (message.action === 'getFileFromIndexedDB') {
else if (message.action === "getFileFromIndexedDB") { handleGetFileFromIndexedDB(message.fileId, sendResponse);
handleGetFileFromIndexedDB(message.fileId, sendResponse); return true; // Keep channel open for async
return true; // Keep the message channel open for async response } 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} return {path, history, resetHistory, changeCurrentIndex}
}; };

View File

@ -638,6 +638,7 @@ const sendMessage = async ()=> {
data: 'RA==', data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier, identifier: onEditMessage?.images[0]?.identifier,
service: onEditMessage?.images[0]?.service, service: onEditMessage?.images[0]?.service,
uploadType: 'base64',
}, },
}, },
(response) => { (response) => {

View File

@ -2844,7 +2844,7 @@ export const Group = ({
{!isMobile && ( {!isMobile && (
<AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups} <AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups}
isDirects={isOpenSideViewDirects} hasUnreadGroups={groupChatHasUnread || isDirects={isOpenSideViewDirects} hasUnreadGroups={groupChatHasUnread ||
groupsAnnHasUnread} setDesktopViewMode={setDesktopViewMode} isApps={desktopViewMode === 'apps'} desktopViewMode={desktopViewMode} /> groupsAnnHasUnread} setDesktopViewMode={setDesktopViewMode} isApps={desktopViewMode === 'apps'} desktopViewMode={desktopViewMode} myAddress={userInfo?.address} />
)} )}
<WalletsAppWrapper /> <WalletsAppWrapper />

View File

@ -229,6 +229,7 @@ export const ListOfGroupPromotions = () => {
data: data, data: data,
identifier: identifier, identifier: identifier,
service: "DOCUMENT", service: "DOCUMENT",
uploadType: 'base64',
}, },
}, },
(response) => { (response) => {

View File

@ -71,7 +71,8 @@ const [isLoading, setIsLoading] = useState(false)
payload: { payload: {
data: avatarBase64, data: avatarBase64,
identifier: "qortal_avatar", identifier: "qortal_avatar",
service: 'THUMBNAIL' service: 'THUMBNAIL',
uploadType: 'base64',
}, },
}, },
(response) => { (response) => {

View File

@ -154,7 +154,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
payload: { payload: {
data: encryptData, data: encryptData,
identifier: "ext_saved_settings", identifier: "ext_saved_settings",
service: 'DOCUMENT_PRIVATE' service: 'DOCUMENT_PRIVATE',
uploadType: 'base64',
}, },
}, },
(response) => { (response) => {

View File

@ -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]);
}

View File

@ -1,264 +1,366 @@
// @ts-nocheck // @ts-nocheck
import { Buffer } from "buffer" import { Buffer } from 'buffer';
import Base58 from "../../deps/Base58" import Base58 from '../../deps/Base58';
import nacl from "../../deps/nacl-fast" import nacl from '../../deps/nacl-fast';
import utils from "../../utils/utils" import utils from '../../utils/utils';
import { createEndpoint, getBaseApi, getKeyPair } from "../../background"; import { createEndpoint, getBaseApi, getKeyPair } from '../../background';
import { sendDataChunksToCore } from '../../qortalRequests/get';
export async function reusableGet(endpoint){ export async function reusableGet(endpoint) {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + endpoint); const response = await fetch(validApi + endpoint);
const data = await response.json(); const data = await response.json();
return data return data;
} }
async function reusablePost(endpoint, _body){ async function reusablePost(endpoint, _body) {
// const validApi = await findUsableApi(); // const validApi = await findUsableApi();
const url = await createEndpoint(endpoint) const url = await createEndpoint(endpoint);
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: _body body: _body,
}); });
let data let data;
try { try {
data = await response.clone().json() data = await response.clone().json();
} catch (e) { } catch (e) {
data = await response.text() data = await response.text();
}
return data
} }
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 ({ export const publishData = async ({
registeredName, registeredName,
file, data,
service, service,
identifier, identifier,
uploadType, uploadType,
isBase64, filename,
filename, withFee,
withFee, title,
title, description,
description, category,
category, tag1,
tag1, tag2,
tag2, tag3,
tag3, tag4,
tag4, tag5,
tag5, feeAmount,
feeAmount sender
}: any) => { }: any) => {
const validateName = async (receiverName: string) => {
const validateName = async (receiverName: string) => { return await reusableGet(`/names/${receiverName}`);
return await reusableGet(`/names/${receiverName}`) };
}
const convertBytesForSigning = async (transactionBytesBase58: string) => { const convertBytesForSigning = async (transactionBytesBase58: string) => {
return await reusablePost('/transactions/convert', transactionBytesBase58) return await reusablePost('/transactions/convert', transactionBytesBase58);
} };
const getArbitraryFee = async () => { const getArbitraryFee = async () => {
const timestamp = Date.now() const timestamp = Date.now();
let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`) let fee = await reusableGet(
`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`
);
return { return {
timestamp, timestamp,
fee: Number(fee), fee: Number(fee),
feeToShow: (Number(fee) / 1e8).toFixed(8) feeToShow: (Number(fee) / 1e8).toFixed(8),
} };
} };
const signArbitraryWithFee = (arbitraryBytesBase58, arbitraryBytesForSigningBase58, keyPair) => { const signArbitraryWithFee = (
if (!arbitraryBytesBase58) { arbitraryBytesBase58,
throw new Error('ArbitraryBytesBase58 not defined') arbitraryBytesForSigningBase58,
} keyPair
) => {
if (!keyPair) { if (!arbitraryBytesBase58) {
throw new Error('keyPair not defined') throw new Error('ArbitraryBytesBase58 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 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) => { return utils.appendBuffer(arbitraryBytesBuffer, signature);
let convertedBytesBase58 = await convertBytesForSigning( };
transactionBytesBase58
)
if (convertedBytesBase58.error) { const processTransactionVersion2 = async (bytes) => {
throw new Error('Error when signing') return await reusablePost(
} '/transactions/process?apiVersion=2',
Base58.encode(bytes)
);
};
const signAndProcessWithFee = async (transactionBytesBase58: string) => {
let convertedBytesBase58 = await convertBytesForSigning(
transactionBytesBase58
);
const resKeyPair = await getKeyPair() if (convertedBytesBase58.error) {
const parsedData = JSON.parse(resKeyPair) throw new Error('Error when signing');
const uint8PrivateKey = Base58.decode(parsedData.privateKey); }
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey
};
let signedArbitraryBytes = signArbitraryWithFee(transactionBytesBase58, convertedBytesBase58, keyPair) const resKeyPair = await getKeyPair()
const response = await processTransactionVersion2(signedArbitraryBytes) 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) { let myResponse = { error: '' };
throw new Error('Error when signing')
} else {
myResponse = response
}
return myResponse if (response === false) {
} throw new Error('Error when signing');
} else {
myResponse = response;
}
const validate = async () => { return myResponse;
let validNameRes = await validateName(registeredName) };
if (validNameRes.error) { const validate = async () => {
throw new Error('Name not found') let validNameRes = await validateName(registeredName);
}
let fee = null if (validNameRes.error) {
throw new Error('Name not found');
}
if (withFee && feeAmount) { let fee = null;
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 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) { let transactionBytes = await uploadData(registeredName, data, fee);
signAndProcessRes = await signAndProcessWithFee(transactionBytes) 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) { let signAndProcessRes;
throw new Error('Error when signing')
}
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 = '' return signAndProcessRes;
let urlSuffix = '' };
if (file != null) { const uploadData = async (registeredName: string, data: any, fee: number) => {
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API let postBody = '';
if (uploadType === 'zip') { let urlSuffix = '';
urlSuffix = '/zip'
}
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API if (data != null) {
else if (uploadType === 'file') { if (uploadType === 'base64') {
urlSuffix = '/base64' urlSuffix = '/base64';
} }
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays if (uploadType === 'base64') {
if (isBase64) { postBody = data;
postBody = file }
} } else {
throw new Error('No data provided');
}
if (!isBase64) { let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
let fileBuffer = new Uint8Array(await file.arrayBuffer()) let paramQueries = '';
postBody = Buffer.from(fileBuffer).toString("base64") if (identifier?.trim().length > 0) {
} uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
}
} paramQueries = paramQueries + `?fee=${fee}`;
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}`
if (identifier?.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}`
}
uploadDataUrl = uploadDataUrl + `?fee=${fee}`
if (filename != null && filename != 'undefined') { if (filename != null && filename != 'undefined') {
uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename) paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
} }
if (title != null && title != 'undefined') { if (title != null && title != 'undefined') {
uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title) paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
} }
if (description != null && description != 'undefined') { if (description != null && description != 'undefined') {
uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description) paramQueries =
} paramQueries + '&description=' + encodeURIComponent(description);
}
if (category != null && category != 'undefined') { if (category != null && category != 'undefined') {
uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category) paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
} }
if (tag1 != null && tag1 != 'undefined') { if (tag1 != null && tag1 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
} }
if (tag2 != null && tag2 != 'undefined') { if (tag2 != null && tag2 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
} }
if (tag3 != null && tag3 != 'undefined') { if (tag3 != null && tag3 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
} }
if (tag4 != null && tag4 != 'undefined') { if (tag4 != null && tag4 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
} }
if (tag5 != null && tag5 != 'undefined') { if (tag5 != null && tag5 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5) 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 { const file = data;
return await validate() // const urlCheck = `/arbitrary/check-tmp-space?totalSize=${file.size}`;
} catch (error: any) {
throw new Error(error?.message) // 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);
}
};

View File

@ -38,7 +38,7 @@ import {
getNameOrAddress, getNameOrAddress,
getAssetBalanceInfo getAssetBalanceInfo
} from "../background"; } from "../background";
import { decryptGroupEncryption, getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; import { decryptGroupEncryption, getAllUserNames, getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption";
import { QORT_DECIMALS } from "../constants/constants"; import { QORT_DECIMALS } from "../constants/constants";
import Base58 from "../deps/Base58"; 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) { function sendToSaveFilePicker(data, sender) {
chrome.tabs.sendMessage(sender.tab.id, { chrome.tabs.sendMessage(sender.tab.id, {
@ -883,7 +916,7 @@ export const publishQDNResource = async (data: any, sender, isFromExtension) =>
if(appFee && appFee > 0 && appFeeRecipient){ if(appFee && appFee > 0 && appFeeRecipient){
hasAppFee = true hasAppFee = true
} }
const registeredName = await getNameInfo(); const registeredName = data?.name || await getNameInfo();
const name = registeredName; const name = registeredName;
if(!name){ if(!name){
throw new Error('User has no Qortal 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"); throw new Error("Encrypting data requires public keys");
} }
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId, sender);
}
if (data.encrypt) { if (data.encrypt) {
try { try {
const resKeyPair = await getKeyPair() const resKeyPair = await getKeyPair()
const parsedData = JSON.parse(resKeyPair) const parsedData = JSON.parse(resKeyPair)
const privateKey = parsedData.privateKey const privateKey = parsedData.privateKey
const userPublicKey = parsedData.publicKey const userPublicKey = parsedData.publicKey
if (data?.fileId) {
data64 = await getFileBase64(data?.fileId, sender);
}
const encryptDataResponse = encryptDataGroup({ const encryptDataResponse = encryptDataGroup({
data64, data64,
publicKeys: data.publicKeys, publicKeys: data.publicKeys,
@ -973,11 +1007,10 @@ const { tag1, tag2, tag3, tag4, tag5 } = result;
try { try {
const resPublish = await publishData({ const resPublish = await publishData({
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: data64, data: data64 ? data64 : data?.fileId,
service: service, service: service,
identifier: encodeURIComponent(identifier), identifier: encodeURIComponent(identifier),
uploadType: "file", uploadType: data64 ? "base64" : "file",
isBase64: true,
filename: filename, filename: filename,
title, title,
description, description,
@ -989,6 +1022,7 @@ const { tag1, tag2, tag3, tag4, tag5 } = result;
tag5, tag5,
apiVersion: 2, apiVersion: 2,
withFee: true, withFee: true,
sender
}); });
if(resPublish?.signature && hasAppFee && checkbox1){ if(resPublish?.signature && hasAppFee && checkbox1){
sendCoinFunc({ sendCoinFunc({
@ -1082,6 +1116,14 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
if(!name){ if(!name){
throw new Error('You need a Qortal name to publish.') 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 appFee = data?.appFee ? +data.appFee : undefined
const appFeeRecipient = data?.appFeeRecipient const appFeeRecipient = data?.appFeeRecipient
let hasAppFee = false let hasAppFee = false
@ -1208,7 +1250,8 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
} }
const service = resource.service; const service = resource.service;
let identifier = resource.identifier; 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 filename = resource.filename;
const title = resource.title; const title = resource.title;
const description = resource.description; const description = resource.description;
@ -1232,26 +1275,34 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
if (resource.fileId) { if (resource.fileId) {
data64 = await getFileFromContentScript(resource.fileId, sender); rawData = resource.fileId;
} }
// if (resource.fileId) {
// data64 = await sendDataChunksToCore(resource.fileId, sender);
// }
if (resourceEncrypt) { if (resourceEncrypt) {
try { try {
if (resource?.fileId) {
rawData = await getFileBase64(resource.fileId, sender);
}
const resKeyPair = await getKeyPair() const resKeyPair = await getKeyPair()
const parsedData = JSON.parse(resKeyPair) const parsedData = JSON.parse(resKeyPair)
const privateKey = parsedData.privateKey const privateKey = parsedData.privateKey
const userPublicKey = parsedData.publicKey const userPublicKey = parsedData.publicKey
const encryptDataResponse = encryptDataGroup({ const encryptDataResponse = encryptDataGroup({
data64, data64: rawData,
publicKeys: data.publicKeys, publicKeys: data.publicKeys,
privateKey, privateKey,
userPublicKey userPublicKey
}); });
if (encryptDataResponse) { if (encryptDataResponse) {
data64 = encryptDataResponse; rawData = encryptDataResponse;
} }
} catch (error) { } catch (error) {
const errorMsg = const errorMsg =
@ -1260,20 +1311,24 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
} }
try { try {
const dataType =
(resource?.base64 || resource?.data64 || resourceEncrypt)
? 'base64'
: 'file';
await retryTransaction(publishData, [ await retryTransaction(publishData, [
{ {
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: data64, data: rawData,
service: service, service: service,
identifier: encodeURIComponent(identifier), identifier: encodeURIComponent(identifier),
uploadType: "file", uploadType: dataType,
isBase64: true,
filename: filename, filename: filename,
title, title,
description, description,
@ -1285,6 +1340,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
tag5, tag5,
apiVersion: 2, apiVersion: 2,
withFee: true, withFee: true,
sender
}, },
], true); ], true);
await new Promise((res) => { await new Promise((res) => {
@ -1298,6 +1354,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
} }
} catch (error) { } catch (error) {
@ -1305,6 +1362,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
reason: error?.message || "Unknown error", reason: error?.message || "Unknown error",
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
} }
} }
@ -1683,6 +1741,46 @@ export const joinGroup = async (data, isFromExtension) => {
export const saveFile = async (data, sender, isFromExtension) => { export const saveFile = async (data, sender, isFromExtension) => {
try { 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 requiredFields = ["filename", "fileId"];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@ -4084,9 +4182,10 @@ export const updateNameRequest = async (data, isFromExtension) => {
const fee = await getFee("UPDATE_NAME"); const fee = await getFee("UPDATE_NAME");
const resPermission = await getUserPermission( const resPermission = await getUserPermission(
{ {
text1: `Do you give this application permission to register this name?`, text1: `Do you give this application permission to update this name?`,
highlightedText: data.newName, text2: `previous name: ${oldName}`,
text2: data?.description, text3: `new name: ${newName}`,
text4: data?.description,
fee: fee.fee, fee: fee.fee,
}, },
isFromExtension isFromExtension
@ -4807,7 +4906,7 @@ export const updateGroupRequest = async (data, isFromExtension) => {
const requiredFields = ["groupId", "newOwner", "type", "approvalThreshold", "minBlock", "maxBlock"]; const requiredFields = ["groupId", "newOwner", "type", "approvalThreshold", "minBlock", "maxBlock"];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -4872,7 +4971,7 @@ export const sellNameRequest = async (data, isFromExtension) => {
const requiredFields = ["salePrice", "nameForSale"]; const requiredFields = ["salePrice", "nameForSale"];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -4917,7 +5016,7 @@ export const cancelSellNameRequest = async (data, isFromExtension) => {
const requiredFields = ["nameForSale"]; const requiredFields = ["nameForSale"];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -4958,7 +5057,7 @@ export const buyNameRequest = async (data, isFromExtension) => {
const requiredFields = ["nameForSale"]; const requiredFields = ["nameForSale"];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -5261,12 +5360,11 @@ const assetBalance = await getAssetBalanceInfo(assetId)
const resPublish = await retryTransaction(publishData, [ const resPublish = await retryTransaction(publishData, [
{ {
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: encryptDataResponse, data: encryptDataResponse,
service: transaction.service, service: transaction.service,
identifier: encodeURIComponent(transaction.identifier), identifier: encodeURIComponent(transaction.identifier),
uploadType: "file", uploadType: "base64",
description: transaction?.description, description: transaction?.description,
isBase64: true,
apiVersion: 2, apiVersion: 2,
withFee: true, withFee: true,
}, },
@ -5302,12 +5400,11 @@ const assetBalance = await getAssetBalanceInfo(assetId)
const resPublish = await retryTransaction(publishData, [ const resPublish = await retryTransaction(publishData, [
{ {
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: encryptDataResponse, data: encryptDataResponse,
service: transaction.service, service: transaction.service,
identifier: encodeURIComponent(transaction.identifier), identifier: encodeURIComponent(transaction.identifier),
uploadType: "file", uploadType: "base64",
description: transaction?.description, description: transaction?.description,
isBase64: true,
apiVersion: 2, apiVersion: 2,
withFee: true, withFee: true,
}, },