mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-05-07 18:27:54 +00:00
added qortal requests
This commit is contained in:
parent
3c00d40093
commit
8c98fcbcdf
90
src/App.tsx
90
src/App.tsx
@ -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" && (
|
||||
|
@ -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`;
|
||||
|
@ -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',
|
||||
];
|
||||
|
||||
|
||||
|
248
src/components/Auth/DownloadWallet.tsx
Normal file
248
src/components/Auth/DownloadWallet.tsx
Normal 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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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',
|
||||
|
||||
|
55
src/hooks/useNameSearch.tsx
Normal file
55
src/hooks/useNameSearch.tsx
Normal 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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
};
|
35
src/transactions/TransferAssetTransaction.ts
Normal file
35
src/transactions/TransferAssetTransaction.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user