handle large uploads

This commit is contained in:
PhilReact 2025-05-22 19:05:58 +03:00
parent 0cfdd5cbc9
commit 0b9f32fd8c
16 changed files with 827 additions and 437 deletions

View File

@ -4,6 +4,8 @@ import com.getcapacitor.BridgeActivity;
import com.github.Qortal.qortalMobile.NativeBcrypt; import com.github.Qortal.qortalMobile.NativeBcrypt;
import com.github.Qortal.qortalMobile.NativePOW; import com.github.Qortal.qortalMobile.NativePOW;
import android.os.Bundle; import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
@Override @Override
@ -12,6 +14,9 @@ public class MainActivity extends BridgeActivity {
registerPlugin(NativePOW.class); registerPlugin(NativePOW.class);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Enable mixed content mode for WebView
WebView webView = this.bridge.getWebView();
WebSettings webSettings = webView.getSettings();
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
} }
} }

View File

@ -19,7 +19,7 @@ const config: CapacitorConfig = {
"splashImmersive": true "splashImmersive": true
}, },
CapacitorHttp: { CapacitorHttp: {
enabled: true, enabled: false,
} }
} }
}; };

View File

@ -1329,6 +1329,7 @@ export async function publishOnQDNCase(request, event) {
try { try {
const { const {
data, data,
name = "",
identifier, identifier,
service, service,
title, title,
@ -1346,6 +1347,7 @@ export async function publishOnQDNCase(request, event) {
identifier, identifier,
service, service,
title, title,
name,
description, description,
category, category,
tag1, tag1,

View File

@ -59,6 +59,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 getData<any>("keyPair").catch(() => null); const res = await getData<any>("keyPair").catch(() => null);
if (res) { if (res) {
@ -151,7 +161,7 @@ async function getKeyPair() {
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,
@ -202,7 +212,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,
@ -223,7 +233,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
@ -242,15 +252,16 @@ export const publishOnQDN = async ({data, identifier, service, title,
tag3, tag3,
tag4, tag4,
tag5, tag5,
name,
uploadType = 'file' uploadType = 'file'
}) => { }) => {
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, service, identifier, uploadType, withFee: true, title,
description, description,
category, category,
tag1, tag1,

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,7 @@ 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 { 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 +83,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("");
@ -99,6 +101,8 @@ export const AppPublish = ({ names, categories }) => {
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [isLoading, setIsLoading] = useState(""); const [isLoading, setIsLoading] = useState("");
const mySortedNames = useSortedMyNames(names, myName);
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({
accept: { accept: {
@ -162,6 +166,25 @@ export const AppPublish = ({ names, categories }) => {
getQapp(name, appType); getQapp(name, appType);
}, [name, appType]); }, [name, appType]);
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 publishApp = async () => { const publishApp = async () => {
try { try {
const data = { const data = {
@ -199,10 +222,10 @@ 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 new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("publishOnQDN", { window.sendMessage("publishOnQDN", {
data: fileBase64, data: file,
service: appType, service: appType,
title, title,
description, description,
@ -213,6 +236,7 @@ export const AppPublish = ({ names, categories }) => {
tag4, tag4,
tag5, tag5,
uploadType: "zip", uploadType: "zip",
name
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
@ -287,7 +311,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

@ -17,7 +17,7 @@ import { AppsLibrary } from "./AppsLibrary";
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
export const Apps = ({ mode, setMode, show , myName}) => { export const Apps = ({ mode, setMode, show , myName, 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)
@ -298,7 +298,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
> >
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />} {mode !== "viewer" && !selectedTab && <Spacer height="30px" />}
{mode === "home" && ( {mode === "home" && (
<AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} myAddress={myAddress} />
)} )}
<AppsLibrary <AppsLibrary
@ -314,7 +314,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{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} />}
<AppsCategory availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} /> <AppsCategory 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]) {
@ -335,7 +335,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{isNewTabWindow && mode === "viewer" && ( {isNewTabWindow && mode === "viewer" && (
<> <>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} myAddress={myAddress} />
</> </>
)} )}
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />} {mode !== "viewer" && !selectedTab && <Spacer height="180px" />}

View File

@ -20,7 +20,7 @@ import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials"; import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
import { AppsPrivate } from "./AppsPrivate"; import { AppsPrivate } from "./AppsPrivate";
export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }) => { export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName, myAddress }) => {
const [qortalUrl, setQortalUrl] = useState('') const [qortalUrl, setQortalUrl] = useState('')
const { showTutorial } = useContext(GlobalContext); const { showTutorial } = useContext(GlobalContext);
@ -146,7 +146,7 @@ export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }
<AppCircleLabel>Library</AppCircleLabel> <AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
<AppsPrivate myName={myName} /> <AppsPrivate myName={myName} myAddress={myAddress} />
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} /> <SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />

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,15 +30,18 @@ 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 { openApp } = useHandlePrivateApps(); const { openApp } = useHandlePrivateApps();
const [names, setNames] = useState([]);
const [name, setName] = useState(0);
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null); const [logo, setLogo] = useState(null);
const [qortalUrl, setQortalUrl] = useState(""); const [qortalUrl, setQortalUrl] = useState("");
@ -48,6 +51,7 @@ export const AppsPrivate = ({myName}) => {
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState( const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
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)
@ -165,6 +169,8 @@ export const AppsPrivate = ({myName}) => {
data: decryptedData, data: decryptedData,
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
uploadType: 'base64',
name,
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
@ -181,7 +187,7 @@ export const AppsPrivate = ({myName}) => {
{ {
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
name: myName, name,
groupId: selectedGroup, groupId: selectedGroup,
}, },
true true
@ -196,6 +202,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);
}; };
@ -432,6 +454,34 @@ 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

@ -10,9 +10,99 @@ import { MyContext } from '../../App';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core';
import { createEndpoint } from '../../background';
import { uint8ArrayToBase64 } from '../../backgroundFunctions/encryption';
export const isNative = Capacitor.isNativePlatform(); export const isNative = Capacitor.isNativePlatform();
export const saveFileInChunksFromUrl = async (
location,
) => {
let fileName = location.filename
let locationUrl = `/arbitrary/${location.service}/${location.name}`;
if (location.identifier) {
locationUrl = locationUrl + `/${location.identifier}`;
}
const endpoint = await createEndpoint(
locationUrl +
`?attachment=true&attachmentFilename=${location?.filename}`
);
const response = await fetch(endpoint);
if (!response.ok || !response.body) {
throw new Error('Failed to fetch file or no readable stream');
}
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
const base64Prefix = `data:${contentType};base64,`;
const getExtensionFromFileName = (name: string): string => {
const lastDotIndex = name.lastIndexOf('.');
return lastDotIndex !== -1 ? name.substring(lastDotIndex) : '';
};
const existingExtension = getExtensionFromFileName(fileName);
if (existingExtension) {
fileName = fileName.substring(0, fileName.lastIndexOf('.'));
}
const mimeTypeToExtension = (mimeType: string): string => {
return mimeToExtensionMap[mimeType] || existingExtension || '';
};
const extension = mimeTypeToExtension(contentType);
const fullFileName = `${fileName}_${Date.now()}${extension}`;
const reader = response.body.getReader();
let isFirstChunk = true;
let done = false;
let buffer = new Uint8Array(0);
const preferredChunkSize = 1024 * 1024; // 1MB
while (!done) {
const result = await reader.read();
done = result.done;
if (result.value) {
// Combine new value with existing buffer
const newBuffer = new Uint8Array(buffer.length + result.value.length);
newBuffer.set(buffer);
newBuffer.set(result.value, buffer.length);
buffer = newBuffer;
// While we have enough data, process 1MB chunks
while (buffer.length >= preferredChunkSize) {
const chunk = buffer.slice(0, preferredChunkSize);
buffer = buffer.slice(preferredChunkSize);
const base64Chunk = uint8ArrayToBase64(chunk);
await Filesystem.writeFile({
path: fullFileName,
data: isFirstChunk ? base64Prefix + base64Chunk : base64Chunk,
directory: Directory.Documents,
recursive: true,
append: !isFirstChunk,
});
isFirstChunk = false;
}
}
}
// Write remaining buffer (if any)
if (buffer.length > 0) {
const base64Chunk = uint8ArrayToBase64(buffer);
await Filesystem.writeFile({
path: fullFileName,
data: isFirstChunk ? base64Prefix + base64Chunk : base64Chunk,
directory: Directory.Documents,
recursive: true,
append: !isFirstChunk,
});
}
};
export const saveFileInChunks = async ( export const saveFileInChunks = async (
blob: Blob, blob: Blob,
@ -586,38 +676,26 @@ isDOMContentLoaded: false
} else if(event?.data?.action === 'SAVE_FILE' } else if(event?.data?.action === 'SAVE_FILE'
){ ){
try { try {
const res = await saveFile( event.data, null, true, { await saveFile(event.data, null, true, {
openSnackGlobal, openSnackGlobal,
setOpenSnackGlobal, setOpenSnackGlobal,
infoSnackCustom, infoSnackCustom,
setInfoSnackCustom setInfoSnackCustom,
});
event.ports[0].postMessage({
result: true,
error: null,
}); });
} catch (error) { } catch (error) {
event.ports[0].postMessage({
result: null,
error: error?.message || 'Failed to save file',
});
} }
} else if ( } else if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
event?.data?.action === 'ENCRYPT_DATA' || 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'
) { ) {
if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE'
){
try {
checkMobileSizeConstraints(event.data)
} catch (error) {
event.ports[0].postMessage({
result: null,
error: error?.message,
});
return;
}
}
let data; let data;
try { try {
data = await storeFilesInIndexedDB(event.data); data = await storeFilesInIndexedDB(event.data);
@ -640,6 +718,29 @@ isDOMContentLoaded: false
error: 'Failed to prepare data for publishing', error: 'Failed to prepare data for publishing',
}); });
} }
} else if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE'
) {
const data = event.data;
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 === '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

View File

@ -574,7 +574,7 @@ export const Group = ({
}); });
} catch (error) { } catch (error) {
console.log("error", error); console.error(error);
} }
}; };
@ -2756,7 +2756,7 @@ export const Group = ({
/> />
)} )}
{isMobile && ( {isMobile && (
<Apps mode={appsMode} setMode={setAppsMode} show={mobileViewMode === "apps"} myName={userInfo?.name} /> <Apps mode={appsMode} setMode={setAppsMode} show={mobileViewMode === "apps"} myName={userInfo?.name} myAddress={userInfo?.address} />
)} )}
{!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}

View File

@ -226,6 +226,7 @@ export const ListOfGroupPromotions = () => {
data: data, data: data,
identifier: identifier, identifier: identifier,
service: "DOCUMENT", service: "DOCUMENT",
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

View File

@ -67,6 +67,7 @@ const [isLoading, setIsLoading] = useState(false)
data: avatarBase64, data: avatarBase64,
identifier: "qortal_avatar", identifier: "qortal_avatar",
service: "THUMBNAIL", service: "THUMBNAIL",
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

View File

@ -155,6 +155,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
data: encryptData, data: encryptData,
identifier: "ext_saved_settings", identifier: "ext_saved_settings",
service: "DOCUMENT_PRIVATE", service: "DOCUMENT_PRIVATE",
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

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,265 +1,365 @@
// @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 } from "../../background"; import { createEndpoint, getBaseApi } from '../../background';
import { getData } from "../../utils/chromeStorage"; import { getData } from '../../utils/chromeStorage';
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() { async function getKeyPair() {
const res = await getData<any>("keyPair").catch(() => null); const res = await getData<any>('keyPair').catch(() => null);
if (res) { if (res) {
return res return res;
} else { } else {
throw new Error("Wallet not authenticated"); 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
}: any) => { }: any) => {
const validateName = async (receiverName: string) => {
return await reusableGet(`/names/${receiverName}`);
};
const validateName = async (receiverName: string) => { const convertBytesForSigning = async (transactionBytesBase58: string) => {
return await reusableGet(`/names/${receiverName}`) return await reusablePost('/transactions/convert', transactionBytesBase58);
} };
const convertBytesForSigning = async (transactionBytesBase58: string) => { const getArbitraryFee = async () => {
return await reusablePost('/transactions/convert', transactionBytesBase58) const timestamp = Date.now();
}
const getArbitraryFee = async () => { let fee = await reusableGet(
const timestamp = Date.now() `/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`
);
let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`) return {
timestamp,
fee: Number(fee),
feeToShow: (Number(fee) / 1e8).toFixed(8),
};
};
return { const signArbitraryWithFee = (
timestamp, arbitraryBytesBase58,
fee: Number(fee), arbitraryBytesForSigningBase58,
feeToShow: (Number(fee) / 1e8).toFixed(8) keyPair
} ) => {
} if (!arbitraryBytesBase58) {
throw new Error('ArbitraryBytesBase58 not defined');
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 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 = 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 = 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 (withFee && feeAmount) {
if (!transactionBytes || transactionBytes.error) { fee = feeAmount;
throw new Error(transactionBytes?.message || 'Error when uploading') } else if (withFee) {
} else if (transactionBytes.includes('Error 500 Internal Server Error')) { const res = await getArbitraryFee();
throw new Error('Error when uploading') if (res.fee) {
} fee = res.fee;
} else {
throw new Error('unable to get fee');
}
}
let signAndProcessRes 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 (withFee) { let signAndProcessRes;
signAndProcessRes = await signAndProcessWithFee(transactionBytes)
}
if (signAndProcessRes?.error) { if (withFee) {
throw new Error('Error when signing') signAndProcessRes = await signAndProcessWithFee(transactionBytes);
} }
return signAndProcessRes if (signAndProcessRes?.error) {
} throw new Error('Error when signing');
}
const uploadData = async (registeredName: string, file:any, fee: number) => { return signAndProcessRes;
};
let postBody = '' const uploadData = async (registeredName: string, data: any, fee: number) => {
let urlSuffix = '' let postBody = '';
let urlSuffix = '';
if (file != null) { if (data != null) {
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API if (uploadType === 'base64') {
if (uploadType === 'zip') { urlSuffix = '/base64';
urlSuffix = '/zip' }
}
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API if (uploadType === 'base64') {
else if (uploadType === 'file') { postBody = data;
urlSuffix = '/base64' }
} } else {
throw new Error('No data provided');
}
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
if (isBase64) { let paramQueries = '';
postBody = file if (identifier?.trim().length > 0) {
} uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
}
if (!isBase64) { paramQueries = paramQueries + `?fee=${fee}`;
let fileBuffer = new Uint8Array(await file.arrayBuffer())
postBody = Buffer.from(fileBuffer).toString("base64")
}
} if (filename != null && filename != 'undefined') {
paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
}
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}` if (title != null && title != 'undefined') {
if (identifier?.trim().length > 0) { paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}` }
}
uploadDataUrl = uploadDataUrl + `?fee=${fee}` if (description != null && description != 'undefined') {
paramQueries =
paramQueries + '&description=' + encodeURIComponent(description);
}
if (category != null && category != 'undefined') {
paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
}
if (filename != null && filename != 'undefined') { if (tag1 != null && tag1 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
} }
if (title != null && title != 'undefined') { if (tag2 != null && tag2 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
} }
if (description != null && description != 'undefined') { if (tag3 != null && tag3 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
} }
if (category != null && category != 'undefined') { if (tag4 != null && tag4 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
} }
if (tag1 != null && tag1 != 'undefined') { if (tag5 != null && tag5 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5);
} }
if (uploadType === 'zip') {
paramQueries = paramQueries + '&isZip=' + true;
}
if (tag2 != null && tag2 != 'undefined') { if (uploadType === 'base64') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2) if (urlSuffix) {
} uploadDataUrl = uploadDataUrl + urlSuffix;
}
uploadDataUrl = uploadDataUrl + paramQueries;
return await reusablePost(uploadDataUrl, postBody);
}
if (tag3 != null && tag3 != 'undefined') { const file = data;
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3) const urlCheck = `/arbitrary/check-tmp-space?totalSize=${file.size}`;
}
if (tag4 != null && tag4 != 'undefined') { const checkEndpoint = await createEndpoint(urlCheck);
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4) const checkRes = await fetch(checkEndpoint);
} if (!checkRes.ok) {
throw new Error('Not enough space on your hard drive');
}
if (tag5 != null && tag5 != 'undefined') { const chunkUrl = uploadDataUrl + `/chunk`;
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5) const chunkSize = 1 * 1024 * 1024; // 1MB
}
return await reusablePost(uploadDataUrl, postBody) 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);
try { await uploadChunkWithRetry(chunkUrl, formData, index);
return await validate() }
} catch (error: any) { const finalizeUrl = uploadDataUrl + `/finalize` + paramQueries;
throw new Error(error?.message)
} 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

@ -37,10 +37,11 @@ import {
getNameOrAddress, getNameOrAddress,
getAssetInfo, getAssetInfo,
transferAsset, transferAsset,
getPublicKey getPublicKey,
isNative
} from "../background"; } from "../background";
import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; import { getNameInfo, uint8ArrayToObject,getAllUserNames } from "../backgroundFunctions/encryption";
import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener"; import { saveFileInChunksFromUrl, showSaveFilePicker } from "../components/Apps/useQortalMessageListener";
import { QORT_DECIMALS } from "../constants/constants"; import { QORT_DECIMALS } from "../constants/constants";
import Base58 from "../deps/Base58"; import Base58 from "../deps/Base58";
import { import {
@ -74,6 +75,7 @@ import ed2curve from "../deps/ed2curve";
import { Sha256 } from "asmcrypto.js"; import { Sha256 } from "asmcrypto.js";
import { isValidBase64WithDecode } from "../utils/decode"; import { isValidBase64WithDecode } from "../utils/decode";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { fileToBase64 } from "../utils/fileReading";
const uid = new ShortUniqueId({ length: 6 }); const uid = new ShortUniqueId({ length: 6 });
@ -914,7 +916,7 @@ export const publishQDNResource = async (
sender, sender,
isFromExtension isFromExtension
) => { ) => {
const requiredFields = ["service"]; const requiredFields = ['service'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (!data[field]) { if (!data[field]) {
@ -922,25 +924,26 @@ export const publishQDNResource = async (
} }
}); });
if (missingFields.length > 0) { if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", "); const missingFieldsString = missingFields.join(', ');
const errorMsg = `Missing fields: ${missingFieldsString}`; const errorMsg = `Missing fields: ${missingFieldsString}`;
throw new Error(errorMsg); throw new Error(errorMsg);
} }
if (!data.fileId && !data.data64 && !data.base64) { if (!data.file && !data.data64 && !data.base64) {
throw new Error("No data or file was submitted"); throw new Error('No data or file was submitted');
} }
// Use "default" if user hasn't specified an identifer // Use "default" if user hasn't specified an identifier
const service = data.service; const service = data.service;
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;
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');
} }
let identifier = data.identifier; let identifier = data.identifier;
let data64 = data.data64 || data.base64; let data64 = data.data64 || data.base64;
@ -948,7 +951,7 @@ export const publishQDNResource = async (
const title = data.title; const title = data.title;
const description = data.description; const description = data.description;
const category = data.category; const category = data.category;
const file = data?.file || data?.blob;
const tags = data?.tags || []; const tags = data?.tags || [];
const result = {}; const result = {};
@ -961,26 +964,26 @@ export const publishQDNResource = async (
const { tag1, tag2, tag3, tag4, tag5 } = result; const { tag1, tag2, tag3, tag4, tag5 } = result;
if (data.identifier == null) { if (data.identifier == null) {
identifier = "default"; identifier = 'default';
}
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId);
} }
if ( if (
data.encrypt && data.encrypt &&
(!data.publicKeys || (!data.publicKeys ||
(Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) (Array.isArray(data.publicKeys) && data.publicKeys.length === 0))
) { ) {
throw new Error("Encrypting data requires public keys"); throw new Error('Encrypting data requires public keys');
} }
if (data.encrypt) { if (data.encrypt) {
try { try {
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
const privateKey = parsedData.privateKey; const privateKey = parsedData.privateKey;
const userPublicKey = parsedData.publicKey; const userPublicKey = parsedData.publicKey;
if (data?.file || data?.blob) {
data64 = await fileToBase64(data?.file || data?.blob);
}
const encryptDataResponse = encryptDataGroup({ const encryptDataResponse = encryptDataGroup({
data64, data64,
publicKeys: data.publicKeys, publicKeys: data.publicKeys,
@ -992,49 +995,46 @@ export const publishQDNResource = async (
} }
} catch (error) { } catch (error) {
throw new Error( throw new Error(
error.message || "Upload failed due to failed encryption" error.message || 'Upload failed due to failed encryption'
); );
} }
} }
const fee = await getFee("ARBITRARY"); const fee = await getFee('ARBITRARY');
const handleDynamicValues = {} const handleDynamicValues = {};
if(hasAppFee){ if (hasAppFee) {
const feePayment = await getFee("PAYMENT"); const feePayment = await getFee('PAYMENT');
handleDynamicValues['appFee'] = +appFee + +feePayment.fee, (handleDynamicValues['appFee'] = +appFee + +feePayment.fee),
handleDynamicValues['checkbox1'] = { (handleDynamicValues['checkbox1'] = {
value: true, value: true,
label: "accept app fee", label: 'accept app fee',
} });
} }
if(data?.encrypt){ if (!!data?.encrypt) {
handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}` handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}`;
} }
const resPermission = await getUserPermission( const resPermission = await getUserPermission(
{ {
text1: "Do you give this application permission to publish to QDN?", text1: 'Do you give this application permission to publish to QDN?',
text2: `service: ${service}`, text2: `service: ${service}`,
text3: `identifier: ${identifier || null}`, text3: `identifier: ${identifier || null}`,
text4: `name: ${registeredName}`,
fee: fee.fee, fee: fee.fee,
...handleDynamicValues ...handleDynamicValues,
}, },
isFromExtension isFromExtension
); );
const { accepted, checkbox1 = false } = resPermission; const { accepted, checkbox1 = false } = resPermission;
if (accepted) { if (accepted) {
try { try {
const resPublish = await publishData({ const resPublish = await publishData({
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: data64, data: data64 ? data64 : file,
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,
@ -1047,18 +1047,21 @@ export const publishQDNResource = async (
apiVersion: 2, apiVersion: 2,
withFee: true, withFee: true,
}); });
if(resPublish?.signature && hasAppFee && checkbox1){ if (resPublish?.signature && hasAppFee && checkbox1) {
sendCoinFunc({ sendCoinFunc(
amount: appFee, {
receiver: appFeeRecipient amount: appFee,
}, true) receiver: appFeeRecipient,
} },
true
);
}
return resPublish; return resPublish;
} catch (error) { } catch (error) {
throw new Error(error?.message || "Upload failed"); throw new Error(error?.message || 'Upload failed');
} }
} else { } else {
throw new Error("User declined request"); throw new Error('User declined request');
} }
}; };
@ -1096,8 +1099,12 @@ export const checkArrrSyncStatus = async (seed) => {
throw new Error("Failed to synchronize after 36 attempts"); throw new Error("Failed to synchronize after 36 attempts");
}; };
export const publishMultipleQDNResources = async (data: any, sender, isFromExtension) => { export const publishMultipleQDNResources = async (
const requiredFields = ["resources"]; data: any,
sender,
isFromExtension
) => {
const requiredFields = ['resources'];
const missingFields: string[] = []; const missingFields: string[] = [];
let feeAmount = null; let feeAmount = null;
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@ -1106,64 +1113,74 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
} }
}); });
if (missingFields.length > 0) { if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", "); const missingFieldsString = missingFields.join(', ');
const errorMsg = `Missing fields: ${missingFieldsString}`; const errorMsg = `Missing fields: ${missingFieldsString}`;
throw new Error(errorMsg); throw new Error(errorMsg);
} }
const resources = data.resources; const resources = data.resources;
if (!Array.isArray(resources)) { if (!Array.isArray(resources)) {
throw new Error("Invalid data"); throw new Error('Invalid data');
} }
if (resources.length === 0) { if (resources.length === 0) {
throw new Error("No resources to publish"); throw new Error('No resources to publish');
} }
const encrypt = data?.encrypt
const encrypt = data?.encrypt;
for (const resource of resources) { for (const resource of resources) {
const resourceEncrypt = encrypt && resource?.disableEncrypt !== true const resourceEncrypt = encrypt && resource?.disableEncrypt !== true;
if (!resourceEncrypt && resource?.service.endsWith("_PRIVATE")) { if (!resourceEncrypt && resource?.service.endsWith('_PRIVATE')) {
const errorMsg = "Only encrypted data can go into private services"; const errorMsg = 'Only encrypted data can go into private services';
throw new Error(errorMsg) throw new Error(errorMsg);
} else if(resourceEncrypt && !resource?.service.endsWith("_PRIVATE")){ } else if (resourceEncrypt && !resource?.service.endsWith('_PRIVATE')) {
const errorMsg = "For an encrypted publish please use a service that ends with _PRIVATE"; const errorMsg =
throw new Error(errorMsg) 'For an encrypted publish please use a service that ends with _PRIVATE';
throw new Error(errorMsg);
} }
} }
const fee = await getFee("ARBITRARY");
const fee = await getFee('ARBITRARY');
const registeredName = await getNameInfo(); const registeredName = await getNameInfo();
const name = registeredName; const name = registeredName;
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 appFee = data?.appFee ? +data.appFee : undefined const userNames = await getAllUserNames();
const appFeeRecipient = data?.appFeeRecipient data.resources?.forEach((item) => {
let hasAppFee = false if (item?.name && !userNames?.includes(item.name))
if(appFee && appFee > 0 && appFeeRecipient){ throw new Error(
hasAppFee = true `The name ${item.name}, does not belong to the publisher.`
);
});
const appFee = data?.appFee ? +data.appFee : undefined;
const appFeeRecipient = data?.appFeeRecipient;
let hasAppFee = false;
if (appFee && appFee > 0 && appFeeRecipient) {
hasAppFee = true;
} }
const handleDynamicValues = {} const handleDynamicValues = {};
if(hasAppFee){ if (hasAppFee) {
const feePayment = await getFee("PAYMENT"); const feePayment = await getFee('PAYMENT');
handleDynamicValues['appFee'] = +appFee + +feePayment.fee, (handleDynamicValues['appFee'] = +appFee + +feePayment.fee),
handleDynamicValues['checkbox1'] = { (handleDynamicValues['checkbox1'] = {
value: true, value: true,
label: "accept app fee", label: 'accept app fee',
} });
} }
if(data?.encrypt){ if (data?.encrypt) {
handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}` handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}`;
} }
const resPermission = await getUserPermission({ const resPermission = await getUserPermission(
text1: "Do you give this application permission to publish to QDN?", {
html: ` text1: 'Do you give this application permission to publish to QDN?',
html: `
<div style="max-height: 30vh; overflow-y: auto;"> <div style="max-height: 30vh; overflow-y: auto;">
<style> <style>
body {
background-color: #121212;
color: #e0e0e0;
}
.resource-container { .resource-container {
display: flex; display: flex;
@ -1172,7 +1189,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
padding: 16px; padding: 16px;
margin: 8px 0; margin: 8px 0;
border-radius: 8px; border-radius: 8px;
background-color: #1e1e1e; background-color: var(--background-default);
} }
.resource-detail { .resource-detail {
@ -1181,7 +1198,7 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
.resource-detail span { .resource-detail span {
font-weight: bold; font-weight: bold;
color: #bb86fc; color: var(--text-primary);
} }
@media (min-width: 600px) { @media (min-width: 600px) {
@ -1204,34 +1221,34 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
<div class="resource-detail"><span>Service:</span> ${ <div class="resource-detail"><span>Service:</span> ${
resource.service resource.service
}</div> }</div>
<div class="resource-detail"><span>Name:</span> ${name}</div> <div class="resource-detail"><span>Name:</span> ${resource?.name || name}</div>
<div class="resource-detail"><span>Identifier:</span> ${ <div class="resource-detail"><span>Identifier:</span> ${
resource.identifier resource.identifier
}</div> }</div>
${ ${
resource.filename resource.filename
? `<div class="resource-detail"><span>Filename:</span> ${resource.filename}</div>` ? `<div class="resource-detail"><span>Filename:</span> ${resource.filename}</div>`
: "" : ''
} }
</div>` </div>`
) )
.join("")} .join('')}
</div> </div>
`, `,
fee: +fee.fee * resources.length, fee: +fee.fee * resources.length,
...handleDynamicValues ...handleDynamicValues,
}, isFromExtension); },
isFromExtension
const { accepted, checkbox1 = false } = resPermission; );
const { accepted, checkbox1 = false } = resPermission;
if (!accepted) { if (!accepted) {
throw new Error("User declined request"); throw new Error('User declined request');
} }
let failedPublishesIdentifiers = []; let failedPublishesIdentifiers = [];
for (const resource of resources) { for (const resource of resources) {
try { try {
const requiredFields = ["service"]; const requiredFields = ['service'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (!resource[field]) { if (!resource[field]) {
@ -1239,34 +1256,35 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
} }
}); });
if (missingFields.length > 0) { if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", "); const missingFieldsString = missingFields.join(', ');
const errorMsg = `Missing fields: ${missingFieldsString}`; const errorMsg = `Missing fields: ${missingFieldsString}`;
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
if (!resource.fileId && !resource.data64 && !resource?.base64) { if (!resource.file && !resource.data64 && !resource?.base64) {
const errorMsg = "No data or file was submitted"; const errorMsg = 'No data or file was submitted';
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
const service = resource.service; const service = resource.service;
let identifier = resource.identifier; let identifier = resource.identifier;
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;
const category = resource.category; const category = resource.category;
const tags = resource?.tags || []; const tags = resource?.tags || [];
const result = {}; const result = {};
// Fill tags dynamically while maintaining backward compatibility // Fill tags dynamically while maintaining backward compatibility
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
result[`tag${i + 1}`] = tags[i] || resource[`tag${i + 1}`] || undefined; result[`tag${i + 1}`] = tags[i] || resource[`tag${i + 1}`] || undefined;
@ -1274,108 +1292,126 @@ export const publishMultipleQDNResources = async (data: any, sender, isFromExten
// Access tag1 to tag5 from result // Access tag1 to tag5 from result
const { tag1, tag2, tag3, tag4, tag5 } = result; const { tag1, tag2, tag3, tag4, tag5 } = result;
const resourceEncrypt = encrypt && resource?.disableEncrypt !== true const resourceEncrypt = encrypt && resource?.disableEncrypt !== true;
if (resource.identifier == null) { if (resource.identifier == null) {
identifier = "default"; identifier = 'default';
} }
if (!resourceEncrypt && service.endsWith("_PRIVATE")) { if (!resourceEncrypt && service.endsWith('_PRIVATE')) {
const errorMsg = "Only encrypted data can go into private services"; const errorMsg = 'Only encrypted data can go into private services';
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
if (resource.fileId) { if (resource.file) {
data64 = await getFileFromContentScript(resource.fileId); rawData = resource.file;
} }
if (resourceEncrypt) { if (resourceEncrypt) {
try { try {
const resKeyPair = await getKeyPair() if (resource?.file) {
const parsedData = resKeyPair rawData = await fileToBase64(resource.file);
const privateKey = parsedData.privateKey }
const userPublicKey = parsedData.publicKey const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const privateKey = parsedData.privateKey;
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 =
error?.message || "Upload failed due to failed encryption"; error?.message || 'Upload failed due to failed encryption';
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
} }
try { try {
await retryTransaction(publishData, [ const dataType =
{ resource?.base64 || resource?.data64 || resourceEncrypt
registeredName: encodeURIComponent(name), ? 'base64'
file: data64, : 'file';
service: service, await retryTransaction(
identifier: encodeURIComponent(identifier), publishData,
uploadType: "file", [
isBase64: true, {
filename: filename, data: rawData,
title, registeredName: encodeURIComponent(resource?.name || name),
description, service: service,
category, identifier: encodeURIComponent(identifier),
tag1, uploadType: dataType,
tag2, // isBase64: true,
tag3, filename: filename,
tag4, title,
tag5, description,
apiVersion: 2, category,
withFee: true, tag1,
}, tag2,
], true); tag3,
tag4,
tag5,
apiVersion: 2,
withFee: true,
},
],
true
);
await new Promise((res) => { await new Promise((res) => {
setTimeout(() => { setTimeout(() => {
res(); res();
}, 1000); }, 1000);
}); });
} catch (error) { } catch (error) {
const errorMsg = error.message || "Upload failed"; const errorMsg = error.message || 'Upload failed';
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
} }
} catch (error) { } catch (error) {
failedPublishesIdentifiers.push({ failedPublishesIdentifiers.push({
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,
}); });
} }
} }
if (failedPublishesIdentifiers.length > 0) { if (failedPublishesIdentifiers.length > 0) {
const obj = { const obj = {
message: "Some resources have failed to publish.", message: 'Some resources have failed to publish.',
}; };
obj["error"] = { obj['error'] = {
unsuccessfulPublishes: failedPublishesIdentifiers, unsuccessfulPublishes: failedPublishesIdentifiers,
}; };
return obj; return obj;
}
if (hasAppFee && checkbox1) {
sendCoinFunc(
{
amount: appFee,
receiver: appFeeRecipient,
},
true
);
} }
if(hasAppFee && checkbox1){
sendCoinFunc({
amount: appFee,
receiver: appFeeRecipient
}, true)
}
return true; return true;
}; };
@ -1737,6 +1773,53 @@ export const joinGroup = async (data, isFromExtension) => {
export const saveFile = async (data, sender, isFromExtension, snackMethods) => { export const saveFile = async (data, sender, isFromExtension, snackMethods) => {
try { try {
if (data?.location) {
const requiredFieldsLocation = ['service', 'name', 'filename'];
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?.location?.filename}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) throw new Error('User declined to save file');
if(isNative){
try {
saveFileInChunksFromUrl(data.location)
} catch (error) {
console.log('save chunks url error', error)
}
return true
}
const a = document.createElement('a');
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?.location?.filename}`
);
a.href = endpoint;
a.download = data.location.filename;
document.body.appendChild(a);
a.click();
a.remove();
return true;
}
const requiredFields = ['filename', 'blob'] const requiredFields = ['filename', 'blob']
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@ -4015,7 +4098,7 @@ export const registerNameRequest = async (data, isFromExtension) => {
}; };
export const updateNameRequest = async (data, isFromExtension) => { export const updateNameRequest = async (data, isFromExtension) => {
const requiredFields = ["newName", "oldName"]; const requiredFields = ['newName', 'oldName'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (!data[field]) { if (!data[field]) {
@ -4023,30 +4106,30 @@ export const updateNameRequest = async (data, isFromExtension) => {
} }
}); });
if (missingFields.length > 0) { if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", "); const missingFieldsString = missingFields.join(', ');
const errorMsg = `Missing fields: ${missingFieldsString}`; const errorMsg = `Missing fields: ${missingFieldsString}`;
throw new Error(errorMsg); throw new Error(errorMsg);
} }
const oldName = data.oldName const oldName = data.oldName;
const newName = data.newName const newName = data.newName;
const description = data?.description || "" const description = data?.description || '';
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
); );
const { accepted } = resPermission; const { accepted } = resPermission;
if (accepted) { if (accepted) {
const response = await updateName({ oldName, newName, description }); const response = await updateName({ oldName, newName, description });
return response return response;
} else { } else {
throw new Error("User declined request"); throw new Error('User declined request');
} }
}; };
@ -4571,7 +4654,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);
} }
}); });
@ -4821,7 +4904,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);
} }
}); });
@ -4866,7 +4949,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);
} }
}); });
@ -4907,7 +4990,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);
} }
}); });
@ -5210,10 +5293,10 @@ 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, isBase64: true,
apiVersion: 2, apiVersion: 2,
@ -5251,10 +5334,10 @@ 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, isBase64: true,
apiVersion: 2, apiVersion: 2,