mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-06-30 19:01:21 +00:00
Merge pull request #38 from Qortal/feature/asset-qortalrequests
Feature/asset qortalrequests
This commit is contained in:
commit
33a8ecd4a8
@ -2074,6 +2074,8 @@ function App() {
|
|||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
maxWidth: '90%',
|
maxWidth: '90%',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
fontSize: '16px',
|
||||||
|
marginBottom: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{messageQortalRequest?.text1}
|
{messageQortalRequest?.text1}
|
||||||
|
@ -937,6 +937,29 @@ export async function getBalanceInfo() {
|
|||||||
return data;
|
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() {
|
export async function getLTCBalance() {
|
||||||
const wallet = await getSaveWallet();
|
const wallet = await getSaveWallet();
|
||||||
let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`;
|
let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`;
|
||||||
@ -2288,6 +2311,34 @@ export async function kickFromGroup({
|
|||||||
return res;
|
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({
|
export async function createGroup({
|
||||||
groupName,
|
groupName,
|
||||||
groupDescription,
|
groupDescription,
|
||||||
|
@ -253,6 +253,8 @@ export const listOfAllQortalRequests = [
|
|||||||
'SELL_NAME',
|
'SELL_NAME',
|
||||||
'CANCEL_SELL_NAME',
|
'CANCEL_SELL_NAME',
|
||||||
'BUY_NAME',
|
'BUY_NAME',
|
||||||
|
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
|
||||||
|
'TRANSFER_ASSET',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const UIQortalRequests = [
|
export const UIQortalRequests = [
|
||||||
@ -313,6 +315,8 @@ export const UIQortalRequests = [
|
|||||||
'SELL_NAME',
|
'SELL_NAME',
|
||||||
'CANCEL_SELL_NAME',
|
'CANCEL_SELL_NAME',
|
||||||
'BUY_NAME',
|
'BUY_NAME',
|
||||||
|
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
|
||||||
|
'TRANSFER_ASSET',
|
||||||
];
|
];
|
||||||
|
|
||||||
async function retrieveFileFromIndexedDB(fileId) {
|
async function retrieveFileFromIndexedDB(fileId) {
|
||||||
|
@ -61,6 +61,8 @@ import {
|
|||||||
buyNameRequest,
|
buyNameRequest,
|
||||||
sellNameRequest,
|
sellNameRequest,
|
||||||
cancelSellNameRequest,
|
cancelSellNameRequest,
|
||||||
|
multiPaymentWithPrivateData,
|
||||||
|
transferAssetRequest,
|
||||||
} from './qortalRequests/get';
|
} from './qortalRequests/get';
|
||||||
import { getData, storeData } from './utils/chromeStorage';
|
import { getData, storeData } from './utils/chromeStorage';
|
||||||
import { executeEvent } from './utils/events';
|
import { executeEvent } from './utils/events';
|
||||||
@ -1755,6 +1757,63 @@ function setupMessageListenerQortalRequest() {
|
|||||||
}
|
}
|
||||||
break;
|
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 'BUY_NAME': {
|
case 'BUY_NAME': {
|
||||||
try {
|
try {
|
||||||
const res = await buyNameRequest(request.payload, isFromExtension);
|
const res = await buyNameRequest(request.payload, isFromExtension);
|
||||||
|
@ -32,6 +32,11 @@ import {
|
|||||||
cancelSellName,
|
cancelSellName,
|
||||||
buyName,
|
buyName,
|
||||||
getBaseApi,
|
getBaseApi,
|
||||||
|
getAssetBalanceInfo,
|
||||||
|
getNameOrAddress,
|
||||||
|
getAssetInfo,
|
||||||
|
getPublicKey,
|
||||||
|
transferAsset,
|
||||||
} from '../background';
|
} from '../background';
|
||||||
import {
|
import {
|
||||||
getNameInfo,
|
getNameInfo,
|
||||||
@ -50,6 +55,7 @@ import { QORT_DECIMALS } from '../constants/constants';
|
|||||||
import Base58 from '../deps/Base58';
|
import Base58 from '../deps/Base58';
|
||||||
import ed2curve from '../deps/ed2curve';
|
import ed2curve from '../deps/ed2curve';
|
||||||
import nacl from '../deps/nacl-fast';
|
import nacl from '../deps/nacl-fast';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
base64ToUint8Array,
|
base64ToUint8Array,
|
||||||
createSymmetricKeyAndNonce,
|
createSymmetricKeyAndNonce,
|
||||||
@ -78,6 +84,10 @@ import { fileToBase64 } from '../utils/fileReading';
|
|||||||
import { mimeToExtensionMap } from '../utils/memeTypes';
|
import { mimeToExtensionMap } from '../utils/memeTypes';
|
||||||
import { RequestQueueWithPromise } from '../utils/queue/queue';
|
import { RequestQueueWithPromise } from '../utils/queue/queue';
|
||||||
import utils from '../utils/utils';
|
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);
|
export const requestQueueGetAtAddresses = new RequestQueueWithPromise(10);
|
||||||
|
|
||||||
@ -4923,3 +4933,399 @@ export const buyNameRequest = async (data, isFromExtension) => {
|
|||||||
throw new Error('User declined request');
|
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;
|
||||||
|
};
|
||||||
|
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 SellNameTransacion from './SellNameTransacion.js'
|
||||||
import CancelSellNameTransacion from './CancelSellNameTransacion.js'
|
import CancelSellNameTransacion from './CancelSellNameTransacion.js'
|
||||||
import BuyNameTransacion from './BuyNameTransacion.js'
|
import BuyNameTransacion from './BuyNameTransacion.js'
|
||||||
|
import TransferAssetTransaction from './TransferAssetTransaction.js'
|
||||||
|
|
||||||
|
|
||||||
export const transactionTypes = {
|
export const transactionTypes = {
|
||||||
@ -35,6 +36,7 @@ export const transactionTypes = {
|
|||||||
7: BuyNameTransacion,
|
7: BuyNameTransacion,
|
||||||
8: CreatePollTransaction,
|
8: CreatePollTransaction,
|
||||||
9: VoteOnPollTransaction,
|
9: VoteOnPollTransaction,
|
||||||
|
12: TransferAssetTransaction,
|
||||||
16: DeployAtTransaction,
|
16: DeployAtTransaction,
|
||||||
18: ChatTransaction,
|
18: ChatTransaction,
|
||||||
181: GroupChatTransaction,
|
181: GroupChatTransaction,
|
||||||
|
@ -13,4 +13,19 @@ export function decodeIfEncoded(input) {
|
|||||||
|
|
||||||
// Return input as-is if not URI-encoded
|
// Return input as-is if not URI-encoded
|
||||||
return input;
|
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