Skip to content

Commit

Permalink
fix: safe bech32 address truncation (#204)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Felix C. Morency <[email protected]>
  • Loading branch information
chalabi2 and fmorency authored Jan 13, 2025
1 parent 738ad4e commit e7e1380
Show file tree
Hide file tree
Showing 14 changed files with 54 additions and 68 deletions.
2 changes: 1 addition & 1 deletion components/admins/components/validatorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export default function ValidatorList({
</td>

<td className="py-4 bg-secondary group-hover:bg-base-300 hidden lg:table-cell">
<TruncatedAddressWithCopy slice={10} address={validator.operator_address} />
<TruncatedAddressWithCopy slice={24} address={validator.operator_address} />
</td>
<td className="py-4 bg-secondary group-hover:bg-base-300 hidden md:table-cell">
{validator.consensus_power?.toString() ?? 'N/A'}
Expand Down
11 changes: 10 additions & 1 deletion components/bank/components/__tests__/historyBox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect, afterEach, describe, mock } from 'bun:test';
import { test, expect, afterEach, describe, mock, jest } from 'bun:test';
import { screen, cleanup, fireEvent } from '@testing-library/react';
import { HistoryBox } from '../historyBox';
import { renderWithChainProvider } from '@/tests/render';
Expand All @@ -7,6 +7,15 @@ import matchers from '@testing-library/jest-dom/matchers';

expect.extend(matchers);

// Mock next/router
const m = jest.fn();
mock.module('next/router', () => ({
useRouter: m.mockReturnValue({
query: {},
push: jest.fn(),
}),
}));

// Mock the hooks
mock.module('@/hooks', () => ({
useTokenFactoryDenomsMetadata: () => ({
Expand Down
2 changes: 1 addition & 1 deletion components/bank/components/historyBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export function HistoryBox({
? tx.data.to_address
: tx.data.from_address
}
slice={6}
slice={24}
/>
) : (
<div className="text-[#00000099] dark:text-[#FFFFFF99]">
Expand Down
2 changes: 1 addition & 1 deletion components/bank/modals/txInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function InfoItem({
<div className="bg-[#FFFFFF66] dark:bg-[#FFFFFF1A] rounded-[16px] p-4">
{isAddress ? (
<div className="flex items-center">
<TruncatedAddressWithCopy address={value} slice={8} />
<TruncatedAddressWithCopy address={value} slice={24} />
<a
href={`${env.explorerUrl}/${label === 'TRANSACTION HASH' ? 'transaction' : 'account'}/${label?.includes('TRANSACTION') ? value?.toUpperCase() : value}`}
target="_blank"
Expand Down
3 changes: 2 additions & 1 deletion components/groups/components/groupControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { TokenList } from '@/components';
import { CombinedBalanceInfo, ExtendedMetadataSDKType } from '@/utils';
import DenomList from '@/components/factory/components/DenomList';
import { useResponsivePageSize } from '@/hooks/useResponsivePageSize';
import env from '@/config/env';

type GroupControlsProps = {
policyAddress: string;
Expand Down Expand Up @@ -231,7 +232,7 @@ export default function GroupControls({
.trim();
}

const { address } = useChain('manifest');
const { address } = useChain(env.chain);
const { groupByMemberData } = useGroupsByMember(address ?? '');

useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions components/groups/components/myGroups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function YourGroups({
const [selectedGroupName, setSelectedGroupName] = useState<string>('Untitled Group');

const router = useRouter();
const { address } = useChain('manifest');
const { address } = useChain(env.chain);

const filteredGroups = groups.groups.filter(group => {
try {
Expand Down Expand Up @@ -635,7 +635,7 @@ function GroupRow({
</td>
<td className="bg-secondary group-hover:bg-base-300 hidden lg:table-cell w-1/6">
<div onClick={e => e.stopPropagation()}>
<TruncatedAddressWithCopy address={policyAddress} slice={12} />
<TruncatedAddressWithCopy address={policyAddress} slice={24} />
</div>
</td>
<td className="bg-secondary group-hover:bg-base-300 rounded-r-[12px] sm:rounded-l-none w-1/6">
Expand Down
15 changes: 10 additions & 5 deletions components/groups/forms/groups/ConfirmationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { cosmos } from '@liftedinit/manifestjs';
import { ThresholdDecisionPolicy } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
import { Duration } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/duration';
import { secondsToHumanReadable } from '@/utils/string';

import env from '@/config/env';
export default function ConfirmationForm({
nextStep,
prevStep,
Expand All @@ -28,10 +28,15 @@ export default function ConfirmationForm({
};

// Convert the object to a JSON string
const jsonString = JSON.stringify(groupMetadata);

const { tx, isSigning, setIsSigning } = useTx('manifest');
const { estimateFee } = useFeeEstimation('manifest');
let jsonString: string;
try {
jsonString = JSON.stringify(groupMetadata);
} catch (error) {
console.error('Failed to serialize group metadata:', error);
throw new Error('Invalid group metadata format');
}
const { tx, isSigning, setIsSigning } = useTx(env.chain);
const { estimateFee } = useFeeEstimation(env.chain);

const minExecutionPeriod: Duration = {
seconds: BigInt(0),
Expand Down
6 changes: 3 additions & 3 deletions components/groups/forms/groups/__tests__/Success.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ describe('Success Component', () => {
screen.getByText('Your transaction was successfully signed and broadcasted.')
).toBeInTheDocument();
expect(screen.getByText('Group Information')).toBeInTheDocument();
expect(screen.getByText('manifest1autho...author')).toBeInTheDocument();
expect(screen.getByText('manifest1efd63...m6rp3z')).toBeInTheDocument();
expect(screen.getByText('manifest1hj5fv...8ws9ct')).toBeInTheDocument();
expect(screen.getByText('manifest1autho...')).toBeInTheDocument();
expect(screen.getByText('manifest1efd63...')).toBeInTheDocument();
expect(screen.getByText('manifest1hj5fv...')).toBeInTheDocument();
const normalizer = getDefaultNormalizer({ collapseWhitespace: true, trim: true });
expect(screen.getByText('2 / 2', { normalizer })).toBeInTheDocument();
});
Expand Down
2 changes: 2 additions & 0 deletions components/groups/modals/memberManagementModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CopyIcon, TrashIcon } from '@/components/icons';
import { MdContacts } from 'react-icons/md';
import { TailwindModal } from '@/components/react/modal';
import env from '@/config/env';
import { truncateAddress } from '@/utils';

interface ExtendedMember extends MemberSDKType {
isNew: boolean;
Expand Down Expand Up @@ -321,6 +322,7 @@ export function MemberManagementModal({
}`}
placeholder="manifest1..."
disabled={!member.isNew || member.markedForDeletion}
value={truncateAddress(field.value)}
/>
{member.isNew && !member.markedForDeletion && (
<button
Expand Down
7 changes: 4 additions & 3 deletions components/groups/modals/voteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { cosmos } from '@liftedinit/manifestjs';
import { useChain } from '@cosmos-kit/react';
import React, { useState } from 'react';
import { CloseIcon } from '@/components/icons';
import env from '@/config/env';
function VotingPopup({
proposalId,
refetch,
Expand All @@ -13,9 +14,9 @@ function VotingPopup({
refetch: () => void;
setIsSigning: (isSigning: boolean) => void;
}) {
const { estimateFee } = useFeeEstimation('manifest');
const { tx } = useTx('manifest');
const { address } = useChain('manifest');
const { estimateFee } = useFeeEstimation(env.chain);
const { tx } = useTx(env.chain);
const { address } = useChain(env.chain);
const [error, setError] = useState<string | null>(null);

const { vote } = cosmos.group.v1.MessageComposer.withTypeUrl;
Expand Down
37 changes: 3 additions & 34 deletions components/react/addressCopy.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import { FiCopy, FiCheck } from 'react-icons/fi';
import { truncateAddress } from '@/utils';

export const TruncatedAddressWithCopy = ({
address,
address = '',
slice,
size,
}: {
Expand Down Expand Up @@ -31,7 +32,7 @@ export const TruncatedAddressWithCopy = ({
}
};

const truncatedAddress = `${address?.slice(0, slice)}...${address?.slice(-6)}`;
const truncatedAddress = truncateAddress(address, slice);
const iconSize = size === 'small' ? 10 : 16;

return (
Expand All @@ -45,35 +46,3 @@ export const TruncatedAddressWithCopy = ({
</span>
);
};

export const AddressWithCopy = ({ address }: { address: string }) => {
const [copied, setCopied] = useState(false);

useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (copied) {
timer = setTimeout(() => setCopied(false), 2000);
}
return () => clearTimeout(timer);
}, [copied]);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(address);
setCopied(true);
} catch (err) {
console.error('Failed to copy: ', err);
}
};

return (
<span
className="flex items-center space-x-2"
onClick={handleCopy}
style={{ cursor: 'pointer' }}
>
<span className="hover:text-primary">{address}</span>
{copied ? <FiCheck size="16" /> : <FiCopy size="16" />}
</span>
);
};
4 changes: 2 additions & 2 deletions components/react/views/Connected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useState } from 'react';
import ProfileAvatar from '@/utils/identicon';
import { useBalance } from '@/hooks/useQueries';
import { CopyIcon } from '@/components/icons';
import { getRealLogo, shiftDigits, truncateString } from '@/utils';
import { getRealLogo, shiftDigits, truncateAddress } from '@/utils';
import Image from 'next/image';
import { MdContacts } from 'react-icons/md';
import { Contacts } from './Contacts';
Expand Down Expand Up @@ -83,7 +83,7 @@ export const Connected = ({
<p className="text-lg font-semibold">{username || 'Anonymous'}</p>
<div className="flex items-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
{truncateString(address || '', 12)}
{truncateAddress(address || '')}
</p>
<button
onClick={copyAddress}
Expand Down
11 changes: 3 additions & 8 deletions components/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useChain } from '@cosmos-kit/react';
import { WalletStatus } from 'cosmos-kit';
import { MdWallet } from 'react-icons/md';
import env from '@/config/env';
import { truncateAddress, truncateString } from '@/utils';

const buttons = {
Disconnected: {
Expand Down Expand Up @@ -130,17 +131,11 @@ export const WalletSection: React.FC<WalletSectionProps> = ({ chainName }) => {
className="font-medium text-xl text-center mb-2 truncate"
title={username || 'Connected user'}
>
{username
? username.length > 20
? `${username.slice(0, 20)}...`
: username
: 'Connected User'}
{username ? truncateString(username, 20) : 'Connected User'}
</p>
<div className="bg-base-100 dark:bg-base-200 rounded-full py-2 px-4 text-center mb-4 flex items-center flex-row justify-between w-full ">
<p className="text-xs truncate flex-grow">
{address
? `${address.slice(0, 12)}...${address.slice(-6)}`
: 'Address not available'}
{address ? truncateAddress(address) : 'Address not available'}
</p>
<button
onClick={() => {
Expand Down
16 changes: 10 additions & 6 deletions utils/string.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { CombinedBalanceInfo } from './types';

export function truncateString(str: string, num: number) {
if (str.length > num) {
return str.slice(0, num) + '...' + str.slice(-6);
} else {
return str;
}
export function truncateString(str: string, prefixLen: number = 6, suffixLen: number = 6): string {
if (str.length <= prefixLen + suffixLen) return str;

return str.slice(0, prefixLen) + '...' + str.slice(-suffixLen);
}

export function truncateAddress(address: string, num: number = 24) {
if (address.length <= num) return address;

return address.slice(0, num) + '...';
}

export function secondsToHumanReadable(seconds: number): string {
Expand Down

0 comments on commit e7e1380

Please sign in to comment.