Skip to content
Merged
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
68 changes: 68 additions & 0 deletions examples/telescope-authz/components/authz/AuthzSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useState } from 'react';
import { ChainName } from 'cosmos-kit';
import { useChain } from '@cosmos-kit/react';
import { Box, Button, Tabs, Text } from '@interchain-ui/react';

import { Grants } from './Grants';
import { GrantModal } from './GrantModal';

export const AuthzSection = ({ chainName }: { chainName: ChainName }) => {
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const { address } = useChain(chainName);

if (!address) {
return (
<Text
fontWeight="$semibold"
fontSize="$lg"
textAlign="center"
color="$textSecondary"
attributes={{ my: '$24' }}
>
Please connect your wallet to view and create grants
</Text>
);
}

return (
<Box mb="$18" minHeight="500px" display="flex" flexDirection="column">
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
mb="$16"
>
<Tabs
tabs={[
{
label: 'Grants to me',
content: null,
},
{
label: 'Grants by me',
content: null,
},
]}
activeTab={activeTab}
onActiveTabChange={(tabId) => setActiveTab(tabId)}
attributes={{ width: '$min' }}
/>
<Button intent="tertiary" onClick={() => setIsOpen(true)}>
Create Grant
</Button>
</Box>

<Grants
chainName={chainName}
role={activeTab === 0 ? 'grantee' : 'granter'}
/>

<GrantModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
chainName={chainName}
/>
</Box>
);
};
132 changes: 132 additions & 0 deletions examples/telescope-authz/components/authz/CustomizationField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Dispatch, SetStateAction, useState } from 'react';
import { Box, NumberField, SelectButton, Text } from '@interchain-ui/react';

import { useValidators } from '@/hooks';
import { Permission, PermissionId } from '@/configs';
import { SelectValidatorsModal } from './SelectValidatorsModal';
import { AccessList } from './GrantModal';

// ==============================================

type SendCustomizationProps = {
value: number | undefined;
onChange: (value: string) => void;
};

const SendCustomization = ({ value, onChange }: SendCustomizationProps) => {
return (
<NumberField
placeholder="Spend Limit (Optional)"
value={value}
onInput={(e) => {
// @ts-ignore
onChange(e.target.value);
}}
formatOptions={{
maximumFractionDigits: 6,
}}
/>
);
};

// ==============================================

type DelegateCustomizationProps = {
value: number | undefined;
onChange: (value: string) => void;
chainName: string;
accessList: AccessList;
setAccessList: Dispatch<SetStateAction<AccessList>>;
};

const DelegateCustomization = ({
value,
onChange,
chainName,
accessList,
setAccessList,
}: DelegateCustomizationProps) => {
const [isOpen, setIsOpen] = useState(false);

const { data } = useValidators(chainName);

const validatorNames = data
? accessList.addresses.map(
(address) => data.find((v) => v.address === address)!.name
)
: [];

return (
<>
<NumberField
placeholder="Max Tokens (Optional)"
value={value}
onInput={(e) => {
// @ts-ignore
onChange(e.target.value);
}}
formatOptions={{
maximumFractionDigits: 6,
}}
/>
<SelectButton
placeholder="Select Validators (Optional)"
onClick={() => setIsOpen(true)}
/>
<Box
display={validatorNames.length > 0 ? 'block' : 'none'}
mt="$2"
px="$2"
>
<Text>
<Text
as="span"
fontWeight="$semibold"
color={
accessList.type === 'allowList' ? '$textSuccess' : '$textDanger'
}
>
{accessList.type === 'allowList' ? 'Allow List' : 'Deny List'}
:&nbsp;
</Text>
{validatorNames.join(', ')}
</Text>
</Box>
<SelectValidatorsModal
chainName={chainName}
accessList={accessList}
setAccessList={setAccessList}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
/>
</>
);
};

// ==============================================

type CustomizationFieldProps =
| ({
permissionType: typeof Permission['Send'];
} & SendCustomizationProps)
| ({
permissionType: typeof Permission['Delegate'];
} & DelegateCustomizationProps);

export const CustomizationField = ({
permissionType,
...rest
}: CustomizationFieldProps): JSX.Element | null => {
const fields: Partial<Record<PermissionId, JSX.Element | null>> = {
send:
permissionType === 'send' ? (
<SendCustomization {...(rest as SendCustomizationProps)} />
) : null,
delegate:
permissionType === 'delegate' ? (
<DelegateCustomization {...(rest as DelegateCustomizationProps)} />
) : null,
};

return fields[permissionType] ?? null;
};
185 changes: 185 additions & 0 deletions examples/telescope-authz/components/authz/GrantCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import Link from 'next/link';
import Image from 'next/image';
import { useState } from 'react';
import {
Box,
Button,
IconButton,
Stack,
Text,
TextField,
} from '@interchain-ui/react';
import { useChain } from '@cosmos-kit/react';

import {
getChainLogoByChainName,
PrettyGrant,
PrettyPermission,
} from '@/utils';
import { useAuthzContext } from '@/context';
import { useAuthzTx, useGrants } from '@/hooks';
import { getCoin, permissionNameToRouteMap } from '@/configs';

import styles from '@/styles/custom.module.css';

type GrantCardProps = {
role: 'granter' | 'grantee';
grant: PrettyGrant;
chainName: string;
onViewDetails: () => void;
};

export const GrantCard = ({
role,
grant,
chainName,
onViewDetails,
}: GrantCardProps) => {
const [isCopied, setIsCopied] = useState(false);
const [isRevoking, setIsRevoking] = useState(false);
const [revokingPermission, setRevokingPermission] =
useState<PrettyPermission>();

const { chain } = useChain(chainName);
const { refetch } = useGrants(chainName);
const { setPermission } = useAuthzContext();
const { authzTx, createRevokeMsg } = useAuthzTx(chainName);

const { address, permissions } = grant;

const isGranter = role === 'granter';
const token = getCoin(chainName);

const copy = (text: string) => {
if (isCopied) return;

navigator.clipboard
.writeText(text)
.then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 800);
})
.catch((error) => {
console.error('Failed to copy:', error);
});
};

const handleRevoke = (permission: PrettyPermission) => {
setIsRevoking(true);

authzTx({
msgs: [createRevokeMsg(permission)],
onSuccess: () => {
refetch();
},
onComplete: () => {
setIsRevoking(false);
},
});
};

return (
<Box
px="$10"
py="$11"
backgroundColor="$cardBg"
borderRadius="$lg"
width="$full"
>
<Stack space="$4" attributes={{ alignItems: 'center', mb: '$10' }}>
<Image
alt={token.name}
src={getChainLogoByChainName(chainName)}
width="30"
height="30"
sizes="100vw"
/>
<Text fontWeight="$semibold" fontSize="$lg">
{chain.pretty_name}
</Text>
</Stack>

<Box position="relative" mb="$10">
<TextField
id="address"
label={isGranter ? 'Grantee' : 'Granter'}
value={address}
inputClassName={styles.customInput}
/>
<Box position="absolute" bottom="$2" right="$2">
<IconButton
icon={isCopied ? 'checkLine' : 'copy'}
size="sm"
intent="secondary"
iconSize={isCopied ? '$xl' : '$md'}
onClick={() => copy(address)}
/>
</Box>
</Box>

<Text
color="$textSecondary"
fontSize="$sm"
fontWeight="$semibold"
lineHeight="$normal"
attributes={{ mb: '$6' }}
>
Permissions
</Text>

<Box
display="flex"
gap="$6"
flexWrap="wrap"
mb="$12"
height="$12"
overflow="hidden"
>
{permissions.map((permission) =>
isGranter ? (
<Button
key={permission.name}
size="sm"
intent="secondary"
rightIcon="close"
iconSize="$lg"
onClick={() => {
handleRevoke(permission);
setRevokingPermission(permission);
}}
disabled={
isRevoking && revokingPermission?.name === permission.name
}
>
{permission.name}
</Button>
) : permissionNameToRouteMap[permission.name] ? (
<Link
href={permissionNameToRouteMap[permission.name]}
style={{ textDecoration: 'none' }}
>
<Button
key={permission.name}
size="sm"
intent="secondary"
rightIcon="arrowRightRounded"
iconSize="$2xs"
onClick={() => setPermission(permission)}
>
{permission.name}
</Button>
</Link>
) : (
<Button key={permission.name} size="sm" intent="secondary">
{permission.name}
</Button>
)
)}
</Box>

<Button intent="tertiary" onClick={onViewDetails}>
View Details
</Button>
</Box>
);
};
Loading