Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,22 @@ export const messages = Object.freeze(
id: 'transaction.review.delegateVoting',
defaultMessage: '!!!Delegating voting to',
},
handleNoDrep: {
id: 'transaction.drep.handleNoDrep',
defaultMessage: '!!!This ADA handle does not have a DRep ID assigned.',
},
handleLookupFailed: {
id: 'transaction.drep.handleLookupFailed',
defaultMessage: '!!!Unable to verify ADA handle. Please try again.',
},
invalidFormat: {
id: 'transaction.drep.invalidFormat',
defaultMessage: '!!!Invalid DRep ID or ADA handle format.',
},
adaHandleNotFound: {
id: 'transaction.drep.adaHandleNotFound',
defaultMessage: '!!!ADA handle not found.',
},
})
);

Expand Down Expand Up @@ -274,5 +290,9 @@ export const useStrings = () => {
findDrepHere: intl.formatMessage(messages.findDrepHere),
delegateToYoroi: intl.formatMessage(messages.delegateToYoroi),
delegateVoting: intl.formatMessage(messages.delegateVoting),
handleNoDrep: intl.formatMessage(messages.handleNoDrep),
handleLookupFailed: intl.formatMessage(messages.handleLookupFailed),
invalidFormat: intl.formatMessage(messages.invalidFormat),
adaHandleNotFound: intl.formatMessage(messages.adaHandleNotFound),
}).current;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LoadingButton } from '@mui/lab';
import { Box, Link, Stack, Typography } from '@mui/material';
import React, { useEffect } from 'react';
import React from 'react';
import { dRepToMaybeCredentialHex } from '../../../../../api/ada/lib/cardanoCrypto/utils';
import { TextInput } from '../../../../components';
import { useTxReviewModal } from '../../module/ReviewTxProvider';
Expand All @@ -16,67 +16,147 @@ import { GovernanceStatusCard } from '../../../governace/useCases/GovernanceStat
import { useGovernanceDelegationToYoroiDrep } from '../../../governace/common/hooks/useGovernanceDelegationToYoroiDrep';
import { useGovernance } from '../../../governace/module/GovernanceContextProvider';

const HANDLE_API = 'https://api.handle.me/handles';

type DrepError = null | 'INVALID_FORMAT' | 'HANDLE_NO_DREP' | 'HANDLE_NOT_FOUND' | 'HANDLE_LOOKUP_FAILED';

const sanitizeHandle = (raw: string) => raw.trim().replace(/^\$/, '');

const looksLikeHandle = (raw: string) => {
const v = raw.trim();
return v.startsWith('$') || v.includes('@');
};

const resolveDrepIdFromHandle = async (raw: string): Promise<{ drepId: string | null; error: DrepError }> => {
const handle = sanitizeHandle(raw);
if (!handle) return { drepId: null, error: 'INVALID_FORMAT' };

try {
const res = await fetch(`${HANDLE_API}/${encodeURIComponent(handle)}`, {
method: 'GET',
headers: { Accept: 'application/json' },
});

if (res.status === 404) {
return { drepId: null, error: 'HANDLE_NOT_FOUND' };
}

if (!res.ok) {
return { drepId: null, error: 'HANDLE_LOOKUP_FAILED' };
}

const data: any = await res.json();

if (!data?.drep) {
return { drepId: null, error: 'HANDLE_NO_DREP' };
}

const drepId = data.drep.cip_105 ?? data.drep.cip_129 ?? null;
if (!drepId) return { drepId: null, error: 'HANDLE_NO_DREP' };

return { drepId, error: null };
} catch {
return { drepId: null, error: 'HANDLE_LOOKUP_FAILED' };
}
};

export const ChooseOtherDrepId = () => {
const { drepId, isLoading, changeModalView, createUnsignedTx, setDrepId } = useTxReviewModal();
const { isLoading, changeModalView, createUnsignedTx, setDrepId } = useTxReviewModal();
const { delegateToDrep } = useGovernanceDelegationToYoroiDrep();
const { isTestnet } = useGovernance();
const strings = useStrings();
const [error, setError] = React.useState(false);
const [drepIdInput, setDrepValueId] = React.useState('');

const findDrepLink = isTestnet ? FIND_DREPS_LINK_TESTNET : FIND_DREPS_LINK;
const yoroiDrepId = isTestnet ? YOROI_DREP_ID_TESTNET : YOROI_DREP_ID;

useEffect(() => {
setError(false);
}, [drepIdInput]);
const [drepIdInput, setDrepIdInput] = React.useState('');
const [error, setError] = React.useState<DrepError>(null);

const getHelperText = (error: DrepError) => {
switch (error) {
case 'HANDLE_NO_DREP':
return strings.handleNoDrep;
case 'HANDLE_NOT_FOUND':
return strings.adaHandleNotFound;
case 'HANDLE_LOOKUP_FAILED':
return strings.handleLookupFailed;
case 'INVALID_FORMAT':
return strings.invalidFormat;
default:
return ' ';
}
};

const confirmDRep = async () => {
const dRepCredentialHex: string | null = dRepToMaybeCredentialHex(drepIdInput);
const raw = drepIdInput.trim();
if (!raw) return;

setError(null);

let resolvedDrepId = raw;

if (looksLikeHandle(raw)) {
const { drepId, error } = await resolveDrepIdFromHandle(raw);

if (error || !drepId) {
setError(error ?? 'HANDLE_LOOKUP_FAILED');
return;
}

resolvedDrepId = drepId;
}

const dRepCredentialHex: string | null = dRepToMaybeCredentialHex(resolvedDrepId);
if (dRepCredentialHex == null) {
setError(true);
} else {
setDrepId({ drepID: drepIdInput });
await createUnsignedTx(dRepCredentialHex);
changeModalView({ modalView: 'operations', title: 'Operations' });
setError('INVALID_FORMAT');
return;
}

setDrepId({ drepID: resolvedDrepId });
await createUnsignedTx(dRepCredentialHex);
changeModalView({ modalView: 'operations', title: 'Operations' });
};

return (
<Stack direction={'column'} justifyContent={'space-between'} height={'100%'}>
<Stack direction="column" justifyContent="space-between" height="100%">
<Stack direction="column">
<Stack sx={{ height: '100%', mt: '24px', p: '24px' }} direction="column">
<Typography variant="body1" color="ds.text_gray_medium" mb="16px">
{strings.findPreferredDrep}
</Typography>

<Box>
<TextInput
id="setDrepValueId"
label="Drep ID"
label="DRep ID or ADA handle"
variant="outlined"
onChange={event => {
setDrepValueId(event.target.value);
setDrepIdInput(event.target.value);
if (error) setError(null);
}}
value={drepId}
error={error}
helperText={error ? 'Incorect Format' : ' '}
value={drepIdInput}
error={Boolean(error)}
helperText={getHelperText(error)}
/>
</Box>
</Stack>
<Stack direction={'column'} sx={{ justifyContent: 'center', alignItems: 'center' }}>
<Stack direction={'row'} alignItems={'center'} gap={8}>

<Stack direction="column" sx={{ justifyContent: 'center', alignItems: 'center' }}>
<Stack direction="row" alignItems="center" gap={8}>
<Typography variant="body1" color="ds.text_gray_medium">
{strings.dontHaveId}
</Typography>

<Link href={findDrepLink} rel="noopener" target="_blank" underline="hover">
{strings.findDrepHere}
</Link>
</Stack>

<Typography variant="body1" color="ds.text_gray_medium">
{strings.delegateToYoroi}
</Typography>
</Stack>

<Stack p={24}>
<GovernanceStatusCard
state={GOVERNANCE_STATUS.DELEGATED}
Expand All @@ -86,15 +166,14 @@ export const ChooseOtherDrepId = () => {
/>
</Stack>
</Stack>

<Stack direction="row" justifyContent="space-between" p="24px">
<LoadingButton
// @ts-ignore
// @ts-ignore
variant="primary"
sx={{ width: '100%' }}
onClick={() => {
confirmDRep();
}}
disabled={drepIdInput === undefined || drepIdInput.length === 0}
onClick={confirmDRep}
disabled={!drepIdInput.trim()}
loading={isLoading}
>
{strings.confirmLabel}
Expand Down
4 changes: 4 additions & 0 deletions packages/yoroi-extension/app/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1546,5 +1546,9 @@
"transaction.fail.cancelByUser": "Transaction cancelled by user",
"transaction.fail.error": "Your transaction has not been processed properly due to technical issues.",
"transaction.success.title": "Transaction signed",
"transaction.drep.handleNoDrep": "This ADA handle does not have a DRep ID assigned.",
"transaction.drep.handleLookupFailed": "Unable to verify ADA handle. Please try again.",
"transaction.drep.invalidFormat": "Invalid DRep ID or ADA handle format.",
"transaction.drep.adaHandleNotFound": "ADA handle not found.",
"transaction.success.description": "It may take a few minutes to display it in the list of wallet transactions."
}
Loading