Transaction parsing for Serum DEX instructions (#8)

Transaction parsing for Serum DEX instructions
This commit is contained in:
philippe-ftx 2020-09-14 20:57:55 +02:00 committed by GitHub
parent 16cf26fad9
commit 6b68ac3685
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 810 additions and 9 deletions

View File

@ -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",

View File

@ -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 (
<>
<Typography
variant="subtitle1"
style={{ fontWeight: 'bold' }}
gutterBottom
>
{TYPE_LABELS[type]}
</Typography>
<LabelValue
label="Market"
value={marketLabel}
link={true}
onClick={() => 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 (
<LabelValue
label={label + ''}
value={address ? getAddressValue(value) : value + ''}
link={address}
onClick={() => address && onOpenAddress(value?.toBase58())}
/>
);
})}
</>
);
}

View File

@ -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 (
<Typography>
{label}:{' '}
{link ? (
<Link href="#" onClick={onClick}>
{value}
</Link>
) : (
<span style={{ color: '#7B7B7B' }}>{value}</span>
)}
</Typography>
);
}

View File

@ -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 (
<>
<Typography
variant="subtitle1"
style={{ fontWeight: 'bold' }}
gutterBottom
>
Place an order
</Typography>
<LabelValue
label="Market"
value={marketLabel}
link={true}
onClick={() => onOpenAddress(marketInfo?.address?.toBase58())}
/>
<LabelValue
label="Side"
value={side.charAt(0).toUpperCase() + side.slice(1)}
/>
<LabelValue
label="Price"
value={market?.priceLotsToNumber(limitPrice) || '' + limitPrice}
/>
<LabelValue
label="Quantity"
value={market?.baseSizeLotsToNumber(maxQuantity) || '' + maxQuantity}
/>
<LabelValue
label="Type"
value={orderType.charAt(0).toUpperCase() + orderType.slice(1)}
/>
</>
);
}

View File

@ -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 (
<>
<Typography
variant="subtitle1"
style={{ fontWeight: 'bold' }}
gutterBottom
>
{TYPE_LABELS[type]}
</Typography>
{data &&
Object.entries(data).map(([key, value]) => {
const dataLabel = DATA_LABELS[key];
if (!dataLabel) {
return null;
}
const { label, address } = dataLabel;
return (
<LabelValue
label={label + ''}
value={address ? value?.toBase58() : value}
link={address}
onClick={() => address && onOpenAddress(value?.toBase58())}
/>
);
})}
</>
);
}

View File

@ -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 (
<>
<Typography
variant="subtitle1"
style={{ fontWeight: 'bold' }}
gutterBottom
>
{TYPE_LABELS[type]}
</Typography>
{data &&
Object.entries(data).map(([key, value]) => {
const dataLabel = DATA_LABELS[key];
if (!dataLabel) {
return null;
}
const { label, address } = dataLabel;
return (
<LabelValue
label={label + ''}
value={address ? getAddressValue(value) : value}
link={address}
onClick={() => address && onOpenAddress(value?.toBase58())}
/>
);
})}
</>
);
}

View File

@ -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 (
<>
<Typography
variant="subtitle1"
style={{ fontWeight: 'bold' }}
gutterBottom
>
Send the following transaction:
</Typography>
<Typography style={{ wordBreak: 'break-all' }}>
{bs58.encode(message)}
</Typography>
</>
);
}

View File

@ -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 (
<DexInstruction
instruction={instruction}
onOpenAddress={onOpenAddress}
/>
);
case 'closeAccount':
case 'initializeAccount':
case 'transfer':
case 'approve':
case 'mintTo':
return (
<TokenInstruction
instruction={instruction}
onOpenAddress={onOpenAddress}
/>
);
case 'create':
return (
<SystemInstruction
instruction={instruction}
onOpenAddress={onOpenAddress}
/>
);
case 'newOrder':
return (
<NewOrder instruction={instruction} onOpenAddress={onOpenAddress} />
);
default:
return <UnknownInstruction message={message} />;
}
};
return (
<Card>
<CardContent>
<Typography variant="h6" component="h1" gutterBottom>
{origin} would like to send the following transaction:
{parsing ? (
<>
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: 20 }}>
<CircularProgress style={{ marginRight: 20 }} />
<Typography
variant="subtitle1"
style={{ fontWeight: 'bold' }}
gutterBottom
>
Parsing transaction:
</Typography>
<Typography className={classes.transaction}>
</div>
<Typography style={{ wordBreak: 'break-all' }}>
{bs58.encode(message)}
</Typography>
</>
) : (
<>
<Typography variant="h6" gutterBottom>
{instructions
? `${origin} wants to:`
: `Unknown transaction data`}
</Typography>
{instructions ? (
instructions.map((instruction) => (
<Box style={{ marginTop: 20 }}>
{getContent(instruction)}
<Divider style={{ marginTop: 20 }} />
</Box>
))
) : (
<UnknownInstruction message={message} />
)}
</>
)}
</CardContent>
<CardActions className={classes.actions}>
<Button onClick={onReject}>Cancel</Button>

382
src/utils/transactions.js Normal file
View File

@ -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];
};

View File

@ -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"