diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..8f6f422 Binary files /dev/null and b/dist.zip differ diff --git a/package-lock.json b/package-lock.json index 8d577ee..5639f96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.16.4", "@mui/material": "^5.15.14", "@testing-library/jest-dom": "^6.4.6", "@testing-library/user-event": "^14.5.2", @@ -1221,6 +1222,31 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", + "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.15.14", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", diff --git a/package.json b/package.json index 67930ed..72083a2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.16.4", "@mui/material": "^5.15.14", "@testing-library/jest-dom": "^6.4.6", "@testing-library/user-event": "^14.5.2", diff --git a/public/content-script.js b/public/content-script.js index ff844c6..2fc77f7 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,66 @@ 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_LTC_BALANCE'){ + + + const hostname = window.location.hostname + const res = await connection(hostname) + if(!res){ + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "USER_INFO", data: { + error: "Not authorized" + }, requestId } + })); + return + } + chrome.runtime.sendMessage({ action: "ltcBalance", payload: { + hostname + }, timeout }, (response) => { + if (response.error) { + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "LTC_BALANCE", data: { + error: response.error + }, requestId } + })); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "LTC_BALANCE", data: response, requestId } + })); + } + }); + } else if (type === 'REQUEST_AUTHENTICATION') { const hostname = window.location.hostname const res = await connection(hostname) if(!res){ @@ -187,5 +245,13 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { type: "LOGOUT", from: 'qortal' }, "*"); - } -}); \ No newline at end of file + } else if (message.type === "RESPONSE_FOR_TRADES") { + // Notify the web page + window.postMessage({ + type: "RESPONSE_FOR_TRADES", + from: 'qortal', + payload: message.message + }, "*"); +} +}); + diff --git a/public/manifest.json b/public/manifest.json index 21ec40c..6f7990a 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Qortal", - "version": "1.0.0", + "version": "1.2.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 0000000..073b179 Binary files /dev/null and b/public/memory-pow.wasm.full differ diff --git a/src/App.tsx b/src/App.tsx index 7814820..a01a4eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,14 +3,16 @@ import reactLogo from "./assets/react.svg"; import viteLogo from "/vite.svg"; import "./App.css"; import { useDropzone } from "react-dropzone"; -import { Box, Input, InputLabel, Tooltip, Typography } from "@mui/material"; +import { Box, CircularProgress, Input, InputLabel, Tooltip, Typography } from "@mui/material"; import { decryptStoredWallet } from "./utils/decryptWallet"; import { CountdownCircleTimer } from "react-countdown-circle-timer"; import Logo1 from "./assets/svgs/Logo1.svg"; import Logo1Dark from "./assets/svgs/Logo1Dark.svg"; - +import RefreshIcon from '@mui/icons-material/Refresh'; import Logo2 from "./assets/svgs/Logo2.svg"; import Copy from "./assets/svgs/Copy.svg"; +import ltcLogo from './assets/ltc.png'; +import qortLogo from './assets/qort.png' import { CopyToClipboard } from "react-copy-to-clipboard"; import Download from "./assets/svgs/Download.svg"; import Logout from "./assets/svgs/Logout.svg"; @@ -55,17 +57,24 @@ type extStates = | "transfer-success-regular" | "transfer-success-request" | "wallet-dropped" + | "web-app-request-buy-order" + | "buy-order-submitted" ; function App() { const [extState, setExtstate] = useState("not-authenticated"); const [backupjson, setBackupjson] = useState(null); const [rawWallet, setRawWallet] = useState(null); + const [ltcBalanceLoading, setLtcBalanceLoading] = useState(false); + const [qortBalanceLoading, setQortBalanceLoading] = useState(false) const [decryptedWallet, setdecryptedWallet] = useState(null); const [requestConnection, setRequestConnection] = useState(null); + const [requestBuyOrder, setRequestBuyOrder] = useState(null); + const [authenticatedMode, setAuthenticatedMode] = useState('qort') const [requestAuthentication, setRequestAuthentication] = useState(null); const [userInfo, setUserInfo] = useState(null); const [balance, setBalance] = useState(null); + const [ltcBalance, setLtcBalance] = useState(null) const [paymentTo, setPaymentTo] = useState(""); const [paymentAmount, setPaymentAmount] = useState(0); const [paymentPassword, setPaymentPassword] = useState(""); @@ -75,7 +84,7 @@ function App() { const [walletToBeDownloaded, setWalletToBeDownloaded] = useState(null); const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] = useState(""); - const [authenticatePassword, setAuthenticatePassword] = + const [authenticatePassword, setAuthenticatePassword] = useState(""); const [sendqortState, setSendqortState] = useState(null); const [isLoading, setIsLoading] = useState(false) @@ -85,11 +94,11 @@ function App() { ] = useState(""); const [walletToBeDownloadedError, setWalletToBeDownloadedError] = useState(""); - const [walletToBeDecryptedError, setWalletToBeDecryptedError] = + const [walletToBeDecryptedError, setWalletToBeDecryptedError] = useState(""); - const holdRefExtState = useRef("not-authenticated") - useEffect(()=> { - if(extState){ + const holdRefExtState = useRef("not-authenticated") + useEffect(() => { + if (extState) { holdRefExtState.current = extState } }, [extState]) @@ -118,14 +127,14 @@ function App() { // Read the file as text reader.readAsText(file); }); - + let error: any = null; let pf: any; try { if (typeof fileContents !== "string") return; pf = JSON.parse(fileContents); - } catch (e) {} + } catch (e) { } try { const requiredFields = [ @@ -153,7 +162,7 @@ function App() { }, }); - + const saveWalletFunc = async (password: string) => { let wallet = structuredClone(rawWallet); @@ -163,7 +172,7 @@ function App() { wallet = await wallet2.generateSaveWalletData( password, crypto.kdfThreads, - () => {} + () => { } ); setWalletToBeDownloaded({ @@ -192,19 +201,30 @@ function App() { tabs[0].id, { from: "popup", subject: "anySubject" }, function (response) { - + } ); } ); }; - - + + const getBalanceFunc = () => { + setQortBalanceLoading(true) chrome.runtime.sendMessage({ action: "balance" }, (response) => { if (response && !response?.error) { setBalance(response); } + setQortBalanceLoading(false) + }); + }; + const getLtcBalanceFunc = () => { + setLtcBalanceLoading(true) + chrome.runtime.sendMessage({ action: "ltcBalance" }, (response) => { + if (response && !response?.error) { + setLtcBalance(response); + } + setLtcBalanceLoading(false) }); }; const sendCoinFunc = () => { @@ -253,7 +273,7 @@ function App() { useEffect(() => { // Listen for messages from the background script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - + // Check if the message is to update the state if (message.action === "UPDATE_STATE_CONFIRM_SEND_QORT") { // Update the component state with the received 'sendqort' state @@ -266,6 +286,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); @@ -276,7 +300,7 @@ function App() { }); }, []); - + //param = isDecline const confirmPayment = (isDecline: boolean) => { if (isDecline) { @@ -301,10 +325,7 @@ function App() { ); return; } - // if (!paymentPassword) { - // setSendPaymentError("Please enter your wallet password"); - // return; - // } + setIsLoading(true) chrome.runtime.sendMessage( { @@ -321,9 +342,51 @@ 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("buy-order-submitted"); + setCountdown(null); + } else { + setSendPaymentError( response?.error || "Unable to perform payment. Please try again." ); @@ -355,18 +418,18 @@ function App() { // useEffect(()=> { // rawWalletRef.current = rawWallet // }, [rawWallet]) - + useEffect(() => { try { setIsLoading(true) 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"); } }); - } catch (error) {} finally { + } catch (error) { } finally { setIsLoading(false) } }, []); @@ -380,7 +443,7 @@ function App() { } }); getBalanceFunc(); - } catch (error) {} + } catch (error) { } }, [address]); useEffect(() => { @@ -389,6 +452,12 @@ function App() { }; }, []); + useEffect(()=> { + if(authenticatedMode === 'ltc' && !ltcBalanceLoading && ltcBalance === null ){ + getLtcBalanceFunc() + } + }, [authenticatedMode]) + const confirmPasswordToDownload = async () => { try { setWalletToBeDownloadedError(""); @@ -397,8 +466,8 @@ function App() { return; } setIsLoading(true) - await new Promise((res)=> { - setTimeout(()=> { + await new Promise((res) => { + setTimeout(() => { res() }, 250) }) @@ -440,8 +509,8 @@ function App() { return; } setIsLoading(true) - await new Promise((res)=> { - setTimeout(()=> { + await new Promise((res) => { + setTimeout(() => { res() }, 250) }) @@ -449,12 +518,14 @@ function App() { const wallet = await res.generateSaveWalletData( walletToBeDownloadedPassword, crypto.kdfThreads, - () => {} + () => { } ); - chrome.runtime.sendMessage({ action: "decryptWallet", payload: { - password: walletToBeDownloadedPassword, - wallet - } }, (response) => { + chrome.runtime.sendMessage({ + action: "decryptWallet", payload: { + password: walletToBeDownloadedPassword, + wallet + } + }, (response) => { if (response && !response?.error) { setRawWallet(wallet); setWalletToBeDownloaded({ @@ -468,18 +539,18 @@ function App() { } }); getBalanceFunc(); - } else if(response?.error){ + } else if (response?.error) { setIsLoading(false) setWalletToBeDecryptedError(response.error) } }); - - + + } catch (error: any) { setWalletToBeDownloadedError(error?.message); setIsLoading(false) - } + } }; const logoutFunc = () => { @@ -489,7 +560,7 @@ function App() { resetAllStates(); } }); - } catch (error) {} + } catch (error) { } }; const returnToMain = () => { @@ -510,9 +581,11 @@ function App() { setRawWallet(null); setdecryptedWallet(null); setRequestConnection(null); + setRequestBuyOrder(null) setRequestAuthentication(null); setUserInfo(null); setBalance(null); + setLtcBalance(null) setPaymentTo(""); setPaymentAmount(0); setPaymentPassword(""); @@ -526,19 +599,21 @@ function App() { setSendqortState(null); }; - const authenticateWallet = async()=> { + const authenticateWallet = async () => { try { setIsLoading(true) setWalletToBeDecryptedError('') - await new Promise((res)=> { - setTimeout(()=> { + await new Promise((res) => { + setTimeout(() => { res() }, 250) }) - chrome.runtime.sendMessage({ action: "decryptWallet", payload: { - password: authenticatePassword, - wallet: rawWallet - } }, (response) => { + chrome.runtime.sendMessage({ + action: "decryptWallet", payload: { + password: authenticatePassword, + wallet: rawWallet + } + }, (response) => { if (response && !response?.error) { setAuthenticatePassword(""); setExtstate("authenticated"); @@ -550,14 +625,14 @@ function App() { } }); getBalanceFunc(); - } else if(response?.error){ + } else if (response?.error) { setIsLoading(false) setWalletToBeDecryptedError(response.error) } }); } catch (error) { setWalletToBeDecryptedError('Unable to authenticate. Wrong password') - } + } } return ( @@ -636,55 +711,91 @@ function App() { - - - - {userInfo?.name} - - - - - {rawWallet?.address0?.slice(0, 6)}... - {rawWallet?.address0?.slice(-4)} - - - - {(balance >= 0) && ( - - {balance?.toFixed(2)} QORT - - )} - - {/*

balance: {balance}

*/} - - {/* { - setExtstate("download-wallet"); - }} - > - Download Wallet - */} - { - setExtstate("send-qort"); - }} - > - Transfer QORT - + {authenticatedMode === 'ltc' ? ( + <> + + + + + {rawWallet?.ltcAddress?.slice(0, 6)}... + {rawWallet?.ltcAddress?.slice(-4)} + + + + {ltcBalanceLoading && } + {ltcBalance && !isNaN(+ltcBalance) && !ltcBalanceLoading && ( + + + {ltcBalance} LTC + + + + + )} + + + ) : ( + <> + + + + {userInfo?.name} + + + + + {rawWallet?.address0?.slice(0, 6)}... + {rawWallet?.address0?.slice(-4)} + + + + {qortBalanceLoading && } + {balance && ( + + {balance} QORT + + )} + + + { + setExtstate("send-qort"); + }} + > + Transfer QORT + + + )} +
@@ -705,6 +816,25 @@ function App() { cursor: "pointer", }} /> + + {authenticatedMode === 'qort' && ( + { + setAuthenticatedMode('ltc') + }} src={ltcLogo} style={{ + cursor: "pointer", + width: '20px', + height: 'auto' + }} /> + )} + {authenticatedMode === 'ltc' && ( + { + setAuthenticatedMode('qort') + }} src={qortLogo} style={{ + cursor: "pointer", + width: '20px', + height: 'auto' + }} /> + )}
)} @@ -766,7 +896,7 @@ function App() { fontWeight: 700, }} > - {balance?.toFixed(2)} QORT + {balance} QORT @@ -817,6 +947,91 @@ function App() { )} + {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" && ( <> @@ -1011,7 +1226,7 @@ function App() { style={{ cursor: "pointer", }} - onClick={()=> { + onClick={() => { setRawWallet(null); setExtstate("not-authenticated"); }} @@ -1046,32 +1261,32 @@ function App() { - - <> - - Wallet Password - - - - setAuthenticatePassword(e.target.value) + + <> + + Wallet Password + + + + setAuthenticatePassword(e.target.value) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + authenticateWallet(); } - onKeyDown={(e) => { - if (e.key === "Enter") { - authenticateWallet(); - } - }} - /> - - - Authenticate - - - {walletToBeDecryptedError} - - + }} + /> + + + Authenticate + + + {walletToBeDecryptedError} + + )} {extState === "download-wallet" && ( @@ -1158,34 +1373,34 @@ function App() { <> {!walletToBeDownloaded && ( <> - - - { - setExtstate("not-authenticated") - }} - src={Return} - /> - + + + { + setExtstate("not-authenticated") + }} + src={Return} + /> +
- - -
+ width: '136px', + height: '154px' + }}> + + + )} + {extState === "buy-order-submitted" && ( + <> + + + + + Your buy order was submitted + + + { + window.close(); + }} + > + Close + + + )} {countdown && ( { + // 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(); @@ -232,14 +275,19 @@ async function getNameOrAddress(receiver) { } } -async function decryptWallet({password, wallet, walletVersion}) { +async function decryptWallet({ password, wallet, walletVersion }) { try { 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 ltcPublicKey = wallet2._addresses[0].ltcWallet.derivedMasterPublicKey + const ltcAddress = wallet2._addresses[0].ltcWallet.address const toSave = { privateKey: Base58.encode(keyPair.privateKey), - publicKey: Base58.encode(keyPair.publicKey) + publicKey: Base58.encode(keyPair.publicKey), + ltcPrivateKey: ltcPrivateKey, + ltcPublicKey : ltcPublicKey } const dataString = JSON.stringify(toSave) await new Promise((resolve, reject) => { @@ -253,7 +301,8 @@ async function decryptWallet({password, wallet, walletVersion}) { }); const newWallet = { ...wallet, - publicKey: Base58.encode(keyPair.publicKey) + publicKey: Base58.encode(keyPair.publicKey), + ltcAddress: ltcAddress } await new Promise((resolve, reject) => { chrome.storage.local.set({ walletInfo: newWallet }, () => { @@ -265,9 +314,163 @@ async function decryptWallet({password, wallet, walletVersion}) { }); }); - return true; + return true; + } catch (error) { + console.log({ error }) + throw new Error(error.message); + } +} + +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 jsonData = { + atAddress: message.atAddress, + foreignKey: message.foreignKey, + receivingAddress: message.receivingAddress + }; + const finalJson = { + callRequest: jsonData, + 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 + ) + if (_response?.error) { + throw new Error(_response?.message) + } + 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: proxyAccountAddress, recipientPublicKey: proxyAccountPublicKey, message }) + if (res?.signature) { + + listenForChatMessageForBuyOrder({ + nodeBaseUrl: buyTradeNodeBaseUrl, + senderAddress: proxyAccountAddress, + senderPublicKey: proxyAccountPublicKey, + signature: res?.signature, + + }) + return { atAddress: crosschainAtInfo.qortalAtAddress, chatSignature: res?.signature, node: buyTradeNodeBaseUrl, qortAddress: address } + } else { + throw new Error("Unable to send buy order message") + } + } catch (error) { - console.log({error}) throw new Error(error.message); } } @@ -279,7 +482,7 @@ async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { throw new Error("Invalid receiver address or name"); const wallet = await getSaveWallet(); let keyPair = '' - if(skipConfirmPassword){ + if (skipConfirmPassword) { const resKeyPair = await getKeyPair() const parsedData = JSON.parse(resKeyPair) const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -290,11 +493,11 @@ async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { }; } else { const response = await decryptStoredWallet(password, wallet); - const wallet2 = new PhraseWallet(response, walletVersion); + const wallet2 = new PhraseWallet(response, walletVersion); - keyPair = wallet2._addresses[0].keyPair + keyPair = wallet2._addresses[0].keyPair } - + const lastRef = await getLastRef(); const fee = await sendQortFee(); @@ -316,31 +519,86 @@ 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 return new Promise((resolve, reject) => { - const attemptFetch = async () => { - if (Date.now() - startTime > maxDuration) { - return reject(new Error("Maximum polling time exceeded")); - } + const attemptFetch = async () => { + if (Date.now() - startTime > maxDuration) { + return reject(new Error("Maximum polling time exceeded")); + } - try { - const response = await fetch(apiCall); - const data = await response.json(); - if (data && data.length > 0) { - resolve(data[0]); // Resolve the promise when data is found - } else { - setTimeout(attemptFetch, retryDelay); - retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes - } - } catch (error) { - reject(error); // Reject the promise on error - } - }; + try { + const response = await fetch(apiCall); + const data = await response.json(); + if (data && data.length > 0) { + resolve(data[0]); // Resolve the promise when data is found + } else { + setTimeout(attemptFetch, retryDelay); + retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes + } + } catch (error) { + reject(error); // Reject the promise on error + } + }; - attemptFetch(); // Initial call to start the polling + attemptFetch(); // Initial call to start the polling + }); +} + +async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { + let retryDelay = 2000; // Start with a 2-second delay + const maxDuration = 360000 * 2; // Maximum duration set to 12 minutes + const startTime = Date.now(); // Record the start time + let triedChatMessage = [] + // Promise to handle polling logic + await new Promise((res)=> { + setTimeout(() => { + res() + }, 40000); + }) + return new Promise((resolve, reject) => { + const attemptFetch = async () => { + if (Date.now() - startTime > maxDuration) { + return reject(new Error("Maximum polling time exceeded")); + } + + try { + const response = await fetch(apiCall); + let data = await response.json(); + data = data.filter((item) => !triedChatMessage.includes(item.signature)) + if (data && data.length > 0) { + const encodedMessageObj = data[0] + 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) + if (parsedMessage?.extra?.chatRequestSignature === signature) { + resolve(parsedMessage); + } else { + triedChatMessage.push(encodedMessageObj.signature) + setTimeout(attemptFetch, retryDelay); + retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes + } + // Resolve the promise when data is found + } else { + setTimeout(attemptFetch, retryDelay); + retryDelay = Math.min(retryDelay * 2, 360000); // Ensure delay does not exceed 6 minutes + } + } catch (error) { + reject(error); // Reject the promise on error + } + }; + + attemptFetch(); // Initial call to start the polling }); } @@ -361,17 +619,17 @@ async function listenForChatMessage({ nodeBaseUrl, senderAddress, senderPublicKe const after = timestamp - 5000 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 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) return { secretCode: decodedMessage }; } catch (error) { console.error(error) @@ -379,6 +637,46 @@ 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 parsedMessageObj = await fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) + + // 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) + chrome.tabs.query({}, function (tabs) { + tabs.forEach(tab => { + chrome.tabs.sendMessage(tab.id, { type: "RESPONSE_FOR_TRADES", message: parsedMessageObj }); + }); + }); + } catch (error) { + console.error(error) + throw new Error(error.message); + } +} + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request) { switch (request.action) { @@ -396,21 +694,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); break; case "getWalletInfo": - - getKeyPair().then(()=> { - chrome.storage.local.get(["walletInfo"], (result) => { - if (chrome.runtime.lastError) { - sendResponse({ error: chrome.runtime.lastError.message }); - } else if (result.walletInfo) { - sendResponse({ walletInfo: result.walletInfo }); - } else { - sendResponse({ error: "No wallet info found" }); - } - }); - }).catch((error)=> { - sendResponse({ error: error.message }); - }) - + + getKeyPair().then(() => { + chrome.storage.local.get(["walletInfo"], (result) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else if (result.walletInfo) { + sendResponse({ walletInfo: result.walletInfo }); + } else { + sendResponse({ error: "No wallet info found" }); + } + }); + }).catch((error) => { + sendResponse({ error: error.message }); + }) + break; case "validApi": findUsableApi() @@ -439,21 +737,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); break; - case "decryptWallet": { - const { password, wallet } = request.payload; + case "decryptWallet": { + const { password, wallet } = request.payload; - decryptWallet({ - password, wallet, walletVersion + decryptWallet({ + password, wallet, walletVersion + }) + .then((hasDecrypted) => { + sendResponse(hasDecrypted); }) - .then((hasDecrypted) => { - sendResponse(hasDecrypted); - }) - .catch((error) => { - sendResponse({ error: error?.message }); - console.error(error.message); - }); - } - + .catch((error) => { + sendResponse({ error: error?.message }); + console.error(error.message); + }); + } + break; case "balance": getBalanceInfo() @@ -464,6 +762,18 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); break; + case "ltcBalance": { + getLTCBalance() + .then((balance) => { + sendResponse(balance); + }) + .catch((error) => { + console.error(error.message); + }); + + + } + break; case "sendCoin": { const { receiver, password, amount } = request.payload; @@ -600,7 +910,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 +1068,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { .catch((error) => { console.error(error.message); }); + } + break; case "sendQort": { @@ -809,6 +1201,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": { @@ -817,11 +1239,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // Handle error console.error(chrome.runtime.lastError.message); } else { - chrome.tabs.query({}, function(tabs) { + chrome.tabs.query({}, function (tabs) { tabs.forEach(tab => { - chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); + chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); }); - }); + }); // Data removed successfully sendResponse(true); } 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..8814c2a --- /dev/null +++ b/src/transactions/signChat.ts @@ -0,0 +1,39 @@ +// @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 }