From 62aed9fea23db87999a781f81ad11cd150444f9f Mon Sep 17 00:00:00 2001 From: Phillip Lang Martinez Date: Fri, 12 Jul 2024 23:44:21 -0400 Subject: [PATCH] buyorder approval --- public/content-script.js | 33 ++- public/manifest.json | 8 +- public/memory-pow.wasm.full | Bin 0 -> 3399 bytes src/App.tsx | 143 +++++++++++- src/background.ts | 339 +++++++++++++++++++++++++++- src/transactions/ChatBase.ts | 145 ++++++++++++ src/transactions/ChatTransaction.ts | 92 ++++++++ src/transactions/signChat.ts | 40 ++++ src/transactions/transactions.ts | 3 +- 9 files changed, 781 insertions(+), 22 deletions(-) create mode 100644 public/memory-pow.wasm.full create mode 100644 src/transactions/ChatBase.ts create mode 100644 src/transactions/ChatTransaction.ts create mode 100644 src/transactions/signChat.ts diff --git a/public/content-script.js b/public/content-script.js index ff844c6..14419f3 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -10,7 +10,6 @@ // In your content script document.addEventListener('qortalExtensionRequests', async (event) => { const { type, payload, requestId, timeout } = event.detail; // Capture the requestId - if (type === 'REQUEST_USER_INFO') { const hostname = window.location.hostname const res = await connection(hostname) @@ -92,7 +91,37 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); } }); - } else if (type === 'REQUEST_AUTHENTICATION') { + } else if (type === 'REQUEST_BUY_ORDER') { + const hostname = window.location.hostname + const res = await connection(hostname) + if(!res){ + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "BUY_ORDER", data: { + error: "Not authorized" + }, requestId } + })); + return + } + + chrome.runtime.sendMessage({ action: "buyOrder", payload: { + qortalAtAddress: payload.qortalAtAddress, + hostname + + }, timeout}, (response) => { + if (response.error) { + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "BUY_ORDER", data: { + error: response.error + }, requestId } + })); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "BUY_ORDER", data: response, requestId } + })); + } + }); + } else if (type === 'REQUEST_AUTHENTICATION') { const hostname = window.location.hostname const res = await connection(hostname) if(!res){ diff --git a/public/manifest.json b/public/manifest.json index 21ec40c..1ad34af 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Qortal", - "version": "1.0.0", + "version": "1.1.0", "icons": { "16": "qort.png", "32": "qort.png", @@ -16,10 +16,14 @@ }, "permissions": [ "storage", "system.display", "activeTab", "tabs" ], + "content_scripts": [ { "matches": [""], "js": ["content-script.js"] } - ] + ], + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live;" + } } diff --git a/public/memory-pow.wasm.full b/public/memory-pow.wasm.full new file mode 100644 index 0000000000000000000000000000000000000000..073b179cab68d0eabc80c9b6ba9590d7e59c2e4f GIT binary patch literal 3399 zcma)8O^h2w7Jk)LZMWV2X^%(AL?e{yPWTICoC!lh_)Qx{$?z8zmR*v=cG`B&j3;*Y z(6%Ru6pazWh(8HMk#>cIGzfum1o5-d0tYS}cW)~M7ovsT6A@exD*+1cb&nl;5(8!R zd-dM;{;FQPy1;EU833?p^#s5P)@9ujqKgALTO-nCYf7GR&tFY6y=D-eQ9$7d6$Il- z;+ABW*-t+&^e)&>Ygf@20Mau(`46TS9Oo2fbgFKn;Z)l}*aFZ`2d$-g80@`$-wLoT zey3US!lMfhd2X%lFCO>8pmp0`P_7B=n***@gXT)7?d=7TahzH!aF$%Z*6=t>qv*C> z$Mb7k$vVzrBdE9yr{cCe&UMFG^4yhJ5{SmDi)9rfwcSd?i^W9x2cdFc`lC!;lOW+{ z4i+P3^ej$Xvy|R`t|6w7IAw{RrIvTUu%Az%j!UIncn%E=6qrBeA?$S!9JC zX4mCJ&rC^X3aMBi4x*EgDVaV?Wu#=bXJxQb>;$@Ck;`bO$#l=kVW%)yO6JI2lNkyI zc6ZqVT!$W&ZmN<|H5q)-qoqgjii3C*$jqU0k(^nS_5$|u^5x`HQdyy`Lw5?Dk)!zp zB@(n@8G}oHgk}kB$4=Zc9o|v(nVMJWEIM_6A$VN@5I{pPq8Zpi49AX`!4c#Nwgmoy z599m~$dV%V!Kt{!VEtb>QNKWv_>RbbkwB6NjmY9HoUo}7vuUKe7>PF^P2Gy*KO{~Q zy2a0;2!a-<(B9K`x(gzus}>0M~fcD+o+Y>%cCJ56N6fJalRHm{t6|B>e{c)UT1 zwq~BdxoFA%M=`2yUxYq`CdUcAgVLwoN6{hiKqP*SqEF)8k+0wY(Q@t~)W9bisdLB< zk=d=3|BC)?)E_2&Dcas4Vh?K-F!?RcwPfxbO3G_U?xBbZFCo+L`6`j#cx4+w zmHq*Lq7)mCfJ*@DqY^^V*lf$8uzkc-`BiB+%s){WPQuoQ(eF2*>R1#@7_C+tYf38{ zAIN|`>*uxMr(d21=cg_~4$;0Q2j`7TkVnm^eiBHNi1b&Z2Ie;qqu;1s0{!UMkDrd@ zI745z*(S5$Ga6}pj>(Peaecp4!RBn&st9*sWVQWDf3JRA!_#2&zdIwZQ)+Bybub1W zp0P$8_Q;WV$+4yiz6<@;&|5RHNg7u<`Smea%WnM1Lf2pP=s=8K_+R6FU&>8mGmo`b z>#u3|iqbE@tyDs9l@-|axuVK*#@6XvfjzMm4~H!wnZXJNq$AM*Ug z_R^37mdJlladpWJqvhR!6*r&~r5JH-d0q{OJdC01D~-Arf*<%*59)r+I}LDqY9$P6 loht6VqEQ<(null); const [decryptedWallet, setdecryptedWallet] = useState(null); const [requestConnection, setRequestConnection] = useState(null); + const [requestBuyOrder, setRequestBuyOrder] = useState(null); + const [requestAuthentication, setRequestAuthentication] = useState(null); const [userInfo, setUserInfo] = useState(null); const [balance, setBalance] = useState(null); @@ -266,6 +269,10 @@ function App() { // Update the component state with the received 'sendqort' state setRequestConnection(message.payload); setExtstate("web-app-request-connection"); + } else if (message.action === "UPDATE_STATE_REQUEST_BUY_ORDER") { + // Update the component state with the received 'sendqort' state + setRequestBuyOrder(message.payload); + setExtstate("web-app-request-buy-order"); } else if (message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION") { // Update the component state with the received 'sendqort' state setRequestAuthentication(message.payload); @@ -301,10 +308,7 @@ function App() { ); return; } - // if (!paymentPassword) { - // setSendPaymentError("Please enter your wallet password"); - // return; - // } + setIsLoading(true) chrome.runtime.sendMessage( { @@ -321,7 +325,49 @@ function App() { if (response === true) { setExtstate("transfer-success-request"); setCountdown(null); - // setSendPaymentSuccess("Payment successfully sent"); + } else { + + setSendPaymentError( + response?.error || "Unable to perform payment. Please try again." + ); + } + setIsLoading(false) + } + ); + }; + + const confirmBuyOrder = (isDecline: boolean) => { + if (isDecline) { + chrome.runtime.sendMessage( + { + action: "buyOrderConfirmation", + payload: { + crosschainAtInfo: requestBuyOrder?.crosschainAtInfo, + interactionId: requestBuyOrder?.interactionId, + isDecline: true, + }, + }, + (response) => { + window.close(); + } + ); + return; + } + + setIsLoading(true) + chrome.runtime.sendMessage( + { + action: "buyOrderConfirmation", + payload: { + crosschainAtInfo: requestBuyOrder?.crosschainAtInfo, + interactionId: requestBuyOrder?.interactionId, + isDecline: false, + }, + }, + (response) => { + if (response === true) { + setExtstate("transfer-success-request"); + setCountdown(null); } else { setSendPaymentError( @@ -362,7 +408,7 @@ function App() { chrome.runtime.sendMessage({ action: "getWalletInfo" }, (response) => { if (response && response?.walletInfo) { setRawWallet(response?.walletInfo); - if(holdRefExtState.current === 'web-app-request-payment' || holdRefExtState.current === 'web-app-request-connection') return + if(holdRefExtState.current === 'web-app-request-payment' || holdRefExtState.current === 'web-app-request-connection' || holdRefExtState.current === 'web-app-request-buy-order') return setExtstate("authenticated"); } }); @@ -510,6 +556,7 @@ function App() { setRawWallet(null); setdecryptedWallet(null); setRequestConnection(null); + setRequestBuyOrder(null) setRequestAuthentication(null); setUserInfo(null); setBalance(null); @@ -816,6 +863,90 @@ function App() { Send + )} + {extState === "web-app-request-buy-order" && ( + <> + + + + The Application

{" "} + {requestBuyOrder?.hostname}

+ is requesting a buy order +
+ + + {requestBuyOrder?.crosschainAtInfo?.qortAmount} QORT + + + + FOR + + + + {requestBuyOrder?.crosschainAtInfo?.expectedForeignAmount} {requestBuyOrder?.crosschainAtInfo?.foreignBlockchain} + + {/* + + + Confirm Wallet Password + + + setPaymentPassword(e.target.value)} + /> */} + + + confirmBuyOrder(false)} + > + accept + + confirmBuyOrder(true)} + > + decline + + + {sendPaymentError} + )} {extState === "web-app-request-payment" && ( <> diff --git a/src/background.ts b/src/background.ts index 9181188..c268ea4 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,19 +1,13 @@ // @ts-nocheck import Base58 from "./deps/Base58"; +import { signChat } from "./transactions/signChat"; import { createTransaction } from "./transactions/transactions"; import { decryptChatMessage } from "./utils/decryptChatMessage"; import { decryptStoredWallet } from "./utils/decryptWallet"; import PhraseWallet from "./utils/generateWallet/phrase-wallet"; import { validateAddress } from "./utils/validateAddress"; +import { Sha256 } from 'asmcrypto.js' -// chrome.storage.local.clear(function() { -// var error = chrome.runtime.lastError; -// if (error) { -// console.error(error); -// } else { -// console.log('Local storage cleared'); -// } -// }); export const walletVersion = 2; // List of your API endpoints @@ -114,6 +108,15 @@ async function connection(hostname) { return isConnected; } +async function getTradeInfo(qortalAtAddress) { + const validApi = await findUsableApi(); + const response = await fetch(validApi + "/crosschain/trade/" + qortalAtAddress); + + if (!response?.ok) throw new Error("Cannot crosschain trade information"); + const data = await response.json(); + return data; +} + async function getBalanceInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -125,6 +128,24 @@ async function getBalanceInfo() { return data; } +const processTransactionVersion2Chat = async (body: any, validApi: string) => { + // const validApi = await findUsableApi(); + const url = validApi + "/transactions/process?apiVersion=2"; + return fetch(url, { + method: "POST", + headers: {}, + body: Base58.encode(body), + }).then(async (response) => { + try { + const json = await response.clone().json(); + return json; + } catch (e) { + return await response.text(); + } + }); +}; + + const processTransactionVersion2 = async (body: any, validApi: string) => { // const validApi = await findUsableApi(); const url = validApi + "/transactions/process?apiVersion=2"; @@ -237,9 +258,11 @@ async function decryptWallet({password, wallet, walletVersion}) { const response = await decryptStoredWallet(password, wallet); const wallet2 = new PhraseWallet(response, walletVersion); const keyPair = wallet2._addresses[0].keyPair; + const ltcPrivateKey = wallet2._addresses[0].ltcWallet.derivedMasterPrivateKey const toSave = { privateKey: Base58.encode(keyPair.privateKey), - publicKey: Base58.encode(keyPair.publicKey) + publicKey: Base58.encode(keyPair.publicKey), + ltcPrivateKey: ltcPrivateKey } const dataString = JSON.stringify(toSave) await new Promise((resolve, reject) => { @@ -272,6 +295,152 @@ async function decryptWallet({password, wallet, walletVersion}) { } } +async function signChatFunc(chatBytesArray, chatNonce, validApi, keyPair ){ + let response + try { + const signedChatBytes = signChat( + chatBytesArray, + chatNonce, + keyPair + ) + const res = await processTransactionVersion2Chat(signedChatBytes, validApi) + response = res + } catch (e) { + console.error(e) + console.error(e.message) + response = false + } + return response +} +function sbrk(size, heap) { + let brk = 512 * 1024 // stack top + let old = brk + brk += size + if (brk > heap.length) throw new Error('heap exhausted') + return old +} + +const computePow = async ({chatBytes, path, difficulty}) => { + let response = null + await new Promise((resolve, reject) => { + const _chatBytesArray = Object.keys(chatBytes).map(function (key) { + return chatBytes[key] + }) + const chatBytesArray = new Uint8Array(_chatBytesArray) + const chatBytesHash = new Sha256().process(chatBytesArray).finish().result + const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }) +const heap = new Uint8Array(memory.buffer) + + const hashPtr = sbrk(32, heap) + const hashAry = new Uint8Array(memory.buffer, hashPtr, 32) + hashAry.set(chatBytesHash) + const workBufferLength = 8 * 1024 * 1024 + const workBufferPtr = sbrk(workBufferLength, heap) + const importObject = { + env: { + memory: memory + } + } + function loadWebAssembly(filename, imports) { + // Fetch the file and compile it + return fetch(filename).then(response => response.arrayBuffer()).then(buffer => WebAssembly.compile(buffer)).then(module => { + // Create the instance. + return new WebAssembly.Instance(module, importObject) + }) + } + loadWebAssembly(path) + .then(wasmModule => { + response = { + nonce: wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty), chatBytesArray + } + resolve() + }) + }) + return response +} + +async function sendChat({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 difficulty = 8; + const callRequest = `curl -X 'POST' 'http://localhost:12391/crosschain/tradebot/respond' -H 'accept: text/plain' -H 'X-API-KEY: keykeykeykey' -H 'Content-Type: application/json' -d '{ "atAddress": "${message.atAddress}", "foreignKey": "${message.foreignKey}", "receivingAddress": "${message.receivingAddress}" }'`; + +// Construct the final JSON object +const finalJson = { + callRequest: callRequest, + extra: "whatever additional data goes here" +}; + const messageStringified = JSON.stringify(finalJson) + const {chatBytes} = await createTransaction( + 18, + keyPair, + { + timestamp: sendTimestamp, + recipient: qortAddress, + recipientPublicKey: recipientPublicKey, + hasChatReference: 0, + message: messageStringified, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1 + }, + + ) + const path = chrome.runtime.getURL('memory-pow.wasm.full'); + + + + + const {nonce, chatBytesArray} = await computePow({ chatBytes, path, difficulty }) + let _response = await signChatFunc(chatBytesArray, + nonce, "https://appnode.qortal.org", keyPair + ) + return _response +} + +async function createBuyOrderTx({crosschainAtInfo}){ + try { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const message = { + atAddress: crosschainAtInfo.qortalAtAddress, + foreignKey: parsedData.ltcPrivateKey, + receivingAddress: address + } + const res = await sendChat({qortAddress: "QXPejUe5Za1KD3zCMViWCX35AreMQ9H7ku", recipientPublicKey: "5hP6stDWybojoDw5t8z9D51nV945oMPX7qBd29rhX1G7", message }) + if(res?.signature){ + const decryptedMessage = await listenForChatMessageForBuyOrder({ + nodeBaseUrl: "https://appnode.qortal.org", + senderAddress: "QXPejUe5Za1KD3zCMViWCX35AreMQ9H7ku", + senderPublicKey: "5hP6stDWybojoDw5t8z9D51nV945oMPX7qBd29rhX1G7", + signature: res?.signature, + + }) + return decryptedMessage + } + + } catch (error) { + throw new Error(error.message); + } +} + async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { try { const confirmReceiver = await getNameOrAddress(receiver); @@ -316,7 +485,7 @@ async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { function fetchMessages(apiCall) { let retryDelay = 2000; // Start with a 2-second delay - const maxDuration = 360000; // Maximum duration set to 6 minutes + const maxDuration = 360000 * 2; // Maximum duration set to 12 minutes const startTime = Date.now(); // Record the start time // Promise to handle polling logic @@ -379,6 +548,42 @@ async function listenForChatMessage({ nodeBaseUrl, senderAddress, senderPublicKe } } +async function listenForChatMessageForBuyOrder({ 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}`; + const encodedMessageObj = await fetchMessages(apiCall) + + 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 decodedMessage = decryptChatMessage(encodedMessageObj.data, keyPair.privateKey, senderPublicKey, encodedMessageObj.reference) + const parsedMessage = JSON.parse(decodedMessage) + return parsedMessage + } catch (error) { + console.error(error) + throw new Error(error.message); + } +} + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request) { switch (request.action) { @@ -600,7 +805,87 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); } break; - case "connection": + case "buyOrder": { + const { qortalAtAddress, hostname } = request.payload; + getTradeInfo(qortalAtAddress) + .then((crosschainAtInfo) => { + const popupUrl = chrome.runtime.getURL("index.html"); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + w.tabs && + w.tabs.some( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ) + ); + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } else { + // No existing popup found, create a new one + chrome.system.display.getInfo((displays) => { + // Assuming the primary display is the first one (adjust logic as needed) + const primaryDisplay = displays[0]; + const screenWidth = primaryDisplay.bounds.width; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create({ + url: chrome.runtime.getURL("index.html"), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: request.timeout ? 0.9 * request.timeout : 20, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_REQUEST_BUY_ORDER", + payload: { + hostname, + crosschainAtInfo, + interactionId, + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); + + + }) + .catch((error) => { + console.error(error.message); + }); + } + + break; + case "connection": { const { hostname } = request.payload; connection(hostname) .then((isConnected) => { @@ -678,6 +963,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { .catch((error) => { console.error(error.message); }); + } + break; case "sendQort": { @@ -809,6 +1096,36 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } + break; + case "buyOrderConfirmation": { + const { crosschainAtInfo, isDecline } = request.payload; + const interactionId2 = request.payload.interactionId; + // Retrieve the stored sendResponse callback + const originalSendResponse = pendingResponses.get(interactionId2); + + if (originalSendResponse) { + if (isDecline) { + originalSendResponse({ error: "User has declined" }); + sendResponse(false); + pendingResponses.delete(interactionId2); + return; + } + createBuyOrderTx({ crosschainAtInfo }) + .then((res) => { + sendResponse(true); + originalSendResponse(res); + pendingResponses.delete(interactionId2); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + originalSendResponse({ error: error.message }); + }); + + } + } + + break; case "logout": { diff --git a/src/transactions/ChatBase.ts b/src/transactions/ChatBase.ts new file mode 100644 index 0000000..85cbdd9 --- /dev/null +++ b/src/transactions/ChatBase.ts @@ -0,0 +1,145 @@ +// @ts-nocheck + +import { QORT_DECIMALS, TX_TYPES } from '../constants/constants' +import nacl from '../deps/nacl-fast' +import Base58 from '../deps/Base58' +import utils from '../utils/utils' + +export default class ChatBase { + static get utils() { + return utils + } + + static get nacl() { + return nacl + } + + static get Base58() { + return Base58 + } + + constructor() { + this.fee = 0 + this.groupID = 0 + this.tests = [ + () => { + if (!(this._type >= 1 && this._type in TX_TYPES)) { + return 'Invalid type: ' + this.type + } + return true + }, + () => { + if (this._fee < 0) { + return 'Invalid fee: ' + this._fee / QORT_DECIMALS + } + return true + }, + () => { + if (this._groupID < 0 || !Number.isInteger(this._groupID)) { + return 'Invalid groupID: ' + this._groupID + } + return true + }, + () => { + if (!(new Date(this._timestamp)).getTime() > 0) { + return 'Invalid timestamp: ' + this._timestamp + } + return true + }, + () => { + if (!(this._lastReference instanceof Uint8Array && this._lastReference.byteLength == 64)) { + return 'Invalid last reference: ' + this._lastReference + } + return true + }, + () => { + if (!(this._keyPair)) { + return 'keyPair must be specified' + } + if (!(this._keyPair.publicKey instanceof Uint8Array && this._keyPair.publicKey.byteLength === 32)) { + return 'Invalid publicKey' + } + if (!(this._keyPair.privateKey instanceof Uint8Array && this._keyPair.privateKey.byteLength === 64)) { + return 'Invalid privateKey' + } + return true + } + ] + } + + set keyPair(keyPair) { + this._keyPair = keyPair + } + + set type(type) { + this.typeText = TX_TYPES[type] + this._type = type + this._typeBytes = this.constructor.utils.int32ToBytes(this._type) + } + + set groupID(groupID) { + this._groupID = groupID + this._groupIDBytes = this.constructor.utils.int32ToBytes(this._groupID) + } + + set timestamp(timestamp) { + this._timestamp = timestamp + this._timestampBytes = this.constructor.utils.int64ToBytes(this._timestamp) + } + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set lastReference(lastReference) { + this._lastReference = lastReference instanceof Uint8Array ? lastReference : this.constructor.Base58.decode(lastReference) + } + + get params() { + return [ + this._typeBytes, + this._timestampBytes, + this._groupIDBytes, + this._lastReference, + this._keyPair.publicKey + ] + } + + get chatBytes() { + const isValid = this.validParams() + if (!isValid.valid) { + throw new Error(isValid.message) + } + + let result = new Uint8Array() + + this.params.forEach(item => { + result = this.constructor.utils.appendBuffer(result, item) + }) + + this._chatBytes = result + + return this._chatBytes + } + + validParams() { + let finalResult = { + valid: true + } + + this.tests.some(test => { + const result = test() + if (result !== true) { + finalResult = { + valid: false, + message: result + } + return true + } + }) + + return finalResult + } + +} diff --git a/src/transactions/ChatTransaction.ts b/src/transactions/ChatTransaction.ts new file mode 100644 index 0000000..ec30c8c --- /dev/null +++ b/src/transactions/ChatTransaction.ts @@ -0,0 +1,92 @@ +// @ts-nocheck + +import ChatBase from './ChatBase' +import nacl from '../deps/nacl-fast' +import ed2curve from '../deps/ed2curve' +import { Sha256 } from 'asmcrypto.js' +import { CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP } from '../constants/constants' + +export default class ChatTransaction extends ChatBase { + constructor() { + super() + this.type = 18 + this.fee = 0 + } + + set recipientPublicKey(recipientPublicKey) { + this._base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? this.constructor.Base58.encode(recipientPublicKey) : recipientPublicKey + this._recipientPublicKey = this.constructor.Base58.decode(this._base58RecipientPublicKey) + } + + set proofOfWorkNonce(proofOfWorkNonce) { + this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce) + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + this._hasReceipient = new Uint8Array(1) + this._hasReceipient[0] = 1 + } + + set hasChatReference(hasChatReference) { + this._hasChatReference = new Uint8Array(1) + this._hasChatReference[0] = hasChatReference + } + + set chatReference(chatReference) { + this._chatReference = chatReference instanceof Uint8Array ? chatReference : this.constructor.Base58.decode(chatReference) + } + + set message(message) { + this.messageText = message; + this._message = this.constructor.utils.stringtoUTF8Array(message) + this._messageLength = this.constructor.utils.int32ToBytes(this._message.length) + } + + set isEncrypted(isEncrypted) { + this._isEncrypted = new Uint8Array(1) + this._isEncrypted[0] = isEncrypted + + if (isEncrypted === 1) { + const convertedPrivateKey = ed2curve.convertSecretKey(this._keyPair.privateKey) + const convertedPublicKey = ed2curve.convertPublicKey(this._recipientPublicKey) + const sharedSecret = new Uint8Array(32) + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + + this._chatEncryptionSeed = new Sha256().process(sharedSecret).finish().result + this._encryptedMessage = nacl.secretbox(this._message, this._lastReference.slice(0, 24), this._chatEncryptionSeed) + } + + this._myMessage = isEncrypted === 1 ? this._encryptedMessage : this._message + this._myMessageLenth = isEncrypted === 1 ? this.constructor.utils.int32ToBytes(this._myMessage.length) : this._messageLength + } + + set isText(isText) { + this._isText = new Uint8Array(1) + this._isText[0] = isText + } + + get params() { + const params = super.params + params.push( + this._proofOfWorkNonce, + this._hasReceipient, + this._recipient, + this._myMessageLenth, + this._myMessage, + this._isEncrypted, + this._isText, + this._feeBytes + ) + + // After the feature trigger timestamp we need to include chat reference + if (new Date(this._timestamp).getTime() >= CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP) { + params.push(this._hasChatReference) + + if (this._hasChatReference[0] == 1) { + params.push(this._chatReference) + } + } + return params + } +} diff --git a/src/transactions/signChat.ts b/src/transactions/signChat.ts new file mode 100644 index 0000000..20b2240 --- /dev/null +++ b/src/transactions/signChat.ts @@ -0,0 +1,40 @@ +// @ts-nocheck + +import nacl from '../deps/nacl-fast' +import utils from '../utils/utils' + +export const signChat = (chatBytes, nonce, keyPair) => { + + if (!chatBytes) { + throw new Error('Chat Bytes not defined') + } + + if (!nonce) { + throw new Error('Nonce not defined') + } + + if (!keyPair) { + throw new Error('keyPair not defined') + } + + const _nonce = utils.int32ToBytes(nonce) + + if (chatBytes.length === undefined) { + const _chatBytesBuffer = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; }) + + const chatBytesBuffer = new Uint8Array(_chatBytesBuffer) + chatBytesBuffer.set(_nonce, 112) + + const signature = nacl.sign.detached(chatBytesBuffer, keyPair.privateKey) + + return utils.appendBuffer(chatBytesBuffer, signature) + } else { + const chatBytesBuffer = new Uint8Array(chatBytes) + chatBytesBuffer.set(_nonce, 112) + + const signature = nacl.sign.detached(chatBytesBuffer, keyPair.privateKey) + + return utils.appendBuffer(chatBytesBuffer, signature) + } +} + diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index b809830..b2af068 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -1,11 +1,12 @@ // @ts-nocheck import PaymentTransaction from './PaymentTransaction.js' +import ChatTransaction from './ChatTransaction.js' export const transactionTypes = { 2: PaymentTransaction, - + 18: ChatTransaction }