From e8a09bd805b17e3073adf4b4497ca8fd4ab3032c Mon Sep 17 00:00:00 2001 From: Nicola Benaglia Date: Sat, 24 May 2025 12:09:50 +0200 Subject: [PATCH] Rename folder and file --- src/App.tsx | 2 +- src/background/background-cases.ts | 2 +- src/background/background.ts | 2 +- src/components/Save/Save.tsx | 2 +- src/hooks/useQortalMessageListener.tsx | 2 +- src/qortal/get.ts | 7005 +++++++++++++++++ .../qortal-requests.ts} | 0 src/qortalRequests/get.ts | 41 +- 8 files changed, 7026 insertions(+), 30 deletions(-) create mode 100644 src/qortal/get.ts rename src/{qortalRequests/qortalRequests.ts => qortal/qortal-requests.ts} (100%) diff --git a/src/App.tsx b/src/App.tsx index fe721a7..4fd9057 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -124,7 +124,7 @@ import { Tutorials } from './components/Tutorials/Tutorials'; import { useHandleTutorials } from './hooks/useHandleTutorials.tsx'; import { useHandleUserInfo } from './hooks/useHandleUserInfo.tsx'; import { Minting } from './components/Minting/Minting'; -import { isRunningGateway } from './qortalRequests/qortalRequests.ts'; +import { isRunningGateway } from './qortal/qortal-requests.ts'; import { QMailStatus } from './components/QMailStatus'; import { GlobalActions } from './components/GlobalActions/GlobalActions'; import { useBlockedAddresses } from './hooks/useBlockUsers.tsx'; diff --git a/src/background/background-cases.ts b/src/background/background-cases.ts index c1aa7df..0392893 100644 --- a/src/background/background-cases.ts +++ b/src/background/background-cases.ts @@ -65,7 +65,7 @@ import { import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../constants/constants'; import Base58 from '../encryption/Base58.ts'; import { encryptSingle } from '../qdn/encryption/group-encryption'; -import { _createPoll, _voteOnPoll } from '../qortalRequests/get'; +import { _createPoll, _voteOnPoll } from '../qortal/get.ts'; import { createTransaction } from '../transactions/transactions'; import { getData, storeData } from '../utils/chromeStorage'; diff --git a/src/background/background.ts b/src/background/background.ts index 90b2f2e..2a7b158 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import '../qortalRequests/qortalRequests'; +import '../qortal/qortal-requests.ts'; import { isArray } from 'lodash'; import { uint8ArrayToObject } from '../encryption/encryption.ts'; import Base58 from '../encryption/Base58'; diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index e213174..cebccf8 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -26,7 +26,7 @@ import { IconWrapper } from '../Desktop/DesktopFooter'; import { Spacer } from '../../common/Spacer'; import { LoadingButton } from '@mui/lab'; import { saveToLocalStorage } from '../Apps/AppsNavBarDesktop'; -import { decryptData, encryptData } from '../../qortalRequests/get'; +import { decryptData, encryptData } from '../../qortal/get.ts'; import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; import { base64ToUint8Array, diff --git a/src/hooks/useQortalMessageListener.tsx b/src/hooks/useQortalMessageListener.tsx index c6c24da..29549dc 100644 --- a/src/hooks/useQortalMessageListener.tsx +++ b/src/hooks/useQortalMessageListener.tsx @@ -2,7 +2,7 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import { executeEvent } from '../utils/events'; import { navigationControllerAtom } from '../atoms/global'; import { Filesystem, Directory } from '@capacitor/filesystem'; -import { saveFile } from '../qortalRequests/get'; +import { saveFile } from '../qortal/get'; import { mimeToExtensionMap } from '../utils/memeTypes'; import { QORTAL_APP_CONTEXT } from '../App'; import FileSaver from 'file-saver'; diff --git a/src/qortal/get.ts b/src/qortal/get.ts new file mode 100644 index 0000000..102a9bf --- /dev/null +++ b/src/qortal/get.ts @@ -0,0 +1,7005 @@ +import { Sha256 } from 'asmcrypto.js'; +import { + createEndpoint, + getBalanceInfo, + getFee, + getKeyPair, + getLastRef, + getSaveWallet, + processTransactionVersion2, + signChatFunc, + joinGroup as joinGroupFunc, + sendQortFee, + sendCoin as sendCoinFunc, + createBuyOrderTx, + performPowTask, + parseErrorResponse, + groupSecretkeys, + registerName, + updateName, + leaveGroup, + inviteToGroup, + getNameInfoForOthers, + kickFromGroup, + banFromGroup, + cancelBan, + makeAdmin, + removeAdmin, + cancelInvitationToGroup, + createGroup, + updateGroup, + sellName, + cancelSellName, + buyName, + getBaseApi, + getAssetBalanceInfo, + getNameOrAddress, + getAssetInfo, + getPublicKey, + transferAsset, +} from '../background/background.ts'; +import { getNameInfo, uint8ArrayToObject } from '../encryption/encryption.ts'; +import { showSaveFilePicker } from '../hooks/useQortalMessageListener.tsx'; +import { getPublishesFromAdminsAdminSpace } from '../components/Chat/AdminSpaceInner.tsx'; +import { extractComponents } from '../components/Chat/MessageDisplay.tsx'; +import { + decryptResource, + getGroupAdmins, + getPublishesFromAdmins, + validateSecretKey, +} from '../components/Group/Group.tsx'; +import { QORT_DECIMALS } from '../constants/constants.ts'; +import Base58 from '../encryption/Base58.ts'; +import ed2curve from '../encryption/ed2curve.ts'; +import nacl from '../encryption/nacl-fast.ts'; +import { + base64ToUint8Array, + createSymmetricKeyAndNonce, + decryptDeprecatedSingle, + decryptGroupDataQortalRequest, + decryptGroupEncryptionWithSharingKey, + decryptSingle, + encryptDataGroup, + encryptSingle, + objectToBase64, + uint8ArrayStartsWith, + uint8ArrayToBase64, +} from '../qdn/encryption/group-encryption.ts'; +import { publishData } from '../qdn/publish/publish.ts'; +import { + getPermission, + isRunningGateway, + setPermission, +} from './qortal-requests.ts'; +import TradeBotCreateRequest from '../transactions/TradeBotCreateRequest.ts'; +import DeleteTradeOffer from '../transactions/TradeBotDeleteRequest.ts'; +import signTradeBotTransaction from '../transactions/signTradeBotTransaction.ts'; +import { createTransaction } from '../transactions/transactions.ts'; +import { executeEvent } from '../utils/events.ts'; +import { fileToBase64 } from '../utils/fileReading/index.ts'; +import { mimeToExtensionMap } from '../utils/memeTypes.ts'; +import { RequestQueueWithPromise } from '../utils/queue/queue.ts'; +import utils from '../utils/utils.ts'; +import ShortUniqueId from 'short-unique-id'; +import { isValidBase64WithDecode } from '../utils/decode.ts'; +import i18n from 'i18next'; + +const uid = new ShortUniqueId({ length: 6 }); + +export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10); + +const sellerForeignFee = { + LITECOIN: { + value: '~0.00005', + ticker: 'LTC', + }, + DOGECOIN: { + value: '~0.005', + ticker: 'DOGE', + }, + BITCOIN: { + value: '~0.0001', + ticker: 'BTC', + }, + DIGIBYTE: { + value: '~0.0005', + ticker: 'DGB', + }, + RAVENCOIN: { + value: '~0.006', + ticker: 'RVN', + }, + PIRATECHAIN: { + value: '~0.0002', + ticker: 'ARRR', + }, +}; + +const btcFeePerByte = 0.000001; +const ltcFeePerByte = 0.0000003; +const dogeFeePerByte = 0.00001; +const dgbFeePerByte = 0.0000001; +const rvnFeePerByte = 0.00001125; + +const MAX_RETRIES = 3; // Set max number of retries + +export async function retryTransaction( + fn, + args, + throwError, + retries = MAX_RETRIES +) { + let attempt = 0; + while (attempt < retries) { + try { + return await fn(...args); + } catch (error) { + console.error(`Attempt ${attempt + 1} failed: ${error.message}`); + attempt++; + if (attempt === retries) { + console.error( + i18n.t('question:message.generic.max_retry_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + if (throwError) { + throw new Error( + error?.message || + i18n.t('question:message.error.process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + } else { + throw new Error( + error?.message || + i18n.t('question:message.error.process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } + await new Promise((res) => setTimeout(res, 10000)); + } + } +} + +function roundUpToDecimals(number, decimals = 8) { + const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals + return Math.ceil(+number * factor) / factor; +} + +export const _createPoll = async ( + { pollName, pollDescription, options }, + isFromExtension, + skipPermission +) => { + const fee = await getFee('CREATE_POLL'); + let resPermission = {}; + if (!skipPermission) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:request_create_poll', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:poll', { + name: pollName, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:description', { + description: pollDescription, + postProcess: 'capitalizeFirstChar', + }), + text4: i18n.t('question:options', { + optionList: options?.join(', '), + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + } + + const { accepted = false } = resPermission; + + if (accepted || skipPermission) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + 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 lastRef = await getLastRef(); + + const tx = await createTransaction(8, keyPair, { + fee: fee.fee, + ownerAddress: address, + rPollName: pollName, + rPollDesc: pollDescription, + rOptions: options, + lastReference: lastRef, + }); + const signedBytes = Base58.encode(tx.signedBytes); + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error( + res?.message || + i18n.t('question:message.error.process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +const _deployAt = async ( + { name, description, tags, creationBytes, amount, assetId, atType }, + isFromExtension +) => { + const fee = await getFee('DEPLOY_AT'); + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:deploy_at', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:name', { + name: name, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:description', { + description: description, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + + const { accepted } = resPermission; + + if (accepted) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const lastReference = await getLastRef(); + 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, + }; + + const tx = await createTransaction(16, keyPair, { + fee: fee.fee, + rName: name, + rDescription: description, + rTags: tags, + rAmount: amount, + rAssetId: assetId, + rCreationBytes: creationBytes, + atType: atType, + lastReference: lastReference, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error( + res?.message || + i18n.t('question:message.error.process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const _voteOnPoll = async ( + { pollName, optionIndex, optionName }, + isFromExtension, + skipPermission +) => { + const fee = await getFee('VOTE_ON_POLL'); + let resPermission = {}; + + if (!skipPermission) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:request_vote_poll', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:poll', { + name: pollName, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:option', { + option: optionName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + } + + const { accepted = false } = resPermission; + + if (accepted || skipPermission) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + 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 lastRef = await getLastRef(); + + const tx = await createTransaction(9, keyPair, { + fee: fee.fee, + voterAddress: address, + rPollName: pollName, + rOptionIndex: optionIndex, + lastReference: lastRef, + }); + const signedBytes = Base58.encode(tx.signedBytes); + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error( + res?.message || + i18n.t('question:message.error.process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +// Map to store resolvers and rejectors by requestId +const fileRequestResolvers = new Map(); + +const handleFileMessage = (event) => { + const { action, requestId, result, error } = event.data; + + if ( + action === 'getFileFromIndexedDBResponse' && + fileRequestResolvers.has(requestId) + ) { + const { resolve, reject } = fileRequestResolvers.get(requestId); + fileRequestResolvers.delete(requestId); // Clean up after resolving + + if (result) { + resolve(result); + } else { + reject( + error || + i18n.t('question:message.error.retrieve_file', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } +}; + +window.addEventListener('message', handleFileMessage); + +function getFileFromContentScript(fileId) { + return new Promise((resolve, reject) => { + const requestId = `getFile_${fileId}_${Date.now()}`; + + fileRequestResolvers.set(requestId, { resolve, reject }); // Store resolvers by requestId + const targetOrigin = window.location.origin; + + // Send the request message + window.postMessage( + { action: 'getFileFromIndexedDB', fileId, requestId }, + targetOrigin + ); + + // Timeout to handle no response scenario + setTimeout(() => { + if (fileRequestResolvers.has(requestId)) { + fileRequestResolvers.get(requestId).reject( + i18n.t('question:message.error.timeout_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + fileRequestResolvers.delete(requestId); // Clean up on timeout + } + }, 10000); // 10-second timeout + }); +} + +const responseResolvers = new Map(); + +const handleMessage = (event) => { + const { action, requestId, result } = event.data; + + // Check if this is the expected response action and if we have a stored resolver + if ( + action === 'QORTAL_REQUEST_PERMISSION_RESPONSE' && + responseResolvers.has(requestId) + ) { + // Resolve the stored promise with the result + responseResolvers.get(requestId)(result || false); + responseResolvers.delete(requestId); // Clean up after resolving + } +}; + +window.addEventListener('message', handleMessage); + +async function getUserPermission(payload, isFromExtension) { + return new Promise((resolve) => { + const requestId = `qortalRequest_${Date.now()}`; + responseResolvers.set(requestId, resolve); // Store resolver by requestId + const targetOrigin = window.location.origin; + + // Send the request message + window.postMessage( + { + action: 'QORTAL_REQUEST_PERMISSION', + payload, + requestId, + isFromExtension, + }, + targetOrigin + ); + + // Optional timeout to handle no response scenario + setTimeout(() => { + if (responseResolvers.has(requestId)) { + responseResolvers.get(requestId)(false); // Resolve with `false` if no response + responseResolvers.delete(requestId); + } + }, 60000); // 30-second timeout + }); +} + +export const getUserAccount = async ({ + isFromExtension, + appInfo, + skipAuth, +}) => { + try { + const value = + (await getPermission(`qAPPAutoAuth-${appInfo?.name}`)) || false; + let skip = false; + if (value) { + skip = true; + } + if (skipAuth) { + skip = true; + } + let resPermission; + if (!skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.authenticate', { + postProcess: 'capitalizeFirstChar', + }), + checkbox1: { + value: false, + label: i18n.t('question:always_authenticate', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + } + + const { accepted = false, checkbox1 = false } = resPermission || {}; + if (resPermission) { + setPermission(`qAPPAutoAuth-${appInfo?.name}`, checkbox1); + } + if (accepted || skip) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const publicKey = wallet.publicKey; + return { + address, + publicKey, + }; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } catch (error) { + throw new Error( + i18n.t('auth:message.error.fetch_user_account', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const encryptData = async (data, sender) => { + let data64 = data.data64 || data.base64; + let publicKeys = data.publicKeys || []; + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); + } + if (!data64) { + throw new Error( + i18n.t('question:message.generic.include_data_encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const privateKey = parsedData.privateKey; + const userPublicKey = parsedData.publicKey; + + const encryptDataResponse = encryptDataGroup({ + data64, + publicKeys: publicKeys, + privateKey, + userPublicKey, + }); + if (encryptDataResponse) { + return encryptDataResponse; + } else { + throw new Error( + i18n.t('question:message.error.encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const encryptQortalGroupData = async (data, sender) => { + let data64 = data?.data64 || data?.base64; + let groupId = data?.groupId; + let isAdmins = data?.isAdmins; + if (!groupId) { + throw new Error( + i18n.t('question:message.generic.provide_group_id', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); + } + if (!data64) { + throw new Error( + i18n.t('question:message.generic.include_data_encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + let secretKeyObject; + if (!isAdmins) { + if ( + groupSecretkeys[groupId] && + groupSecretkeys[groupId].secretKeyObject && + groupSecretkeys[groupId]?.timestamp && + Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[groupId].secretKeyObject; + } + + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); + + const publish = await getPublishesFromAdmins(names, groupId); + if (publish === false) + throw new Error( + i18n.t('question:message.error.no_group_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); + + const res = await fetch(url); + const resData = await res.text(); + + const decryptedKey: any = await decryptResource(resData, true); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error( + i18n.t('auth:message.error.invalid_secret_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[groupId] = { + secretKeyObject, + timestamp: Date.now(), + }; + } + } else { + if ( + groupSecretkeys[`admins-${groupId}`] && + groupSecretkeys[`admins-${groupId}`].secretKeyObject && + groupSecretkeys[`admins-${groupId}`]?.timestamp && + Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject; + } + + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); + + const publish = await getPublishesFromAdminsAdminSpace(names, groupId); + if (publish === false) + throw new Error( + i18n.t('question:message.error.no_group_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); + + const res = await fetch(url); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData, true); + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error( + i18n.t('auth:message.error.invalid_secret_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[`admins-${groupId}`] = { + secretKeyObject, + timestamp: Date.now(), + }; + } + } + + const resGroupEncryptedResource = encryptSingle({ + data64, + secretKeyObject: secretKeyObject, + }); + + if (resGroupEncryptedResource) { + return resGroupEncryptedResource; + } else { + throw new Error( + i18n.t('question:message.error.encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const decryptQortalGroupData = async (data, sender) => { + let data64 = data?.data64 || data?.base64; + let groupId = data?.groupId; + let isAdmins = data?.isAdmins; + if (!groupId) { + throw new Error( + i18n.t('question:message.generic.provide_group_id', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + if (!data64) { + throw new Error( + i18n.t('question:message.generic.include_data_encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + let secretKeyObject; + if (!isAdmins) { + if ( + groupSecretkeys[groupId] && + groupSecretkeys[groupId].secretKeyObject && + groupSecretkeys[groupId]?.timestamp && + Date.now() - groupSecretkeys[groupId]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[groupId].secretKeyObject; + } + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); + + const publish = await getPublishesFromAdmins(names, groupId); + if (publish === false) + throw new Error( + i18n.t('question:message.error.no_group_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); + + const res = await fetch(url); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData, true); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error( + i18n.t('auth:message.error.invalid_secret_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[groupId] = { + secretKeyObject, + timestamp: Date.now(), + }; + } + } else { + if ( + groupSecretkeys[`admins-${groupId}`] && + groupSecretkeys[`admins-${groupId}`].secretKeyObject && + groupSecretkeys[`admins-${groupId}`]?.timestamp && + Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp < 1200000 + ) { + secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject; + } + if (!secretKeyObject) { + const { names } = await getGroupAdmins(groupId); + + const publish = await getPublishesFromAdminsAdminSpace(names, groupId); + if (publish === false) + throw new Error( + i18n.t('question:message.error.no_group_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + const url = await createEndpoint( + `/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64&rebuild=true` + ); + + const res = await fetch(url); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData, true); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error( + i18n.t('auth:message.error.invalid_secret_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + secretKeyObject = decryptedKeyToObject; + groupSecretkeys[`admins-${groupId}`] = { + secretKeyObject, + timestamp: Date.now(), + }; + } + } + + const resGroupDecryptResource = decryptSingle({ + data64, + secretKeyObject: secretKeyObject, + skipDecodeBase64: true, + }); + if (resGroupDecryptResource) { + return resGroupDecryptResource; + } else { + throw new Error( + i18n.t('question:message.error.encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const encryptDataWithSharingKey = async (data, sender) => { + let data64 = data?.data64 || data?.base64; + let publicKeys = data.publicKeys || []; + if (data?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); + } + if (!data64) { + throw new Error( + i18n.t('question:message.generic.include_data_encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const symmetricKey = createSymmetricKeyAndNonce(); + const dataObject = { + data: data64, + key: symmetricKey.messageKey, + }; + const dataObjectBase64 = await objectToBase64(dataObject); + + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const privateKey = parsedData.privateKey; + const userPublicKey = parsedData.publicKey; + + const encryptDataResponse = encryptDataGroup({ + data64: dataObjectBase64, + publicKeys: publicKeys, + privateKey, + userPublicKey, + customSymmetricKey: symmetricKey.messageKey, + }); + if (encryptDataResponse) { + return encryptDataResponse; + } else { + throw new Error( + i18n.t('question:message.error.encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const decryptDataWithSharingKey = async (data, sender) => { + const { encryptedData, key } = data; + + if (!encryptedData) { + throw new Error( + i18n.t('question:message.generic.include_data_decrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + const decryptedData = await decryptGroupEncryptionWithSharingKey({ + data64EncryptedData: encryptedData, + key, + }); + const base64ToObject = JSON.parse(atob(decryptedData)); + + if (!base64ToObject.data) + throw new Error( + i18n.t('question:message.error.no_data_encrypted_resource', { + postProcess: 'capitalizeFirstChar', + }) + ); + return base64ToObject.data; +}; + +export const getHostedData = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const resPermission = await getUserPermission( + { + text1: i18n.t('question:message.error.submit_sell_order', { + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const limit = data?.limit ? data?.limit : 20; + const query = data?.query ? data?.query : ''; + const offset = data?.offset ? data?.offset : 0; + + let urlPath = `/arbitrary/hosted/resources/?limit=${limit}&offset=${offset}`; + if (query) { + urlPath = urlPath + `&query=${query}`; + } + + const url = await createEndpoint(urlPath); + const response = await fetch(url); + const dataResponse = await response.json(); + return dataResponse; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const deleteHostedData = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['hostedData']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.delete_hosts_resources', { + size: data?.hostedData?.length, + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const { hostedData } = data; + + for (const hostedDataItem of hostedData) { + try { + const url = await createEndpoint( + `/arbitrary/resource/${hostedDataItem.service}/${hostedDataItem.name}/${hostedDataItem.identifier}` + ); + await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + console.log(error); + } + } + + return true; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_delete_hosted_resources', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; +export const decryptData = async (data) => { + const { encryptedData, publicKey } = data; + + if (!encryptedData) { + throw new Error(`Missing fields: encryptedData`); + } + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8Array = base64ToUint8Array(encryptedData); + const startsWithQortalEncryptedData = uint8ArrayStartsWith( + uint8Array, + 'qortalEncryptedData' + ); + if (startsWithQortalEncryptedData) { + if (!publicKey) { + throw new Error(`Missing fields: publicKey`); + } + + const decryptedDataToBase64 = decryptDeprecatedSingle( + uint8Array, + publicKey, + uint8PrivateKey + ); + return decryptedDataToBase64; + } + const startsWithQortalGroupEncryptedData = uint8ArrayStartsWith( + uint8Array, + 'qortalGroupEncryptedData' + ); + if (startsWithQortalGroupEncryptedData) { + const decryptedData = decryptGroupDataQortalRequest( + encryptedData, + parsedData.privateKey + ); + const decryptedDataToBase64 = uint8ArrayToBase64(decryptedData); + return decryptedDataToBase64; + } + throw new Error( + i18n.t('question:message.error.encrypt', { + postProcess: 'capitalizeFirstChar', + }) + ); +}; + +export const getListItems = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['list_name']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const value = (await getPermission('qAPPAutoLists')) || false; + + let skip = false; + if (value) { + skip = true; + } + let resPermission; + let acceptedVar; + let checkbox1Var; + if (!skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.access_list', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: data.list_name, + checkbox1: { + value: value, + label: i18n.t('question:always_retrieve_list', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + const { accepted, checkbox1 } = resPermission; + acceptedVar = accepted; + checkbox1Var = checkbox1; + setPermission('qAPPAutoLists', checkbox1); + } + + if (acceptedVar || skip) { + const url = await createEndpoint(`/lists/${data.list_name}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + + const list = await response.json(); + return list; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_share_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const addListItems = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['list_name', 'items']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const items = data.items; + const list_name = data.list_name; + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.all_item_list', { + name: list_name, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: items.join(', '), + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const url = await createEndpoint(`/lists/${list_name}`); + const body = { + items: items, + }; + const bodyToString = JSON.stringify(body); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: bodyToString, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.add_to_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_add_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const deleteListItems = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['list_name']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (!data?.item && !data?.items) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: 'items', + postProcess: 'capitalizeFirstChar', + }) + ); + } + const item = data?.item; + const items = data?.items; + const list_name = data.list_name; + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.remove_from_list', { + name: list_name, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: items ? JSON.stringify(items) : item, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const url = await createEndpoint(`/lists/${list_name}`); + const body = { + items: items || [item], + }; + const bodyToString = JSON.stringify(body); + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: bodyToString, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.add_to_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_delete_from_list', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const publishQDNResource = async ( + data: any, + sender, + isFromExtension +) => { + const requiredFields = ['service']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (!data.file && !data.data64 && !data.base64) { + throw new Error( + i18n.t('question:message.error.no_data_file_submitted', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + // 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 registeredName = await getNameInfo(); + const name = registeredName; + if (!name) { + throw new Error( + i18n.t('question:message.error.user_qortal_name', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + let identifier = data.identifier; + let data64 = data.data64 || data.base64; + const filename = data.filename; + const title = data.title; + const description = data.description; + const category = data.category; + + 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?.file || data?.blob) { + data64 = await fileToBase64(data?.file || data?.blob); + } + if ( + data.encrypt && + (!data.publicKeys || + (Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) + ) { + throw new Error( + i18n.t('question:message.error.encryption_requires_public_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + if (data.encrypt) { + try { + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const privateKey = parsedData.privateKey; + const userPublicKey = parsedData.publicKey; + const encryptDataResponse = encryptDataGroup({ + data64, + publicKeys: data.publicKeys, + privateKey, + userPublicKey, + }); + if (encryptDataResponse) { + data64 = encryptDataResponse; + } + } catch (error) { + throw new Error( + error.message || + i18n.t('question:message.error.upload_encryption', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } + + const fee = await getFee('ARBITRARY'); + + const handleDynamicValues = {}; + if (hasAppFee) { + const feePayment = await getFee('PAYMENT'); + + (handleDynamicValues['appFee'] = +appFee + +feePayment.fee), + (handleDynamicValues['checkbox1'] = { + value: true, + label: i18n.t('question:accept_app_fee', { + postProcess: 'capitalizeFirstChar', + }), + }); + } + if (!!data?.encrypt) { + handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}`; + } + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.publish_qdn', { + postProcess: 'capitalizeFirstChar', + }), + text2: `service: ${service}`, + text3: `identifier: ${identifier || null}`, + fee: fee.fee, + ...handleDynamicValues, + }, + isFromExtension + ); + const { accepted, checkbox1 = false } = resPermission; + if (accepted) { + try { + const resPublish = await 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, + }); + if (resPublish?.signature && hasAppFee && checkbox1) { + sendCoinFunc( + { + amount: appFee, + receiver: appFeeRecipient, + }, + true + ); + } + return resPublish; + } catch (error) { + throw new Error(error?.message || 'Upload failed'); + } + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const checkArrrSyncStatus = async (seed) => { + const _url = await createEndpoint(`/crosschain/arrr/syncstatus`); + let tries = 0; // Track the number of attempts + + while (tries < 36) { + const response = await fetch(_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: seed, + }); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res.indexOf('<') > -1 || res !== 'Synchronized') { + // Wait 2 seconds before trying again + await new Promise((resolve) => setTimeout(resolve, 2000)); + tries += 1; + } else { + // If the response doesn't meet the two conditions, exit the function + return; + } + } + + // If we exceed N tries, throw an error + throw new Error( + i18n.t('question:message.error.synchronization_attempts', { + quantity: 36, + postProcess: 'capitalizeFirstChar', + }) + ); +}; + +export const publishMultipleQDNResources = async ( + data: any, + sender, + isFromExtension +) => { + const requiredFields = ['resources']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const resources = data.resources; + if (!Array.isArray(resources)) { + throw new Error( + i18n.t('group:message.generic.invalid_data', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + if (resources.length === 0) { + throw new Error( + i18n.t('question:message.error.no_resources_publish', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + const encrypt = data?.encrypt; + + for (const resource of resources) { + const resourceEncrypt = encrypt && resource?.disableEncrypt !== true; + + if (!resourceEncrypt && resource?.service.endsWith('_PRIVATE')) { + const errorMsg = i18n.t('question:message.error.only_encrypted_data', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } else if (resourceEncrypt && !resource?.service.endsWith('_PRIVATE')) { + const errorMsg = i18n.t('question:message.error.use_private_service', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + } + + const fee = await getFee('ARBITRARY'); + const registeredName = await getNameInfo(); + const name = registeredName; + + if (!name) { + throw new Error( + i18n.t('question:message.error.registered_name', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + 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'); + + (handleDynamicValues['appFee'] = +appFee + +feePayment.fee), + (handleDynamicValues['checkbox1'] = { + value: true, + label: i18n.t('question:accept_app_fee', { + postProcess: 'capitalizeFirstChar', + }), + }); + } + if (data?.encrypt) { + handleDynamicValues['highlightedText'] = `isEncrypted: ${!!data.encrypt}`; + } + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.publish_qdn', { + postProcess: 'capitalizeFirstChar', + }), + html: ` +
+ + + ${data.resources + .map( + (resource) => ` +
+
Service: ${ + resource.service + }
+
Name: ${name}
+
Identifier: ${ + resource.identifier + }
+ ${ + resource.filename + ? `
Filename: ${resource.filename}
` + : '' + } +
` + ) + .join('')} +
+ + `, + fee: +fee.fee * resources.length, + ...handleDynamicValues, + }, + isFromExtension + ); + + const { accepted, checkbox1 = false } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + type FailedPublish = { + reason: string; + identifier: any; + service: any; + }; + + const failedPublishesIdentifiers: FailedPublish[] = []; + + for (const resource of resources) { + try { + const requiredFields = ['service']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!resource[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + service: resource.service, + }); + continue; + } + if (!resource.file && !resource.data64 && !resource?.base64) { + const errorMsg = i18n.t( + 'question:message.error.no_data_file_submitted', + { + postProcess: 'capitalizeFirstChar', + } + ); + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + service: resource.service, + }); + continue; + } + const service = resource.service; + let identifier = resource.identifier; + let data64 = 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; + } + + // Access tag1 to tag5 from result + const { tag1, tag2, tag3, tag4, tag5 } = result; + const resourceEncrypt = encrypt && resource?.disableEncrypt !== true; + if (resource.identifier == null) { + identifier = 'default'; + } + if (!resourceEncrypt && service.endsWith('_PRIVATE')) { + const errorMsg = i18n.t('question:message.error.only_encrypted_data', { + postProcess: 'capitalizeFirstChar', + }); + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + service: resource.service, + }); + continue; + } + if (resource.file) { + data64 = await fileToBase64(resource.file); + } + if (resourceEncrypt) { + try { + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const privateKey = parsedData.privateKey; + const userPublicKey = parsedData.publicKey; + const encryptDataResponse = encryptDataGroup({ + data64, + publicKeys: data.publicKeys, + privateKey, + userPublicKey, + }); + if (encryptDataResponse) { + data64 = encryptDataResponse; + } + } catch (error) { + const errorMsg = + error?.message || + i18n.t('question:message.error.upload_encryption', { + postProcess: 'capitalizeFirstChar', + }); + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + service: resource.service, + }); + 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 + ); + await new Promise((res) => { + setTimeout(() => { + res(); + }, 1000); + }); + } catch (error) { + const errorMsg = + error.message || + i18n.t('question:message.error.upload', { + postProcess: 'capitalizeFirstChar', + }); + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + service: resource.service, + }); + } + } catch (error) { + failedPublishesIdentifiers.push({ + reason: + error?.message || + i18n.t('question:message.error.unknown_error', { + postProcess: 'capitalizeFirstChar', + }), + identifier: resource.identifier, + service: resource.service, + }); + } + } + if (failedPublishesIdentifiers.length > 0) { + const obj = { + message: i18n.t('question:message.error.resources_publish', { + postProcess: 'capitalizeFirstChar', + }), + }; + obj['error'] = { + unsuccessfulPublishes: failedPublishesIdentifiers, + }; + return obj; + } + if (hasAppFee && checkbox1) { + sendCoinFunc( + { + amount: appFee, + receiver: appFeeRecipient, + }, + true + ); + } + return true; +}; + +export const voteOnPoll = async (data, isFromExtension) => { + const requiredFields = ['pollName', 'optionIndex']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const pollName = data.pollName; + const optionIndex = data.optionIndex; + let pollInfo = null; + try { + const url = await createEndpoint(`/polls/${encodeURIComponent(pollName)}`); + const response = await fetch(url); + if (!response.ok) { + const errorMessage = await parseErrorResponse( + response, + i18n.t('question:message.error.fetch_poll', { + postProcess: 'capitalizeFirstChar', + }) + ); + throw new Error(errorMessage); + } + + pollInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_poll', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (!pollInfo || pollInfo.error) { + const errorMsg = + (pollInfo && pollInfo.message) || + i18n.t('question:message.error.no_poll', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + try { + const optionName = pollInfo.pollOptions[optionIndex].optionName; + const resVoteOnPoll = await _voteOnPoll( + { pollName, optionIndex, optionName }, + isFromExtension + ); + return resVoteOnPoll; + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.poll_vote', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const createPoll = async (data, isFromExtension) => { + const requiredFields = [ + 'pollName', + 'pollDescription', + 'pollOptions', + 'pollOwnerAddress', + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const pollName = data.pollName; + const pollDescription = data.pollDescription; + const pollOptions = data.pollOptions; + try { + const resCreatePoll = await _createPoll( + { + pollName, + pollDescription, + options: pollOptions, + }, + isFromExtension + ); + return resCreatePoll; + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.poll_create', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +function isBase64(str) { + const base64Regex = + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + return base64Regex.test(str) && str.length % 4 === 0; +} + +function checkValue(value) { + if (typeof value === 'string') { + if (isBase64(value)) { + return 'string'; + } else { + return 'string'; + } + } else if (typeof value === 'object' && value !== null) { + return 'object'; + } else { + throw new Error( + i18n.t('question:message.error.invalid_fullcontent', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +} + +export const sendChatMessage = async (data, isFromExtension, appInfo) => { + const message = data?.message; + const fullMessageObject = data?.fullMessageObject || data?.fullContent; + const recipient = data?.destinationAddress || data.recipient; + const groupId = data.groupId; + const isRecipient = groupId === undefined; + const chatReference = data?.chatReference; + if (groupId === undefined && recipient === undefined) { + throw new Error( + i18n.t('question:provide_recipient_group_id', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + let fullMessageObjectType; + if (fullMessageObject) { + fullMessageObjectType = checkValue(fullMessageObject); + } + const value = + (await getPermission(`qAPPSendChatMessage-${appInfo?.name}`)) || false; + let skip = false; + if (value) { + skip = true; + } + let resPermission; + if (!skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_chat_message', { + postProcess: 'capitalizeFirstChar', + }), + text2: isRecipient + ? i18n.t('question:to_recipient', { + recipient: recipient, + postProcess: 'capitalizeFirstChar', + }) + : i18n.t('question:to_group', { + group_id: groupId, + postProcess: 'capitalizeFirstChar', + }), + text3: fullMessageObject + ? fullMessageObjectType === 'string' + ? `${fullMessageObject?.slice(0, 25)}${fullMessageObject?.length > 25 ? '...' : ''}` + : `${JSON.stringify(fullMessageObject)?.slice(0, 25)}${JSON.stringify(fullMessageObject)?.length > 25 ? '...' : ''}` + : `${message?.slice(0, 25)}${message?.length > 25 ? '...' : ''}`, + checkbox1: { + value: false, + label: i18n.t('question:always_chat_messages', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + } + const { accepted = false, checkbox1 = false } = resPermission || {}; + if (resPermission && accepted) { + setPermission(`qAPPSendChatMessage-${appInfo?.name}`, checkbox1); + } + if (accepted || skip) { + const tiptapJson = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: message, + }, + ], + }, + ], + }; + const messageObject = fullMessageObject + ? fullMessageObject + : { + messageText: tiptapJson, + images: [], + repliedTo: '', + version: 3, + }; + + let stringifyMessageObject = JSON.stringify(messageObject); + if (fullMessageObjectType === 'string') { + stringifyMessageObject = messageObject; + } + + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + if (!hasEnoughBalance) { + throw new Error( + i18n.t('group:message.error.qortals_required', { + quantity: 4, + postProcess: 'capitalizeFirstChar', + }) + ); + } + if (isRecipient && recipient) { + const url = await createEndpoint(`/addresses/publickey/${recipient}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_recipient_public_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let key; + let hasPublicKey; + let res; + const contentType = response.headers.get('content-type'); + + // If the response is JSON, parse it as JSON + if (contentType && contentType.includes('application/json')) { + res = await response.json(); + } else { + // Otherwise, treat it as plain text + res = await response.text(); + } + if (res?.error === 102) { + key = ''; + hasPublicKey = false; + } else if (res !== false) { + key = res; + hasPublicKey = true; + } else { + key = ''; + hasPublicKey = false; + } + + if (!hasPublicKey && isRecipient) { + throw new Error( + 'Cannot send an encrypted message to this user since they do not have their publickey on chain.' + ); + } + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let sendTimestamp = Date.now(); + + let reference = Base58.encode(_reference); + 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 handleDynamicValues = {}; + if (chatReference) { + handleDynamicValues['chatReference'] = chatReference; + } + + const tx = await createTransaction(18, keyPair, { + timestamp: sendTimestamp, + recipient: recipient, + recipientPublicKey: key, + hasChatReference: chatReference ? 1 : 0, + message: stringifyMessageObject, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + ...handleDynamicValues, + }); + + const chatBytes = tx.chatBytes; + const difficulty = 8; + const { nonce, chatBytesArray } = await performPowTask( + chatBytes, + difficulty + ); + + let _response = await signChatFunc(chatBytesArray, nonce, null, keyPair); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; + } else if (!isRecipient && groupId) { + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + 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 handleDynamicValues = {}; + if (chatReference) { + handleDynamicValues['chatReference'] = chatReference; + } + + const txBody = { + timestamp: Date.now(), + groupID: Number(groupId), + hasReceipient: 0, + hasChatReference: chatReference ? 1 : 0, + message: stringifyMessageObject, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, // Set default to not encrypted for groups + isText: 1, + ...handleDynamicValues, + }; + + const tx = await createTransaction(181, keyPair, txBody); + + // if (!hasEnoughBalance) { + // throw new Error("Must have at least 4 QORT to send a chat message"); + // } + + const chatBytes = tx.chatBytes; + const difficulty = 8; + const { nonce, chatBytesArray } = await performPowTask( + chatBytes, + difficulty + ); + + let _response = await signChatFunc(chatBytesArray, nonce, null, keyPair); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; + } else { + throw new Error( + i18n.t('question:provide_recipient_group_id', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_send_message', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const joinGroup = async (data, isFromExtension) => { + const requiredFields = ['groupId']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${data.groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const fee = await getFee('JOIN_GROUP'); + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:message.generic.confirm_join_group', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const groupId = data.groupId; + + if (!groupInfo || groupInfo.error) { + const errorMsg = + (groupInfo && groupInfo.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + try { + const resJoinGroup = await joinGroupFunc({ groupId }); + return resJoinGroup; + } catch (error) { + throw new Error( + error?.message || + i18n.t('group:message.error.group_join', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_join', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const saveFile = async (data, sender, isFromExtension, snackMethods) => { + try { + const requiredFields = ['filename', 'blob']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const filename = data.filename; + const blob = data.blob; + const resPermission = await getUserPermission( + { + text1: i18n.t('question:download_file', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${filename}`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const mimeType = blob.type || data.mimeType; + let backupExention = filename.split('.').pop(); + if (backupExention) { + backupExention = '.' + backupExention; + } + const fileExtension = mimeToExtensionMap[mimeType] || backupExention; + let fileHandleOptions = {}; + if (!mimeType) { + throw new Error( + i18n.t('question:message.error.mime_type', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + if (!fileExtension) { + throw new Error( + i18n.t('question:message.error.file_extension', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + if (fileExtension && mimeType) { + fileHandleOptions = { + accept: { + [mimeType]: [fileExtension], + }, + }; + } + + showSaveFilePicker( + { + filename, + mimeType, + blob, + }, + snackMethods + ); + return true; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_save_file', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } catch (error) { + throw new Error( + error?.message || + i18n.t('core:message.error.initiate_download', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const deployAt = async (data, isFromExtension) => { + const requiredFields = [ + 'name', + 'description', + 'tags', + 'creationBytes', + 'amount', + 'assetId', + 'type', + ]; + + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + try { + const resDeployAt = await _deployAt( + { + name: data.name, + description: data.description, + tags: data.tags, + creationBytes: data.creationBytes, + amount: data.amount, + assetId: data.assetId, + atType: data.type, + }, + isFromExtension + ); + return resDeployAt; + } catch (error) { + throw new Error( + error?.message || + i18n.t('group:message.error.group_join', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getUserWallet = async (data, isFromExtension, appInfo) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const isGateway = await isRunningGateway(); + + if (data?.coin === 'ARRR' && isGateway) + throw new Error( + i18n.t('question:message.error.gateway_wallet_local_node', { + token: 'ARRR', + postProcess: 'capitalizeFirstChar', + }) + ); + + const value = + (await getPermission( + `qAPPAutoGetUserWallet-${appInfo?.name}-${data.coin}` + )) || false; + let skip = false; + if (value) { + skip = true; + } + + let resPermission; + + if (!skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.get_wallet_info', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: i18n.t('question:always_retrieve_wallet', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + } + const { accepted = false, checkbox1 = false } = resPermission || {}; + + if (resPermission) { + setPermission( + `qAPPAutoGetUserWallet-${appInfo?.name}-${data.coin}`, + checkbox1 + ); + } + + if (accepted || skip) { + let coin = data.coin; + let userWallet = {}; + let arrrAddress = ''; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const arrrSeed58 = parsedData.arrrSeed58; + if (coin === 'ARRR') { + const bodyToString = arrrSeed58; + const url = await createEndpoint(`/crosschain/arrr/walletaddress`); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: bodyToString, + }); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + arrrAddress = res; + } + switch (coin) { + case 'QORT': + userWallet['address'] = address; + userWallet['publickey'] = parsedData.publicKey; + break; + case 'BTC': + userWallet['address'] = parsedData.btcAddress; + userWallet['publickey'] = parsedData.btcPublicKey; + break; + case 'LTC': + userWallet['address'] = parsedData.ltcAddress; + userWallet['publickey'] = parsedData.ltcPublicKey; + break; + case 'DOGE': + userWallet['address'] = parsedData.dogeAddress; + userWallet['publickey'] = parsedData.dogePublicKey; + break; + case 'DGB': + userWallet['address'] = parsedData.dgbAddress; + userWallet['publickey'] = parsedData.dgbPublicKey; + break; + case 'RVN': + userWallet['address'] = parsedData.rvnAddress; + userWallet['publickey'] = parsedData.rvnPublicKey; + break; + case 'ARRR': + await checkArrrSyncStatus(parsedData.arrrSeed58); + userWallet['address'] = arrrAddress; + break; + default: + break; + } + return userWallet; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getWalletBalance = async ( + data, + bypassPermission?: boolean, + isFromExtension?: boolean, + appInfo?: any +) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const isGateway = await isRunningGateway(); + + if (data?.coin === 'ARRR' && isGateway) + throw new Error( + i18n.t('question:message.error.gateway_balance_local_node', { + token: 'ARRR', + postProcess: 'capitalizeFirstChar', + }) + ); + + const value = + (await getPermission( + `qAPPAutoWalletBalance-${appInfo?.name}-${data.coin}` + )) || false; + let skip = false; + if (value) { + skip = true; + } + let resPermission; + + if (!bypassPermission && !skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.fetch_balance', { + coin: data.coin, // TODO highlight coin in the modal + postProcess: 'capitalizeFirstChar', + }), + checkbox1: { + value: true, + label: i18n.t('question:always_retrieve_balance', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + } + const { accepted = false, checkbox1 = false } = resPermission || {}; + if (resPermission) { + setPermission( + `qAPPAutoWalletBalance-${appInfo?.name}-${data.coin}`, + checkbox1 + ); + } + if (accepted || bypassPermission || skip) { + let coin = data.coin; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + if (coin === 'QORT') { + let qortAddress = address; + try { + const url = await createEndpoint(`/addresses/balance/${qortAddress}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_balance', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.fetch_wallet', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else { + let _url = ``; + let _body = null; + switch (coin) { + case 'BTC': + _url = await createEndpoint(`/crosschain/btc/walletbalance`); + + _body = parsedData.btcPublicKey; + break; + case 'LTC': + _url = await createEndpoint(`/crosschain/ltc/walletbalance`); + _body = parsedData.ltcPublicKey; + break; + case 'DOGE': + _url = await createEndpoint(`/crosschain/doge/walletbalance`); + _body = parsedData.dogePublicKey; + break; + case 'DGB': + _url = await createEndpoint(`/crosschain/dgb/walletbalance`); + _body = parsedData.dgbPublicKey; + break; + case 'RVN': + _url = await createEndpoint(`/crosschain/rvn/walletbalance`); + _body = parsedData.rvnPublicKey; + break; + case 'ARRR': + await checkArrrSyncStatus(parsedData.arrrSeed58); + _url = await createEndpoint(`/crosschain/arrr/walletbalance`); + _body = parsedData.arrrSeed58; + break; + default: + break; + } + try { + const response = await fetch(_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: _body, + }); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + if (isNaN(Number(res))) { + throw new Error( + i18n.t('question:message.error.fetch_balance', { + postProcess: 'capitalizeFirstChar', + }) + ); + } else { + return (Number(res) / 1e8).toFixed(8); + } + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.fetch_balance', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +const getPirateWallet = async (arrrSeed58) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.error.gateway_retrieve_balance', { + token: 'PIRATECHAIN', + postProcess: 'capitalizeFirstChar', + }) + ); + } + const bodyToString = arrrSeed58; + await checkArrrSyncStatus(bodyToString); + const url = await createEndpoint(`/crosschain/arrr/walletaddress`); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: bodyToString, + }); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; +}; + +export const getUserWalletFunc = async (coin) => { + let userWallet = {}; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + switch (coin) { + case 'QORT': + userWallet['address'] = address; + userWallet['publickey'] = parsedData.publicKey; + break; + case 'BTC': + case 'BITCOIN': + userWallet['address'] = parsedData.btcAddress; + userWallet['publickey'] = parsedData.btcPublicKey; + break; + case 'LTC': + case 'LITECOIN': + userWallet['address'] = parsedData.ltcAddress; + userWallet['publickey'] = parsedData.ltcPublicKey; + break; + case 'DOGE': + case 'DOGECOIN': + userWallet['address'] = parsedData.dogeAddress; + userWallet['publickey'] = parsedData.dogePublicKey; + break; + case 'DGB': + case 'DIGIBYTE': + userWallet['address'] = parsedData.dgbAddress; + userWallet['publickey'] = parsedData.dgbPublicKey; + break; + case 'RVN': + case 'RAVENCOIN': + userWallet['address'] = parsedData.rvnAddress; + userWallet['publickey'] = parsedData.rvnPublicKey; + break; + case 'ARRR': + case 'PIRATECHAIN': + const arrrAddress = await getPirateWallet(parsedData.arrrSeed58); + userWallet['address'] = arrrAddress; + break; + default: + break; + } + return userWallet; +}; + +export const getUserWalletInfo = async (data, isFromExtension, appInfo) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (data?.coin === 'ARRR') { + throw new Error( + i18n.t('question:message.error.token_not_supported', { + token: 'ARRR', + postProcess: 'capitalizeFirstChar', + }) + ); + } + const value = + (await getPermission(`getUserWalletInfo-${appInfo?.name}-${data.coin}`)) || + false; + let skip = false; + if (value) { + skip = true; + } + let resPermission; + + if (!skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.get_wallet_info', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: i18n.t('question:always_retrieve_wallet', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + } + const { accepted = false, checkbox1 = false } = resPermission || {}; + + if (resPermission) { + setPermission(`getUserWalletInfo-${appInfo?.name}-${data.coin}`, checkbox1); + } + + if (accepted || skip) { + let coin = data.coin; + let walletKeys = await getUserWalletFunc(coin); + + const _url = await createEndpoint( + `/crosschain/` + data.coin.toLowerCase() + `/addressinfos` + ); + let _body = { xpub58: walletKeys['publickey'] }; + try { + const response = await fetch(_url, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(_body), + }); + if (!response?.ok) + throw new Error( + i18n.t('question:message.error.fetch_wallet_info', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.fetch_wallet', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getUserWalletTransactions = async ( + data, + isFromExtension, + appInfo +) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const value = + (await getPermission( + `getUserWalletTransactions-${appInfo?.name}-${data.coin}` + )) || false; + let skip = false; + if (value) { + skip = true; + } + let resPermission; + + if (!skip) { + resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.get_wallet_transactions', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: i18n.t('question:always_retrieve_wallet_transactions', { + postProcess: 'capitalizeFirstChar', + }), + }, + }, + isFromExtension + ); + } + const { accepted = false, checkbox1 = false } = resPermission || {}; + + if (resPermission) { + setPermission( + `getUserWalletTransactions-${appInfo?.name}-${data.coin}`, + checkbox1 + ); + } + + if (accepted || skip) { + const coin = data.coin; + const walletKeys = await getUserWalletFunc(coin); + let publicKey; + if (data?.coin === 'ARRR') { + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + publicKey = parsedData.arrrSeed58; + } else { + publicKey = walletKeys['publickey']; + } + + const _url = await createEndpoint( + `/crosschain/` + data.coin.toLowerCase() + `/wallettransactions` + ); + const _body = publicKey; + try { + const response = await fetch(_url, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: _body, + }); + if (!response?.ok) + throw new Error( + i18n.t('question:message.error.fetch_wallet_transactions', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.fetch_wallet_transactions', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getCrossChainServerInfo = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const _url = `/crosschain/` + data.coin.toLowerCase() + `/serverinfos`; + try { + const url = await createEndpoint(_url); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_generic', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res.servers; + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.server_info', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getTxActivitySummary = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const coin = data.coin; + const url = `/crosschain/txactivity?foreignBlockchain=${coin}`; // No apiKey here + + try { + const endpoint = await createEndpoint(url); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_generic', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; // Return full response here + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.transaction_activity_summary', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getForeignFee = async (data) => { + const requiredFields = ['coin', 'type']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const { coin, type } = data; + const url = `/crosschain/${coin.toLowerCase()}/${type}`; + + try { + const endpoint = await createEndpoint(url); + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_generic', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; // Return full response here + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.get_foreign_fee', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +function calculateRateFromFee(totalFee, sizeInBytes) { + const fee = (totalFee / sizeInBytes) * 1000; + return fee.toFixed(0); +} + +export const updateForeignFee = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['coin', 'type', 'value']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const { coin, type, value } = data; + + const text3 = + type === 'feerequired' + ? i18n.t('question:sats', { + amount: value, + postProcess: 'capitalizeFirstChar', + }) + : i18n.t('question:sats_per_kb', { + amount: value, + postProcess: 'capitalizeFirstChar', + }); + const text4 = + type === 'feerequired' + ? i18n.t('question:message.generic.calculate_fee', { + amount: value, + rate: calculateRateFromFee(value, 300), + postProcess: 'capitalizeFirstChar', + }) + : ''; + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.update_foreign_fee', { + postProcess: 'capitalizeFirstChar', + }), + text2: `type: ${type === 'feerequired' ? 'unlocking' : 'locking'}`, + text3: i18n.t('question:value', { + value: text3, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:coin', { + coin: coin, + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + + const { accepted } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const url = `/crosschain/${coin.toLowerCase()}/update${type}`; + const valueStringified = JSON.stringify(+value); + + const endpoint = await createEndpoint(url); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: valueStringified, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.update_foreign_fee', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; // Return full response here +}; + +export const getServerConnectionHistory = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const coin = data.coin.toLowerCase(); + const url = `/crosschain/${coin.toLowerCase()}/serverconnectionhistory`; + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_connection_history', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return full response here + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.fetch_connection_history', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const setCurrentForeignServer = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const { coin, host, port, type } = data; + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.set_current_server', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:server_type', { + type: type, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:server_host', { + host: host, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:coin', { + coin: coin, + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + + const { accepted } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const body = { + hostName: host, + port: port, + connectionType: type, + }; + + const url = `/crosschain/${coin.toLowerCase()}/setcurrentserver`; + + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.server_current_set', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response +}; + +export const addForeignServer = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const { coin, host, port, type } = data; + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.server_add', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:server_type', { + type: type, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:server_host', { + host: host, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:coin', { + coin: coin, + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + + const { accepted } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const body = { + hostName: host, + port: port, + connectionType: type, + }; + + const url = `/crosschain/${coin.toLowerCase()}/addserver`; + + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.server_current_add', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response +}; + +export const removeForeignServer = async (data, isFromExtension) => { + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const { coin, host, port, type } = data; + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.server_remove', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:server_type', { + type: type, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:server_host', { + host: host, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:coin', { + coin: coin, + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + + const { accepted } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const body = { + hostName: host, + port: port, + connectionType: type, + }; + + const url = `/crosschain/${coin.toLowerCase()}/removeserver`; + + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.server_remove', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response +}; + +export const getDaySummary = async () => { + const url = `/admin/summary`; // Simplified endpoint URL + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available for constructing the full URL + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + }, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.retrieve_summary', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.retrieve_summary', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getNodeInfo = async () => { + const url = `/admin/info`; // Simplified endpoint URL + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available for constructing the full URL + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + }, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.node_info', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.node_info', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getNodeStatus = async () => { + const url = `/admin/status`; // Simplified endpoint URL + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available for constructing the full URL + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + }, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.node_status', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.node_status', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const getArrrSyncStatus = async () => { + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const arrrSeed = parsedData.arrrSeed58; + const url = `/crosschain/arrr/syncstatus`; // Simplified endpoint URL + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available for constructing the full URL + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + }, + body: arrrSeed, + }); + + let res; + + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + return res; // Return the full response + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.retrieve_sync_status', { + token: 'ARRR', + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const sendCoin = async (data, isFromExtension) => { + const requiredFields = ['coin', 'amount']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (!data?.destinationAddress && !data?.recipient) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: 'recipient', + postProcess: 'capitalizeFirstChar', + }) + ); + } + let checkCoin = data.coin; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const isGateway = await isRunningGateway(); + + if (checkCoin !== 'QORT' && isGateway) + throw new Error( + i18n.t('question:message.error.gateway_non_qort_local_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + if (checkCoin === 'QORT') { + // Params: data.coin, data.recipient, data.amount, data.fee + // TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction + // then set the response string from the core to the `response` variable (defined above) + // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + + const url = await createEndpoint(`/addresses/balance/${address}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_balance', { + postProcess: 'capitalizeFirstChar', + }) + ); + let walletBalance; + try { + walletBalance = await response.clone().json(); + } catch (e) { + walletBalance = await response.text(); + } + if (isNaN(Number(walletBalance))) { + const errorMsg = i18n.t('question:message.error.fetch_balance_token', { + token: 'QORT', + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const transformDecimals = (Number(walletBalance) * QORT_DECIMALS).toFixed( + 0 + ); + const walletBalanceDecimals = Number(transformDecimals); + const amountDecimals = Number(amount) * QORT_DECIMALS; + const fee: number = await sendQortFee(); + if (amountDecimals + fee * QORT_DECIMALS > walletBalanceDecimals) { + const errorMsg = i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (amount <= 0) { + const errorMsg = i18n.t('core:message.error.invalid_amount', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + if (recipient.length === 0) { + const errorMsg = i18n.t('question:message.error.empty_receiver', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:to_recipient', { + recipient: recipient, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${amount} ${checkCoin}`, + fee: fee, + confirmCheckbox: true, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const makePayment = await sendCoinFunc( + { amount, password: null, receiver: recipient }, + true + ); + return makePayment.res?.data; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else if (checkCoin === 'BTC') { + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + const xprv58 = parsedData.btcPrivateKey; + const feePerByte = data.fee ? data.fee : btcFeePerByte; + + const btcWalletBalance = await getWalletBalance({ coin: checkCoin }, true); + + if (isNaN(Number(btcWalletBalance))) { + throw new Error( + i18n.t('question:message.error.fetch_balance_token', { + token: 'BTC', + postProcess: 'capitalizeFirstChar', + }) + ); + } + const btcWalletBalanceDecimals = Number(btcWalletBalance); + const btcAmountDecimals = Number(amount); + const fee = feePerByte * 500; // default 0.00050000 + if (btcAmountDecimals + fee > btcWalletBalanceDecimals) { + throw new Error( + i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:to_recipient', { + recipient: recipient, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${amount} ${checkCoin}`, + foreignFee: `${fee} BTC`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + bitcoinAmount: amount, + feePerByte: feePerByte, + }; + const url = await createEndpoint(`/crosschain/btc/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.send', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else if (checkCoin === 'LTC') { + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + const xprv58 = parsedData.ltcPrivateKey; + const feePerByte = data.fee ? data.fee : ltcFeePerByte; + const ltcWalletBalance = await getWalletBalance({ coin: checkCoin }, true); + + if (isNaN(Number(ltcWalletBalance))) { + const errorMsg = i18n.t('question:message.error.fetch_balance_token', { + token: 'LTC', + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const ltcWalletBalanceDecimals = Number(ltcWalletBalance); + const ltcAmountDecimals = Number(amount); + const fee = feePerByte * 1000; // default 0.00030000 + if (ltcAmountDecimals + fee > ltcWalletBalanceDecimals) { + throw new Error( + i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:to_recipient', { + recipient: recipient, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${amount} ${checkCoin}`, + foreignFee: `${fee} LTC`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const url = await createEndpoint(`/crosschain/ltc/send`); + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + litecoinAmount: amount, + feePerByte: feePerByte, + }; + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.send', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else if (checkCoin === 'DOGE') { + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + const xprv58 = parsedData.dogePrivateKey; + const feePerByte = data.fee ? data.fee : dogeFeePerByte; + const dogeWalletBalance = await getWalletBalance({ coin: checkCoin }, true); + if (isNaN(Number(dogeWalletBalance))) { + const errorMsg = i18n.t('question:message.error.fetch_balance_token', { + token: 'DOGE', + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const dogeWalletBalanceDecimals = Number(dogeWalletBalance); + const dogeAmountDecimals = Number(amount); + const fee = feePerByte * 5000; // default 0.05000000 + if (dogeAmountDecimals + fee > dogeWalletBalanceDecimals) { + const errorMsg = i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:to_recipient', { + recipient: recipient, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${amount} ${checkCoin}`, + foreignFee: `${fee} DOGE`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + dogecoinAmount: amount, + feePerByte: feePerByte, + }; + const url = await createEndpoint(`/crosschain/doge/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.send', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else if (checkCoin === 'DGB') { + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + const xprv58 = parsedData.dbgPrivateKey; + const feePerByte = data.fee ? data.fee : dgbFeePerByte; + const dgbWalletBalance = await getWalletBalance({ coin: checkCoin }, true); + if (isNaN(Number(dgbWalletBalance))) { + const errorMsg = i18n.t('question:message.error.fetch_balance_token', { + token: 'DGB', + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const dgbWalletBalanceDecimals = Number(dgbWalletBalance); + const dgbAmountDecimals = Number(amount); + const fee = feePerByte * 500; // default 0.00005000 + if (dgbAmountDecimals + fee > dgbWalletBalanceDecimals) { + const errorMsg = i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + foreignFee: `${fee} DGB`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + digibyteAmount: amount, + feePerByte: feePerByte, + }; + const url = await createEndpoint(`/crosschain/dgb/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.send', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else if (checkCoin === 'RVN') { + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + const xprv58 = parsedData.rvnPrivateKey; + const feePerByte = data.fee ? data.fee : rvnFeePerByte; + const rvnWalletBalance = await getWalletBalance({ coin: checkCoin }, true); + if (isNaN(Number(rvnWalletBalance))) { + const errorMsg = i18n.t('question:message.error.fetch_balance_token', { + token: 'RVN', + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const rvnWalletBalanceDecimals = Number(rvnWalletBalance); + const rvnAmountDecimals = Number(amount); + const fee = feePerByte * 500; // default 0.00562500 + if (rvnAmountDecimals + fee > rvnWalletBalanceDecimals) { + const errorMsg = i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + foreignFee: `${fee} RVN`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + ravencoinAmount: amount, + feePerByte: feePerByte, + }; + const url = await createEndpoint(`/crosschain/rvn/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.send', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } else if (checkCoin === 'ARRR') { + const amount = Number(data.amount); + const recipient = data?.recipient || data.destinationAddress; + const memo = data?.memo; + const arrrWalletBalance = await getWalletBalance({ coin: checkCoin }, true); + + if (isNaN(Number(arrrWalletBalance))) { + const errorMsg = i18n.t('question:message.error.fetch_balance_token', { + token: 'ARR', + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const arrrWalletBalanceDecimals = Number(arrrWalletBalance); + const arrrAmountDecimals = Number(amount); + const fee = 0.0001; + if (arrrAmountDecimals + fee > arrrWalletBalanceDecimals) { + const errorMsg = i18n.t('question:message.error.insufficient_funds', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.send_coins', { + postProcess: 'capitalizeFirstChar', + }), + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + foreignFee: `${fee} ARRR`, + }, + isFromExtension + ); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + entropy58: parsedData.arrrSeed58, + receivingAddress: recipient, + arrrAmount: amount, + memo: memo, + }; + const url = await createEndpoint(`/crosschain/arrr/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.send', { + postProcess: 'capitalizeFirstChar', + }) + ); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } +}; + +function calculateFeeFromRate(feePerKb, sizeInBytes) { + return (feePerKb / 1000) * sizeInBytes; +} + +const getBuyingFees = async (foreignBlockchain) => { + const ticker = sellerForeignFee[foreignBlockchain].ticker; + if (!ticker) throw new Error('invalid foreign blockchain'); + const unlockFee = await getForeignFee({ + coin: ticker, + type: 'feerequired', + }); + const lockFee = await getForeignFee({ + coin: ticker, + type: 'feekb', + }); + return { + ticker: ticker, + lock: { + sats: lockFee, + fee: lockFee / QORT_DECIMALS, + }, + unlock: { + sats: unlockFee, + fee: unlockFee / QORT_DECIMALS, + feePerKb: +calculateRateFromFee(+unlockFee, 300) / QORT_DECIMALS, + }, + }; +}; + +export const createBuyOrder = async (data, isFromExtension) => { + const requiredFields = ['crosschainAtInfo', 'foreignBlockchain']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const isGateway = await isRunningGateway(); + const foreignBlockchain = data.foreignBlockchain; + const atAddresses = data.crosschainAtInfo?.map( + (order) => order.qortalAtAddress + ); + + const atPromises = atAddresses.map((atAddress) => + requestQueueGetAtAddresses.enqueue(async () => { + const url = await createEndpoint(`/crosschain/trade/${atAddress}`); + const resAddress = await fetch(url); + const resData = await resAddress.json(); + if (foreignBlockchain !== resData?.foreignBlockchain) { + throw new Error( + i18n.t('core:message.error.same_foreign_blockchain', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + return resData; + }) + ); + + const crosschainAtInfo = await Promise.all(atPromises); + + try { + const buyingFees = await getBuyingFees(foreignBlockchain); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.buy_order', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:permission.buy_order_quantity', { + quantity: atAddresses?.length, + postProcess: 'capitalizeFirstChar', + }), + text3: i18n.t('question:permission.buy_order_ticker', { + qort_amount: crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.qortAmount; + }, 0), + foreign_amount: roundUpToDecimals( + crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.expectedForeignAmount; + }, 0) + ), + ticker: buyingFees.ticker, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('auth:node.using_public_gateway', { + gateway: isGateway, + postProcess: 'capitalizeFirstChar', + }), + fee: '', + html: ` +
+ + +
+
${i18n.t('question:total_unlocking_fee', { + postProcess: 'capitalizeFirstChar', + })}
+
${(+buyingFees?.unlock?.fee * atAddresses?.length)?.toFixed(8)} ${buyingFees.ticker}
+
+ ${i18n.t('question:permission.buy_order_fee_estimation', { + quantity: atAddresses?.length, + fee: buyingFees?.unlock?.feePerKb?.toFixed(8), + ticker: buyingFees.ticker, + postProcess: 'capitalizeFirstChar', + })} +
+
${i18n.t('question:total_locking_fee', { + postProcess: 'capitalizeFirstChar', + })}
+
${i18n.t('question:permission.buy_order_per_kb', { + fee: +buyingFees?.lock.fee.toFixed(8), + ticker: buyingFees.ticker, + postProcess: 'capitalizeFirstChar', + })} +
+
+
+`, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const resBuyOrder = await createBuyOrderTx({ + crosschainAtInfo, + isGateway, + foreignBlockchain, + }); + return resBuyOrder; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.buy_order', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +const cancelTradeOfferTradeBot = async (body, keyPair) => { + const txn = new DeleteTradeOffer().createTransaction(body); + const url = await createEndpoint(`/crosschain/tradeoffer`); + const bodyToString = JSON.stringify(txn); + + const deleteTradeBotResponse = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: bodyToString, + }); + + if (!deleteTradeBotResponse.ok) { + throw new Error( + i18n.t('question:message.error.update_tradebot', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + const unsignedTxn = await deleteTradeBotResponse.text(); + const signedTxnBytes = await signTradeBotTransaction(unsignedTxn, keyPair); + const signedBytes = Base58.encode(signedTxnBytes); + + let res; + try { + res = await processTransactionVersion2(signedBytes); + } catch (error) { + return { + error: i18n.t('question:message.error.cancel_sell_order', { + postProcess: 'capitalizeFirstChar', + }), + failedTradeBot: { + atAddress: body.atAddress, + creatorAddress: body.creatorAddress, + }, + }; + } + if (res?.error) { + return { + error: i18n.t('question:message.error.cancel_sell_order', { + postProcess: 'capitalizeFirstChar', + }), + failedTradeBot: { + atAddress: body.atAddress, + creatorAddress: body.creatorAddress, + }, + }; + } + if (res?.signature) { + return res; + } else { + throw new Error( + i18n.t('question:message.error.cancel_sell_order', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; +const findFailedTradebot = async (createBotCreationTimestamp, body) => { + //wait 5 secs + const wallet = await getSaveWallet(); + const address = wallet.address0; + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 5000); + }); + const url = await createEndpoint( + `/crosschain/tradebot?foreignBlockchain=LITECOIN` + ); + + const tradeBotsReponse = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await tradeBotsReponse.json(); + const latestItem2 = data + .filter((item) => item.creatorAddress === address) + .sort((a, b) => b.timestamp - a.timestamp)[0]; + const latestItem = data + .filter( + (item) => + item.creatorAddress === address && + +item.foreignAmount === +body.foreignAmount + ) + .sort((a, b) => b.timestamp - a.timestamp)[0]; + if ( + latestItem && + createBotCreationTimestamp - latestItem.timestamp <= 5000 && + createBotCreationTimestamp > latestItem.timestamp // Ensure latestItem's timestamp is before createBotCreationTimestamp + ) { + return latestItem; + } else { + return null; + } +}; +const tradeBotCreateRequest = async (body, keyPair) => { + const txn = new TradeBotCreateRequest().createTransaction(body); + const url = await createEndpoint(`/crosschain/tradebot/create`); + const bodyToString = JSON.stringify(txn); + + const unsignedTxnResponse = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: bodyToString, + }); + if (!unsignedTxnResponse.ok) + throw new Error( + i18n.t('question:message.error.create_tradebot', { + postProcess: 'capitalizeFirstChar', + }) + ); + const createBotCreationTimestamp = Date.now(); + const unsignedTxn = await unsignedTxnResponse.text(); + const signedTxnBytes = await signTradeBotTransaction(unsignedTxn, keyPair); + const signedBytes = Base58.encode(signedTxnBytes); + + let res; + try { + res = await processTransactionVersion2(signedBytes); + } catch (error) { + const findFailedTradeBot = await findFailedTradebot( + createBotCreationTimestamp, + body + ); + return { + error: i18n.t('question:message.error.create_sell_order', { + postProcess: 'capitalizeFirstChar', + }), + failedTradeBot: findFailedTradeBot, + }; + } + + if (res?.signature) { + return res; + } else { + throw new Error( + i18n.t('question:message.error.create_sell_order', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const createSellOrder = async (data, isFromExtension) => { + const requiredFields = ['qortAmount', 'foreignBlockchain', 'foreignAmount']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const parsedForeignAmount = Number(data.foreignAmount)?.toFixed(8); + + const receivingAddress = await getUserWalletFunc(data.foreignBlockchain); + try { + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.sell_order', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:permission.order_detail', { + qort_amount: data.qortAmount, + foreign_amount: parsedForeignAmount, + ticker: data.foreignBlockchain, + postProcess: 'capitalizeFirstChar', + }), + fee: '0.02', + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const userPublicKey = parsedData.publicKey; + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const response = await tradeBotCreateRequest( + { + creatorPublicKey: userPublicKey, + qortAmount: parseFloat(data.qortAmount), + fundingQortAmount: parseFloat(data.qortAmount) + 0.01, + foreignBlockchain: data.foreignBlockchain, + foreignAmount: parseFloat(parsedForeignAmount), + tradeTimeout: 120, + receivingAddress: receivingAddress.address, + }, + keyPair + ); + + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.submit_sell_order', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const cancelSellOrder = async (data, isFromExtension) => { + const requiredFields = ['atAddress']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const url = await createEndpoint(`/crosschain/trade/${data.atAddress}`); + const resAddress = await fetch(url); + const resData = await resAddress.json(); + + if (!resData?.qortalAtAddress) + throw new Error( + i18n.t('question:message.error.at_info', { + postProcess: 'capitalizeFirstChar', + }) + ); + + try { + const fee = await getFee('MESSAGE'); + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.cancel_sell_order', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:permission.order_detail', { + qort_amount: resData.qortAmount, + foreign_amount: resData.expectedForeignAmount, + ticker: resData.foreignBlockchain, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const userPublicKey = parsedData.publicKey; + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const response = await cancelTradeOfferTradeBot( + { + creatorPublicKey: userPublicKey, + atAddress: data.atAddress, + }, + keyPair + ); + + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + } catch (error) { + throw new Error( + error?.message || + i18n.t('question:message.error.submit_sell_order', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const openNewTab = async (data, isFromExtension) => { + const requiredFields = ['qortalLink']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const res = extractComponents(data.qortalLink); + if (res) { + const { service, name, identifier, path } = res; + if (!service && !name) + throw new Error( + i18n.t('auth:message.error.invalid_qortal_link', { + postProcess: 'capitalizeFirstChar', + }) + ); + executeEvent('addTab', { data: { service, name, identifier, path } }); + executeEvent('open-apps-mode', {}); + return true; + } else { + throw new Error( + i18n.t('auth:message.error.invalid_qortal_link', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const adminAction = async (data, isFromExtension) => { + const requiredFields = ['type']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + // For actions that require a value, check for 'value' field + const actionsRequiringValue = [ + 'addpeer', + 'removepeer', + 'forcesync', + 'addmintingaccount', + 'removemintingaccount', + ]; + if (actionsRequiringValue.includes(data.type.toLowerCase()) && !data.value) { + missingFields.push('value'); + } + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const isGateway = await isRunningGateway(); + if (isGateway) { + throw new Error( + i18n.t('question:message.generic.no_action_public_node', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + let apiEndpoint = ''; + let method = 'GET'; // Default method + let includeValueInBody = false; + switch (data.type.toLowerCase()) { + case 'stop': + apiEndpoint = await createEndpoint('/admin/stop'); + break; + case 'restart': + apiEndpoint = await createEndpoint('/admin/restart'); + break; + case 'bootstrap': + apiEndpoint = await createEndpoint('/admin/bootstrap'); + break; + case 'addmintingaccount': + apiEndpoint = await createEndpoint('/admin/mintingaccounts'); + method = 'POST'; + includeValueInBody = true; + break; + case 'removemintingaccount': + apiEndpoint = await createEndpoint('/admin/mintingaccounts'); + method = 'DELETE'; + includeValueInBody = true; + break; + case 'forcesync': + apiEndpoint = await createEndpoint('/admin/forcesync'); + method = 'POST'; + includeValueInBody = true; + break; + case 'addpeer': + apiEndpoint = await createEndpoint('/peers'); + method = 'POST'; + includeValueInBody = true; + break; + case 'removepeer': + apiEndpoint = await createEndpoint('/peers'); + method = 'DELETE'; + includeValueInBody = true; + break; + default: + throw new Error( + i18n.t('question:message.error.unknown_admin_action_type', { + type: data.type, + postProcess: 'capitalizeFirstChar', + }) + ); + } + // Prepare the permission prompt text + let permissionText = i18n.t('question:permission.perform_admin_action', { + type: data.type, + postProcess: 'capitalizeFirstChar', + }); + + if (data.value) { + permissionText += + ' ' + + i18n.t('question:permission.perform_admin_action_with_value', { + value: data.value, + postProcess: 'capitalizeFirstChar', + }); + } + + const resPermission = await getUserPermission( + { + text1: permissionText, + }, + isFromExtension + ); + + const { accepted } = resPermission; + + if (accepted) { + // Set up options for the API call + const options: RequestInit = { + method: method, + headers: {}, + }; + if (includeValueInBody) { + options.headers['Content-Type'] = 'text/plain'; + options.body = data.value; + } + const response = await fetch(apiEndpoint, options); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.perform_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const signTransaction = async (data, isFromExtension) => { + const requiredFields = ['unsignedBytes']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const shouldProcess = data?.process || false; + const _url = await createEndpoint( + '/transactions/decode?ignoreValidityChecks=false' + ); + + const _body = data.unsignedBytes; + const response = await fetch(_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: _body, + }); + + if (!response.ok) + throw new Error( + i18n.t('question:message.error.decode_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + const decodedData = await response.json(); + const resPermission = await getUserPermission( + { + text1: shouldProcess + ? i18n.t('question:permission.sign_process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + : i18n.t('question:permission.sign_transaction', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t( + 'question:message.generic.read_transaction_carefully', + { postProcess: 'capitalizeFirstChar' } + ), + text2: `Tx type: ${decodedData.type}`, + json: decodedData, + }, + isFromExtension + ); + + const { accepted } = resPermission; + if (accepted) { + let urlConverted = await createEndpoint('/transactions/convert'); + + const responseConverted = await fetch(urlConverted, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: data.unsignedBytes, + }); + 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, + }; + const convertedBytes = await responseConverted.text(); + const txBytes = Base58.decode(data.unsignedBytes); + const _arbitraryBytesBuffer = Object.keys(txBytes).map(function (key) { + return txBytes[key]; + }); + const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer); + const txByteSigned = Base58.decode(convertedBytes); + const _bytesForSigningBuffer = Object.keys(txByteSigned).map( + function (key) { + return txByteSigned[key]; + } + ); + const bytesForSigningBuffer = new Uint8Array(_bytesForSigningBuffer); + const signature = nacl.sign.detached( + bytesForSigningBuffer, + keyPair.privateKey + ); + const signedBytes = utils.appendBuffer(arbitraryBytesBuffer, signature); + const signedBytesToBase58 = Base58.encode(signedBytes); + if (!shouldProcess) { + return signedBytesToBase58; + } + const res = await processTransactionVersion2(signedBytesToBase58); + if (!res?.signature) + throw new Error( + res?.message || + i18n.t('question:message.error.process_transaction', { + postProcess: 'capitalizeFirstChar', + }) + ); + return res; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +const missingFieldsFunc = (data, requiredFields) => { + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } +}; + +const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values +const buildQueryParams = (data) => { + const allowedParams = [ + 'name', + 'service', + 'identifier', + 'mimeType', + 'fileName', + 'encryptionType', + 'key', + ]; + return Object.entries(data) + .map(([key, value]) => { + if ( + value === undefined || + value === null || + value === false || + !allowedParams.includes(key) + ) + return null; // Skip null, undefined, or false + if (typeof value === 'boolean') return `${key}=${value}`; // Handle boolean values + return `${key}=${encode(value)}`; // Encode other values + }) + .filter(Boolean) // Remove null values + .join('&'); // Join with `&` +}; +export const createAndCopyEmbedLink = async (data, isFromExtension) => { + const requiredFields = ['type']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + switch (data.type) { + case 'POLL': { + missingFieldsFunc(data, ['type', 'name']); + + const queryParams = [ + `name=${encode(data.name)}`, + data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists + ] + .filter(Boolean) // Remove null values + .join('&'); // Join with `&` + const link = `qortal://use-embed/POLL?${queryParams}`; + try { + await navigator.clipboard.writeText(link); + } catch (error) { + throw new Error( + i18n.t('question:message.error.copy_clipboard', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + return link; + } + case 'IMAGE': + case 'ATTACHMENT': { + missingFieldsFunc(data, ['type', 'name', 'service', 'identifier']); + if (data?.encryptionType === 'private' && !data?.key) { + throw new Error( + i18n.t('question:message.generic.provide_key_shared_link', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const queryParams = buildQueryParams(data); + + const link = `qortal://use-embed/${data.type}?${queryParams}`; + + try { + await navigator.clipboard.writeText(link); + } catch (error) { + throw new Error( + i18n.t('question:message.error.copy_clipboard', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + return link; + } + + default: + throw new Error( + i18n.t('question:message.error.invalid_type', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const registerNameRequest = async (data, isFromExtension) => { + const requiredFields = ['name']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const fee = await getFee('REGISTER_NAME'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.register_name', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: data.name, + text2: data?.description, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const name = data.name; + const description = data?.description || ''; + const response = await registerName({ name, description }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const updateNameRequest = async (data, isFromExtension) => { + const requiredFields = ['newName', 'oldName']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const oldName = data.oldName; + const newName = data.newName; + const description = data?.description || ''; + const fee = await getFee('UPDATE_NAME'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.register_name', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: data.newName, + text2: data?.description, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await updateName({ oldName, newName, description }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const leaveGroupRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const fee = await getFee('LEAVE_GROUP'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.leave_group', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: `${groupInfo.groupName}`, + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await leaveGroup({ groupId }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const inviteToGroupRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'inviteTime', 'inviteeAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.inviteeAddress; + const inviteTime = data?.inviteTime; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('GROUP_INVITE'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.invite', { + invitee: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await inviteToGroup({ + groupId, + qortalAddress, + inviteTime, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const kickFromGroupRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'qortalAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.qortalAddress; + const reason = data?.reason; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('GROUP_KICK'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.kick', { + partecipant: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await kickFromGroup({ + groupId, + qortalAddress, + rBanReason: reason, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const banFromGroupRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'qortalAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.qortalAddress; + const rBanTime = data?.banTime; + const reason = data?.reason; + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('GROUP_BAN'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.ban', { + partecipant: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await banFromGroup({ + groupId, + qortalAddress, + rBanTime, + rBanReason: reason, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const cancelGroupBanRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'qortalAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.qortalAddress; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('CANCEL_GROUP_BAN'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.cancel_ban', { + partecipant: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await cancelBan({ + groupId, + qortalAddress, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const addGroupAdminRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'qortalAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.qortalAddress; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('ADD_GROUP_ADMIN'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.add_admin', { + invitee: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await makeAdmin({ + groupId, + qortalAddress, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const removeGroupAdminRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'qortalAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.qortalAddress; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('REMOVE_GROUP_ADMIN'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.remove_admin', { + partecipant: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await removeAdmin({ + groupId, + qortalAddress, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const cancelGroupInviteRequest = async (data, isFromExtension) => { + const requiredFields = ['groupId', 'qortalAddress']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = data.groupId; + const qortalAddress = data?.qortalAddress; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(qortalAddress); + + const fee = await getFee('CANCEL_GROUP_INVITE'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.cancel_group_invite', { + invitee: displayInvitee || qortalAddress, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + + const { accepted } = resPermission; + + if (accepted) { + const response = await cancelInvitationToGroup({ + groupId, + qortalAddress, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const createGroupRequest = async (data, isFromExtension) => { + const requiredFields = [ + 'approvalThreshold', + 'groupId', + 'groupName', + 'maxBlock', + 'minBlock', + 'qortalAddress', + 'type', + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (data[field] !== undefined && data[field] !== null) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupName = data.groupName; + const description = data?.description || ''; + const type = +data.type; + const approvalThreshold = +data?.approvalThreshold; + const minBlock = +data?.minBlock; + const maxBlock = +data.maxBlock; + + const fee = await getFee('CREATE_GROUP'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.create_group', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await createGroup({ + groupName, + groupDescription: description, + groupType: type, + groupApprovalThreshold: approvalThreshold, + minBlock, + maxBlock, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +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) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const groupId = +data.groupId; + const newOwner = data.newOwner; + const description = data?.description || ''; + const type = +data.type; + const approvalThreshold = +data?.approvalThreshold; + const minBlock = +data?.minBlock; + const maxBlock = +data.maxBlock; + + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${groupId}`); + const response = await fetch(url); + if (!response.ok) + throw new Error( + i18n.t('question:message.error.fetch_group', { + postProcess: 'capitalizeFirstChar', + }) + ); + + groupInfo = await response.json(); + } catch (error) { + const errorMsg = + (error && error.message) || + i18n.t('question:message.error.no_group_found', { + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const displayInvitee = await getNameInfoForOthers(newOwner); + + const fee = await getFee('CREATE_GROUP'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.update_group', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:permission.update_group_detail', { + owner: displayInvitee || newOwner, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('group:group.group_name', { + name: groupInfo?.groupName, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await updateGroup({ + groupId, + newOwner, + newIsOpen: type, + newDescription: description, + newApprovalThreshold: approvalThreshold, + newMinimumBlockDelay: minBlock, + newMaximumBlockDelay: maxBlock, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const decryptAESGCMRequest = async (data, isFromExtension) => { + const requiredFields = ['encryptedData', 'iv', 'senderPublicKey']; + requiredFields.forEach((field) => { + if (!data[field]) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: field, + postProcess: 'capitalizeFirstChar', + }) + ); + } + }); + + const encryptedData = data.encryptedData; + const iv = data.iv; + const senderPublicKeyBase58 = data.senderPublicKey; + + // Decode keys and IV + const senderPublicKey = Base58.decode(senderPublicKeyBase58); + const resKeyPair = await getKeyPair(); // Assume this retrieves the current user's keypair + const uint8PrivateKey = Base58.decode(resKeyPair.privateKey); + + // Convert ed25519 keys to Curve25519 + const convertedPrivateKey = ed2curve.convertSecretKey(uint8PrivateKey); + const convertedPublicKey = ed2curve.convertPublicKey(senderPublicKey); + + // Generate shared secret + const sharedSecret = new Uint8Array(32); + nacl.lowlevel.crypto_scalarmult( + sharedSecret, + convertedPrivateKey, + convertedPublicKey + ); + + // Derive encryption key + const encryptionKey: Uint8Array = new Sha256() + .process(sharedSecret) + .finish().result; + + // Convert IV and ciphertext from Base64 + const base64ToUint8Array = (base64) => + Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + const ivUint8Array = base64ToUint8Array(iv); + const ciphertext = base64ToUint8Array(encryptedData); + // Validate IV and key lengths + if (ivUint8Array.length !== 12) { + throw new Error( + i18n.t('question:message.error.invalid_encryption_iv', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + if (encryptionKey.length !== 32) { + throw new Error( + i18n.t('question:message.error.invalid_encryption_key', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + try { + // Decrypt data + const algorithm = { name: 'AES-GCM', iv: ivUint8Array }; + const cryptoKey = await crypto.subtle.importKey( + 'raw', + encryptionKey, + algorithm, + false, + ['decrypt'] + ); + const decryptedArrayBuffer = await crypto.subtle.decrypt( + algorithm, + cryptoKey, + ciphertext + ); + + // Return decrypted data as Base64 + return uint8ArrayToBase64(new Uint8Array(decryptedArrayBuffer)); + } catch (error) { + console.error('Decryption failed:', error); + throw new Error( + i18n.t('question:message.error.decrypt_message', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const sellNameRequest = async (data, isFromExtension) => { + const requiredFields = ['salePrice', 'nameForSale']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (data[field] !== undefined && data[field] !== null) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const name = data.nameForSale; + const sellPrice = +data.salePrice; + + const validApi = await getBaseApi(); + + const response = await fetch(validApi + '/names/' + name); + const nameData = await response.json(); + if (!nameData) + throw new Error( + i18n.t('auth:message.error.name_not_existing', { + postProcess: 'capitalizeFirstChar', + }) + ); + + if (nameData?.isForSale) + throw new Error( + i18n.t('question:message.error.name_already_for_sale', { + postProcess: 'capitalizeFirstChar', + }) + ); + const fee = await getFee('SELL_NAME'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.sell_name_transaction', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t( + 'question:permission.sell_name_transaction_detail', + { + name: name, + price: sellPrice, + postProcess: 'capitalizeFirstChar', + } + ), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await sellName({ + name, + sellPrice, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const cancelSellNameRequest = async (data, isFromExtension) => { + const requiredFields = ['nameForSale']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (data[field] !== undefined && data[field] !== null) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + const name = data.nameForSale; + const validApi = await getBaseApi(); + + const response = await fetch(validApi + '/names/' + name); + const nameData = await response.json(); + if (!nameData?.isForSale) + throw new Error( + i18n.t('question:message.error.name_not_for_sale', { + postProcess: 'capitalizeFirstChar', + }) + ); + + const fee = await getFee('CANCEL_SELL_NAME'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.sell_name_cancel', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:name', { + name: name, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await cancelSellName({ + name, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const buyNameRequest = async (data, isFromExtension) => { + const requiredFields = ['nameForSale']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (data[field] !== undefined && data[field] !== null) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = i18n.t('question:message.error.missing_fields', { + fields: missingFieldsString, + postProcess: 'capitalizeFirstChar', + }); + throw new Error(errorMsg); + } + + const name = data.nameForSale; + const validApi = await getBaseApi(); + const response = await fetch(validApi + '/names/' + name); + const nameData = await response.json(); + + if (!nameData?.isForSale) + throw new Error( + i18n.t('question:message.error.name_not_for_sale', { + postProcess: 'capitalizeFirstChar', + }) + ); + + const sellerAddress = nameData.owner; + const sellPrice = +nameData.salePrice; + + const fee = await getFee('BUY_NAME'); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.buy_name', { + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:permission.buy_name_detail', { + name: name, + price: sellPrice, + postProcess: 'capitalizeFirstChar', + }), + fee: fee.fee, + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const response = await buyName({ + name, + sellerAddress, + sellPrice, + }); + return response; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; + +export const signForeignFees = async (data, isFromExtension) => { + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.sign_fee', { + postProcess: 'capitalizeFirstChar', + }), + }, + isFromExtension + ); + const { accepted } = resPermission; + if (accepted) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + 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, + }; + + const unsignedFeesUrl = await createEndpoint( + `/crosschain/unsignedfees/${address}` + ); + + const unsignedFeesResponse = await fetch(unsignedFeesUrl); + + const unsignedFees = await unsignedFeesResponse.json(); + + const signedFees = []; + + unsignedFees.forEach((unsignedFee) => { + const unsignedDataDecoded = Base58.decode(unsignedFee.data); + + const signature = nacl.sign.detached( + unsignedDataDecoded, + keyPair.privateKey + ); + + const signedFee = { + timestamp: unsignedFee.timestamp, + data: `${Base58.encode(signature)}`, + atAddress: unsignedFee.atAddress, + fee: unsignedFee.fee, + }; + + signedFees.push(signedFee); + }); + + const signedFeesUrl = await createEndpoint(`/crosschain/signedfees`); + + await fetch(signedFeesUrl, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: `${JSON.stringify(signedFees)}`, + }); + + return true; + } else { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } +}; +export const multiPaymentWithPrivateData = async (data, isFromExtension) => { + const requiredFields = ['payments', 'assetId']; + requiredFields.forEach((field) => { + if (data[field] === undefined || data[field] === null) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: field, + postProcess: 'capitalizeFirstChar', + }) + ); + } + }); + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + const privateKey = parsedData.privateKey; + const userPublicKey = parsedData.publicKey; + const { fee: paymentFee } = await getFee('TRANSFER_ASSET'); + const { fee: arbitraryFee } = await getFee('ARBITRARY'); + + let name = null; + const payments = data.payments; + const assetId = data.assetId; + const pendingTransactions = []; + const pendingAdditionalArbitraryTxs = []; + const additionalArbitraryTxsWithoutPayment = + data?.additionalArbitraryTxsWithoutPayment || []; + let totalAmount = 0; + let fee = 0; + for (const payment of payments) { + const paymentRefId = uid.rnd(); + const requiredFieldsPayment = ['recipient', 'amount']; + + for (const field of requiredFieldsPayment) { + if (!payment[field]) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: field, + postProcess: 'capitalizeFirstChar', + }) + ); + } + } + + const confirmReceiver = await getNameOrAddress(payment.recipient); + if (confirmReceiver.error) { + throw new Error( + i18n.t('question:message.error.invalid_receiver', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const receiverPublicKey = await getPublicKey(confirmReceiver); + + const amount = +payment.amount.toFixed(8); + + pendingTransactions.push({ + type: 'PAYMENT', + recipientAddress: confirmReceiver, + amount: amount, + paymentRefId, + }); + + fee = fee + +paymentFee; + totalAmount = totalAmount + amount; + + if (payment.arbitraryTxs && payment.arbitraryTxs.length > 0) { + for (const arbitraryTx of payment.arbitraryTxs) { + const requiredFieldsArbitraryTx = ['service', 'identifier', 'base64']; + + for (const field of requiredFieldsArbitraryTx) { + if (!arbitraryTx[field]) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: field, + postProcess: 'capitalizeFirstChar', + }) + ); + } + } + + if (!name) { + const getName = await getNameInfo(); + if (!getName) + throw new Error( + i18n.t('question:message.error.registered_name', { + postProcess: 'capitalizeFirstChar', + }) + ); + name = getName; + } + + const isValid = isValidBase64WithDecode(arbitraryTx.base64); + if (!isValid) + throw new Error( + i18n.t('core:message.error.invalid_base64', { + postProcess: 'capitalizeFirstChar', + }) + ); + if (!arbitraryTx?.service?.includes('_PRIVATE')) + throw new Error( + i18n.t('question:message.generic.private_service', { + postProcess: 'capitalizeFirstChar', + }) + ); + const additionalPublicKeys = arbitraryTx?.additionalPublicKeys || []; + pendingTransactions.push({ + type: 'ARBITRARY', + identifier: arbitraryTx.identifier, + service: arbitraryTx.service, + base64: arbitraryTx.base64, + description: arbitraryTx?.description || '', + paymentRefId, + publicKeys: [receiverPublicKey, ...additionalPublicKeys], + }); + + fee = fee + +arbitraryFee; + } + } + } + + if ( + additionalArbitraryTxsWithoutPayment && + additionalArbitraryTxsWithoutPayment.length > 0 + ) { + for (const arbitraryTx of additionalArbitraryTxsWithoutPayment) { + const requiredFieldsArbitraryTx = ['service', 'identifier', 'base64']; + + for (const field of requiredFieldsArbitraryTx) { + if (!arbitraryTx[field]) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: field, + postProcess: 'capitalizeFirstChar', + }) + ); + } + } + + if (!name) { + const getName = await getNameInfo(); + if (!getName) + throw new Error( + i18n.t('question:message.error.registered_name', { + postProcess: 'capitalizeFirstChar', + }) + ); + name = getName; + } + + const isValid = isValidBase64WithDecode(arbitraryTx.base64); + if (!isValid) + throw new Error( + i18n.t('core:message.error.invalid_base64', { + postProcess: 'capitalizeFirstChar', + }) + ); + if (!arbitraryTx?.service?.includes('_PRIVATE')) + throw new Error( + i18n.t('question:message.generic.private_service', { + postProcess: 'capitalizeFirstChar', + }) + ); + const additionalPublicKeys = arbitraryTx?.additionalPublicKeys || []; + pendingAdditionalArbitraryTxs.push({ + type: 'ARBITRARY', + identifier: arbitraryTx.identifier, + service: arbitraryTx.service, + base64: arbitraryTx.base64, + description: arbitraryTx?.description || '', + publicKeys: additionalPublicKeys, + }); + + fee = fee + +arbitraryFee; + } + } + + if (!name) + throw new Error( + i18n.t('question:message.error.registered_name', { + postProcess: 'capitalizeFirstChar', + }) + ); + const balance = await getBalanceInfo(); + + if (+balance < fee) + throw new Error( + i18n.t('question:message.error.insufficient_balance_qort', { + postProcess: 'capitalizeFirstChar', + }) + ); + const assetBalance = await getAssetBalanceInfo(assetId); + const assetInfo = await getAssetInfo(assetId); + if (assetBalance < totalAmount) + throw new Error( + i18n.t('question:message.error.insufficient_balance', { + postProcess: 'capitalizeFirstChar', + }) + ); + + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.pay_publish', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:assets_used_pay', { + asset: assetInfo.name, + postProcess: 'capitalizeFirstChar', + }), + html: ` +
+ + + ${pendingTransactions + .filter((item) => item.type === 'PAYMENT') + .map( + (payment) => ` +
+
Recipient: ${ + payment.recipientAddress + }
+
Amount: ${payment.amount}
+
` + ) + .join('')} + ${[...pendingTransactions, ...pendingAdditionalArbitraryTxs] + .filter((item) => item.type === 'ARBITRARY') + .map( + (arbitraryTx) => ` +
+
Service: ${ + arbitraryTx.service + }
+
Name: ${name}
+
Identifier: ${ + arbitraryTx.identifier + }
+
` + ) + .join('')} +
+ + `, + highlightedText: `Total Amount: ${totalAmount}`, + fee: fee, + }, + isFromExtension + ); + + const { accepted, checkbox1 = false } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + + // const failedTxs = [] + const paymentsDone = {}; + + const transactionsDone = []; + + for (const transaction of pendingTransactions) { + const type = transaction.type; + + if (type === 'PAYMENT') { + const makePayment = await retryTransaction( + transferAsset, + [ + { + amount: transaction.amount, + assetId, + recipient: transaction.recipientAddress, + }, + ], + true + ); + if (makePayment) { + transactionsDone.push(makePayment?.signature); + if (transaction.paymentRefId) { + paymentsDone[transaction.paymentRefId] = makePayment; + } + } + } else if (type === 'ARBITRARY' && paymentsDone[transaction.paymentRefId]) { + const objectToEncrypt = { + data: transaction.base64, + payment: paymentsDone[transaction.paymentRefId], + }; + + const toBase64 = await retryTransaction( + objectToBase64, + [objectToEncrypt], + true + ); + + if (!toBase64) continue; // Skip if encryption fails + + const encryptDataResponse = await retryTransaction( + encryptDataGroup, + [ + { + data64: toBase64, + publicKeys: transaction.publicKeys, + privateKey, + userPublicKey, + }, + ], + true + ); + + if (!encryptDataResponse) continue; // Skip if encryption fails + + const resPublish = await retryTransaction( + publishData, + [ + { + registeredName: encodeURIComponent(name), + file: encryptDataResponse, + service: transaction.service, + identifier: encodeURIComponent(transaction.identifier), + uploadType: 'file', + description: transaction?.description, + isBase64: true, + apiVersion: 2, + withFee: true, + }, + ], + true + ); + + if (resPublish?.signature) { + transactionsDone.push(resPublish?.signature); + } + } + } + + for (const transaction of pendingAdditionalArbitraryTxs) { + const objectToEncrypt = { + data: transaction.base64, + }; + + const toBase64 = await retryTransaction( + objectToBase64, + [objectToEncrypt], + true + ); + + if (!toBase64) continue; // Skip if encryption fails + + const encryptDataResponse = await retryTransaction( + encryptDataGroup, + [ + { + data64: toBase64, + publicKeys: transaction.publicKeys, + privateKey, + userPublicKey, + }, + ], + true + ); + + if (!encryptDataResponse) continue; // Skip if encryption fails + + const resPublish = await retryTransaction( + publishData, + [ + { + registeredName: encodeURIComponent(name), + file: encryptDataResponse, + service: transaction.service, + identifier: encodeURIComponent(transaction.identifier), + uploadType: 'file', + description: transaction?.description, + isBase64: true, + apiVersion: 2, + withFee: true, + }, + ], + true + ); + + if (resPublish?.signature) { + transactionsDone.push(resPublish?.signature); + } + } + + return transactionsDone; +}; + +export const transferAssetRequest = async (data, isFromExtension) => { + const requiredFields = ['amount', 'assetId', 'recipient']; + requiredFields.forEach((field) => { + if (data[field] === undefined || data[field] === null) { + throw new Error( + i18n.t('question:message.error.missing_fields', { + fields: field, + postProcess: 'capitalizeFirstChar', + }) + ); + } + }); + const amount = data.amount; + const assetId = data.assetId; + const recipient = data.recipient; + + const { fee } = await getFee('TRANSFER_ASSET'); + const balance = await getBalanceInfo(); + + if (+balance < +fee) + throw new Error( + i18n.t('question:message.error.insufficient_balance_qort', { + postProcess: 'capitalizeFirstChar', + }) + ); + const assetBalance = await getAssetBalanceInfo(assetId); + if (assetBalance < amount) + throw new Error( + i18n.t('question:message.error.insufficient_balance', { + postProcess: 'capitalizeFirstChar', + }) + ); + const confirmReceiver = await getNameOrAddress(recipient); + if (confirmReceiver.error) { + throw new Error( + i18n.t('question:message.error.invalid_receiver', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const assetInfo = await getAssetInfo(assetId); + const resPermission = await getUserPermission( + { + text1: i18n.t('question:permission.transfer_asset', { + postProcess: 'capitalizeFirstChar', + }), + text2: i18n.t('question:asset_name', { + asset: assetInfo?.name, + postProcess: 'capitalizeFirstChar', + }), + highlightedText: i18n.t('question:amount_qty', { + quantity: amount, + postProcess: 'capitalizeFirstChar', + }), + fee: fee, + }, + isFromExtension + ); + + const { accepted } = resPermission; + if (!accepted) { + throw new Error( + i18n.t('question:message.generic.user_declined_request', { + postProcess: 'capitalizeFirstChar', + }) + ); + } + const res = await transferAsset({ + amount, + recipient: confirmReceiver, + assetId, + }); + return res; +}; diff --git a/src/qortalRequests/qortalRequests.ts b/src/qortal/qortal-requests.ts similarity index 100% rename from src/qortalRequests/qortalRequests.ts rename to src/qortal/qortal-requests.ts diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 7957dc4..6819842 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -37,27 +37,18 @@ import { getAssetInfo, getPublicKey, transferAsset, -<<<<<<< HEAD -} from '../background'; -import { - getAllUserNames, - getNameInfo, - uint8ArrayToObject, -} from '../backgroundFunctions/encryption'; -======= } from '../background/background.ts'; import { getNameInfo, uint8ArrayToObject } from '../encryption/encryption.ts'; ->>>>>>> 2d01b3e (Create encryption folder and move files) -import { showSaveFilePicker } from '../hooks/useQortalMessageListener'; -import { getPublishesFromAdminsAdminSpace } from '../components/Chat/AdminSpaceInner'; -import { extractComponents } from '../components/Chat/MessageDisplay'; +import { showSaveFilePicker } from '../hooks/useQortalMessageListener.tsx'; +import { getPublishesFromAdminsAdminSpace } from '../components/Chat/AdminSpaceInner.tsx'; +import { extractComponents } from '../components/Chat/MessageDisplay.tsx'; import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey, -} from '../components/Group/Group'; -import { QORT_DECIMALS } from '../constants/constants'; +} from '../components/Group/Group.tsx'; +import { QORT_DECIMALS } from '../constants/constants.ts'; import Base58 from '../encryption/Base58.ts'; import ed2curve from '../encryption/ed2curve.ts'; import nacl from '../encryption/nacl-fast.ts'; @@ -73,24 +64,24 @@ import { objectToBase64, uint8ArrayStartsWith, uint8ArrayToBase64, -} from '../qdn/encryption/group-encryption'; +} from '../qdn/encryption/group-encryption.ts'; import { publishData } from '../qdn/publish/publish.ts'; import { getPermission, isRunningGateway, setPermission, } from './qortalRequests.ts'; -import TradeBotCreateRequest from '../transactions/TradeBotCreateRequest'; -import DeleteTradeOffer from '../transactions/TradeBotDeleteRequest'; -import signTradeBotTransaction from '../transactions/signTradeBotTransaction'; -import { createTransaction } from '../transactions/transactions'; -import { executeEvent } from '../utils/events'; -import { fileToBase64 } from '../utils/fileReading'; -import { mimeToExtensionMap } from '../utils/memeTypes'; -import { RequestQueueWithPromise } from '../utils/queue/queue'; -import utils from '../utils/utils'; +import TradeBotCreateRequest from '../transactions/TradeBotCreateRequest.ts'; +import DeleteTradeOffer from '../transactions/TradeBotDeleteRequest.ts'; +import signTradeBotTransaction from '../transactions/signTradeBotTransaction.ts'; +import { createTransaction } from '../transactions/transactions.ts'; +import { executeEvent } from '../utils/events.ts'; +import { fileToBase64 } from '../utils/fileReading/index.ts'; +import { mimeToExtensionMap } from '../utils/memeTypes.ts'; +import { RequestQueueWithPromise } from '../utils/queue/queue.ts'; +import utils from '../utils/utils.ts'; import ShortUniqueId from 'short-unique-id'; -import { isValidBase64WithDecode } from '../utils/decode'; +import { isValidBase64WithDecode } from '../utils/decode.ts'; import i18n from 'i18next'; const uid = new ShortUniqueId({ length: 6 });