diff --git a/src/App.tsx b/src/App.tsx index 2a3d1b6..d779131 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3253,6 +3253,8 @@ function App() { lineHeight: 1.2, maxWidth: "90%", textAlign: "center", + fontSize: '16px', + marginBottom: '10px' }} > {messageQortalRequestExtension?.text1} diff --git a/src/background.ts b/src/background.ts index 5bd922b..570486f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -952,6 +952,28 @@ 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 getLTCBalance() { const wallet = await getSaveWallet(); let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`; @@ -2268,6 +2290,39 @@ export async function kickFromGroup({ return res; } +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 createGroup({ groupName, groupDescription, diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index bef9a99..d3c92ca 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -259,7 +259,9 @@ export const listOfAllQortalRequests = [ 'UPDATE_GROUP', 'SELL_NAME', 'CANCEL_SELL_NAME', - 'BUY_NAME' + 'BUY_NAME', + 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA', + 'TRANSFER_ASSET' ] export const UIQortalRequests = [ @@ -319,7 +321,9 @@ export const UIQortalRequests = [ 'UPDATE_GROUP', 'SELL_NAME', 'CANCEL_SELL_NAME', - 'BUY_NAME' + 'BUY_NAME', + 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA', + 'TRANSFER_ASSET' ]; diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index f034dbd..37e0126 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,6 +1,6 @@ import { gateways, getApiKeyFromStorage } from "./background"; import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener"; -import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getNodeInfo, getNodeStatus, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll, getArrrSyncStatus, updateGroupRequest, buyNameRequest, sellNameRequest, cancelSellNameRequest } from "./qortalRequests/get"; +import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getNodeInfo, getNodeStatus, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll, getArrrSyncStatus, updateGroupRequest, buyNameRequest, sellNameRequest, cancelSellNameRequest, multiPaymentWithPrivateData, transferAssetRequest } from "./qortalRequests/get"; import { getData, storeData } from "./utils/chromeStorage"; import { executeEvent } from "./utils/events"; @@ -1314,6 +1314,45 @@ 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; + } default: break; } diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 4ae357f..dcc391e 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -35,6 +35,11 @@ import { cancelSellName, buyName, getBaseApi, + getAssetBalanceInfo, + getNameOrAddress, + getAssetInfo, + getPublicKey, + transferAsset, } from "../background"; import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption"; import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener"; @@ -75,6 +80,10 @@ import { fileToBase64 } from "../utils/fileReading"; import { mimeToExtensionMap } from "../utils/memeTypes"; import { RequestQueueWithPromise } from "../utils/queue/queue"; import utils from "../utils/utils"; +import ShortUniqueId from "short-unique-id"; +import { isValidBase64WithDecode } from "../utils/decode"; + +const uid = new ShortUniqueId({ length: 6 }); export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10); @@ -4886,4 +4895,367 @@ export const buyNameRequest = async (data, isFromExtension) => { } else { throw new Error("User declined request"); } -}; \ No newline at end of file +}; + +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: ` +