From 6b68ac36859ca367f1a0db40f6cb9d200bab94ac Mon Sep 17 00:00:00 2001 From: philippe-ftx <62417741+philippe-ftx@users.noreply.github.com> Date: Mon, 14 Sep 2020 20:57:55 +0200 Subject: [PATCH] Transaction parsing for Serum DEX instructions (#8) Transaction parsing for Serum DEX instructions --- package.json | 1 + src/components/instructions/DexInstruction.js | 72 ++++ src/components/instructions/LabelValue.js | 18 + src/components/instructions/NewOrder.js | 47 +++ .../instructions/SystemInstruction.js | 51 +++ .../instructions/TokenInstruction.js | 70 ++++ .../instructions/UnknownInstruction.js | 20 + src/pages/PopupPage.js | 111 ++++- src/utils/transactions.js | 382 ++++++++++++++++++ yarn.lock | 47 ++- 10 files changed, 810 insertions(+), 9 deletions(-) create mode 100644 src/components/instructions/DexInstruction.js create mode 100644 src/components/instructions/LabelValue.js create mode 100644 src/components/instructions/NewOrder.js create mode 100644 src/components/instructions/SystemInstruction.js create mode 100644 src/components/instructions/TokenInstruction.js create mode 100644 src/components/instructions/UnknownInstruction.js create mode 100644 src/utils/transactions.js diff --git a/package.json b/package.json index 6cee13e..7fa57ba 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", + "@project-serum/serum": "^0.12.7", "@solana/web3.js": "^0.71.9", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", diff --git a/src/components/instructions/DexInstruction.js b/src/components/instructions/DexInstruction.js new file mode 100644 index 0000000..a0ab066 --- /dev/null +++ b/src/components/instructions/DexInstruction.js @@ -0,0 +1,72 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import LabelValue from './LabelValue'; +import { useWallet, useWalletPublicKeys } from '../../utils/wallet'; + +const TYPE_LABELS = { + cancelOrder: 'Cancel order', + newOrder: 'Place order', + settleFunds: 'Settle funds', + matchOrders: 'Match orders', +}; + +const DATA_LABELS = { + side: { label: 'Side', address: false }, + orderId: { label: 'Order Id', address: false }, + limit: { label: 'Limit', address: false }, + basePubkey: { label: 'Base wallet', address: true }, + quotePubkey: { label: 'Quote wallet', address: true }, +}; + +export default function DexInstruction({ instruction, onOpenAddress }) { + const wallet = useWallet(); + const [publicKeys] = useWalletPublicKeys(); + const { type, data, marketInfo } = instruction; + + const marketLabel = + marketInfo?.name + (marketInfo?.deprecated ? '(deprecated)' : '') || + marketInfo?.address?.toBase58() || + 'Unknown'; + + const getAddressValue = (address) => { + const isOwned = publicKeys.some((ownedKey) => ownedKey.equals(address)); + const isOwner = wallet.publicKey.equals(address); + return isOwner + ? 'This wallet' + : (isOwned ? '(Owned) ' : '') + address?.toBase58(); + }; + + return ( + <> + + {TYPE_LABELS[type]} + + onOpenAddress(marketInfo?.address?.toBase58())} + /> + {data && + Object.entries(data).map(([key, value]) => { + const dataLabel = DATA_LABELS[key]; + if (!dataLabel) { + return null; + } + const { label, address } = dataLabel; + return ( + address && onOpenAddress(value?.toBase58())} + /> + ); + })} + + ); +} diff --git a/src/components/instructions/LabelValue.js b/src/components/instructions/LabelValue.js new file mode 100644 index 0000000..d4fc29b --- /dev/null +++ b/src/components/instructions/LabelValue.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Link from '@material-ui/core/Link'; +import Typography from '@material-ui/core/Typography'; + +export default function LabelValue({ label, value, link = false, onClick }) { + return ( + + {label}:{' '} + {link ? ( + + {value} + + ) : ( + {value} + )} + + ); +} diff --git a/src/components/instructions/NewOrder.js b/src/components/instructions/NewOrder.js new file mode 100644 index 0000000..c796beb --- /dev/null +++ b/src/components/instructions/NewOrder.js @@ -0,0 +1,47 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import LabelValue from './LabelValue'; + +export default function Neworder({ instruction, onOpenAddress }) { + const { data, market, marketInfo } = instruction; + const { side, limitPrice, maxQuantity, orderType } = data; + + const marketLabel = + marketInfo?.name + (marketInfo?.deprecated ? '(deprecated)' : '') || + marketInfo?.address?.toBase58() || + 'Unknown'; + + return ( + <> + + Place an order + + onOpenAddress(marketInfo?.address?.toBase58())} + /> + + + + + + ); +} diff --git a/src/components/instructions/SystemInstruction.js b/src/components/instructions/SystemInstruction.js new file mode 100644 index 0000000..86cfd48 --- /dev/null +++ b/src/components/instructions/SystemInstruction.js @@ -0,0 +1,51 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import LabelValue from './LabelValue'; + +const TYPE_LABELS = { + create: 'Create account', +}; + +const DATA_LABELS = { + toPubkey: { label: 'To', address: true }, + accountPubkey: { label: 'Account', address: true }, + basePubkey: { label: 'Base', address: true }, + seed: { label: 'Seed', address: false }, + noncePubkey: { label: 'Nonce', address: true }, + authorizedPubkey: { label: 'Authorized', address: true }, + newAuthorizedPubkey: { label: 'New authorized', address: true }, + newAccountPubkey: { label: 'New account', address: true }, + amount: { label: 'Amount', address: false }, +}; + +export default function SystemInstruction({ instruction, onOpenAddress }) { + const { type, data } = instruction; + + return ( + <> + + {TYPE_LABELS[type]} + + {data && + Object.entries(data).map(([key, value]) => { + const dataLabel = DATA_LABELS[key]; + if (!dataLabel) { + return null; + } + const { label, address } = dataLabel; + return ( + address && onOpenAddress(value?.toBase58())} + /> + ); + })} + + ); +} diff --git a/src/components/instructions/TokenInstruction.js b/src/components/instructions/TokenInstruction.js new file mode 100644 index 0000000..fc39b6a --- /dev/null +++ b/src/components/instructions/TokenInstruction.js @@ -0,0 +1,70 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import LabelValue from './LabelValue'; +import { useWallet, useWalletPublicKeys } from '../../utils/wallet'; +import { TOKEN_MINTS } from '@project-serum/serum'; + +const TYPE_LABELS = { + initializeMint: 'Initialize mint', + initializeAccount: 'Initialize account', + transfer: 'Transfer', + approve: 'Approve', + mintTo: 'Mint to', + closeAccount: 'Close account', +}; + +const DATA_LABELS = { + amount: { label: 'Amount', address: false }, + accountPubkey: { label: 'Account', address: true }, + mintPubkey: { label: 'Mint', address: true }, + sourcePubkey: { label: 'Source', address: true }, + destinationPubkey: { label: 'Destination', address: true }, + ownerPubkey: { label: 'Owner', address: true }, +}; + +export default function TokenInstruction({ instruction, onOpenAddress }) { + const wallet = useWallet(); + const [publicKeys] = useWalletPublicKeys(); + const { type, data } = instruction; + + const getAddressValue = (address) => { + const tokenMint = TOKEN_MINTS.find((token) => + token.address.equals(address), + ); + const isOwned = publicKeys.some((ownedKey) => ownedKey.equals(address)); + const isOwner = wallet.publicKey.equals(address); + return tokenMint + ? tokenMint.name + : isOwner + ? 'This wallet' + : (isOwned ? '(Owned) ' : '') + address?.toBase58(); + }; + + return ( + <> + + {TYPE_LABELS[type]} + + {data && + Object.entries(data).map(([key, value]) => { + const dataLabel = DATA_LABELS[key]; + if (!dataLabel) { + return null; + } + const { label, address } = dataLabel; + return ( + address && onOpenAddress(value?.toBase58())} + /> + ); + })} + + ); +} diff --git a/src/components/instructions/UnknownInstruction.js b/src/components/instructions/UnknownInstruction.js new file mode 100644 index 0000000..ed3b48f --- /dev/null +++ b/src/components/instructions/UnknownInstruction.js @@ -0,0 +1,20 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import bs58 from 'bs58'; + +export default function UnknownInstruction({ message }) { + return ( + <> + + Send the following transaction: + + + {bs58.encode(message)} + + + ); +} diff --git a/src/pages/PopupPage.js b/src/pages/PopupPage.js index ab04b5a..21aac35 100644 --- a/src/pages/PopupPage.js +++ b/src/pages/PopupPage.js @@ -1,6 +1,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useWallet } from '../utils/wallet'; -import { Typography } from '@material-ui/core'; +import { decodeMessage } from '../utils/transactions'; +import { useConnection, useSolanaExplorerUrlSuffix } from '../utils/connection'; +import { Typography, Divider } from '@material-ui/core'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Box from '@material-ui/core/Box'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardActions from '@material-ui/core/CardActions'; @@ -10,6 +14,11 @@ import { makeStyles } from '@material-ui/core/styles'; import assert from 'assert'; import bs58 from 'bs58'; import nacl from 'tweetnacl'; +import NewOrder from '../components/instructions/NewOrder'; +import UnknownInstruction from '../components/instructions/UnknownInstruction'; +import SystemInstruction from '../components/instructions/SystemInstruction'; +import DexInstruction from '../components/instructions/DexInstruction'; +import TokenInstruction from '../components/instructions/TokenInstruction'; export default function PopupPage({ opener }) { const wallet = useWallet(); @@ -182,18 +191,104 @@ function ApproveConnectionForm({ origin, onApprove }) { function ApproveSignatureForm({ origin, message, onApprove, onReject }) { const classes = useStyles(); + const explorerUrlSuffix = useSolanaExplorerUrlSuffix(); + const connection = useConnection(); + const wallet = useWallet(); - // TODO: decode message + const [parsing, setParsing] = useState(true); + const [instructions, setInstructions] = useState(null); + + useEffect(() => { + decodeMessage(connection, wallet, message).then((instructions) => { + setInstructions(instructions); + setParsing(false); + }); + }, [message, connection, wallet]); + + const onOpenAddress = (address) => { + address && + window.open( + 'https://explorer.solana.com/address/' + address + explorerUrlSuffix, + '_blank', + ); + }; + + const getContent = (instruction) => { + switch (instruction?.type) { + case 'cancelOrder': + case 'matchOrders': + case 'settleFunds': + return ( + + ); + case 'closeAccount': + case 'initializeAccount': + case 'transfer': + case 'approve': + case 'mintTo': + return ( + + ); + case 'create': + return ( + + ); + case 'newOrder': + return ( + + ); + default: + return ; + } + }; return ( - - {origin} would like to send the following transaction: - - - {bs58.encode(message)} - + {parsing ? ( + <> +
+ + + Parsing transaction: + +
+ + {bs58.encode(message)} + + + ) : ( + <> + + {instructions + ? `${origin} wants to:` + : `Unknown transaction data`} + + {instructions ? ( + instructions.map((instruction) => ( + + {getContent(instruction)} + + + )) + ) : ( + + )} + + )}
diff --git a/src/utils/transactions.js b/src/utils/transactions.js new file mode 100644 index 0000000..3e45f7a --- /dev/null +++ b/src/utils/transactions.js @@ -0,0 +1,382 @@ +import bs58 from 'bs58'; +import { Message, SystemInstruction } from '@solana/web3.js'; +import { + decodeInstruction, + decodeTokenInstructionData, + Market, + MARKETS, + TokenInstructions, + SETTLE_FUNDS_BASE_WALLET_INDEX, + SETTLE_FUNDS_QUOTE_WALLET_INDEX, +} from '@project-serum/serum'; + +export const decodeMessage = async (connection, wallet, message) => { + // get message object + const transactionMessage = Message.from(message); + if (!transactionMessage?.instructions || !transactionMessage?.accountKeys) { + return; + } + + // get owned keys (used for security checks) + const publicKey = wallet.publicKey; + + // market caching + const marketCache = {}; + + // get instructions + const instructions = []; + for (var i = 0; i < transactionMessage.instructions.length; i++) { + let transactionInstruction = transactionMessage.instructions[i]; + const instruction = await toInstruction( + connection, + publicKey, + transactionMessage?.accountKeys, + transactionInstruction, + marketCache, + i, + ); + instructions.push(instruction); + } + return instructions; +}; + +const toInstruction = async ( + connection, + publicKey, + accountKeys, + instruction, + marketCache, + index, +) => { + if (!instruction?.data || !instruction?.accounts) { + return; + } + + // get instruction data + const decoded = bs58.decode(instruction.data); + let decodedInstruction; + + // try dex instruction decoding + try { + decodedInstruction = decodeInstruction(decoded); + console.log('[' + index + '] Handled as dex instruction'); + return await handleDexInstruction( + connection, + instruction.accounts, + accountKeys, + decodedInstruction, + marketCache, + ); + } catch {} + + // try token decoding + try { + decodedInstruction = decodeTokenInstruction(decoded); + console.log('[' + index + '] Handled as token instruction'); + return handleTokenInstruction( + publicKey, + instruction.accounts, + decodedInstruction, + accountKeys, + ); + } catch {} + + // try system instruction decoding + try { + const systemInstruction = handleSystemInstruction( + publicKey, + instruction, + accountKeys, + ); + console.log('[' + index + '] Handled as system instruction'); + return systemInstruction; + } catch {} + + // all decodings failed + console.log('[' + index + '] Failed, data: ' + JSON.stringify(decoded)); + return; +}; + +const handleDexInstruction = async ( + connection, + accounts, + accountKeys, + decodedInstruction, + marketCache, +) => { + if (!decodedInstruction || Object.keys(decodedInstruction).length > 1) { + return; + } + + // get market info + const marketInfo = + accountKeys && + MARKETS.find( + (market) => + accountKeys.findIndex((accountKey) => + accountKey.equals(market.address), + ) > -1, + ); + + // get market + let market; + try { + market = + marketInfo && + (marketCache[marketInfo.address.toBase58()] || + (await Market.load( + connection, + marketInfo.address, + {}, + marketInfo.programId, + ))); + if (market) marketCache[marketInfo.address.toBase58()] = market; + } catch (e) { + console.log('Error loading market: ' + e.message); + } + + // get data + const type = Object.keys(decodedInstruction)[0]; + let data = decodedInstruction[type]; + if (type === 'settleFunds') { + const settleFundsData = getSettleFundsData(accounts, accountKeys); + if (!settleFundsData) { + return; + } else { + data = { ...data, ...settleFundsData }; + } + } + + return { + type, + data, + market, + marketInfo, + }; +}; + +const decodeTokenInstruction = (bufferData) => { + if (!bufferData) { + return; + } + + if (bufferData.length === 1) { + if (bufferData[0] === 1) { + return { initializeAccount: {} }; + } else if (bufferData[0] === 9) { + return { closeAccount: {} }; + } + } else { + return decodeTokenInstructionData(bufferData); + } +}; + +const handleSystemInstruction = (publicKey, instruction, accountKeys) => { + const { programIdIndex, accounts, data } = instruction; + if (!programIdIndex || !accounts || !data) { + return; + } + + // construct system instruction + const systemInstruction = { + programId: accountKeys[programIdIndex], + keys: accounts.map((accountIndex) => ({ + pubkey: accountKeys[accountIndex], + })), + data: bs58.decode(data), + }; + + // get layout + let decoded; + const type = SystemInstruction.decodeInstructionType(systemInstruction); + switch (type) { + case 'Create': + decoded = SystemInstruction.decodeCreateAccount(systemInstruction); + break; + case 'CreateWithSeed': + decoded = SystemInstruction.decodeCreateWithSeed(systemInstruction); + break; + case 'Allocate': + decoded = SystemInstruction.decodeAllocate(systemInstruction); + break; + case 'AllocateWithSeed': + decoded = SystemInstruction.decodeAllocateWithSeed(systemInstruction); + break; + case 'Assign': + decoded = SystemInstruction.decodeAssign(systemInstruction); + break; + case 'AssignWithSeed': + decoded = SystemInstruction.decodeAssignWithSeed(systemInstruction); + break; + case 'Transfer': + decoded = SystemInstruction.decodeTransfer(systemInstruction); + break; + case 'AdvanceNonceAccount': + decoded = SystemInstruction.decodeNonceAdvance(systemInstruction); + break; + case 'WithdrawNonceAccount': + decoded = SystemInstruction.decodeNonceWithdraw(systemInstruction); + break; + case 'InitializeNonceAccount': + decoded = SystemInstruction.decodeNonceInitialize(systemInstruction); + break; + case 'AuthorizeNonceAccount': + decoded = SystemInstruction.decodeNonceAuthorize(systemInstruction); + break; + default: + return; + } + + if ( + !decoded || + (decoded.fromPubkey && !publicKey.equals(decoded.fromPubkey)) + ) { + return; + } + + return { + type: type.charAt(0).toLowerCase() + type.slice(1), + data: decoded, + }; +}; + +const handleTokenInstruction = ( + publicKey, + accounts, + decodedInstruction, + accountKeys, +) => { + if (!decodedInstruction || Object.keys(decodedInstruction).length > 1) { + return; + } + + // get data + const type = Object.keys(decodedInstruction)[0]; + let data = decodedInstruction[type]; + if (type === 'initializeAccount') { + const initializeAccountData = getInitializeAccountData( + publicKey, + accounts, + accountKeys, + ); + data = { ...data, ...initializeAccountData }; + } else if (type === 'transfer') { + const transferData = getTransferData(publicKey, accounts, accountKeys); + data = { ...data, ...transferData }; + } else if (type === 'closeAccount') { + const closeAccountData = getCloseAccountData( + publicKey, + accounts, + accountKeys, + ); + data = { ...data, ...closeAccountData }; + } + + return { + type, + data, + }; +}; + +const getSettleFundsData = (accounts, accountKeys) => { + const basePubkey = getAccountByIndex( + accounts, + accountKeys, + SETTLE_FUNDS_BASE_WALLET_INDEX, + ); + + const quotePubkey = getAccountByIndex( + accounts, + accountKeys, + SETTLE_FUNDS_QUOTE_WALLET_INDEX, + ); + + if (!basePubkey || !quotePubkey) { + return; + } + + return { basePubkey, quotePubkey }; +}; + +const getTransferData = (publicKey, accounts, accountKeys) => { + const sourcePubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.TRANSFER_SOURCE_INDEX, + ); + + const destinationPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.TRANSFER_DESTINATION_INDEX, + ); + + const ownerPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.TRANSFER_OWNER_INDEX, + ); + + if (!ownerPubkey || !publicKey.equals(ownerPubkey)) { + return; + } + + return { sourcePubkey, destinationPubkey, ownerPubkey }; +}; + +const getInitializeAccountData = (publicKey, accounts, accountKeys) => { + const accountPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.INITIALIZE_ACCOUNT_ACCOUNT_INDEX, + ); + + const mintPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.INITIALIZE_ACCOUNT_MINT_INDEX, + ); + + const ownerPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.INITIALIZE_ACCOUNT_OWNER_INDEX, + ); + + if (!ownerPubkey || !publicKey.equals(ownerPubkey)) { + return; + } + + return { accountPubkey, mintPubkey, ownerPubkey }; +}; + +const getCloseAccountData = (publicKey, accounts, accountKeys) => { + const sourcePubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.TRANSFER_SOURCE_INDEX, + ); + + const destinationPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.TRANSFER_DESTINATION_INDEX, + ); + + const ownerPubkey = getAccountByIndex( + accounts, + accountKeys, + TokenInstructions.TRANSFER_OWNER_INDEX, + ); + + if (!ownerPubkey || !publicKey.equals(ownerPubkey)) { + return; + } + + return { sourcePubkey, destinationPubkey, ownerPubkey }; +}; + +const getAccountByIndex = (accounts, accountKeys, accountIndex) => { + const index = accounts.length > accountIndex && accounts[accountIndex]; + return accountKeys?.length > index && accountKeys[index]; +}; diff --git a/yarn.lock b/yarn.lock index 9bb3c2e..074d4f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1578,6 +1578,15 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@project-serum/serum@^0.12.7": + version "0.12.7" + resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.12.7.tgz#bc98841dbbc928e78c07a2568f0a603beaa6460c" + integrity sha512-NJrseIF6ZI/prOWsM1Q0IjuCSDLtMnLxI2FRV/1dbKX/1HhQOfErkJT1Ab0efUBsqF+5zXvtotcO7/PeHHB1Vg== + dependencies: + "@solana/web3.js" "^0.71.10" + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" @@ -1588,6 +1597,27 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@solana/web3.js@^0.71.10": + version "0.71.14" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.71.14.tgz#b21f9613cb2e27defc93264bd894761689d209e3" + integrity sha512-23jWjzMxSOKzcAUzLBaD5p0YRJys6A9cEdWZQtPV/CV7bmo5JNIdPDR+UhzPxe1L3WX3bu7KAmAjcIERCXKDfQ== + dependencies: + "@babel/runtime" "^7.3.1" + bn.js "^5.0.0" + bs58 "^4.0.1" + buffer "^5.4.3" + buffer-layout "^1.2.0" + crypto-hash "^1.2.2" + esdoc-inject-style-plugin "^1.0.0" + jayson "^3.0.1" + mz "^2.7.0" + node-fetch "^2.2.0" + npm-run-all "^4.1.5" + rpc-websockets "^7.4.0" + superstruct "^0.8.3" + tweetnacl "^1.0.0" + ws "^7.0.0" + "@solana/web3.js@^0.71.9": version "0.71.9" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.71.9.tgz#d9a5683a2697f4db42b7e4bdcf8b513e5db493e1" @@ -5074,7 +5104,7 @@ eventemitter3@4.0.4, eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== -eventemitter3@^4.0.6: +eventemitter3@^4.0.6, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -10671,6 +10701,21 @@ rpc-websockets@^7.1.0: bufferutil "^4.0.1" utf-8-validate "^5.0.2" +rpc-websockets@^7.4.0: + version "7.4.2" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.4.2.tgz#9e85ca7451e64a2015996c7a361cbff37c3ad597" + integrity sha512-kUpYcnbEU/BeAxGTlfySZ/tp9FU+TLSgONbViyx6hQsIh8876uxggJWzVOCe+CztBvuCOAOd0BXyPlKfcflykw== + dependencies: + "@babel/runtime" "^7.11.2" + assert-args "^1.2.1" + circular-json "^0.5.9" + eventemitter3 "^4.0.7" + uuid "^8.3.0" + ws "^7.3.1" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"