parent
00e21b13bc
commit
9f15154b7b
|
@ -5,7 +5,7 @@
|
|||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@project-serum/serum": "^0.12.17",
|
||||
"@project-serum/serum": "^0.12.18",
|
||||
"@solana/web3.js": "^0.71.9",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
|
|
|
@ -21,7 +21,7 @@ const DATA_LABELS = {
|
|||
export default function DexInstruction({ instruction, onOpenAddress }) {
|
||||
const wallet = useWallet();
|
||||
const [publicKeys] = useWalletPublicKeys();
|
||||
const { type, data, market, marketInfo } = instruction;
|
||||
const { type, data, market, marketInfo, knownProgramId } = instruction;
|
||||
|
||||
const marketLabel =
|
||||
(marketInfo &&
|
||||
|
@ -44,7 +44,7 @@ export default function DexInstruction({ instruction, onOpenAddress }) {
|
|||
style={{ fontWeight: 'bold' }}
|
||||
gutterBottom
|
||||
>
|
||||
{TYPE_LABELS[type]}
|
||||
{TYPE_LABELS[type]} {!knownProgramId ? '(Unknown DEX program)' : null}
|
||||
</Typography>
|
||||
<LabelValue
|
||||
label="Market"
|
||||
|
@ -65,6 +65,7 @@ export default function DexInstruction({ instruction, onOpenAddress }) {
|
|||
const { label, address } = dataLabel;
|
||||
return (
|
||||
<LabelValue
|
||||
key={key}
|
||||
label={label + ''}
|
||||
value={address ? getAddressValue(value) : value + ''}
|
||||
link={address}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import LabelValue from './LabelValue';
|
||||
import { useWallet } from '../../utils/wallet';
|
||||
|
||||
export default function Neworder({ instruction, onOpenAddress }) {
|
||||
const { data, market, marketInfo } = instruction;
|
||||
const { side, limitPrice, maxQuantity, orderType } = data;
|
||||
const wallet = useWallet();
|
||||
const { data, market, marketInfo, knownProgramId } = instruction;
|
||||
const { side, limitPrice, maxQuantity, orderType, ownerPubkey } = data;
|
||||
|
||||
const marketLabel =
|
||||
(marketInfo &&
|
||||
|
@ -12,6 +14,11 @@ export default function Neworder({ instruction, onOpenAddress }) {
|
|||
market?._decoded?.ownAddress?.toBase58() ||
|
||||
'Unknown';
|
||||
|
||||
const getAddressValue = (address) => {
|
||||
const isOwner = wallet.publicKey.equals(address);
|
||||
return isOwner ? 'This wallet' : address?.toBase58() || 'Unknown';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
|
@ -19,7 +26,7 @@ export default function Neworder({ instruction, onOpenAddress }) {
|
|||
style={{ fontWeight: 'bold' }}
|
||||
gutterBottom
|
||||
>
|
||||
Place an order
|
||||
Place an order {!knownProgramId ? '(Unknown DEX program)' : null}
|
||||
</Typography>
|
||||
<LabelValue
|
||||
label="Market"
|
||||
|
@ -47,6 +54,14 @@ export default function Neworder({ instruction, onOpenAddress }) {
|
|||
label="Type"
|
||||
value={orderType.charAt(0).toUpperCase() + orderType.slice(1)}
|
||||
/>
|
||||
<LabelValue
|
||||
label="Owner"
|
||||
link={ownerPubkey}
|
||||
value={ownerPubkey ? getAddressValue(ownerPubkey) : ownerPubkey}
|
||||
onOpenAddress={() =>
|
||||
ownerPubkey && onOpenAddress(ownerPubkey?.toBase58())
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ export default function SystemInstruction({ instruction, onOpenAddress }) {
|
|||
const { label, address } = dataLabel;
|
||||
return (
|
||||
<LabelValue
|
||||
key={key}
|
||||
label={label + ''}
|
||||
value={address ? value?.toBase58() : value}
|
||||
link={address}
|
||||
|
|
|
@ -58,6 +58,7 @@ export default function TokenInstruction({ instruction, onOpenAddress }) {
|
|||
const { label, address } = dataLabel;
|
||||
return (
|
||||
<LabelValue
|
||||
key={key}
|
||||
label={label + ''}
|
||||
value={address ? getAddressValue(value) : value}
|
||||
link={address}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
import { useWallet, useWalletPublicKeys } from '../utils/wallet';
|
||||
import { decodeMessage } from '../utils/transactions';
|
||||
import { useConnection, useSolanaExplorerUrlSuffix } from '../utils/connection';
|
||||
import { Typography, Divider } from '@material-ui/core';
|
||||
import {
|
||||
Typography,
|
||||
Divider,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
SnackbarContent,
|
||||
} 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';
|
||||
|
@ -16,9 +22,11 @@ import bs58 from 'bs58';
|
|||
import nacl from 'tweetnacl';
|
||||
import NewOrder from '../components/instructions/NewOrder';
|
||||
import UnknownInstruction from '../components/instructions/UnknownInstruction';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
import SystemInstruction from '../components/instructions/SystemInstruction';
|
||||
import DexInstruction from '../components/instructions/DexInstruction';
|
||||
import TokenInstruction from '../components/instructions/TokenInstruction';
|
||||
import { useLocalStorageState } from '../utils/utils';
|
||||
|
||||
export default function PopupPage({ opener }) {
|
||||
const wallet = useWallet();
|
||||
|
@ -37,6 +45,7 @@ export default function PopupPage({ opener }) {
|
|||
const [connectedAccount, setConnectedAccount] = useState(null);
|
||||
const hasConnectedAccount = !!connectedAccount;
|
||||
const [requests, setRequests] = useState([]);
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
|
||||
// Send a disconnect event if this window is closed, this component is
|
||||
// unmounted, or setConnectedAccount(null) is called.
|
||||
|
@ -82,12 +91,13 @@ export default function PopupPage({ opener }) {
|
|||
!connectedAccount.publicKey.equals(wallet.publicKey)
|
||||
) {
|
||||
// Approve the parent page to connect to this wallet.
|
||||
function connect() {
|
||||
function connect(autoApprove) {
|
||||
setConnectedAccount(wallet.account);
|
||||
postMessage({
|
||||
method: 'connected',
|
||||
params: { publicKey: wallet.publicKey.toBase58() },
|
||||
params: { publicKey: wallet.publicKey.toBase58(), autoApprove },
|
||||
});
|
||||
setAutoApprove(autoApprove);
|
||||
focusParent();
|
||||
}
|
||||
|
||||
|
@ -125,9 +135,9 @@ export default function PopupPage({ opener }) {
|
|||
focusParent();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ApproveSignatureForm
|
||||
autoApprove={autoApprove}
|
||||
origin={origin}
|
||||
message={message}
|
||||
onApprove={sendSignature}
|
||||
|
@ -161,11 +171,41 @@ const useStyles = makeStyles((theme) => ({
|
|||
actions: {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
snackbarRoot: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
warningMessage: {
|
||||
margin: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
warningIcon: {
|
||||
marginRight: theme.spacing(1),
|
||||
fontSize: 24,
|
||||
},
|
||||
warningTitle: {
|
||||
color: theme.palette.warning.light,
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
},
|
||||
warningContainer: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
divider: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
function ApproveConnectionForm({ origin, onApprove }) {
|
||||
const wallet = useWallet();
|
||||
const classes = useStyles();
|
||||
const [autoApprove, setAutoApprove] = useState(false);
|
||||
let [dismissed, setDismissed] = useLocalStorageState(
|
||||
'dismissedAutoApproveWarning',
|
||||
false,
|
||||
);
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
@ -178,10 +218,47 @@ function ApproveConnectionForm({ origin, onApprove }) {
|
|||
<Typography>{wallet.publicKey.toBase58()}</Typography>
|
||||
</div>
|
||||
<Typography>Only connect with sites you trust.</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={autoApprove}
|
||||
onChange={() => setAutoApprove(!autoApprove)}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={`Automatically approve transactions from ${origin}`}
|
||||
/>
|
||||
{!dismissed && autoApprove && (
|
||||
<SnackbarContent
|
||||
className={classes.warningContainer}
|
||||
message={
|
||||
<div>
|
||||
<span className={classes.warningTitle}>
|
||||
<WarningIcon className={classes.warningIcon} />
|
||||
Use at your own risk.
|
||||
</span>
|
||||
<Typography className={classes.warningMessage}>
|
||||
This setting allows sending some transactions on your behalf
|
||||
without requesting your permission for the remainder of this
|
||||
session.
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
action={[
|
||||
<Button onClick={() => setDismissed('1')}>I understand</Button>,
|
||||
]}
|
||||
classes={{ root: classes.snackbarRoot }}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions className={classes.actions}>
|
||||
<Button onClick={window.close}>Cancel</Button>
|
||||
<Button color="primary" onClick={onApprove}>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => onApprove(autoApprove)}
|
||||
disabled={!dismissed && autoApprove}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</CardActions>
|
||||
|
@ -189,11 +266,120 @@ function ApproveConnectionForm({ origin, onApprove }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
||||
function isSafeInstruction(publicKeys, owner, instructions) {
|
||||
let unsafe = false;
|
||||
const states = {
|
||||
CREATED: 0,
|
||||
OWNED: 1,
|
||||
CLOSED_TO_OWNED_DESTINATION: 2,
|
||||
};
|
||||
const accountStates = {};
|
||||
|
||||
function isOwned(pubkey) {
|
||||
if (!pubkey) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
publicKeys?.some((ownedAccountPubkey) =>
|
||||
ownedAccountPubkey.equals(pubkey),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return accountStates[pubkey.toBase58()] === states.OWNED;
|
||||
}
|
||||
|
||||
instructions.forEach((instruction) => {
|
||||
if (!instruction) {
|
||||
unsafe = true;
|
||||
} else {
|
||||
if (
|
||||
['cancelOrder', 'matchOrders', 'newOrder', 'settleFunds'].includes(
|
||||
instruction.type,
|
||||
)
|
||||
) {
|
||||
// Verify that the DEX program ID is known
|
||||
if (!instruction.knownProgramId) {
|
||||
unsafe = true;
|
||||
}
|
||||
}
|
||||
if (['cancelOrder', 'matchOrders'].includes(instruction.type)) {
|
||||
// It is always considered safe to cancel orders, match orders
|
||||
} else if (instruction.type === 'systemCreate') {
|
||||
let { newAccountPubkey } = instruction.data;
|
||||
if (!newAccountPubkey) {
|
||||
unsafe = true;
|
||||
} else {
|
||||
accountStates[newAccountPubkey.toBase58()] = states.CREATED;
|
||||
}
|
||||
} else if (instruction.type === 'newOrder') {
|
||||
// New order instructions are safe if the owner is this wallet
|
||||
let { openOrdersPubkey, ownerPubkey } = instruction.data;
|
||||
if (ownerPubkey && owner.equals(ownerPubkey)) {
|
||||
accountStates[openOrdersPubkey.toBase58()] = states.OWNED;
|
||||
} else {
|
||||
unsafe = false;
|
||||
}
|
||||
} else if (instruction.type === 'initializeAccount') {
|
||||
// New SPL token accounts are only considered safe if they are owned by this wallet and newly created
|
||||
let { ownerPubkey, accountPubkey } = instruction.data;
|
||||
if (
|
||||
owner &&
|
||||
ownerPubkey &&
|
||||
owner.equals(ownerPubkey) &&
|
||||
accountPubkey &&
|
||||
accountStates[accountPubkey.toBase58()] === states.CREATED
|
||||
) {
|
||||
accountStates[accountPubkey.toBase58()] = states.OWNED;
|
||||
} else {
|
||||
unsafe = true;
|
||||
}
|
||||
} else if (instruction.type === 'settleFunds') {
|
||||
// Settling funds is only safe if the destinations are owned
|
||||
let { basePubkey, quotePubkey } = instruction.data;
|
||||
if (!isOwned(basePubkey) || !isOwned(quotePubkey)) {
|
||||
unsafe = true;
|
||||
}
|
||||
} else if (instruction.type === 'closeAccount') {
|
||||
// Closing is only safe if the destination is owned
|
||||
let { sourcePubkey, destinationPubkey } = instruction.data;
|
||||
if (isOwned(destinationPubkey)) {
|
||||
accountStates[sourcePubkey.toBase58()] =
|
||||
states.CLOSED_TO_OWNED_DESTINATION;
|
||||
} else {
|
||||
unsafe = true;
|
||||
}
|
||||
} else {
|
||||
unsafe = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check that all accounts are owned
|
||||
if (
|
||||
Object.values(accountStates).some(
|
||||
(state) =>
|
||||
![states.CLOSED_TO_OWNED_DESTINATION, states.OWNED].includes(state),
|
||||
)
|
||||
) {
|
||||
unsafe = true;
|
||||
}
|
||||
|
||||
return !unsafe;
|
||||
}
|
||||
|
||||
function ApproveSignatureForm({
|
||||
origin,
|
||||
message,
|
||||
onApprove,
|
||||
onReject,
|
||||
autoApprove,
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const explorerUrlSuffix = useSolanaExplorerUrlSuffix();
|
||||
const connection = useConnection();
|
||||
const wallet = useWallet();
|
||||
const [publicKeys] = useWalletPublicKeys();
|
||||
|
||||
const [parsing, setParsing] = useState(true);
|
||||
const [instructions, setInstructions] = useState(null);
|
||||
|
@ -205,6 +391,22 @@ function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
|||
});
|
||||
}, [message, connection, wallet]);
|
||||
|
||||
const safe = useMemo(() => {
|
||||
return (
|
||||
publicKeys &&
|
||||
instructions &&
|
||||
isSafeInstruction(publicKeys, wallet.publicKey, instructions)
|
||||
);
|
||||
}, [publicKeys, instructions, wallet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (safe && autoApprove) {
|
||||
console.log('Auto approving safe transaction');
|
||||
onApprove();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [safe, autoApprove]);
|
||||
|
||||
const onOpenAddress = (address) => {
|
||||
address &&
|
||||
window.open(
|
||||
|
@ -285,8 +487,8 @@ function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
|||
: `Unknown transaction data`}
|
||||
</Typography>
|
||||
{instructions ? (
|
||||
instructions.map((instruction) => (
|
||||
<Box style={{ marginTop: 20 }}>
|
||||
instructions.map((instruction, i) => (
|
||||
<Box style={{ marginTop: 20 }} key={i}>
|
||||
{getContent(instruction)}
|
||||
<Divider style={{ marginTop: 20 }} />
|
||||
</Box>
|
||||
|
@ -305,6 +507,24 @@ function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
|||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{!safe && (
|
||||
<SnackbarContent
|
||||
className={classes.warningContainer}
|
||||
message={
|
||||
<div>
|
||||
<span className={classes.warningTitle}>
|
||||
<WarningIcon className={classes.warningIcon} />
|
||||
Nonstandard DEX transaction
|
||||
</span>
|
||||
<Typography className={classes.warningMessage}>
|
||||
Sollet does not recognize this transaction as a standard
|
||||
Serum DEX transaction
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
classes={{ root: classes.snackbarRoot }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
@ -10,7 +10,7 @@ export const TOKEN_PROGRAM_ID = new PublicKey(
|
|||
);
|
||||
|
||||
export const WRAPPED_SOL_MINT = new PublicKey(
|
||||
'So11111111111111111111111111111111111111111',
|
||||
'So11111111111111111111111111111111111111112',
|
||||
);
|
||||
|
||||
export const MEMO_PROGRAM_ID = new PublicKey(
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
TokenInstructions,
|
||||
SETTLE_FUNDS_BASE_WALLET_INDEX,
|
||||
SETTLE_FUNDS_QUOTE_WALLET_INDEX,
|
||||
NEW_ORDER_OPEN_ORDERS_INDEX,
|
||||
NEW_ORDER_OWNER_INDEX,
|
||||
} from '@project-serum/serum';
|
||||
|
||||
const marketCache = {};
|
||||
|
@ -121,11 +123,11 @@ const handleDexInstruction = async (
|
|||
);
|
||||
|
||||
// get market
|
||||
let market;
|
||||
let market, programIdAddress;
|
||||
try {
|
||||
const marketAddress =
|
||||
marketInfo?.address || getAccountByIndex(accounts, accountKeys, 0);
|
||||
const programIdAddress =
|
||||
programIdAddress =
|
||||
marketInfo?.programId ||
|
||||
getAccountByIndex([programIdIndex], accountKeys, 0);
|
||||
const strAddress = marketAddress.toBase58();
|
||||
|
@ -164,9 +166,14 @@ const handleDexInstruction = async (
|
|||
} else {
|
||||
data = { ...data, ...settleFundsData };
|
||||
}
|
||||
} else if (type === 'newOrder') {
|
||||
const newOrderData = getNewOrderData(accounts, accountKeys);
|
||||
data = { ...data, ...newOrderData };
|
||||
}
|
||||
|
||||
return {
|
||||
knownProgramId: MARKETS.some(
|
||||
({ programId }) => programIdAddress && programIdAddress.equals(programId),
|
||||
),
|
||||
type,
|
||||
data,
|
||||
market,
|
||||
|
@ -297,6 +304,20 @@ const handleTokenInstruction = (
|
|||
};
|
||||
};
|
||||
|
||||
const getNewOrderData = (accounts, accountKeys) => {
|
||||
const openOrdersPubkey = getAccountByIndex(
|
||||
accounts,
|
||||
accountKeys,
|
||||
NEW_ORDER_OPEN_ORDERS_INDEX,
|
||||
);
|
||||
const ownerPubkey = getAccountByIndex(
|
||||
accounts,
|
||||
accountKeys,
|
||||
NEW_ORDER_OWNER_INDEX,
|
||||
);
|
||||
return { openOrdersPubkey, ownerPubkey };
|
||||
};
|
||||
|
||||
const getSettleFundsData = (accounts, accountKeys) => {
|
||||
const basePubkey = getAccountByIndex(
|
||||
accounts,
|
||||
|
|
|
@ -130,12 +130,18 @@ export function useWalletPublicKeys() {
|
|||
wallet.getTokenAccountInfo,
|
||||
wallet.getTokenAccountInfo,
|
||||
);
|
||||
let publicKeys = [
|
||||
const getPublicKeys = () => [
|
||||
wallet.account.publicKey,
|
||||
...(tokenAccountInfo
|
||||
? tokenAccountInfo.map(({ publicKey }) => publicKey)
|
||||
: []),
|
||||
];
|
||||
const serialized = getPublicKeys()
|
||||
.map((pubKey) => pubKey?.toBase58() || '')
|
||||
.toString();
|
||||
|
||||
// Prevent users from re-rendering unless the list of public keys actually changes
|
||||
let publicKeys = useMemo(getPublicKeys, [serialized]);
|
||||
return [publicKeys, loaded];
|
||||
}
|
||||
|
||||
|
|
|
@ -1578,10 +1578,10 @@
|
|||
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.17":
|
||||
version "0.12.17"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.12.17.tgz#f22dbfd4ef659a722f7720ef87a8f4d4bc929a9f"
|
||||
integrity sha512-4Tkzg+Hx3F6YvKGDyR5bQ3PlMLf2fLwvWjRhjIeJWAUqyX7ZiGqvZz+NJ2VTFESo9wRoMcBPZccd52E/pkSUXQ==
|
||||
"@project-serum/serum@^0.12.18":
|
||||
version "0.12.18"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.12.18.tgz#6c8527d92d6944081883b3a4db545db185609818"
|
||||
integrity sha512-Cf3k+vswc34oZkyZzMWfSaD9bZvsPtGFDtrpIkQ0xz8jXJH5t//qFcW+PK8o/AuQRuA26YTSEjEEy/0vOpW8SA==
|
||||
dependencies:
|
||||
"@solana/web3.js" "^0.71.10"
|
||||
bn.js "^5.1.2"
|
||||
|
|
Loading…
Reference in New Issue