Automatic transaction approval (#13)

Automatic transaction approval
This commit is contained in:
nishadsingh1 2020-09-22 20:31:52 +08:00 committed by GitHub
parent 00e21b13bc
commit 9f15154b7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 289 additions and 24 deletions

View File

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

View File

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

View File

@ -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())
}
/>
</>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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