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": {
|
||||
"@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",
|
||||
|
|
|
@ -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 { 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:
|
||||
</Typography>
|
||||
<Typography className={classes.transaction}>
|
||||
{bs58.encode(message)}
|
||||
</Typography>
|
||||
{parsing ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: 20 }}>
|
||||
<CircularProgress style={{ marginRight: 20 }} />
|
||||
<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>
|
||||
<CardActions className={classes.actions}>
|
||||
<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"
|
||||
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"
|
||||
|
|
Loading…
Reference in New Issue