From 1f6e11107d580fc2d54e6322546f6a0455ade787 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Fri, 11 Dec 2020 07:19:04 -0800 Subject: [PATCH] Sign many transactions at once (#60) --- src/pages/PopupPage.js | 249 ++++++++++++++++++++++++++--------------- 1 file changed, 160 insertions(+), 89 deletions(-) diff --git a/src/pages/PopupPage.js b/src/pages/PopupPage.js index 412094c..526a133 100644 --- a/src/pages/PopupPage.js +++ b/src/pages/PopupPage.js @@ -78,7 +78,10 @@ export default function PopupPage({ opener }) { useEffect(() => { function messageHandler(e) { if (e.origin === origin && e.source === window.opener) { - if (e.data.method !== 'signTransaction') { + if ( + e.data.method !== 'signTransaction' && + e.data.method !== 'signAllTransactions' + ) { postMessage({ error: 'Unsupported method', id: e.data.id }); } @@ -106,11 +109,29 @@ export default function PopupPage({ opener }) { if (requests.length > 0) { const request = requests[0]; - assert(request.method === 'signTransaction'); - const message = bs58.decode(request.params.message); + assert( + request.method === 'signTransaction' || + request.method === 'signAllTransactions', + ); - async function sendSignature() { + let messages = + request.method === 'signTransaction' + ? [bs58.decode(request.params.message)] + : request.params.messages.map((m) => bs58.decode(m)); + + async function onApprove() { setRequests((requests) => requests.slice(1)); + if (request.method === 'signTransaction') { + sendSignature(messages[0]); + } else { + sendAllSignatures(messages); + } + if (requests.length === 1) { + focusParent(); + } + } + + async function sendSignature(message) { postMessage({ result: { signature: await wallet.createSignature(message), @@ -118,9 +139,19 @@ export default function PopupPage({ opener }) { }, id: request.id, }); - if (requests.length === 1) { - focusParent(); - } + } + + async function sendAllSignatures(messages) { + const signatures = await Promise.all( + messages.map((m) => wallet.createSignature(m)), + ); + postMessage({ + result: { + signatures, + publicKey: wallet.publicKey.toBase58(), + }, + id: request.id, + }); } function sendReject() { @@ -138,8 +169,8 @@ export default function PopupPage({ opener }) { key={request.id} autoApprove={autoApprove} origin={origin} - message={message} - onApprove={sendSignature} + messages={messages} + onApprove={onApprove} onReject={sendReject} /> ); @@ -269,7 +300,7 @@ function ApproveConnectionForm({ origin, onApprove }) { ); } -function isSafeInstruction(publicKeys, owner, instructions) { +function isSafeInstruction(publicKeys, owner, txInstructions) { let unsafe = false; const states = { CREATED: 0, @@ -292,60 +323,62 @@ function isSafeInstruction(publicKeys, owner, instructions) { return accountStates[pubkey.toBase58()] === states.OWNED; } - instructions.forEach((instruction) => { - if (!instruction) { - unsafe = true; - } else { - 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 = true; - } - } 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 { + txInstructions.forEach((instructions) => { + instructions.forEach((instruction) => { + if (!instruction) { unsafe = true; + } else { + 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 = true; + } + } 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 @@ -363,7 +396,7 @@ function isSafeInstruction(publicKeys, owner, instructions) { function ApproveSignatureForm({ origin, - message, + messages, onApprove, onReject, autoApprove, @@ -375,24 +408,30 @@ function ApproveSignatureForm({ const [publicKeys] = useWalletPublicKeys(); const [parsing, setParsing] = useState(true); - const [instructions, setInstructions] = useState(null); + // An array of arrays, where each element is the set of instructions for a + // single transaction. + const [txInstructions, setTxInstructions] = useState(null); const buttonRef = useRef(); + const isMultiTx = messages.length > 1; + useEffect(() => { - decodeMessage(connection, wallet, message).then((instructions) => { - setInstructions(instructions); - setParsing(false); - }); - }, [message, connection, wallet]); + Promise.all(messages.map((m) => decodeMessage(connection, wallet, m))).then( + (txInstructions) => { + setTxInstructions(txInstructions); + setParsing(false); + }, + ); + }, [messages, connection, wallet]); const validator = useMemo(() => { return { safe: publicKeys && - instructions && - isSafeInstruction(publicKeys, wallet.publicKey, instructions), + txInstructions && + isSafeInstruction(publicKeys, wallet.publicKey, txInstructions), }; - }, [publicKeys, instructions, wallet]); + }, [publicKeys, txInstructions, wallet]); useEffect(() => { if (validator.safe && autoApprove) { @@ -460,6 +499,37 @@ function ApproveSignatureForm({ } }; + const txLabel = (idx) => { + return ( + <> + + Transaction {idx.toString()} + + + + ); + }; + + const txListItem = (instructions, txIdx) => { + const ixs = instructions.map((instruction, i) => ( + + {getContent(instruction)} + + + )); + + if (!isMultiTx) { + return ixs; + } + + return ( + + {txLabel(txIdx)} + {ixs} + + ); + }; + return ( @@ -478,27 +548,26 @@ function ApproveSignatureForm({ style={{ fontWeight: 'bold' }} gutterBottom > - Parsing transaction: + Parsing transaction{isMultiTx > 0 ? 's' : ''}: - - {bs58.encode(message)} - + {messages.map((message, idx) => ( + + {bs58.encode(message)} + + ))} ) : ( <> - {instructions + {txInstructions ? `${origin} wants to:` : `Unknown transaction data`} - {instructions ? ( - instructions.map((instruction, i) => ( - - {getContent(instruction)} - - - )) + {txInstructions ? ( + txInstructions.map((instructions, txIdx) => + txListItem(instructions, txIdx), + ) ) : ( <> - Unknown transaction: - - - {bs58.encode(message)} + Unknown transaction{isMultiTx > 0 ? 's' : ''}: + {messages.map((message) => ( + + {bs58.encode(message)} + + ))} )} {!validator.safe && ( @@ -543,7 +614,7 @@ function ApproveSignatureForm({ color="primary" onClick={onApprove} > - Approve + Approve{isMultiTx ? ' All' : ''}