mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-07-04 04:41:20 +00:00
474 lines
12 KiB
TypeScript
474 lines
12 KiB
TypeScript
// @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 { executeEvent } from '../../utils/events';
|
|
|
|
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,
|
|
});
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(errorText);
|
|
}
|
|
let data;
|
|
try {
|
|
data = await response.clone().json();
|
|
} catch (e) {
|
|
data = await response.text();
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function reusablePostStream(endpoint, _body) {
|
|
const url = await createEndpoint(endpoint);
|
|
|
|
const headers = {};
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: _body,
|
|
});
|
|
|
|
return response; // return the actual response so calling code can use response.ok
|
|
}
|
|
|
|
async function uploadChunkWithRetry(endpoint, formData, index, maxRetries = 3) {
|
|
let attempt = 0;
|
|
while (attempt < maxRetries) {
|
|
try {
|
|
const response = await reusablePostStream(endpoint, formData);
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(errorText);
|
|
}
|
|
return; // Success
|
|
} catch (err) {
|
|
attempt++;
|
|
console.warn(
|
|
`Chunk ${index} failed (attempt ${attempt}): ${err.message}`
|
|
);
|
|
if (attempt >= maxRetries) {
|
|
throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`);
|
|
}
|
|
// Wait 10 seconds before next retry
|
|
await new Promise((res) => setTimeout(res, 25_000));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function resuablePostRetry(
|
|
endpoint,
|
|
body,
|
|
maxRetries = 3,
|
|
appInfo,
|
|
resourceInfo
|
|
) {
|
|
let attempt = 0;
|
|
while (attempt < maxRetries) {
|
|
try {
|
|
const response = await reusablePost(endpoint, body);
|
|
|
|
return response;
|
|
} catch (err) {
|
|
attempt++;
|
|
if (attempt >= maxRetries) {
|
|
throw new Error(
|
|
err instanceof Error
|
|
? err?.message || `Failed to make request`
|
|
: `Failed to make request`
|
|
);
|
|
}
|
|
if (appInfo?.tabId && resourceInfo) {
|
|
executeEvent('receiveChunks', {
|
|
tabId: appInfo.tabId,
|
|
publishLocation: {
|
|
name: resourceInfo?.name,
|
|
identifier: resourceInfo?.identifier,
|
|
service: resourceInfo?.service,
|
|
},
|
|
retry: true,
|
|
});
|
|
}
|
|
// Wait 10 seconds before next retry
|
|
await new Promise((res) => setTimeout(res, 25_000));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getKeyPair() {
|
|
const res = await getData<any>('keyPair').catch(() => null);
|
|
if (res) {
|
|
return res;
|
|
} else {
|
|
throw new Error('Wallet not authenticated');
|
|
}
|
|
}
|
|
|
|
export const publishData = async ({
|
|
registeredName,
|
|
data,
|
|
service,
|
|
identifier,
|
|
uploadType,
|
|
filename,
|
|
withFee,
|
|
title,
|
|
description,
|
|
category,
|
|
tag1,
|
|
tag2,
|
|
tag3,
|
|
tag4,
|
|
tag5,
|
|
feeAmount,
|
|
appInfo
|
|
}: any) => {
|
|
const validateName = async (receiverName: string) => {
|
|
return await reusableGet(`/names/${receiverName}`);
|
|
};
|
|
|
|
const convertBytesForSigning = async (transactionBytesBase58: string) => {
|
|
return await resuablePostRetry(
|
|
'/transactions/convert',
|
|
transactionBytesBase58,
|
|
3,
|
|
appInfo,
|
|
{ identifier, name: registeredName, service }
|
|
);
|
|
};
|
|
|
|
const getArbitraryFee = async () => {
|
|
const timestamp = Date.now();
|
|
|
|
let fee = await reusableGet(
|
|
`/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}`
|
|
);
|
|
|
|
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 processTransactionVersion2 = async (bytes) => {
|
|
return await resuablePostRetry(
|
|
'/transactions/process?apiVersion=2',
|
|
Base58.encode(bytes),
|
|
3,
|
|
appInfo,
|
|
{ identifier, name: registeredName, service }
|
|
);
|
|
};
|
|
|
|
const signAndProcessWithFee = async (transactionBytesBase58: string) => {
|
|
let convertedBytesBase58 = await convertBytesForSigning(
|
|
transactionBytesBase58
|
|
);
|
|
|
|
if (convertedBytesBase58.error) {
|
|
throw new Error('Error when signing');
|
|
}
|
|
|
|
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 signedArbitraryBytes = signArbitraryWithFee(
|
|
transactionBytesBase58,
|
|
convertedBytesBase58,
|
|
keyPair
|
|
);
|
|
const response = await processTransactionVersion2(signedArbitraryBytes);
|
|
|
|
let myResponse = { error: '' };
|
|
|
|
if (response === false) {
|
|
throw new Error('Error when signing');
|
|
} else {
|
|
myResponse = response;
|
|
}
|
|
if (appInfo?.tabId) {
|
|
executeEvent('receiveChunks', {
|
|
tabId: appInfo.tabId,
|
|
publishLocation: {
|
|
name: registeredName,
|
|
identifier,
|
|
service,
|
|
},
|
|
processed: true,
|
|
});
|
|
}
|
|
return myResponse;
|
|
};
|
|
|
|
const validate = async () => {
|
|
let validNameRes = await validateName(registeredName);
|
|
|
|
if (validNameRes.error) {
|
|
throw new Error('Name not found');
|
|
}
|
|
|
|
let fee = null;
|
|
|
|
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, 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');
|
|
}
|
|
|
|
let signAndProcessRes;
|
|
|
|
if (withFee) {
|
|
signAndProcessRes = await signAndProcessWithFee(transactionBytes);
|
|
}
|
|
|
|
if (signAndProcessRes?.error) {
|
|
throw new Error('Error when signing');
|
|
}
|
|
|
|
return signAndProcessRes;
|
|
};
|
|
|
|
const uploadData = async (registeredName: string, data: any, fee: number) => {
|
|
let postBody = '';
|
|
let urlSuffix = '';
|
|
|
|
if (data != null) {
|
|
if (uploadType === 'base64') {
|
|
urlSuffix = '/base64';
|
|
}
|
|
|
|
if (uploadType === 'base64') {
|
|
postBody = data;
|
|
}
|
|
} else {
|
|
throw new Error('No data provided');
|
|
}
|
|
|
|
let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
|
|
let paramQueries = '';
|
|
if (identifier?.trim().length > 0) {
|
|
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
|
|
}
|
|
|
|
paramQueries = paramQueries + `?fee=${fee}`;
|
|
|
|
if (filename != null && filename != 'undefined') {
|
|
paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
|
|
}
|
|
|
|
if (title != null && title != 'undefined') {
|
|
paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
|
|
}
|
|
|
|
if (description != null && description != 'undefined') {
|
|
paramQueries =
|
|
paramQueries + '&description=' + encodeURIComponent(description);
|
|
}
|
|
|
|
if (category != null && category != 'undefined') {
|
|
paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
|
|
}
|
|
|
|
if (tag1 != null && tag1 != 'undefined') {
|
|
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
|
|
}
|
|
|
|
if (tag2 != null && tag2 != 'undefined') {
|
|
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
|
|
}
|
|
|
|
if (tag3 != null && tag3 != 'undefined') {
|
|
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
|
|
}
|
|
|
|
if (tag4 != null && tag4 != 'undefined') {
|
|
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
|
|
}
|
|
|
|
if (tag5 != null && tag5 != 'undefined') {
|
|
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5);
|
|
}
|
|
if (uploadType === 'zip') {
|
|
paramQueries = paramQueries + '&isZip=' + true;
|
|
}
|
|
|
|
if (uploadType === 'base64') {
|
|
if (urlSuffix) {
|
|
uploadDataUrl = uploadDataUrl + urlSuffix;
|
|
}
|
|
uploadDataUrl = uploadDataUrl + paramQueries;
|
|
if (appInfo?.tabId) {
|
|
executeEvent('receiveChunks', {
|
|
tabId: appInfo.tabId,
|
|
publishLocation: {
|
|
name: registeredName,
|
|
identifier,
|
|
service,
|
|
},
|
|
chunksSubmitted: 1,
|
|
totalChunks: 1,
|
|
processed: false,
|
|
filename: filename || title || `${service}-${identifier || ''}`,
|
|
});
|
|
}
|
|
return await resuablePostRetry(uploadDataUrl, postBody, 3, appInfo, {
|
|
identifier,
|
|
name: registeredName,
|
|
service,
|
|
});
|
|
}
|
|
|
|
const file = data;
|
|
const urlCheck = `/arbitrary/check/tmp?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 = 5 * 1024 * 1024; // 5MB
|
|
|
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
if (appInfo?.tabId) {
|
|
executeEvent('receiveChunks', {
|
|
tabId: appInfo.tabId,
|
|
publishLocation: {
|
|
name: registeredName,
|
|
identifier,
|
|
service,
|
|
},
|
|
chunksSubmitted: 0,
|
|
totalChunks,
|
|
processed: false,
|
|
filename:
|
|
file?.name || filename || title || `${service}-${identifier || ''}`,
|
|
});
|
|
}
|
|
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);
|
|
if (appInfo?.tabId) {
|
|
executeEvent('receiveChunks', {
|
|
tabId: appInfo.tabId,
|
|
publishLocation: {
|
|
name: registeredName,
|
|
identifier,
|
|
service,
|
|
},
|
|
chunksSubmitted: index + 1,
|
|
totalChunks,
|
|
});
|
|
}
|
|
|
|
}
|
|
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);
|
|
}
|
|
}; |