Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rifeljm/#622 walletconnect bypass readonly commands #2055

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 100 additions & 53 deletions packages/gui/src/components/settings/SettingsIntegration.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useGetLoggedInFingerprintQuery } from '@chia-network/api-react';
import { Flex, MenuItem, SettingsHR, SettingsSection, SettingsTitle, SettingsText } from '@chia-network/core';
import { t, Trans } from '@lingui/macro';
import {
Expand Down Expand Up @@ -33,8 +34,14 @@ export default function SettingsIntegration() {
padding: 2,
};
const { disconnect } = useWalletConnectContext();
const { enabled, setEnabled, allowConfirmationFingerprintChange, setAllowConfirmationFingerprintChange } =
useWalletConnectPreferences();
const {
enabled,
setEnabled,
allowConfirmationFingerprintChange,
setAllowConfirmationFingerprintChange,
bypassReadonlyCommands,
setBypassReadonlyCommands,
} = useWalletConnectPreferences();

const [topic, setTopic] = React.useState<string | null>(null);
const [bypassCommands, setBypassCommands] = React.useState<Record<string, boolean> | undefined>();
Expand All @@ -45,6 +52,7 @@ export default function SettingsIntegration() {
useWalletConnectPairs();

const pairs = get();
const { data: fingerprint } = useGetLoggedInFingerprintQuery();

const refreshBypassCommands = React.useCallback(() => {
if (selectedPair.current) {
Expand All @@ -65,12 +73,15 @@ export default function SettingsIntegration() {
if (!pair) {
return;
}
const tempObj = { ...bypassReadonlyCommands };
delete tempObj[pair.topic];
setBypassReadonlyCommands(tempObj);
disconnect(pair.topic);
setTopic(null);
setBypassCommands(undefined);
selectedPair.current = undefined;
setAutocompleteKey((localKey) => localKey + 1); // hack to force autocomplete to re-render. without this, the selected value doesn't change
}, [disconnect, selectedPair, setTopic, setBypassCommands]);
}, [disconnect, selectedPair, setTopic, setBypassCommands, bypassReadonlyCommands, setBypassReadonlyCommands]);

const handleBypassCommandChange = useCallback(
(command: string, newState: boolean) => {
Expand Down Expand Up @@ -117,6 +128,8 @@ export default function SettingsIntegration() {
return walletConnectCommands.filter((c) => commandKeys.includes(c.command));
}, [bypassCommands]);

const readonlyCommands = walletConnectCommands.filter((c) => !!c.bypassConfirm).map((c) => c.command);

useEffect(() => {
if (topic && pairs.length > 0) {
const pair = pairs.find((localPair) => localPair.topic === topic);
Expand All @@ -131,6 +144,11 @@ export default function SettingsIntegration() {
}
}, [topic, pairs, refreshBypassCommands]);

const skipConfirmation =
bypassReadonlyCommands && topic && fingerprint && bypassReadonlyCommands[topic]
? bypassReadonlyCommands[topic].indexOf(fingerprint) > -1
: false;

return (
<Grid container style={{ maxWidth: '624px' }} gap={3}>
<Grid item>
Expand Down Expand Up @@ -280,61 +298,90 @@ export default function SettingsIntegration() {
<Grid container marginTop="8px">
<Flex flexDirection="column" gap={2}>
<Flex flexDirection="column" gap={1}>
{commands.length > 0 ? (
<Box {...borderStyle}>
<Typography variant="h6">
<Trans>Skip Confirmation for Commands</Trans>
</Typography>
<Grid spacing={2} container marginTop="4px" marginRight="-32px">
{commands.map((commandInfo: WalletConnectCommand, idx: number) => (
<Grid item key={`grid-command-${commandInfo.command}`} width="624px" marginLeft="8px">
<Flex flexDirection="row" alignItems="center" justifyContent="spaceBetween" gap={1}>
<Grid item style={{ width: '400px' }}>
<SettingsTitle>{commandInfo.label ?? commandInfo.command}</SettingsTitle>
</Grid>
<Grid item container xs justifyContent="flex-end" marginTop="-6px" marginRight="8px">
<FormControl size="small">
<Select
value={(bypassCommands ?? {})[commandInfo.command] ? 1 : 0}
id={`${idx}`}
onChange={() =>
handleBypassCommandChange(
commandInfo.command,
!(bypassCommands ?? {})[commandInfo.command]
)
}
>
<MenuItem value={1}>
<Trans>Always Allow</Trans>
</MenuItem>
<MenuItem value={0} divider>
<Trans>Always Reject</Trans>
</MenuItem>
<MenuItem onClick={() => handleRemoveBypassCommand(commandInfo.command)}>
<Trans>Require Confirmation</Trans>
</MenuItem>
</Select>
</FormControl>
</Grid>
</Flex>
<Box {...borderStyle}>
<Typography variant="h6">
<Trans>Skip Confirmation for Commands</Trans>
</Typography>
<Grid spacing={2} container marginTop="4px" marginRight="-32px">
{commands.map((commandInfo: WalletConnectCommand, idx: number) => (
<Grid item key={`grid-command-${commandInfo.command}`} width="624px" marginLeft="8px">
<Flex flexDirection="row" alignItems="center" justifyContent="spaceBetween" gap={1}>
<Grid item style={{ width: '400px' }}>
<SettingsText>{commandInfo.description ?? ''}</SettingsText>
<SettingsTitle>{commandInfo.label ?? commandInfo.command}</SettingsTitle>
</Grid>
<Grid item container xs justifyContent="flex-end" marginTop="-6px" marginRight="8px">
<FormControl size="small">
<Select
value={(bypassCommands ?? {})[commandInfo.command] ? 1 : 0}
id={`${idx}`}
onChange={(e) => {
if (e.target.value === 'requireConfirmation') return;
handleBypassCommandChange(
commandInfo.command,
!(bypassCommands ?? {})[commandInfo.command]
);
}}
>
<MenuItem value={1}>
<Trans>Always Allow</Trans>
</MenuItem>
<MenuItem value={0} divider>
<Trans>Always Reject</Trans>
</MenuItem>
<MenuItem
value="requireConfirmation"
onClick={() => handleRemoveBypassCommand(commandInfo.command)}
>
<Trans>Require Confirmation</Trans>
</MenuItem>
</Select>
</FormControl>
</Grid>
</Flex>
<Grid item style={{ width: '400px' }}>
<SettingsText>{commandInfo.description ?? ''}</SettingsText>
</Grid>
))}
</Grid>
</Box>
) : (
<Box {...borderStyle}>
<Grid item width="590px">
<Typography variant="subtitle1">
<Trans>No Custom Permissions</Trans>
</Typography>
</Grid>
))}
</Grid>
<Grid spacing={2} container marginTop="4px" marginRight="-32px">
<Grid item width="624px" marginLeft="8px">
<Flex flexDirection="row" alignItems="center" justifyContent="spaceBetween" gap={1}>
<Grid item style={{ maxWidth: '400px' }}>
<SettingsTitle>
<Trans>All Read-Only Commands</Trans>
</SettingsTitle>
</Grid>
<Grid item container xs justifyContent="flex-end" marginTop="-6px" marginRight="8px">
<FormControl size="small">
<Select
value={skipConfirmation}
onChange={(evt: PointerEvent) => {
setBypassReadonlyCommands({
...bypassReadonlyCommands,
[topic]: evt.target.value
? (bypassReadonlyCommands?.[topic] || []).concat(fingerprint)
: (bypassReadonlyCommands?.[topic] || []).filter((f) => f !== fingerprint),
});
}}
>
<MenuItem value>
<Trans>Always Allow</Trans>
</MenuItem>
<MenuItem value={false} onClick={() => {}}>
<Trans>Require Confirmation</Trans>
</MenuItem>
</Select>
</FormControl>
</Grid>
</Flex>
<Grid item style={{ maxWidth: '400px' }}>
<SettingsText>{readonlyCommands.join(', ')}</SettingsText>
</Grid>
</Grid>
</Box>
)}
</Grid>
</Box>
</Flex>

{topic && commands.length > 0 && (
<>
<Grid item style={{ width: '624px' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Form,
Loading,
useCurrencyCode,
TooltipIcon,
} from '@chia-network/core';
import { Trans, t } from '@lingui/macro';
import CloseIcon from '@mui/icons-material/Close';
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function WalletConnectAddConnectionDialog(props: WalletConnectAdd
const { onClose = () => {}, open = false } = props;

const [step, setStep] = useState<Step>(Step.CONNECT);
const [bypassCheckbox, toggleBypassCheckbox] = useState(false);
const { pair, isLoading: isLoadingWallet } = useWalletConnectContext();
const { data: keys, isLoading: isLoadingPublicKeys } = useGetKeysQuery({});
const [sortedWallets] = usePrefs('sortedWallets', []);
Expand All @@ -72,7 +74,12 @@ export default function WalletConnectAddConnectionDialog(props: WalletConnectAdd
defaultValue: [],
});

const { allowConfirmationFingerprintChange, setAllowConfirmationFingerprintChange } = useWalletConnectPreferences();
const {
allowConfirmationFingerprintChange,
setAllowConfirmationFingerprintChange,
bypassReadonlyCommands,
setBypassReadonlyCommands,
} = useWalletConnectPreferences();

const sortedKeysMemo = React.useMemo(() => {
const sortedKeys: any[] = sortedWallets
Expand All @@ -98,7 +105,7 @@ export default function WalletConnectAddConnectionDialog(props: WalletConnectAdd
}
}, [fingerprint, methods]);

async function handleSubmit(values: FormData) {
function handleSubmit(values: FormData) {
const { uri, fingerprints } = values;
if (!uri) {
throw new Error(t`Please enter a URI`);
Expand All @@ -117,8 +124,20 @@ export default function WalletConnectAddConnectionDialog(props: WalletConnectAdd
setAllowConfirmationFingerprintChange(true);
}

const topic = await pair(uri, selectedFingerprints, mainnet);
onClose(topic);
/* for some bizarre reason, "pair" function below needs to be written as a Promise, otherwise
a caching mechanism (useState) in useLocalStorage will not work reliably */
pair(uri, selectedFingerprints, mainnet).then((topic: any) => {
if (bypassCheckbox) {
if (!bypassReadonlyCommands[topic.toString()]) {
bypassReadonlyCommands[topic.toString()] = [];
}
setBypassReadonlyCommands({
...bypassReadonlyCommands,
[topic.toString()]: (bypassReadonlyCommands[topic.toString()] || []).concat(selectedFingerprints),
});
}
onClose(topic);
});
}

function handleToggleSelectFingerprint(fingerprintLocal: number) {
Expand Down Expand Up @@ -212,6 +231,33 @@ export default function WalletConnectAddConnectionDialog(props: WalletConnectAdd
return null;
}

function renderQuietModeOption() {
return (
<Flex
sx={{ cursor: 'pointer' }}
alignItems="center"
onClick={() => {
toggleBypassCheckbox(!bypassCheckbox);
}}
>
<Checkbox checked={bypassCheckbox} disableRipple sx={{ paddingLeft: 0 }} />
<Flex flexDirection="row" alignItems="center" gap={1}>
<Typography variant="body2">
<Trans>Skip confirmation for all read-only commands</Trans>
</Typography>
<TooltipIcon>
<Trans>
By default, a prompt will be presented each time a WalletConnect command is invoked. Select this option if
you would like to skip the prompt for all read-only WalletConnect commands.
<p />
Commands that create a transaction or otherwise modify your wallet will still require confirmation.
</Trans>
</TooltipIcon>
</Flex>
</Flex>
);
}

return (
<Dialog onClose={handleClose} maxWidth="xs" open={open} fullWidth>
<DialogTitle>
Expand Down Expand Up @@ -259,6 +305,7 @@ export default function WalletConnectAddConnectionDialog(props: WalletConnectAdd
{renderSelectedAsPills()}
</Flex>
{renderKeysMultiSelect()}
{renderQuietModeOption()}
</Flex>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Box, Divider, Dialog, DialogContent, DialogTitle, IconButton, Typograph
import React, { useMemo, useState } from 'react';

import useWalletConnectContext from '../../hooks/useWalletConnectContext';
import useWalletConnectPreferences from '../../hooks/useWalletConnectPreferences';

import WalletConnectActiveSessions from './WalletConnectActiveSessions';
import HeroImage from './images/walletConnectConnected.svg';
Expand All @@ -22,6 +23,7 @@ export default function WalletConnectConnectedDialog(props: WalletConnectAddConn
const showError = useShowError();
const { pairs, disconnect, isLoading: isLoadingWallet } = useWalletConnectContext();
const { data: keys, isLoading: isLoadingPublicKeys } = useGetKeysQuery({});
const { bypassReadonlyCommands, setBypassReadonlyCommands } = useWalletConnectPreferences();

const pair = pairs.getPair(topic);

Expand Down Expand Up @@ -50,6 +52,9 @@ export default function WalletConnectConnectedDialog(props: WalletConnectAddConn
setIsProcessing(true);

try {
const tempObj = { ...bypassReadonlyCommands };
delete tempObj[pair.topic];
setBypassReadonlyCommands(tempObj);
await disconnect(topic);
onClose();
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function WalletConnectConnections(props: WalletConnectConnections
const { onClose } = props;
const openDialog = useOpenDialog();
const showError = useShowError();
const { enabled, setEnabled } = useWalletConnectPreferences();
const { enabled, setEnabled, bypassReadonlyCommands, setBypassReadonlyCommands } = useWalletConnectPreferences();
const { disconnect, pairs, isLoading } = useWalletConnectContext();

const handleAddConnection = useCallback(async () => {
Expand All @@ -37,6 +37,9 @@ export default function WalletConnectConnections(props: WalletConnectConnections

async function handleDisconnect(topic: string) {
try {
const tempObj = { ...bypassReadonlyCommands };
delete tempObj[topic];
setBypassReadonlyCommands(tempObj);
onClose?.();
await disconnect(topic);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Divider, Dialog, DialogContent, DialogTitle, IconButton, Typography } f
import React, { useMemo, useState } from 'react';

import useWalletConnectContext from '../../hooks/useWalletConnectContext';
import useWalletConnectPreferences from '../../hooks/useWalletConnectPreferences';

import WalletConnectActiveSessions from './WalletConnectActiveSessions';
import WalletConnectMetadata from './WalletConnectMetadata';
Expand All @@ -23,6 +24,8 @@ export default function WalletConnectPairInfoDialog(props: WalletConnectPairInfo
const { pairs, disconnect, isLoading: isLoadingWallet } = useWalletConnectContext();
const { data: keys, isLoading: isLoadingPublicKeys } = useGetKeysQuery({});

const { bypassReadonlyCommands, setBypassReadonlyCommands } = useWalletConnectPreferences();

const pair = useMemo(() => pairs.getPair(topic), [topic, pairs]);

const selectedKeys = useMemo(() => {
Expand Down Expand Up @@ -51,6 +54,9 @@ export default function WalletConnectPairInfoDialog(props: WalletConnectPairInfo
setIsProcessing(true);

try {
const tempObj = { ...bypassReadonlyCommands };
delete tempObj[topic];
setBypassReadonlyCommands(tempObj);
await disconnect(topic);
onClose();
} catch (e) {
Expand Down
Loading