From 9f15154b7b5bb38edc76d3ca530647bd11fa12be Mon Sep 17 00:00:00 2001 From: nishadsingh1 Date: Tue, 22 Sep 2020 20:31:52 +0800 Subject: [PATCH] Automatic transaction approval (#13) Automatic transaction approval --- package.json | 2 +- src/components/instructions/DexInstruction.js | 5 +- src/components/instructions/NewOrder.js | 21 +- .../instructions/SystemInstruction.js | 1 + .../instructions/TokenInstruction.js | 1 + src/pages/PopupPage.js | 238 +++++++++++++++++- src/utils/tokens/instructions.js | 2 +- src/utils/transactions.js | 27 +- src/utils/wallet.js | 8 +- yarn.lock | 8 +- 10 files changed, 289 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 7c4de2b..1f9ef55 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/instructions/DexInstruction.js b/src/components/instructions/DexInstruction.js index 99f8391..812edfc 100644 --- a/src/components/instructions/DexInstruction.js +++ b/src/components/instructions/DexInstruction.js @@ -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} { + const isOwner = wallet.publicKey.equals(address); + return isOwner ? 'This wallet' : address?.toBase58() || 'Unknown'; + }; + return ( <> - Place an order + Place an order {!knownProgramId ? '(Unknown DEX program)' : null} + + ownerPubkey && onOpenAddress(ownerPubkey?.toBase58()) + } + /> ); } diff --git a/src/components/instructions/SystemInstruction.js b/src/components/instructions/SystemInstruction.js index eb7815e..84adec4 100644 --- a/src/components/instructions/SystemInstruction.js +++ b/src/components/instructions/SystemInstruction.js @@ -41,6 +41,7 @@ export default function SystemInstruction({ instruction, onOpenAddress }) { const { label, address } = dataLabel; return ( ({ 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 ( @@ -178,10 +218,47 @@ function ApproveConnectionForm({ origin, onApprove }) { {wallet.publicKey.toBase58()} Only connect with sites you trust. + + setAutoApprove(!autoApprove)} + color="primary" + /> + } + label={`Automatically approve transactions from ${origin}`} + /> + {!dismissed && autoApprove && ( + + + + Use at your own risk. + + + This setting allows sending some transactions on your behalf + without requesting your permission for the remainder of this + session. + + + } + action={[ + , + ]} + classes={{ root: classes.snackbarRoot }} + /> + )} - @@ -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`} {instructions ? ( - instructions.map((instruction) => ( - + instructions.map((instruction, i) => ( + {getContent(instruction)} @@ -305,6 +507,24 @@ function ApproveSignatureForm({ origin, message, onApprove, onReject }) { )} + {!safe && ( + + + + Nonstandard DEX transaction + + + Sollet does not recognize this transaction as a standard + Serum DEX transaction + + + } + classes={{ root: classes.snackbarRoot }} + /> + )} )} diff --git a/src/utils/tokens/instructions.js b/src/utils/tokens/instructions.js index a09de2f..e2c8aae 100644 --- a/src/utils/tokens/instructions.js +++ b/src/utils/tokens/instructions.js @@ -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( diff --git a/src/utils/transactions.js b/src/utils/transactions.js index 18b9307..182ac81 100644 --- a/src/utils/transactions.js +++ b/src/utils/transactions.js @@ -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, diff --git a/src/utils/wallet.js b/src/utils/wallet.js index d8c5577..979b418 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -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]; } diff --git a/yarn.lock b/yarn.lock index 7aa412c..2074930 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"