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.NativePOW;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class MainActivity extends BridgeActivity {
@Override
@ -12,6 +14,9 @@ public class MainActivity extends BridgeActivity {
registerPlugin(NativePOW.class);
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
},
CapacitorHttp: {
enabled: true,
enabled: false,
}
}
};

View File

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

View File

@ -59,6 +59,16 @@ export async function getNameInfo() {
return "";
}
}
export async function getAllUserNames() {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi();
const response = await fetch(validApi + '/names/address/' + address);
const nameData = await response.json();
return nameData.map((item) => item.name);
}
async function getKeyPair() {
const res = await getData<any>("keyPair").catch(() => null);
if (res) {
@ -151,7 +161,7 @@ async function getKeyPair() {
if(encryptedData){
const registeredName = await getNameInfo()
const data = await publishData({
registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true
registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true
})
return {
data,
@ -202,7 +212,7 @@ export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousD
if(encryptedData){
const registeredName = await getNameInfo()
const data = await publishData({
registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true
registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true
})
return {
data,
@ -223,7 +233,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
const registeredName = await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish')
const data = await publishData({
registeredName, file: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'file', isBase64: true, withFee: true
registeredName, data: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'base64', withFee: true
})
return data
@ -242,15 +252,16 @@ export const publishOnQDN = async ({data, identifier, service, title,
tag3,
tag4,
tag5,
name,
uploadType = 'file'
}) => {
if(data && service){
const registeredName = await getNameInfo()
const registeredName = name || await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish')
const res = await publishData({
registeredName, file: data, service, identifier, uploadType, isBase64: true, withFee: true, title,
registeredName, data, service, identifier, uploadType, withFee: true, title,
description,
category,
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 {
AppCircle,
AppCircleContainer,
@ -49,6 +49,7 @@ import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getFee } from "../../background";
import { fileToBase64 } from "../../utils/fileReading";
import { useSortedMyNames } from "../../hooks/useSortedMyNames";
const CustomSelect = styled(Select)({
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 [title, setTitle] = useState("");
const [description, setDescription] = useState("");
@ -99,6 +101,8 @@ export const AppPublish = ({ names, categories }) => {
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const [isLoading, setIsLoading] = useState("");
const mySortedNames = useSortedMyNames(names, myName);
const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
const { getRootProps, getInputProps } = useDropzone({
accept: {
@ -162,6 +166,25 @@ export const AppPublish = ({ names, categories }) => {
getQapp(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 () => {
try {
const data = {
@ -199,10 +222,10 @@ export const AppPublish = ({ names, categories }) => {
publishFee: fee.fee + " QORT",
});
setIsLoading("Publishing... Please wait.");
const fileBase64 = await fileToBase64(file);
await new Promise((res, rej) => {
window.sendMessage("publishOnQDN", {
data: fileBase64,
data: file,
service: appType,
title,
description,
@ -213,6 +236,7 @@ export const AppPublish = ({ names, categories }) => {
tag4,
tag5,
uploadType: "zip",
name
})
.then((response) => {
if (!response?.error) {
@ -287,7 +311,7 @@ export const AppPublish = ({ names, categories }) => {
</em>{" "}
{/* This is the placeholder item */}
</CustomMenuItem>
{names.map((name) => {
{mySortedNames.map((name) => {
return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
})}
</CustomSelect>

View File

@ -17,7 +17,7 @@ import { AppsLibrary } from "./AppsLibrary";
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 [selectedAppInfo, setSelectedAppInfo] = 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 === "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
@ -314,7 +314,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{mode === "appInfo" && !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} />
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />}
{mode === "publish" && !selectedTab && <AppPublish categories={categories} myAddress={myAddress} />}
{tabs.map((tab) => {
if (!iframeRefs.current[tab.tabId]) {
@ -335,7 +335,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{isNewTabWindow && mode === "viewer" && (
<>
<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" />}

View File

@ -20,7 +20,7 @@ import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
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 { showTutorial } = useContext(GlobalContext);
@ -146,7 +146,7 @@ export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }
<AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
<AppsPrivate myName={myName} />
<AppsPrivate myName={myName} myAddress={myAddress} />
<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 {
Avatar,
Box,
@ -30,15 +30,18 @@ import {
PublishQAppInfo,
} from "./Apps-styles";
import ImageUploader from "../../common/ImageUploader";
import { isMobile, MyContext } from "../../App";
import { getBaseApiReact, isMobile, MyContext } from "../../App";
import { fileToBase64 } from "../../utils/fileReading";
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
import { getFee } from "../../background";
import { useSortedMyNames } from "../../hooks/useSortedMyNames";
const maxFileSize = 50 * 1024 * 1024; // 50MB
export const AppsPrivate = ({myName}) => {
export const AppsPrivate = ({myName, myAddress}) => {
const { openApp } = useHandlePrivateApps();
const [names, setNames] = useState([]);
const [name, setName] = useState(0);
const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null);
const [qortalUrl, setQortalUrl] = useState("");
@ -48,6 +51,7 @@ export const AppsPrivate = ({myName}) => {
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
myGroupsWhereIAmAdminAtom
);
const mySortedNames = useSortedMyNames(names, myName);
const myGroupsWhereIAmAdmin = useMemo(()=> {
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
@ -165,6 +169,8 @@ export const AppsPrivate = ({myName}) => {
data: decryptedData,
identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service,
uploadType: 'base64',
name,
})
.then((response) => {
if (!response?.error) {
@ -181,7 +187,7 @@ export const AppsPrivate = ({myName}) => {
{
identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service,
name: myName,
name,
groupId: selectedGroup,
},
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) => {
setValueTabPrivateApp(newValue);
};
@ -432,6 +454,34 @@ export const AppsPrivate = ({myName}) => {
{file ? "Change" : "Choose"} File
</PublishQAppChoseFile>
<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
sx={{
display: "flex",

View File

@ -10,9 +10,99 @@ import { MyContext } from '../../App';
import FileSaver from 'file-saver';
import { Capacitor } from '@capacitor/core';
import { createEndpoint } from '../../background';
import { uint8ArrayToBase64 } from '../../backgroundFunctions/encryption';
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 (
blob: Blob,
@ -586,38 +676,26 @@ isDOMContentLoaded: false
} else if(event?.data?.action === 'SAVE_FILE'
){
try {
const res = await saveFile( event.data, null, true, {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom
await saveFile(event.data, null, true, {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom,
});
event.ports[0].postMessage({
result: true,
error: null,
});
} catch (error) {
event.ports[0].postMessage({
result: null,
error: error?.message || 'Failed to save file',
});
}
} 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'
) {
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;
try {
data = await storeFilesInIndexedDB(event.data);
@ -640,6 +718,29 @@ isDOMContentLoaded: false
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' ||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
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) {
console.log("error", error);
console.error(error);
}
};
@ -2756,7 +2756,7 @@ export const Group = ({
/>
)}
{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 && (
<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,
identifier: identifier,
service: "DOCUMENT",
uploadType: 'base64',
})
.then((response) => {
if (!response?.error) {

View File

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

View File

@ -155,6 +155,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
data: encryptData,
identifier: "ext_saved_settings",
service: "DOCUMENT_PRIVATE",
uploadType: 'base64',
})
.then((response) => {
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
import { Buffer } from "buffer"
import Base58 from "../../deps/Base58"
import nacl from "../../deps/nacl-fast"
import utils from "../../utils/utils"
import { createEndpoint, getBaseApi } from "../../background";
import { getData } from "../../utils/chromeStorage";
import { Buffer } from 'buffer';
import Base58 from '../../deps/Base58';
import nacl from '../../deps/nacl-fast';
import utils from '../../utils/utils';
import { createEndpoint, getBaseApi } from '../../background';
import { getData } from '../../utils/chromeStorage';
export async function reusableGet(endpoint){
const validApi = await getBaseApi();
const response = await fetch(validApi + endpoint);
const data = await response.json();
return data
}
async function reusablePost(endpoint, _body){
// const validApi = await findUsableApi();
const url = await createEndpoint(endpoint)
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: _body
export async function reusableGet(endpoint) {
const validApi = await getBaseApi();
const response = await fetch(validApi + endpoint);
const data = await response.json();
return data;
}
async function reusablePost(endpoint, _body) {
// const validApi = await findUsableApi();
const url = await createEndpoint(endpoint);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: _body,
});
let data
let data;
try {
data = await response.clone().json()
data = await response.clone().json();
} catch (e) {
data = await response.text()
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 getData<any>("keyPair").catch(() => null);
if (res) {
return res
} else {
throw new Error("Wallet not authenticated");
}
const res = await getData<any>('keyPair').catch(() => null);
if (res) {
return res;
} else {
throw new Error('Wallet not authenticated');
}
}
export const publishData = async ({
registeredName,
file,
service,
identifier,
uploadType,
isBase64,
filename,
withFee,
title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5,
feeAmount
registeredName,
data,
service,
identifier,
uploadType,
filename,
withFee,
title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5,
feeAmount,
}: any) => {
const validateName = async (receiverName: string) => {
return await reusableGet(`/names/${receiverName}`)
}
const validateName = async (receiverName: string) => {
return await reusableGet(`/names/${receiverName}`);
};
const convertBytesForSigning = async (transactionBytesBase58: string) => {
return await reusablePost('/transactions/convert', transactionBytesBase58)
}
const convertBytesForSigning = async (transactionBytesBase58: string) => {
return await reusablePost('/transactions/convert', transactionBytesBase58);
};
const getArbitraryFee = async () => {
const timestamp = Date.now()
const getArbitraryFee = async () => {
const timestamp = Date.now();
let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`)
let fee = await reusableGet(
`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`
);
return {
timestamp,
fee: Number(fee),
feeToShow: (Number(fee) / 1e8).toFixed(8)
}
}
return {
timestamp,
fee: Number(fee),
feeToShow: (Number(fee) / 1e8).toFixed(8),
};
};
const signArbitraryWithFee = (arbitraryBytesBase58, arbitraryBytesForSigningBase58, keyPair) => {
if (!arbitraryBytesBase58) {
throw new Error('ArbitraryBytesBase58 not defined')
}
if (!keyPair) {
throw new Error('keyPair not defined')
}
const arbitraryBytes = Base58.decode(arbitraryBytesBase58)
const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map(function (key) { return arbitraryBytes[key]; })
const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer)
const arbitraryBytesForSigning = Base58.decode(arbitraryBytesForSigningBase58)
const _arbitraryBytesForSigningBuffer = Object.keys(arbitraryBytesForSigning).map(function (key) { return arbitraryBytesForSigning[key]; })
const arbitraryBytesForSigningBuffer = new Uint8Array(_arbitraryBytesForSigningBuffer)
const signature = nacl.sign.detached(arbitraryBytesForSigningBuffer, keyPair.privateKey)
return utils.appendBuffer(arbitraryBytesBuffer, signature)
const signArbitraryWithFee = (
arbitraryBytesBase58,
arbitraryBytesForSigningBase58,
keyPair
) => {
if (!arbitraryBytesBase58) {
throw new Error('ArbitraryBytesBase58 not defined');
}
const processTransactionVersion2 = async (bytes) => {
if (!keyPair) {
throw new Error('keyPair not defined');
}
return await reusablePost('/transactions/process?apiVersion=2', Base58.encode(bytes))
}
const arbitraryBytes = Base58.decode(arbitraryBytesBase58);
const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map(
function (key) {
return arbitraryBytes[key];
}
);
const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer);
const arbitraryBytesForSigning = Base58.decode(
arbitraryBytesForSigningBase58
);
const _arbitraryBytesForSigningBuffer = Object.keys(
arbitraryBytesForSigning
).map(function (key) {
return arbitraryBytesForSigning[key];
});
const arbitraryBytesForSigningBuffer = new Uint8Array(
_arbitraryBytesForSigningBuffer
);
const signature = nacl.sign.detached(
arbitraryBytesForSigningBuffer,
keyPair.privateKey
);
const signAndProcessWithFee = async (transactionBytesBase58: string) => {
let convertedBytesBase58 = await convertBytesForSigning(
transactionBytesBase58
)
return utils.appendBuffer(arbitraryBytesBuffer, signature);
};
if (convertedBytesBase58.error) {
throw new Error('Error when signing')
}
const processTransactionVersion2 = async (bytes) => {
return await reusablePost(
'/transactions/process?apiVersion=2',
Base58.encode(bytes)
);
};
const signAndProcessWithFee = async (transactionBytesBase58: string) => {
let convertedBytesBase58 = await convertBytesForSigning(
transactionBytesBase58
);
const resKeyPair = await getKeyPair()
const parsedData = resKeyPair
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey
};
if (convertedBytesBase58.error) {
throw new Error('Error when signing');
}
let signedArbitraryBytes = signArbitraryWithFee(transactionBytesBase58, convertedBytesBase58, keyPair)
const response = await processTransactionVersion2(signedArbitraryBytes)
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
let myResponse = { error: '' }
let signedArbitraryBytes = signArbitraryWithFee(
transactionBytesBase58,
convertedBytesBase58,
keyPair
);
const response = await processTransactionVersion2(signedArbitraryBytes);
if (response === false) {
throw new Error('Error when signing')
} else {
myResponse = response
}
let myResponse = { error: '' };
return myResponse
}
if (response === false) {
throw new Error('Error when signing');
} else {
myResponse = response;
}
const validate = async () => {
let validNameRes = await validateName(registeredName)
return myResponse;
};
if (validNameRes.error) {
throw new Error('Name not found')
}
const validate = async () => {
let validNameRes = await validateName(registeredName);
let fee = null
if (validNameRes.error) {
throw new Error('Name not found');
}
if (withFee && feeAmount) {
fee = feeAmount
} else if (withFee) {
const res = await getArbitraryFee()
if (res.fee) {
fee = res.fee
} else {
throw new Error('unable to get fee')
}
}
let transactionBytes = await uploadData(registeredName, file, fee)
if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading')
} else if (transactionBytes.includes('Error 500 Internal Server Error')) {
throw new Error('Error when uploading')
}
let fee = null;
let signAndProcessRes
if (withFee && feeAmount) {
fee = feeAmount;
} else if (withFee) {
const res = await getArbitraryFee();
if (res.fee) {
fee = res.fee;
} else {
throw new Error('unable to get fee');
}
}
if (withFee) {
signAndProcessRes = await signAndProcessWithFee(transactionBytes)
}
let transactionBytes = await uploadData(registeredName, data, fee);
if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading');
} else if (transactionBytes.includes('Error 500 Internal Server Error')) {
throw new Error('Error when uploading');
}
if (signAndProcessRes?.error) {
throw new Error('Error when signing')
}
let signAndProcessRes;
return signAndProcessRes
}
if (withFee) {
signAndProcessRes = await signAndProcessWithFee(transactionBytes);
}
const uploadData = async (registeredName: string, file:any, fee: number) => {
if (signAndProcessRes?.error) {
throw new Error('Error when signing');
}
let postBody = ''
let urlSuffix = ''
return signAndProcessRes;
};
if (file != null) {
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API
if (uploadType === 'zip') {
urlSuffix = '/zip'
}
const uploadData = async (registeredName: string, data: any, fee: number) => {
let postBody = '';
let urlSuffix = '';
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API
else if (uploadType === 'file') {
urlSuffix = '/base64'
}
if (data != null) {
if (uploadType === 'base64') {
urlSuffix = '/base64';
}
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays
if (isBase64) {
postBody = file
}
if (uploadType === 'base64') {
postBody = data;
}
} else {
throw new Error('No data provided');
}
if (!isBase64) {
let fileBuffer = new Uint8Array(await file.arrayBuffer())
postBody = Buffer.from(fileBuffer).toString("base64")
}
let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
let paramQueries = '';
if (identifier?.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
}
}
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}`
if (identifier?.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}`
}
uploadDataUrl = uploadDataUrl + `?fee=${fee}`
paramQueries = paramQueries + `?fee=${fee}`;
if (filename != null && filename != 'undefined') {
uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename)
}
if (filename != null && filename != 'undefined') {
paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
}
if (title != null && title != 'undefined') {
uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title)
}
if (title != null && title != 'undefined') {
paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
}
if (description != null && description != 'undefined') {
uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description)
}
if (description != null && description != 'undefined') {
paramQueries =
paramQueries + '&description=' + encodeURIComponent(description);
}
if (category != null && category != 'undefined') {
uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category)
}
if (category != null && category != 'undefined') {
paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
}
if (tag1 != null && tag1 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1)
}
if (tag1 != null && tag1 != 'undefined') {
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
}
if (tag2 != null && tag2 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2)
}
if (tag2 != null && tag2 != 'undefined') {
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
}
if (tag3 != null && tag3 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3)
}
if (tag3 != null && tag3 != 'undefined') {
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
}
if (tag4 != null && tag4 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4)
}
if (tag4 != null && tag4 != 'undefined') {
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
}
if (tag5 != null && tag5 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5)
}
if (tag5 != null && tag5 != 'undefined') {
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5);
}
if (uploadType === 'zip') {
paramQueries = paramQueries + '&isZip=' + true;
}
return await reusablePost(uploadDataUrl, postBody)
}
if (uploadType === 'base64') {
if (urlSuffix) {
uploadDataUrl = uploadDataUrl + urlSuffix;
}
uploadDataUrl = uploadDataUrl + paramQueries;
return await reusablePost(uploadDataUrl, postBody);
}
try {
return await validate()
} catch (error: any) {
throw new Error(error?.message)
}
}
const file = data;
const urlCheck = `/arbitrary/check-tmp-space?totalSize=${file.size}`;
const checkEndpoint = await createEndpoint(urlCheck);
const checkRes = await fetch(checkEndpoint);
if (!checkRes.ok) {
throw new Error('Not enough space on your hard drive');
}
const chunkUrl = uploadDataUrl + `/chunk`;
const chunkSize = 1 * 1024 * 1024; // 1MB
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

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