Transaction parsing for Serum DEX instructions (#8)
Transaction parsing for Serum DEX instructions
This commit is contained in:
parent
16cf26fad9
commit
6b68ac3685
|
@ -5,6 +5,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
|
"@project-serum/serum": "^0.12.7",
|
||||||
"@solana/web3.js": "^0.71.9",
|
"@solana/web3.js": "^0.71.9",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
|
|
|
@ -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())}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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())}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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())}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useWallet } from '../utils/wallet';
|
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 Card from '@material-ui/core/Card';
|
||||||
import CardContent from '@material-ui/core/CardContent';
|
import CardContent from '@material-ui/core/CardContent';
|
||||||
import CardActions from '@material-ui/core/CardActions';
|
import CardActions from '@material-ui/core/CardActions';
|
||||||
|
@ -10,6 +14,11 @@ import { makeStyles } from '@material-ui/core/styles';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import bs58 from 'bs58';
|
import bs58 from 'bs58';
|
||||||
import nacl from 'tweetnacl';
|
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 }) {
|
export default function PopupPage({ opener }) {
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
|
@ -182,18 +191,104 @@ function ApproveConnectionForm({ origin, onApprove }) {
|
||||||
|
|
||||||
function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
||||||
const classes = useStyles();
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" component="h1" gutterBottom>
|
{parsing ? (
|
||||||
{origin} would like to send the following transaction:
|
<>
|
||||||
</Typography>
|
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: 20 }}>
|
||||||
<Typography className={classes.transaction}>
|
<CircularProgress style={{ marginRight: 20 }} />
|
||||||
{bs58.encode(message)}
|
<Typography
|
||||||
</Typography>
|
variant="subtitle1"
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
Parsing transaction:
|
||||||
|
</Typography>
|
||||||
|
</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>
|
</CardContent>
|
||||||
<CardActions className={classes.actions}>
|
<CardActions className={classes.actions}>
|
||||||
<Button onClick={onReject}>Cancel</Button>
|
<Button onClick={onReject}>Cancel</Button>
|
||||||
|
|
|
@ -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];
|
||||||
|
};
|
47
yarn.lock
47
yarn.lock
|
@ -1578,6 +1578,15 @@
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
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":
|
"@sheerun/mutationobserver-shim@^0.3.2":
|
||||||
version "0.3.3"
|
version "0.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25"
|
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"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
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":
|
"@solana/web3.js@^0.71.9":
|
||||||
version "0.71.9"
|
version "0.71.9"
|
||||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.71.9.tgz#d9a5683a2697f4db42b7e4bdcf8b513e5db493e1"
|
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"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384"
|
||||||
integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==
|
integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==
|
||||||
|
|
||||||
eventemitter3@^4.0.6:
|
eventemitter3@^4.0.6, eventemitter3@^4.0.7:
|
||||||
version "4.0.7"
|
version "4.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||||
|
@ -10671,6 +10701,21 @@ rpc-websockets@^7.1.0:
|
||||||
bufferutil "^4.0.1"
|
bufferutil "^4.0.1"
|
||||||
utf-8-validate "^5.0.2"
|
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:
|
rsvp@^4.8.4:
|
||||||
version "4.8.5"
|
version "4.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||||
|
|
Loading…
Reference in New Issue