diff --git a/src/App.tsx b/src/App.tsx index 8bc8a60..aebad47 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -628,7 +628,9 @@ function App() { const qortalRequestPermissonFromExtension = async (message, sender, sendResponse) => { if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow) { try { - + if(message?.payload?.checkbox1){ + qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1 + } await showQortalRequestExtension(message?.payload); if (qortalRequestCheckbox1Ref.current) { @@ -1069,7 +1071,7 @@ function App() { resetAllRecoil() }; - function roundUpToDecimals(number, decimals = 8) { + function roundUpToDecimals(number, decimals = 8) { const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals return Math.ceil(+number * factor) / factor; } @@ -1463,10 +1465,11 @@ function App() { textDecoration: "underline", }} onClick={() => { - chrome.tabs.create({ url: "https://www.qort.trade" }); + executeEvent("addTab", { data: { service: 'APP', name: 'q-trade' } }); + executeEvent("open-apps-mode", { }); }} > - Get QORT at qort.trade + Get QORT at Q-Trade @@ -2769,6 +2772,25 @@ function App() { )} + {messageQortalRequestExtension?.foreignFee && ( + <> + + + + {"Foreign Fee: "} + {messageQortalRequestExtension?.foreignFee} + + + + )} {messageQortalRequestExtension?.checkbox1 && ( { }); }; +export const getForeignKey = async (foreignBlockchain)=> { + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + switch (foreignBlockchain) { + case "LITECOIN": + return parsedData.ltcPrivateKey + + default: + return null + } +} + const pauseAllQueues = () => controlAllQueues("pause"); const resumeAllQueues = () => controlAllQueues("resume"); const checkDifference = (createdTimestamp) => { @@ -109,7 +124,7 @@ const checkDifference = (createdTimestamp) => { Date.now() - createdTimestamp < timeDifferenceForNotificationChatsBackground ); }; -const getApiKeyFromStorage = async () => { +export const getApiKeyFromStorage = async () => { return new Promise((resolve, reject) => { chrome.storage.local.get("apiKey", (result) => { if (chrome.runtime.lastError) { @@ -172,6 +187,7 @@ export const isUsingLocal = async () => { + export const createEndpoint = async (endpoint, customApi?: string) => { if (customApi) { return `${customApi}${endpoint}`; @@ -1255,6 +1271,69 @@ async function getDataPublishes(groupId, type) { }); } +async function sendChatForBuyOrder({ qortAddress, recipientPublicKey, message }) { + console.log('test3', qortAddress, recipientPublicKey, message) + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let sendTimestamp = Date.now(); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + const difficulty = 8; + const jsonData = { + addresses: message.addresses, + foreignKey: message.foreignKey, + receivingAddress: message.receivingAddress, + }; + const finalJson = { + callRequest: jsonData, + extra: "whatever additional data goes here", + }; + const messageStringified = JSON.stringify(finalJson); + + const tx = await createTransaction(18, keyPair, { + timestamp: sendTimestamp, + recipient: qortAddress, + recipientPublicKey: recipientPublicKey, + hasChatReference: 0, + message: messageStringified, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + }); + if (!hasEnoughBalance) { + throw new Error('You must have at least 4 QORT to trade using the gateway.') + } + const path = `${import.meta.env.BASE_URL}memory-pow.wasm.full`; + + const { nonce, chatBytesArray } = await computePow({ + chatBytes: tx.chatBytes, + path, + difficulty, + }); + let _response = await signChatFunc( + chatBytesArray, + nonce, + "https://appnode.qortal.org", + keyPair + ); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; +} + async function addDataPublishes(newData, groupId, type) { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -1931,6 +2010,120 @@ async function createBuyOrderTx({ crosschainAtInfo, useLocal }) { } } + +export async function createBuyOrderTxQortalRequest({ crosschainAtInfo, isGateway, foreignBlockchain }) { + try { + console.log('test2', crosschainAtInfo, isGateway, foreignBlockchain) + if (!isGateway) { + const wallet = await getSaveWallet(); + + const address = wallet.address0; + + const message = { + addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + foreignKey: await getForeignKey(foreignBlockchain), + receivingAddress: address, + }; + let responseVar; + const txn = new TradeBotRespondMultipleRequest().createTransaction( + message + ); + + + const url = await createEndpoint('/crosschain/tradebot/respondmultiple') + + const responseFetch = await fetch( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(txn), + } + ); + + const res = await responseFetch.json(); + + if (res === false) { + responseVar = { + response: "Unable to execute buy order", + success: false, + }; + } else { + responseVar = { response: res, success: true }; + } + const { response, success } = responseVar; + let responseMessage; + if (success) { + responseMessage = { + callResponse: response, + extra: { + message: "Transaction processed successfully!", + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + senderAddress: address, + node: url + }, + }; + } else { + responseMessage = { + callResponse: "ERROR", + extra: { + message: response, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + senderAddress: address, + node: url + }, + }; + } + + return responseMessage + } + const wallet = await getSaveWallet(); + const address = wallet.address0; + + + const message = { + addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + foreignKey: await getForeignKey(foreignBlockchain), + receivingAddress: address, + }; + const res = await sendChatForBuyOrder({ + qortAddress: proxyAccountAddress, + recipientPublicKey: proxyAccountPublicKey, + message, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + }); + + + if (res?.signature) { + + const message = await listenForChatMessageForBuyOrderQortalRequest({ + nodeBaseUrl: buyTradeNodeBaseUrl, + senderAddress: proxyAccountAddress, + senderPublicKey: proxyAccountPublicKey, + signature: res?.signature, + }); + + const responseMessage = { + callResponse: message.callResponse, + extra: { + message: message?.extra?.message, + senderAddress: address, + node: buyTradeNodeBaseUrl, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + } + } + + return responseMessage + } else { + throw new Error("Unable to send buy order message"); + } + } catch (error) { + throw new Error(error.message); + } +} + async function sendChatNotification( res, groupId, @@ -2544,6 +2737,40 @@ async function listenForChatMessageForBuyOrder({ } } +async function listenForChatMessageForBuyOrderQortalRequest({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + signature, +}) { + try { + let validApi = ""; + const checkIfNodeBaseUrlIsAcceptable = apiEndpoints.find( + (item) => item === nodeBaseUrl + ); + if (checkIfNodeBaseUrlIsAcceptable) { + validApi = checkIfNodeBaseUrlIsAcceptable; + } else { + validApi = await findUsableApi(); + } + const wallet = await getSaveWallet(); + const address = wallet.address0; + const before = Date.now() + 1200000; + const after = Date.now(); + const apiCall = `${validApi}/chat/messages?involving=${senderAddress}&involving=${address}&reverse=true&limit=1&before=${before}&after=${after}&encoding=BASE64`; + const parsedMessageObj = await fetchMessagesForBuyOrders( + apiCall, + signature, + senderPublicKey + ); + + return parsedMessageObj + } catch (error) { + console.error(error); + throw new Error(error.message); + } +} + export function removeDuplicateWindow(popupUrl) { chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index b444c22..e3eff98 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -106,9 +106,18 @@ export const Apps = ({ mode, setMode, show , myName}) => { } }, []); useEffect(() => { - getQapps(); getCategories() - }, [getQapps, getCategories]); + }, [getCategories]); + + useEffect(() => { + getQapps(); + + const interval = setInterval(() => { + getQapps(); + }, 20 * 60 * 1000); // 20 minutes in milliseconds + + return () => clearInterval(interval); + }, [getQapps]); const selectedAppInfoFunc = (e) => { const data = e.detail?.data; @@ -292,6 +301,7 @@ export const Apps = ({ mode, setMode, show , myName}) => { myName={myName} hasPublishApp={!!(myApp || myWebsite)} categories={categories} + getQapps={getQapps} /> {mode === "appInfo" && !selectedTab && } diff --git a/src/components/Apps/AppsCategory.tsx b/src/components/Apps/AppsCategory.tsx index a999c95..6873f6e 100644 --- a/src/components/Apps/AppsCategory.tsx +++ b/src/components/Apps/AppsCategory.tsx @@ -30,16 +30,7 @@ import { Spacer } from "../../common/Spacer"; import { AppInfoSnippet } from "./AppInfoSnippet"; import { Virtuoso } from "react-virtuoso"; import { executeEvent } from "../../utils/events"; -const officialAppList = [ - "q-tube", - "q-blog", - "q-share", - "q-support", - "q-mail", - "qombo", - "q-fund", - "q-shop", -]; + const ScrollerStyled = styled('div')({ // Hide scrollbar for WebKit browsers (Chrome, Safari) diff --git a/src/components/Apps/AppsCategoryDesktop.tsx b/src/components/Apps/AppsCategoryDesktop.tsx index 91e818d..a1798ec 100644 --- a/src/components/Apps/AppsCategoryDesktop.tsx +++ b/src/components/Apps/AppsCategoryDesktop.tsx @@ -38,16 +38,7 @@ import { AppInfoSnippet } from "./AppInfoSnippet"; import { Virtuoso } from "react-virtuoso"; import { executeEvent } from "../../utils/events"; import { AppsDesktopLibraryBody, AppsDesktopLibraryHeader } from "./AppsDesktop-styles"; -const officialAppList = [ - "q-tube", - "q-blog", - "q-share", - "q-support", - "q-mail", - "qombo", - "q-fund", - "q-shop", -]; + const ScrollerStyled = styled("div")({ // Hide scrollbar for WebKit browsers (Chrome, Safari) diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx index 7065782..ab4eba9 100644 --- a/src/components/Apps/AppsDesktop.tsx +++ b/src/components/Apps/AppsDesktop.tsx @@ -109,10 +109,20 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop // dispatch(setIsLoadingGlobal(false)) } }, []); + useEffect(() => { - getQapps(); getCategories() - }, [getQapps, getCategories]); + }, [getCategories]); + + useEffect(() => { + getQapps(); + + const interval = setInterval(() => { + getQapps(); + }, 20 * 60 * 1000); // 20 minutes in milliseconds + + return () => clearInterval(interval); + }, [getQapps]); const selectedAppInfoFunc = (e) => { const data = e.detail?.data; @@ -386,6 +396,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop myName={myName} hasPublishApp={!!(myApp || myWebsite)} categories={categories} + getQapps={getQapps} /> {mode === "appInfo" && !selectedTab && } diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index cfe5b3f..868b297 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -26,6 +26,7 @@ import IconClearInput from "../../assets/svgs/ClearInput.svg"; import qappDevelopText from "../../assets/svgs/qappDevelopText.svg"; import qappDots from "../../assets/svgs/qappDots.svg"; import ReturnSVG from '../../assets/svgs/Return.svg' +import RefreshIcon from "@mui/icons-material/Refresh"; import { Spacer } from "../../common/Spacer"; import { AppInfoSnippet } from "./AppInfoSnippet"; @@ -41,6 +42,7 @@ const officialAppList = [ "qombo", "q-fund", "q-shop", + "q-trade" ]; const ScrollerStyled = styled('div')({ @@ -76,7 +78,7 @@ const ScrollerStyled = styled('div')({ "-ms-overflow-style": "none", }); -export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow, categories={categories} }) => { +export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow, categories, getQapps }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); const { rootHeight } = useContext(MyContext); @@ -132,6 +134,11 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i justifyContent: "center", }} > + @@ -159,6 +166,21 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i )} + { + getQapps() + }} + > + + + diff --git a/src/components/Apps/AppsLibraryDesktop.tsx b/src/components/Apps/AppsLibraryDesktop.tsx index 20a631a..f7fe9c0 100644 --- a/src/components/Apps/AppsLibraryDesktop.tsx +++ b/src/components/Apps/AppsLibraryDesktop.tsx @@ -39,6 +39,8 @@ import { Spacer } from "../../common/Spacer"; import { AppInfoSnippet } from "./AppInfoSnippet"; import { Virtuoso } from "react-virtuoso"; import { executeEvent } from "../../utils/events"; +import RefreshIcon from "@mui/icons-material/Refresh"; + import { AppsDesktopLibraryBody, AppsDesktopLibraryHeader, @@ -55,6 +57,7 @@ const officialAppList = [ "qombo", "q-fund", "q-shop", + "q-trade" ]; const ScrollerStyled = styled("div")({ @@ -96,7 +99,7 @@ export const AppsLibraryDesktop = ({ myName, hasPublishApp, isShow, - categories = { categories }, + categories, getQapps }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); @@ -169,6 +172,11 @@ export const AppsLibraryDesktop = ({ }} > + + { + getQapps() + }} + > + + + diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index a9d4714..9b41c49 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -139,7 +139,8 @@ const UIQortalRequests = [ 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO', 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', - 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY' + 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', + 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY' ]; @@ -369,6 +370,7 @@ isDOMContentLoaded: false const sendMessageToRuntime = (message, eventPort) => { chrome?.runtime?.sendMessage(message, (response) => { + console.log('runtimeres', response) if (response.error) { eventPort.postMessage({ result: null, diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 822f04b..12308b1 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -93,7 +93,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { if (res) { const { service, name, identifier, path } = res; executeEvent("addTab", { data: { service, name, identifier, path } }); - executeEvent("open-dev-mode", { }); + executeEvent("open-apps-mode", { }); } } diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index f1b4189..7f8b5ac 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -1313,10 +1313,10 @@ export const Group = ({ }; useEffect(() => { - subscribeToEvent("open-dev-mode", openDevModeFunc); + subscribeToEvent("open-apps-mode", openDevModeFunc); return () => { - unsubscribeFromEvent("open-dev-mode", openDevModeFunc); + unsubscribeFromEvent("open-apps-mode", openDevModeFunc); }; }, []); diff --git a/src/components/Mobile/MobileFooter.tsx b/src/components/Mobile/MobileFooter.tsx index 09d8438..e622e78 100644 --- a/src/components/Mobile/MobileFooter.tsx +++ b/src/components/Mobile/MobileFooter.tsx @@ -15,6 +15,7 @@ import { WalletIcon } from "../../assets/Icons/WalletIcon"; import { HubsIcon } from "../../assets/Icons/HubsIcon"; import { TradingIcon } from "../../assets/Icons/TradingIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; +import { executeEvent } from "../../utils/events"; const IconWrapper = ({ children, label, color }) => { return ( @@ -184,7 +185,8 @@ export const MobileFooter = ({ /> { - chrome.tabs.create({ url: "https://www.qort.trade"}); + executeEvent("addTab", { data: { service: 'APP', name: 'q-trade' } }); + executeEvent("open-apps-mode", { }); }} icon={ diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 3174148..5348685 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,5 @@ -import { addForeignServer, addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; +import { getApiKeyFromStorage } from "./background"; +import { addForeignServer, addListItems, cancelSellOrder, createBuyOrder, createPoll, createSellOrder, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; @@ -26,6 +27,16 @@ function getLocalStorage(key) { }); } + export const isRunningGateway = async ()=> { + let isGateway = true; + const apiKey = await getApiKeyFromStorage(); + if (apiKey && (apiKey?.url && !gateways.some(gateway => apiKey?.url?.includes(gateway)))) { + isGateway = false; + } + + return isGateway + } + export async function setPermission(key, value) { try { @@ -422,6 +433,52 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "CREATE_TRADE_BUY_ORDER": { + const data = request.payload; + + + createBuyOrder(data, isFromExtension).then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + break; + } + + case "CREATE_TRADE_SELL_ORDER": { + const data = request.payload; + createSellOrder(data, isFromExtension).then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "CANCEL_TRADE_SELL_ORDER": { + const data = request.payload; + cancelSellOrder(data, isFromExtension).then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "IS_USING_GATEWAY": { + isRunningGateway().then((res) => { + console.log('isusing', res) + sendResponse({isGateway: res}); + }) + .catch((error) => { + sendResponse({ error: 'unable to determine if using gateway' }); + }); + break; + } } } return true; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index de91590..988ac17 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -12,7 +12,8 @@ import { joinGroup as joinGroupFunc, sendQortFee, sendCoin as sendCoinFunc, - isUsingLocal + isUsingLocal, + createBuyOrderTxQortalRequest } from "../background"; import { getNameInfo } from "../backgroundFunctions/encryption"; import { QORT_DECIMALS } from "../constants/constants"; @@ -26,10 +27,12 @@ import { uint8ArrayToBase64, } from "../qdn/encryption/group-encryption"; import { publishData } from "../qdn/publish/pubish"; -import { getPermission, setPermission } from "../qortalRequests"; +import { getPermission, setPermission, isRunningGateway } from "../qortalRequests"; import { createTransaction } from "../transactions/transactions"; import { mimeToExtensionMap } from "../utils/memeTypes"; - +import TradeBotCreateRequest from "../transactions/TradeBotCreateRequest"; +import DeleteTradeOffer from "../transactions/TradeBotDeleteRequest"; +import signTradeBotTransaction from "../transactions/signTradeBotTransaction"; const btcFeePerByte = 0.00000100 const ltcFeePerByte = 0.00000030 @@ -37,6 +40,19 @@ const dogeFeePerByte = 0.00001000 const dgbFeePerByte = 0.00000010 const rvnFeePerByte = 0.00001125 +const sellerForeignFee = { + 'LITECOIN': { + value: '~0.00005', + ticker: 'LTC' + } +} + + +function roundUpToDecimals(number, decimals = 8) { + const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals + return Math.ceil(+number * factor) / factor; +} + const _createPoll = async ({pollName, pollDescription, options}, isFromExtension) => { const fee = await getFee("CREATE_POLL"); @@ -1428,22 +1444,31 @@ export const getWalletBalance = async (data, bypassPermission?: boolean, isFromE const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } + + const value = (await getPermission(`qAPPAutoWalletBalance-${data.coin}`)) || false; + console.log('value', value) + let skip = false; + if (value) { + skip = true; + } let resPermission - if(!bypassPermission){ + if(!bypassPermission && !skip){ resPermission = await getUserPermission({ text1: "Do you give this application permission to fetch your", highlightedText: `${data.coin} balance`, + checkbox1: { + value: true, + label: "Always allow balance to be retrieved automatically", + }, }, isFromExtension); - } else { - resPermission = { - accepted: false - } - } + } - const { accepted } = resPermission; - - if (accepted || bypassPermission) { + const { accepted = false, checkbox1 = false } = resPermission || {}; + if(resPermission){ + setPermission(`qAPPAutoWalletBalance-${data.coin}`, checkbox1); + } + if (accepted || bypassPermission || skip) { let coin = data.coin; const wallet = await getSaveWallet(); const address = wallet.address0; @@ -1530,7 +1555,7 @@ export const getWalletBalance = async (data, bypassPermission?: boolean, isFromE } }; -const getUserWalletFunc = async (coin) => { +export const getUserWalletFunc = async (coin) => { let userWallet = {}; const wallet = await getSaveWallet(); const address = wallet.address0; @@ -2461,3 +2486,310 @@ export const sendCoin = async (data, isFromExtension) => { } } }; + + +export const createBuyOrder = async (data, isFromExtension) => { + console.log('data', data) + const requiredFields = [ + "crosschainAtInfo", + "foreignBlockchain" + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const isGateway = await isRunningGateway() + const foreignBlockchain = data.foreignBlockchain + const crosschainAtInfo = data.crosschainAtInfo; + const atAddresses = data.crosschainAtInfo?.map((order)=> order.qortalAtAddress); + console.log('test', isGateway, foreignBlockchain , crosschainAtInfo, atAddresses) + try { + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to perform a buy order?", + text2: `${atAddresses?.length}${" "} + ${`buy order${ + atAddresses?.length === 1 ? "" : "s" + }`}`, + text3: `${crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.qortAmount; + }, 0)} QORT FOR ${roundUpToDecimals( + crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.foreignAmount; + }, 0) + )} + ${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`, + highlightedText: `Is using gateway: ${isGateway}`, + fee: '', + foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}` + }, isFromExtension); + const { accepted } = resPermission; + if (accepted) { + const resBuyOrder = await createBuyOrderTxQortalRequest( + { + crosschainAtInfo, + isGateway, + foreignBlockchain + } + ); + return resBuyOrder; + } else { + throw new Error("User declined request"); + } + } catch (error) { + throw new Error(error?.message || "Failed to submit trade order."); + } +}; + + const cancelTradeOfferTradeBot = async (body, keyPair) => { + const txn = new DeleteTradeOffer().createTransaction(body) + const url = await createEndpoint(`/crosschain/tradeoffer`); + const bodyToString = JSON.stringify(txn); + + const deleteTradeBotResponse = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + + if(!deleteTradeBotResponse.ok) throw new Error('Unable to update tradebot') + const unsignedTxn = await deleteTradeBotResponse.text() + const signedTxnBytes = await signTradeBotTransaction( + unsignedTxn, + keyPair + ) + const signedBytes = Base58.encode(signedTxnBytes); + + let res + try { + res = await processTransactionVersion2(signedBytes) + } catch (error) { + return { + error: "Failed to Cancel Sell Order. Try again!", + failedTradeBot: { + atAddress: body.atAddress, + creatorAddress: body.creatorAddress + } + } + } + if(res?.error){ + return { + error: "Failed to Cancel Sell Order. Try again!", + failedTradeBot: { + atAddress: body.atAddress, + creatorAddress: body.creatorAddress + } + } + } + if (res?.signature){ + return res + } else { + throw new Error("Failed to Cancel Sell Order. Try again!") + } +} +const findFailedTradebot = async (createBotCreationTimestamp, body)=> { + //wait 5 secs + const wallet = await getSaveWallet(); + const address = wallet.address0; + await new Promise((res)=> { + setTimeout(() => { + res(null) + }, 5000); + }) + const url = await createEndpoint(`/crosschain/tradebot?foreignBlockchain=LITECOIN`); + + const tradeBotsReponse = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await tradeBotsReponse.json() + const latestItem2 = data + .filter( + (item) => + item.creatorAddress === address + ).sort((a, b) => b.timestamp - a.timestamp)[0] + const latestItem = data + .filter( + (item) => + item.creatorAddress === address && + +item.foreignAmount === +body.foreignAmount + ) + .sort((a, b) => b.timestamp - a.timestamp)[0]; + if ( + latestItem && + createBotCreationTimestamp - latestItem.timestamp <= 5000 && + createBotCreationTimestamp > latestItem.timestamp // Ensure latestItem's timestamp is before createBotCreationTimestamp + ) { + + return latestItem + } else { + return null + } + +} +const tradeBotCreateRequest = async (body, keyPair)=> { + const txn = new TradeBotCreateRequest().createTransaction(body) + const url = await createEndpoint(`/crosschain/tradebot/create`); + const bodyToString = JSON.stringify(txn); + + const unsignedTxnResponse = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + if(!unsignedTxnResponse.ok) throw new Error('Unable to create tradebot') + const createBotCreationTimestamp = Date.now() + const unsignedTxn = await unsignedTxnResponse.text() + const signedTxnBytes = await signTradeBotTransaction( + unsignedTxn, + keyPair + ) + const signedBytes = Base58.encode(signedTxnBytes); + + let res + try { + res = await processTransactionVersion2(signedBytes) + } catch (error) { + const findFailedTradeBot = await findFailedTradebot(createBotCreationTimestamp, body) + return { + error: "Failed to Create Sell Order. Try again!", + failedTradeBot: findFailedTradeBot + } + } + + if (res?.signature){ + return res + } else { + throw new Error("Failed to Create Sell Order. Try again!") + } + +} + +export const createSellOrder = async (data, isFromExtension) => { + + const requiredFields = [ + "qortAmount", + "foreignBlockchain", + "foreignAmount" + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + +const receivingAddress = await getUserWalletFunc('LTC') + try { + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to perform a sell order?", + text2: `${data.qortAmount}${" "} + ${`QORT`}`, + text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, + fee: '0.02' + }, isFromExtension); + const { accepted } = resPermission; + if (accepted) { + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair); + + const userPublicKey = parsedData.publicKey + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const response = await tradeBotCreateRequest({ + creatorPublicKey: userPublicKey, + qortAmount: parseFloat(data.qortAmount), + fundingQortAmount: parseFloat(data.qortAmount) + 0.001, + foreignBlockchain: data.foreignBlockchain, + foreignAmount: parseFloat(data.foreignAmount), + tradeTimeout: 120, + receivingAddress: receivingAddress.address + }, keyPair) + + return response + + } else { + throw new Error("User declined request"); + } + } catch (error) { + throw new Error(error?.message || "Failed to submit sell order."); + } +}; + +export const cancelSellOrder = async (data, isFromExtension) => { + + const requiredFields = [ + "qortAmount", + "foreignBlockchain", + "foreignAmount", + "atAddress" + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + try { + const fee = await getFee("MESSAGE"); + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to perform cancel a sell order?", + text2: `${data.qortAmount}${" "} + ${`QORT`}`, + text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, + fee: fee.fee + }, isFromExtension); + const { accepted } = resPermission; + if (accepted) { + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair); + + const userPublicKey = parsedData.publicKey + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + const response = await cancelTradeOfferTradeBot({ + creatorPublicKey: userPublicKey, + atAddress: data.atAddress + }, keyPair) + + return response + + } else { + throw new Error("User declined request"); + } + } catch (error) { + throw new Error(error?.message || "Failed to submit sell order."); + } +}; \ No newline at end of file diff --git a/src/transactions/TradeBotCreateRequest.ts b/src/transactions/TradeBotCreateRequest.ts new file mode 100644 index 0000000..800ddec --- /dev/null +++ b/src/transactions/TradeBotCreateRequest.ts @@ -0,0 +1,65 @@ +// @ts-nocheck + +/** + * CrossChain - TradeBot Create Request (Sell Action) + * + * These are special types of transactions (JSON ENCODED) + */ + +export default class TradeBotCreateRequest { + constructor() { + // ... + } + + createTransaction(txnReq) { + this.creatorPublicKey(txnReq.creatorPublicKey) + this.qortAmount(txnReq.qortAmount) + this.fundingQortAmount(txnReq.fundingQortAmount) + this.foreignBlockchain(txnReq.foreignBlockchain) + this.foreignAmount(txnReq.foreignAmount) + this.tradeTimeout(txnReq.tradeTimeout) + this.receivingAddress(txnReq.receivingAddress) + + return this.txnRequest() + } + + creatorPublicKey(creatorPublicKey) { + this._creatorPublicKey = creatorPublicKey + } + + qortAmount(qortAmount) { + this._qortAmount = qortAmount + } + + fundingQortAmount(fundingQortAmount) { + this._fundingQortAmount = fundingQortAmount + } + + foreignBlockchain(foreignBlockchain) { + this._foreignBlockchain = foreignBlockchain + } + + foreignAmount(foreignAmount) { + this._foreignAmount = foreignAmount + } + + tradeTimeout(tradeTimeout) { + this._tradeTimeout = tradeTimeout + } + + receivingAddress(receivingAddress) { + this._receivingAddress = receivingAddress + } + + txnRequest() { + return { + creatorPublicKey: this._creatorPublicKey, + qortAmount: this._qortAmount, + fundingQortAmount: this._fundingQortAmount, + foreignBlockchain: this._foreignBlockchain, + foreignAmount: this._foreignAmount, + tradeTimeout: this._tradeTimeout, + receivingAddress: this._receivingAddress + } + } +} diff --git a/src/transactions/TradeBotDeleteRequest.ts b/src/transactions/TradeBotDeleteRequest.ts new file mode 100644 index 0000000..cd3c1b4 --- /dev/null +++ b/src/transactions/TradeBotDeleteRequest.ts @@ -0,0 +1,35 @@ +// @ts-nocheck + +/** + * CrossChain - DELETE TradeOffer + * + * These are special types of transactions (JSON ENCODED) + */ + +export default class DeleteTradeOffer { + constructor() { + // ... + } + + createTransaction(txnReq) { + this.creatorPublicKey(txnReq.creatorPublicKey) + this.atAddress(txnReq.atAddress) + + return this.txnRequest() + } + + creatorPublicKey(creatorPublicKey) { + this._creatorPublicKey = creatorPublicKey + } + + atAddress(atAddress) { + this._atAddress = atAddress + } + + txnRequest() { + return { + creatorPublicKey: this._creatorPublicKey, + atAddress: this._atAddress + } + } +} diff --git a/src/transactions/signTradeBotTransaction.ts b/src/transactions/signTradeBotTransaction.ts new file mode 100644 index 0000000..929898f --- /dev/null +++ b/src/transactions/signTradeBotTransaction.ts @@ -0,0 +1,30 @@ +// @ts-nocheck + + +import nacl from '../deps/nacl-fast' +import Base58 from '../deps/Base58' +import utils from '../utils/utils' + +const signTradeBotTransaction = async (unsignedTxn, keyPair) => { + if (!unsignedTxn) { + throw new Error('Unsigned Transaction Bytes not defined') + } + + if (!keyPair) { + throw new Error('keyPair not defined') + } + + const txnBuffer = Base58.decode(unsignedTxn) + + if (keyPair.privateKey.length === undefined) { + const _privateKey = Object.keys(keyPair.privateKey).map(function (key) { return keyPair.privateKey[key] }) + const privateKey = new Uint8Array(_privateKey) + const signature = nacl.sign.detached(txnBuffer, privateKey) + return utils.appendBuffer(txnBuffer, signature) + } else { + const signature = nacl.sign.detached(txnBuffer, keyPair.privateKey) + return utils.appendBuffer(txnBuffer, signature) + } +} + +export default signTradeBotTransaction