From 8e8c14348bd63ae0fe49b5323cf397d76ce6014e Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Fri, 24 Jan 2025 03:25:27 -0500 Subject: [PATCH] Add SIGN_TRANSACTION qortalRequest action --- .../core/components/qdn-action-types.js | 3 + .../plugins/core/qdn/browser/browser.src.js | 124 ++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/plugins/plugins/core/components/qdn-action-types.js b/plugins/plugins/core/components/qdn-action-types.js index cd795600..ecff51bd 100644 --- a/plugins/plugins/core/components/qdn-action-types.js +++ b/plugins/plugins/core/components/qdn-action-types.js @@ -114,3 +114,6 @@ export const OPEN_PROFILE = 'OPEN_PROFILE' // ADMIN_ACTION export const ADMIN_ACTION = 'ADMIN_ACTION' + +// SIGN_TRANSACTION +export const SIGN_TRANSACTION = 'SIGN_TRANSACTION' diff --git a/plugins/plugins/core/qdn/browser/browser.src.js b/plugins/plugins/core/qdn/browser/browser.src.js index 4134a335..96e86d18 100644 --- a/plugins/plugins/core/qdn/browser/browser.src.js +++ b/plugins/plugins/core/qdn/browser/browser.src.js @@ -2,6 +2,8 @@ import { html, LitElement } from 'lit' import { Epml } from '../../../../epml' import { Loader, publishData } from '../../../utils/classes' import { QORT_DECIMALS } from '../../../../../crypto/api/constants' +import nacl from '../../../../../crypto/api/deps/nacl-fast' +import utils from '../../../../../crypto/api/deps/utils' import { mimeToExtensionMap } from '../../components/qdn-action-constants' import { base64ToUint8Array, @@ -370,6 +372,114 @@ class WebBrowser extends LitElement { break } + case actions.SIGN_TRANSACTION: { + // Provide a default response string + let response = '{"error": "Request could not be fulfilled"}'; + try { + // 1) Validate required fields + const requiredFields = ['unsignedBytes']; + const missingFields = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + response = JSON.stringify({ error: `Missing fields: ${missingFieldsString}` }); + } else { + // 2) Decode transaction + const shouldProcess = !!data.process; // or (data.process || false) + const decodeResponse = await parentEpml.request('apiCall', { + url: `/transactions/decode?ignoreValidityChecks=false&apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: data.unsignedBytes + }); + if (!decodeResponse || decodeResponse.error) { + const errMsg = (decodeResponse && decodeResponse.message) + ? decodeResponse.message + : 'Failed to decode transaction'; + response = JSON.stringify({ error: errMsg }); + } else { + // 3) Prompt the user (similar to SEND_COIN, etc.) + const signTxModal = await showModalAndWait(actions.SIGN_TRANSACTION, { + decodedTxType: decodeResponse.type, + shouldProcess + }); + if (signTxModal.action === 'reject') { + // User declined + response = JSON.stringify({ error: 'User declined request' }); + } else { + // 4) Convert the transaction + const convertResponse = await parentEpml.request('apiCall', { + url: `/transactions/convert?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: data.unsignedBytes + }); + if (!convertResponse || typeof convertResponse !== 'string') { + response = JSON.stringify({ error: 'Failed to convert transaction bytes' }); + } else { + // 5) Retrieve key pair (adapt to your UI) + const seed32 = window.parent.reduxStore.getState().app.selectedAddress.seed; + const base58PubKey = window.parent.reduxStore.getState().app.selectedAddress.base58PublicKey; + const publicKey32 = window.parent.Base58.decode(base58PubKey); + const fullSecretKey = new Uint8Array(64); + fullSecretKey.set(seed32, 0); // first 32 bytes = seed + fullSecretKey.set(publicKey32, 32); // next 32 bytes = decoded public key + // 6) Sign the converted bytes + const convertedBytes = window.parent.Base58.decode(convertResponse); + const bytesForSigning = new Uint8Array(Object.values(convertedBytes)); + const originalBytes = window.parent.Base58.decode(data.unsignedBytes); + const originalBytesBuffer = new Uint8Array(Object.values(originalBytes)); + const signature = nacl.sign.detached(bytesForSigning, fullSecretKey); + const signedCombined = utils.appendBuffer(originalBytesBuffer, signature); + const signedBytesBase58 = window.parent.Base58.encode(signedCombined); + // 7) If user does not want to process, just return signed bytes + if (!shouldProcess) { + response = JSON.stringify(signedBytesBase58); + } else { + // 8) Otherwise, process transaction on-chain + const processResult = await parentEpml.request('apiCall', { + url: `/transactions/process?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: signedBytesBase58 + }); + if (!processResult || !processResult.signature) { + const msg = (processResult && processResult.message) + ? processResult.message + : 'Transaction was not able to be processed'; + response = JSON.stringify({ error: msg }); + } else { + response = JSON.stringify(processResult); + } + } + } + } + } + } + } catch (error) { + // If anything fails in the try + response = JSON.stringify({ error: error.message || 'Failed to sign transaction' }); + } + // 9) Post the final response back to the web app + let responseObj; + try { + responseObj = JSON.parse(response); + } catch (e) { + responseObj = response; // If not valid JSON + } + if (responseObj.error != null) { + event.ports[0].postMessage({ result: null, error: responseObj }); + } else { + event.ports[0].postMessage({ result: responseObj, error: null }); + } + // Only one break for the entire case + break; + } + case actions.ENCRYPT_DATA: { try { let dataSentBack = {} @@ -3902,6 +4012,20 @@ async function showModalAndWait(type, data) { ` : ''} + ${type === actions.SIGN_TRANSACTION ? ` + + ` : ''} + ${type === actions.PUBLISH_MULTIPLE_QDN_RESOURCES ? `