From 8b1ad92606df40432278ab28f29a18c901ddbfa0 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Apr 2025 17:19:49 +0300 Subject: [PATCH 01/25] added asset qrs --- src/App.tsx | 2 + src/background.ts | 55 +++ .../Apps/useQortalMessageListener.tsx | 8 +- src/qortalRequests.ts | 41 +- src/qortalRequests/get.ts | 374 +++++++++++++++++- src/transactions/TransferAssetTransaction.ts | 35 ++ src/transactions/transactions.ts | 2 + src/utils/decode.ts | 17 +- 8 files changed, 529 insertions(+), 5 deletions(-) create mode 100644 src/transactions/TransferAssetTransaction.ts 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: ` +
+ + + ${pendingTransactions. + filter((item)=> item.type === 'PAYMENT').map( + (payment) => ` +
+
Recipient: ${ + payment.recipientAddress + }
+
Amount: ${payment.amount}
+
` + ) + .join("")} + ${[...pendingTransactions, ...pendingAdditionalArbitraryTxs]. + filter((item)=> item.type === 'ARBITRARY').map( + (arbitraryTx) => ` +
+
Service: ${ + arbitraryTx.service + }
+
Name: ${name}
+
Identifier: ${ + arbitraryTx.identifier + }
+
` + ) + .join("")} +
+ + `, + highlightedText: `Total Amount: ${totalAmount}`, + fee: fee + }, + isFromExtension + ); + const { accepted, checkbox1 = false } = resPermission; + if (!accepted) { + throw new Error("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 +} \ No newline at end of file diff --git a/src/transactions/TransferAssetTransaction.ts b/src/transactions/TransferAssetTransaction.ts new file mode 100644 index 0000000..9d0bedb --- /dev/null +++ b/src/transactions/TransferAssetTransaction.ts @@ -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 + } +} diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index f989f3c..a117560 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -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 = { @@ -35,6 +36,7 @@ export const transactionTypes = { 7: BuyNameTransacion, 8: CreatePollTransaction, 9: VoteOnPollTransaction, + 12: TransferAssetTransaction, 16: DeployAtTransaction, 18: ChatTransaction, 181: GroupChatTransaction, diff --git a/src/utils/decode.ts b/src/utils/decode.ts index 3123810..06fdb2b 100644 --- a/src/utils/decode.ts +++ b/src/utils/decode.ts @@ -13,4 +13,19 @@ export function decodeIfEncoded(input) { // Return input as-is if not URI-encoded return input; - } \ No newline at end of file + } + + 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; + } + }; \ No newline at end of file From 170e99b4d0cde3044c8e57e03b2e7ea1e4cf32bf Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 1 May 2025 12:05:07 +0300 Subject: [PATCH 02/25] add qr for sign_fee --- .../Apps/useQortalMessageListener.tsx | 2 + src/qortalRequests.ts | 27 ++++++++ src/qortalRequests/get.ts | 65 +++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 8770807..17ce580 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -253,6 +253,7 @@ export const listOfAllQortalRequests = [ 'SELL_NAME', 'CANCEL_SELL_NAME', 'BUY_NAME', + 'SIGN_FOREIGN_FEES', ]; export const UIQortalRequests = [ @@ -313,6 +314,7 @@ export const UIQortalRequests = [ 'SELL_NAME', 'CANCEL_SELL_NAME', 'BUY_NAME', + 'SIGN_FOREIGN_FEES', ]; async function retrieveFileFromIndexedDB(fileId) { diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 701c433..4951a0d 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -61,6 +61,7 @@ import { buyNameRequest, sellNameRequest, cancelSellNameRequest, + signForeignFees, } from './qortalRequests/get'; import { getData, storeData } from './utils/chromeStorage'; import { executeEvent } from './utils/events'; @@ -1833,6 +1834,32 @@ function setupMessageListenerQortalRequest() { } 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; } diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index a160305..5f71707 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -4923,3 +4923,68 @@ export const buyNameRequest = async (data, isFromExtension) => { throw new Error('User declined request'); } }; + +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'); + } +}; From ab1eaeb338a602064af8a85aa19e90cb614a1c7e Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sat, 3 May 2025 00:12:39 +0300 Subject: [PATCH 03/25] fixes --- src/App.tsx | 34 +++++++------ src/components/Apps/AppsDesktop.tsx | 19 ++++++- src/components/QortPayment.tsx | 6 +++ src/components/ReactionPicker.tsx | 29 +++++++---- src/components/Theme/ThemeContext.tsx | 8 +-- src/components/Theme/ThemeManager.tsx | 20 +++++--- src/qortalRequests/get.ts | 73 +++++++++++++++++++++++++-- 7 files changed, 147 insertions(+), 42 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8a36a7d..b57bc95 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1028,12 +1028,7 @@ function App() { const logoutFunc = useCallback(async () => { try { - if (hasSettingsChanged) { - await showUnsavedChanges({ - message: - 'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.', - }); // TODO translate - } else if (extState === 'authenticated') { + if (extState === 'authenticated') { await showUnsavedChanges({ message: 'Are you sure you would like to logout?', }); @@ -3014,13 +3009,16 @@ function App() { })} - { returnToMain(); }} > - {t('core:action.continue', { postProcess: 'capitalize' })} - + + {t('core:action.continue', { postProcess: 'capitalize' })} + + )} {extState === 'transfer-success-request' && ( @@ -3221,7 +3219,7 @@ function App() { onClick={onOkUnsavedChanges} autoFocus > - {t('core:action.decline', { + {t('core:action.continue_logout', { postProcess: 'capitalize', })} @@ -3270,6 +3268,8 @@ function App() { lineHeight: 1.2, maxWidth: '90%', textAlign: 'center', + fontSize: '16px', + marginBottom: '10px', }} > {messageQortalRequestExtension?.text1} @@ -3342,11 +3342,15 @@ function App() { )} {messageQortalRequestExtension?.html && ( -
+ <> + + +
+ )} diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx index b5a5cba..80f57b9 100644 --- a/src/components/Apps/AppsDesktop.tsx +++ b/src/components/Apps/AppsDesktop.tsx @@ -414,8 +414,23 @@ export const AppsDesktop = ({ setDesktopViewMode('dev'); }} > - - + + )} diff --git a/src/components/QortPayment.tsx b/src/components/QortPayment.tsx index d9baa65..3b48590 100644 --- a/src/components/QortPayment.tsx +++ b/src/components/QortPayment.tsx @@ -158,6 +158,12 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => { value={paymentPassword} onChange={(e) => setPaymentPassword(e.target.value)} autoComplete="off" + onKeyDown={(e) => { + if (e.key === 'Enter') { + if (isLoadingSendCoin) return; + sendCoinFunc(); + } + }} /> diff --git a/src/components/ReactionPicker.tsx b/src/components/ReactionPicker.tsx index c95adcf..911a396 100644 --- a/src/components/ReactionPicker.tsx +++ b/src/components/ReactionPicker.tsx @@ -27,15 +27,25 @@ export const ReactionPicker = ({ onReaction }) => { if (showPicker) { setShowPicker(false); } else { - // Get the button's position const buttonRect = buttonRef.current.getBoundingClientRect(); const pickerWidth = 350; + const pickerHeight = 400; // Match Picker height prop - // Calculate position to align the right edge of the picker with the button's right edge - setPickerPosition({ - top: buttonRect.bottom + window.scrollY, // Position below the button - left: buttonRect.right + window.scrollX - pickerWidth, // Align right edges - }); + // Initial position (below the button) + let top = buttonRect.bottom + window.scrollY; + let left = buttonRect.right + window.scrollX - pickerWidth; + + // If picker would overflow bottom, show it above the button + const overflowBottom = + top + pickerHeight > window.innerHeight + window.scrollY; + if (overflowBottom) { + top = buttonRect.top + window.scrollY - pickerHeight; + } + + // Optional: prevent overflow on the left too + if (left < 0) left = 0; + + setPickerPosition({ top, left }); setShowPicker(true); } }; @@ -92,12 +102,13 @@ export const ReactionPicker = ({ onReaction }) => { allowExpandReactions={true} autoFocusSearch={false} emojiStyle={EmojiStyle.NATIVE} - height="450" + height={400} onEmojiClick={handlePicker} onReactionClick={handleReaction} - reactionsDefaultOpen={true} + // reactionsDefaultOpen={true} + // open={true} theme={Theme.DARK} - width="350" + width={350} />
, document.body diff --git a/src/components/Theme/ThemeContext.tsx b/src/components/Theme/ThemeContext.tsx index 72d4f3b..3ebace0 100644 --- a/src/components/Theme/ThemeContext.tsx +++ b/src/components/Theme/ThemeContext.tsx @@ -25,7 +25,7 @@ const ThemeContext = createContext({ toggleTheme: () => {}, userThemes: [defaultTheme], addUserTheme: (themes) => {}, - setUserTheme: (theme) => {}, + setUserTheme: (theme, themes) => {}, currentThemeId: 'default', }); @@ -83,13 +83,13 @@ export const ThemeProvider = ({ children }) => { saveSettings(themes); }; - const setUserTheme = (theme) => { + const setUserTheme = (theme, themes) => { if (theme.id === 'default') { setCurrentThemeId('default'); - saveSettings(userThemes, themeMode, 'default'); + saveSettings(themes || userThemes, themeMode, 'default'); } else { setCurrentThemeId(theme.id); - saveSettings(userThemes, themeMode, theme.id); + saveSettings(themes || userThemes, themeMode, theme.id); } }; diff --git a/src/components/Theme/ThemeManager.tsx b/src/components/Theme/ThemeManager.tsx index eca0e9f..badcb42 100644 --- a/src/components/Theme/ThemeManager.tsx +++ b/src/components/Theme/ThemeManager.tsx @@ -119,7 +119,7 @@ export default function ThemeManager() { const newTheme = { ...themeDraft, id: uid.rnd() }; const updatedThemes = [...userThemes, newTheme]; addUserTheme(updatedThemes); - setUserTheme(newTheme); + setUserTheme(newTheme, updatedThemes); } setOpenEditor(false); }; @@ -135,19 +135,22 @@ export default function ThemeManager() { ); if (defaultTheme) { - setUserTheme(defaultTheme); + setUserTheme(defaultTheme, updatedThemes); } else { // Emergency fallback - setUserTheme({ - light: lightThemeOptions, - dark: darkThemeOptions, - }); + setUserTheme( + { + light: lightThemeOptions, + dark: darkThemeOptions, + }, + updatedThemes + ); } } }; const handleApplyTheme = (theme) => { - setUserTheme(theme); + setUserTheme(theme, null); }; const handleColorChange = (mode, fieldPath, color) => { @@ -210,7 +213,8 @@ export default function ThemeManager() { const newTheme = { ...importedTheme, id: uid.rnd() }; const updatedThemes = [...userThemes, newTheme]; addUserTheme(updatedThemes); - setUserTheme(newTheme); + + setUserTheme(newTheme, updatedThemes); } catch (error) { console.error(error); } diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index d7b040e..0a0221a 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -2680,7 +2680,7 @@ export const updateForeignFee = async (data) => { const { coin, type, value } = data; const url = `/crosschain/${coin.toLowerCase()}/update${type}`; - + const valueStringified = JSON.stringify(+value); try { const endpoint = await createEndpoint(url); const response = await fetch(endpoint, { @@ -2689,7 +2689,7 @@ export const updateForeignFee = async (data) => { Accept: '*/*', 'Content-Type': 'application/json', }, - body: JSON.stringify({ value }), + body: valueStringified, }); if (!response.ok) throw new Error('Failed to update foreign fee'); @@ -3493,6 +3493,35 @@ 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) => { const requiredFields = ['crosschainAtInfo', 'foreignBlockchain']; const missingFields: string[] = []; @@ -3528,6 +3557,7 @@ export const createBuyOrder = async (data, isFromExtension) => { const crosschainAtInfo = await Promise.all(atPromises); try { + const buyingFees = await getBuyingFees(foreignBlockchain); const resPermission = await getUserPermission( { text1: @@ -3541,10 +3571,45 @@ export const createBuyOrder = async (data, isFromExtension) => { 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: ` +
+ + +
+
Total Unlocking Fee:
+
${(+buyingFees?.unlock?.byteFee300 * atAddresses?.length)?.toFixed(8)} ${buyingFees.ticker}
+
+ 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)} +
+ +
Total Locking Fee:
+
${+buyingFees?.unlock.fee.toFixed(8)} ${buyingFees.ticker} per kb
+ +
+
+`, }, isFromExtension ); From b1af797ad32243c94f4d724af9849ce5868be4bd Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sat, 3 May 2025 01:58:37 +0300 Subject: [PATCH 04/25] fix colors --- src/background.ts | 2 +- src/components/Apps/AppsDevModeNavBar.tsx | 2 +- src/qortalRequests/get.ts | 31 +++++++++-------------- src/styles/theme-dark.ts | 5 ++++ src/styles/theme-light.ts | 5 ++++ 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/background.ts b/src/background.ts index dca7e62..b34a01b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -3689,7 +3689,7 @@ export const checkThreads = async (bringBack) => { dataToBringBack.push(thread); } } catch (error) { - conosle.log({ error }); + console.log({ error }); } } diff --git a/src/components/Apps/AppsDevModeNavBar.tsx b/src/components/Apps/AppsDevModeNavBar.tsx index 1c0422a..1bfcf42 100644 --- a/src/components/Apps/AppsDevModeNavBar.tsx +++ b/src/components/Apps/AppsDevModeNavBar.tsx @@ -180,7 +180,7 @@ export const AppsDevModeNavBar = () => { > @@ -3605,7 +3602,7 @@ export const createBuyOrder = async (data, isFromExtension) => {
Total Locking Fee:
-
${+buyingFees?.unlock.fee.toFixed(8)} ${buyingFees.ticker} per kb
+
${+buyingFees?.lock.fee.toFixed(8)} ${buyingFees.ticker} per kb
@@ -5204,19 +5201,15 @@ export const multiPaymentWithPrivateData = async (data, isFromExtension) => { html: `