added qortal requests

This commit is contained in:
PhilReact 2025-05-04 01:30:40 +03:00
parent 3c00d40093
commit 8c98fcbcdf
11 changed files with 1140 additions and 132 deletions

View File

@ -155,6 +155,7 @@ import { BuyQortInformation } from "./components/BuyQortInformation";
import { InstallPWA } from "./components/InstallPWA";
import { QortPayment } from "./components/QortPayment";
import { PdfViewer } from "./common/PdfViewer";
import { DownloadWallet } from "./components/Auth/DownloadWallet";
type extStates =
@ -2584,87 +2585,14 @@ function App() {
)}
{extState === "download-wallet" && (
<>
<Spacer height="22px" />
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "flex-start",
paddingLeft: "22px",
boxSizing: "border-box",
}}
>
<img
style={{
cursor: "pointer",
}}
onClick={returnToMain}
src={Return}
/>
</Box>
<Spacer height="10px" />
<div
className="image-container"
style={{
width: "136px",
height: "154px",
}}
>
<img src={Logo1Dark} className="base-image" />
</div>
<Spacer height="35px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}}
>
<TextP
sx={{
textAlign: "start",
lineHeight: "24px",
fontSize: "20px",
fontWeight: 600,
}}
>
Download Account
</TextP>
</Box>
<Spacer height="35px" />
{!walletToBeDownloaded && (
<>
<CustomLabel htmlFor="standard-adornment-password">
Confirm Wallet Password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={walletToBeDownloadedPassword}
onChange={(e) =>
setWalletToBeDownloadedPassword(e.target.value)
}
/>
<Spacer height="20px" />
<CustomButton onClick={confirmPasswordToDownload}>
Confirm password
</CustomButton>
<ErrorText>{walletToBeDownloadedError}</ErrorText>
</>
)}
{walletToBeDownloaded && (
<>
<CustomButton onClick={async ()=> {
await saveFileToDiskFunc()
await showInfo({
message: isNative ? `Your account file was saved to internal storage, in the document folder. Keep that file secure.` : `Your account file was downloaded by your browser. Keep that file secure.` ,
})
}}>
Download account
</CustomButton>
</>
)}
<DownloadWallet
returnToMain={returnToMain}
setIsLoading={setIsLoading}
showInfo={showInfo}
rawWallet={rawWallet}
setWalletToBeDownloaded={setWalletToBeDownloaded}
walletToBeDownloaded={walletToBeDownloaded}
/>
</>
)}
{extState === "create-wallet" && (

View File

@ -928,6 +928,59 @@ export async function getBalanceInfo() {
const data = await response.json();
return data;
}
export async function getAssetBalanceInfo(assetId: number) {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi();
const response = await fetch(validApi + `/assets/balances?address=${address}&assetid=${assetId}&ordering=ASSET_BALANCE_ACCOUNT&limit=1`);
if (!response?.ok) throw new Error("Cannot fetch asset balance");
const data = await response.json();
return +data?.[0]?.balance
}
export async function getAssetInfo(assetId: number) {
const validApi = await getBaseApi();
const response = await fetch(validApi + `/assets/info?assetId=${assetId}`);
if (!response?.ok) throw new Error("Cannot fetch asset info");
const data = await response.json();
return data
}
export async function transferAsset({
amount,
recipient,
assetId,
}) {
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 feeres = await getFee("TRANSFER_ASSET");
const tx = await createTransaction(12, keyPair, {
fee: feeres.fee,
recipient: recipient,
amount: amount,
assetId: assetId,
lastReference: lastReference,
});
const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
throw new Error(res?.message || "Transaction was not able to be processed");
return res;
}
export async function getLTCBalance() {
const wallet = await getSaveWallet();
let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`;

View File

@ -259,7 +259,9 @@ export function openIndexedDB() {
'UPDATE_GROUP',
'SELL_NAME',
'CANCEL_SELL_NAME',
'BUY_NAME'
'BUY_NAME', 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET',
'SIGN_FOREIGN_FEES',
]
@ -275,7 +277,9 @@ const UIQortalRequests = [
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN', 'REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS', 'GET_NODE_INFO',
'GET_NODE_STATUS', 'GET_ARRR_SYNC_STATUS', 'SHOW_PDF_READER', 'UPDATE_GROUP', 'SELL_NAME',
'CANCEL_SELL_NAME',
'BUY_NAME'
'BUY_NAME', 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET',
'SIGN_FOREIGN_FEES',
];

View File

@ -0,0 +1,248 @@
import {
Box,
Checkbox,
FormControlLabel,
Typography,
useTheme,
} from '@mui/material';
import { Spacer } from '../../common/Spacer';
import { PasswordField } from '../PasswordField/PasswordField';
import { ErrorText } from '../ErrorText/ErrorText';
import Logo1Dark from '../../assets/svgs/Logo1Dark.svg';
import { saveFileToDisk } from '../../utils/generateWallet/generateWallet';
import { useState } from 'react';
import { decryptStoredWallet } from '../../utils/decryptWallet';
import PhraseWallet from '../../utils/generateWallet/phrase-wallet';
import { crypto, walletVersion } from '../../constants/decryptWallet';
import Return from "../../assets/svgs/Return.svg";
import { CustomButton, CustomLabel, TextP } from '../../App-styles';
export const DownloadWallet = ({
returnToMain,
setIsLoading,
showInfo,
rawWallet,
setWalletToBeDownloaded,
walletToBeDownloaded,
}) => {
const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] =
useState<string>('');
const [newPassword, setNewPassword] = useState<string>('');
const [keepCurrentPassword, setKeepCurrentPassword] = useState<boolean>(true);
const theme = useTheme();
const [walletToBeDownloadedError, setWalletToBeDownloadedError] =
useState<string>('');
const saveFileToDiskFunc = async () => {
try {
await saveFileToDisk(
walletToBeDownloaded.wallet,
walletToBeDownloaded.qortAddress
);
} catch (error: any) {
setWalletToBeDownloadedError(error?.message);
}
};
const saveWalletFunc = async (password: string, newPassword) => {
let wallet = structuredClone(rawWallet);
const res = await decryptStoredWallet(password, wallet);
const wallet2 = new PhraseWallet(res, wallet?.version || walletVersion);
const passwordToUse = newPassword || password;
wallet = await wallet2.generateSaveWalletData(
passwordToUse,
crypto.kdfThreads,
() => {}
);
setWalletToBeDownloaded({
wallet,
qortAddress: rawWallet.address0,
});
return {
wallet,
qortAddress: rawWallet.address0,
};
};
const confirmPasswordToDownload = async () => {
try {
setWalletToBeDownloadedError('');
if (!keepCurrentPassword && !newPassword) {
setWalletToBeDownloadedError(
'Please enter a new password'
);
return;
}
if (!walletToBeDownloadedPassword) {
setWalletToBeDownloadedError(
'Please enter your password'
);
return;
}
setIsLoading(true);
await new Promise<void>((res) => {
setTimeout(() => {
res();
}, 250);
});
const newPasswordForWallet = !keepCurrentPassword ? newPassword : null;
const res = await saveWalletFunc(
walletToBeDownloadedPassword,
newPasswordForWallet
);
} catch (error: any) {
setWalletToBeDownloadedError(error?.message);
} finally {
setIsLoading(false);
}
};
return (
<>
<Spacer height="22px" />
<Box
sx={{
boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}}
>
<img
style={{
cursor: "pointer",
height: '24px'
}}
onClick={returnToMain}
src={Return}
/>
</Box>
<Spacer height="10px" />
<div
className="image-container"
style={{
width: '136px',
height: '154px',
}}
>
<img src={Logo1Dark} className="base-image" />
</div>
<Spacer height="35px" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<TextP
sx={{
textAlign: 'start',
lineHeight: '24px',
fontSize: '20px',
fontWeight: 600,
}}
>
Download account
</TextP>
</Box>
<Spacer height="35px" />
{!walletToBeDownloaded && (
<>
<CustomLabel htmlFor="standard-adornment-password">
Confirm password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={walletToBeDownloadedPassword}
onChange={(e) => setWalletToBeDownloadedPassword(e.target.value)}
/>
<Spacer height="20px" />
<FormControlLabel
sx={{
margin: 0,
}}
control={
<Checkbox
onChange={(e) => setKeepCurrentPassword(e.target.checked)}
checked={keepCurrentPassword}
edge="start"
tabIndex={-1}
disableRipple
sx={{
'&.Mui-checked': {
color: theme.palette.text.secondary,
},
'& .MuiSvgIcon-root': {
color: theme.palette.text.secondary,
},
}}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography sx={{ fontSize: '14px' }}>
Keep current password
</Typography>
</Box>
}
/>
<Spacer height="20px" />
{!keepCurrentPassword && (
<>
<CustomLabel htmlFor="standard-adornment-password">
New password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<Spacer height="20px" />
</>
)}
<CustomButton onClick={confirmPasswordToDownload}>
Confirm wallet password
</CustomButton>
<ErrorText>{walletToBeDownloadedError}</ErrorText>
</>
)}
{walletToBeDownloaded && (
<>
<CustomButton
onClick={async () => {
await saveFileToDiskFunc();
await showInfo({
message: 'Keep your account file secure',
});
}}
>
Download account
</CustomButton>
</>
)}
</>
);
};

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { DrawerUserLookup } from "../Drawer/DrawerUserLookup";
import {
Avatar,
@ -16,6 +16,7 @@ import {
Typography,
Table,
CircularProgress,
Autocomplete,
} from "@mui/material";
import { getAddressInfo, getNameOrAddress } from "../../background";
import { getBaseApiReact } from "../../App";
@ -26,6 +27,7 @@ import { formatTimestamp } from "../../utils/time";
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import SearchIcon from '@mui/icons-material/Search';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import { useNameSearch } from "../../hooks/useNameSearch";
function formatAddress(str) {
if (str.length <= 12) return str;
@ -38,6 +40,9 @@ function formatAddress(str) {
export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
const [nameOrAddress, setNameOrAddress] = useState("");
const [inputValue, setInputValue] = useState('');
const { results, isLoading } = useNameSearch(inputValue);
const options = useMemo(() => results?.map((item) => item.name), [results]);
const [errorMessage, setErrorMessage] = useState("");
const [addressInfo, setAddressInfo] = useState(null);
const [isLoadingUser, setIsLoadingUser] = useState(false);
@ -106,6 +111,7 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
setIsOpenDrawerLookup(false)
setNameOrAddress('')
setErrorMessage('')
setInputValue('');
setPayments([])
setIsLoadingUser(false)
setIsLoadingPayments(false)
@ -134,27 +140,66 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
flexShrink: 0,
}}
>
<TextField
autoFocus
<Autocomplete
value={nameOrAddress}
onChange={(e) => setNameOrAddress(e.target.value)}
size="small"
placeholder="Address or Name"
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter" && nameOrAddress) {
lookupFunc();
onChange={(event: any, newValue: string | null) => {
if (!newValue) {
setNameOrAddress('');
return;
}
setNameOrAddress(newValue);
lookupFunc(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
id="controllable-states-demo"
loading={isLoading}
options={options}
sx={{ width: 300 }}
size="small"
renderInput={(params) => (
<TextField
autoFocus
autoComplete="off"
{...params}
label="Address or Name"
onKeyDown={(e) => {
if (e.key === 'Enter' && nameOrAddress) {
lookupFunc(inputValue);
}
}}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: 'white',
},
'&:hover fieldset': {
borderColor: 'white',
},
'&.Mui-focused fieldset': {
borderColor: 'white',
},
'& input': {
color: 'white',
},
},
'& .MuiInputLabel-root': {
color: 'white',
},
'& .MuiInputLabel-root.Mui-focused': {
color: 'white',
},
'& .MuiAutocomplete-endAdornment svg': {
color: 'white',
},
}}
/>
)}
/>
<ButtonBase onClick={()=> {
lookupFunc();
}} >
<SearchIcon sx={{
color: 'white',
marginRight: '20px'
}} />
</ButtonBase>
<ButtonBase sx={{
marginLeft: 'auto',

View File

@ -0,0 +1,55 @@
import { useCallback, useEffect, useState } from 'react';
import { getBaseApiReact } from '../App';
interface NameListItem {
name: string;
address: string;
}
export const useNameSearch = (value: string, limit = 20) => {
const [nameList, setNameList] = useState<NameListItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const checkIfNameExisits = useCallback(
async (name: string, listLimit: number) => {
try {
if (!name) {
setNameList([]);
return;
}
const res = await fetch(
`${getBaseApiReact()}/names/search?query=${name}&prefix=true&limit=${listLimit}`
);
const data = await res.json();
setNameList(
data?.map((item: any) => {
return {
name: item.name,
address: item.owner,
};
})
);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
},
[]
);
// Debounce logic
useEffect(() => {
setIsLoading(true);
const handler = setTimeout(() => {
checkIfNameExisits(value, limit);
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [value, limit, checkIfNameExisits]);
return {
isLoading,
results: nameList,
};
};

View File

@ -1,6 +1,6 @@
import { gateways, getApiKeyFromStorage } from "./background";
import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener";
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, buyNameRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellNameRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getArrrSyncStatus, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getNodeInfo, getNodeStatus, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sellNameRequest, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateGroupRequest, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, buyNameRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellNameRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getArrrSyncStatus, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getNodeInfo, getNodeStatus, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, multiPaymentWithPrivateData, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sellNameRequest, sendChatMessage, sendCoin, setCurrentForeignServer, signForeignFees, signTransaction, transferAssetRequest, updateForeignFee, updateGroupRequest, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
import { getData, storeData } from "./utils/chromeStorage";
import { executeEvent } from "./utils/events";
@ -462,7 +462,7 @@ export const isRunningGateway = async ()=> {
case "UPDATE_FOREIGN_FEE": {
try {
const res = await updateForeignFee(request.payload);
const res = await updateForeignFee(request.payload, isFromExtension);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
@ -502,7 +502,7 @@ export const isRunningGateway = async ()=> {
case "SET_CURRENT_FOREIGN_SERVER": {
try {
const res = await setCurrentForeignServer(request.payload);
const res = await setCurrentForeignServer(request.payload, isFromExtension);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
@ -522,7 +522,7 @@ export const isRunningGateway = async ()=> {
case "ADD_FOREIGN_SERVER": {
try {
const res = await addForeignServer(request.payload);
const res = await addForeignServer(request.payload, isFromExtension);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
@ -542,7 +542,7 @@ export const isRunningGateway = async ()=> {
case "REMOVE_FOREIGN_SERVER": {
try {
const res = await removeForeignServer(request.payload);
const res = await removeForeignServer(request.payload, isFromExtension);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
@ -1282,6 +1282,70 @@ export const isRunningGateway = async ()=> {
}
break;
}
case "MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA" : {
try {
const res = await multiPaymentWithPrivateData(request.payload, isFromExtension)
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error?.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
case "TRANSFER_ASSET" : {
try {
const res = await transferAssetRequest(request.payload, isFromExtension)
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error?.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
case 'SIGN_FOREIGN_FEES': {
try {
const res = await signForeignFees(request.payload, isFromExtension);
event.source.postMessage(
{
requestId: request.requestId,
action: request.action,
payload: res,
type: 'backgroundMessageResponse',
},
event.origin
);
} catch (error) {
event.source.postMessage(
{
requestId: request.requestId,
action: request.action,
error: error.message,
type: 'backgroundMessageResponse',
},
event.origin
);
}
break;
}
default:
break;
}

View File

@ -32,7 +32,12 @@ import {
getBaseApi,
buyName,
cancelSellName,
sellName
sellName,
getAssetBalanceInfo,
getNameOrAddress,
getAssetInfo,
transferAsset,
getPublicKey
} from "../background";
import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption";
import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener";
@ -67,6 +72,11 @@ import utils from "../utils/utils";
import { RequestQueueWithPromise } from "../utils/queue/queue";
import ed2curve from "../deps/ed2curve";
import { Sha256 } from "asmcrypto.js";
import { isValidBase64WithDecode } from "../utils/decode";
import ShortUniqueId from "short-unique-id";
const uid = new ShortUniqueId({ length: 6 });
export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10);
@ -2367,7 +2377,7 @@ export const getTxActivitySummary = async (data) => {
}
};
export const updateForeignFee = async (data) => {
export const updateForeignFee = async (data, isFromExtension) => {
const isGateway = await isRunningGateway();
if (isGateway) {
throw new Error("This action cannot be done through a public node");
@ -2388,9 +2398,25 @@ export const getTxActivitySummary = async (data) => {
}
const { coin, type, value } = data;
const url = `/crosschain/${coin.toLowerCase()}/update${type}`;
const resPermission = await getUserPermission(
{
text1: `Do you give this application to update foreign fees on your node?`,
text2: `type: ${type === 'feerequired' ? 'unlocking' : 'locking'}`,
text3: `value: ${value}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const url = `/crosschain/${coin.toLowerCase()}/update${type}`;
const valueStringified = JSON.stringify(+value);
try {
const endpoint = await createEndpoint(url);
const response = await fetch(endpoint, {
method: 'POST',
@ -2398,7 +2424,7 @@ export const getTxActivitySummary = async (data) => {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ value }),
body: valueStringified,
});
if (!response.ok) throw new Error('Failed to update foreign fee');
@ -2412,9 +2438,7 @@ export const getTxActivitySummary = async (data) => {
throw new Error(res.message);
}
return res; // Return full response here
} catch (error) {
throw new Error(error?.message || 'Error in update foreign fee');
}
};
export const getServerConnectionHistory = async (data) => {
@ -2466,7 +2490,7 @@ export const getTxActivitySummary = async (data) => {
}
};
export const setCurrentForeignServer = async (data) => {
export const setCurrentForeignServer = async (data, isFromExtension) => {
const isGateway = await isRunningGateway();
if (isGateway) {
throw new Error("This action cannot be done through a public node");
@ -2488,6 +2512,22 @@ export const getTxActivitySummary = async (data) => {
}
const { coin, host, port, type } = data;
const resPermission = await getUserPermission(
{
text1: `Do you give this application to set the current server?`,
text2: `type: ${type}`,
text3: `host: ${host}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const body = {
hostName: host,
port: port,
@ -2496,7 +2536,7 @@ export const getTxActivitySummary = async (data) => {
const url = `/crosschain/${coin.toLowerCase()}/setcurrentserver`;
try {
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
const response = await fetch(endpoint, {
method: 'POST',
@ -2521,13 +2561,11 @@ export const getTxActivitySummary = async (data) => {
}
return res; // Return the full response
} catch (error) {
throw new Error(error?.message || 'Error in set current server');
}
};
export const addForeignServer = async (data) => {
export const addForeignServer = async (data, isFromExtension) => {
const isGateway = await isRunningGateway();
if (isGateway) {
throw new Error("This action cannot be done through a public node");
@ -2549,6 +2587,23 @@ export const getTxActivitySummary = async (data) => {
}
const { coin, host, port, type } = data;
const resPermission = await getUserPermission(
{
text1: `Do you give this application to add a server?`,
text2: `type: ${type}`,
text3: `host: ${host}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const body = {
hostName: host,
port: port,
@ -2557,7 +2612,7 @@ export const getTxActivitySummary = async (data) => {
const url = `/crosschain/${coin.toLowerCase()}/addserver`;
try {
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
const response = await fetch(endpoint, {
method: 'POST',
@ -2582,12 +2637,10 @@ export const getTxActivitySummary = async (data) => {
}
return res; // Return the full response
} catch (error) {
throw new Error(error.message || 'Error in adding server');
}
};
export const removeForeignServer = async (data) => {
export const removeForeignServer = async (data, isFromExtension) => {
const isGateway = await isRunningGateway();
if (isGateway) {
throw new Error("This action cannot be done through a public node");
@ -2609,6 +2662,21 @@ export const getTxActivitySummary = async (data) => {
}
const { coin, host, port, type } = data;
const resPermission = await getUserPermission(
{
text1: `Do you give this application to remove a server?`,
text2: `type: ${type}`,
text3: `host: ${host}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const body = {
hostName: host,
port: port,
@ -2617,7 +2685,7 @@ export const getTxActivitySummary = async (data) => {
const url = `/crosschain/${coin.toLowerCase()}/removeserver`;
try {
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
const response = await fetch(endpoint, {
method: 'POST',
@ -2642,9 +2710,7 @@ export const getTxActivitySummary = async (data) => {
}
return res; // Return the full response
} catch (error) {
throw new Error(error?.message || 'Error in removing server');
}
};
export const getDaySummary = async () => {
@ -3101,7 +3167,34 @@ export const sendCoin = async (data, isFromExtension) => {
};
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,
byteFee300: calculateFeeFromRate(+unlockFee, 300) / QORT_DECIMALS,
},
};
};
export const createBuyOrder = async (data, isFromExtension) => {
@ -3141,6 +3234,8 @@ export const createBuyOrder = async (data, isFromExtension) => {
const crosschainAtInfo = await Promise.all(atPromises);
try {
const buyingFees = await getBuyingFees(foreignBlockchain);
const resPermission = await getUserPermission({
text1: "Do you give this application permission to perform a buy order?",
text2: `${atAddresses?.length}${" "}
@ -3154,10 +3249,46 @@ const crosschainAtInfo = await Promise.all(atPromises);
return latest + +cur?.expectedForeignAmount;
}, 0)
)}
${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`,
${` ${buyingFees.ticker}`}`,
highlightedText: `Is using public node: ${isGateway}`,
fee: '',
foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}`
html: `
<div style="max-height: 30vh; overflow-y: auto; font-family: sans-serif;">
<style>
.fee-container {
background-color: #1e1e1e;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.fee-label {
font-weight: bold;
color: #bb86fc;
margin-bottom: 4px;
}
.fee-description {
font-size: 14px;
color: #cccccc;
margin-bottom: 16px;
}
</style>
<div class="fee-container">
<div class="fee-label">Total Unlocking Fee:</div>
<div>${(+buyingFees?.unlock?.byteFee300 * atAddresses?.length)?.toFixed(8)} ${buyingFees.ticker}</div>
<div class="fee-description">
This fee is an estimate based on ${atAddresses?.length} ${atAddresses?.length > 1 ? 'orders' : 'order'} at a 300 byte cost of ${buyingFees?.unlock?.byteFee300?.toFixed(8)}
</div>
<div class="fee-label">Total Locking Fee:</div>
<div>${+buyingFees?.unlock.fee.toFixed(8)} ${buyingFees.ticker} per kb</div>
</div>
</div>
`,
}, isFromExtension);
const { accepted } = resPermission;
if (accepted) {
@ -4804,4 +4935,432 @@ export const buyNameRequest = async (data, isFromExtension) => {
} else {
throw new Error("User declined request");
}
};
export const multiPaymentWithPrivateData = async (data, isFromExtension) => {
const requiredFields = ["payments", "assetId"];
requiredFields.forEach((field) => {
if (data[field] === undefined || data[field] === null) {
throw new Error(`Missing required field: ${field}`);
}
});
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(`Missing required field: ${field}`);
}
}
const confirmReceiver = await getNameOrAddress(payment.recipient);
if (confirmReceiver.error) {
throw new Error("Invalid receiver address or name");
}
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(`Missing required field: ${field}`);
}
}
if (!name) {
const getName = await getNameInfo();
if (!getName) throw new Error("Name needed to publish");
name = getName;
}
const isValid = isValidBase64WithDecode(arbitraryTx.base64);
if (!isValid) throw new Error("Invalid base64 data");
if(!arbitraryTx?.service?.includes('_PRIVATE')) throw new Error('Please use a PRIVATE service')
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(`Missing required field: ${field}`);
}
}
if (!name) {
const getName = await getNameInfo();
if (!getName) throw new Error("Name needed to publish");
name = getName;
}
const isValid = isValidBase64WithDecode(arbitraryTx.base64);
if (!isValid) throw new Error("Invalid base64 data");
if(!arbitraryTx?.service?.includes('_PRIVATE')) throw new Error('Please use a PRIVATE service')
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('A name is needed to publish')
const balance = await getBalanceInfo();
if(+balance < fee) throw new Error('Your QORT balance is insufficient')
const assetBalance = await getAssetBalanceInfo(assetId)
const assetInfo = await getAssetInfo(assetId)
if(assetBalance < totalAmount) throw new Error('Your asset balance is insufficient')
const resPermission = await getUserPermission(
{
text1: "Do you give this application permission to make the following payments and publishes?",
text2: `Asset used in payments: ${assetInfo.name}`,
html: `
<div style="max-height: 30vh; overflow-y: auto;">
<style>
body {
background-color: #121212;
color: #e0e0e0;
}
.resource-container {
display: flex;
flex-direction: column;
border: 1px solid #444;
padding: 16px;
margin: 8px 0;
border-radius: 8px;
background-color: #1e1e1e;
}
.resource-detail {
margin-bottom: 8px;
}
.resource-detail span {
font-weight: bold;
color: #bb86fc;
}
@media (min-width: 600px) {
.resource-container {
flex-direction: row;
flex-wrap: wrap;
}
.resource-detail {
flex: 1 1 45%;
margin-bottom: 0;
padding: 4px 0;
}
}
</style>
${pendingTransactions.
filter((item)=> item.type === 'PAYMENT').map(
(payment) => `
<div class="resource-container">
<div class="resource-detail"><span>Recipient:</span> ${
payment.recipientAddress
}</div>
<div class="resource-detail"><span>Amount:</span> ${payment.amount}</div>
</div>`
)
.join("")}
${[...pendingTransactions, ...pendingAdditionalArbitraryTxs].
filter((item)=> item.type === 'ARBITRARY').map(
(arbitraryTx) => `
<div class="resource-container">
<div class="resource-detail"><span>Service:</span> ${
arbitraryTx.service
}</div>
<div class="resource-detail"><span>Name:</span> ${name}</div>
<div class="resource-detail"><span>Identifier:</span> ${
arbitraryTx.identifier
}</div>
</div>`
)
.join("")}
</div>
`,
highlightedText: `Total Amount: ${totalAmount}`,
fee: fee
},
isFromExtension
);
const { accepted, checkbox1 = false } = resPermission;
if (!accepted) {
throw new Error("User declined request");
}
// 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(`Missing required field: ${field}`);
}
});
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('Your QORT balance is insufficient')
const assetBalance = await getAssetBalanceInfo(assetId)
if(assetBalance < amount) throw new Error('Your asset balance is insufficient')
const confirmReceiver = await getNameOrAddress(recipient);
if (confirmReceiver.error) {
throw new Error("Invalid receiver address or name");
}
const assetInfo = await getAssetInfo(assetId)
const resPermission = await getUserPermission(
{
text1: `Do you give this application permission to transfer the following asset?`,
text2: `Asset: ${assetInfo?.name}`,
highlightedText: `Amount: ${amount}`,
fee: fee
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error("User declined request");
}
const res = await transferAsset({amount, recipient: confirmReceiver, assetId})
return res
}
export const signForeignFees = async (data, isFromExtension) => {
const resPermission = await getUserPermission(
{
text1: `Do you give this application permission to sign the required fees for all your trade offers?`,
},
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('User declined request');
}
};

View File

@ -0,0 +1,35 @@
// @ts-nocheck
import { QORT_DECIMALS } from '../constants/constants'
import TransactionBase from './TransactionBase'
export default class TransferAssetTransaction extends TransactionBase {
constructor() {
super()
this.type = 12
}
set recipient(recipient) {
this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient)
}
set amount(amount) {
this._amount = Math.round(amount * QORT_DECIMALS)
this._amountBytes = this.constructor.utils.int64ToBytes(this._amount)
}
set assetId(assetId) {
this._assetId = this.constructor.utils.int64ToBytes(assetId)
}
get params() {
const params = super.params
params.push(
this._recipient,
this._assetId,
this._amountBytes,
this._feeBytes
)
return params
}
}

View File

@ -24,6 +24,7 @@ import UpdateGroupTransaction from './UpdateGroupTransaction.js'
import SellNameTransacion from './SellNameTransacion.js'
import CancelSellNameTransacion from './CancelSellNameTransacion.js'
import BuyNameTransacion from './BuyNameTransacion.js'
import TransferAssetTransaction from './TransferAssetTransaction.js'
export const transactionTypes = {
3: RegisterNameTransaction,
@ -34,6 +35,7 @@ export const transactionTypes = {
7: BuyNameTransacion,
8: CreatePollTransaction,
9: VoteOnPollTransaction,
12: TransferAssetTransaction,
16: DeployAtTransaction,
18: ChatTransaction,
181: GroupChatTransaction,

View File

@ -13,4 +13,19 @@ export function decodeIfEncoded(input) {
// Return input as-is if not URI-encoded
return input;
}
}
export const isValidBase64 = (str: string): boolean => {
if (typeof str !== "string" || str.length % 4 !== 0) return false;
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
return base64Regex.test(str);
};
export const isValidBase64WithDecode = (str: string): boolean => {
try {
return isValidBase64(str) && Boolean(atob(str));
} catch {
return false;
}
};