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
2 changes: 1 addition & 1 deletion cmd/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ var (
Short: "delete the key associated with the address or nickname from the keystore",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
writeToConsole(client.KeystoreDelete(argGetAddrOrNickname(args[0])))
writeToConsole(client.KeystoreDelete(argGetAddrOrNickname(args[0]), getPassword()))
},
}

Expand Down
6 changes: 4 additions & 2 deletions cmd/rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3541,13 +3541,14 @@ $ curl -X POST http://localhost:50003/v1/admin/keystore-import-raw \

**Route:** `/v1/admin/keystore-delete`

**Description**: removes a key from the keystore using either the address or nickname
**Description**: removes a key from the keystore using either the address or nickname after validating the password

**HTTP Method**: `POST`

**Request**:
- **nickname**: `string` - the nickname associated with the key
- **address**: `string` - the address associated with the key
- **password**: `string` - **(required)** the plain-text password used to encrypt the key

**Response**: `hex-string` - the 20 byte address of the newly imported key

Expand All @@ -3556,7 +3557,8 @@ $ curl -X POST http://localhost:50003/v1/admin/keystore-import-raw \
$ curl -X POST http://localhost:50003/v1/admin/keystore-delete \
-H "Content-Type: application/json" \
-d '{
"nickname":"my_key_2"
"nickname":"my_key_2",
"password":"my_password"
}'

> "b0b4a45ca70104ecc943a49e4553f0e7e1135b01"
Expand Down
6 changes: 4 additions & 2 deletions cmd/rpc/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ func (s *Server) KeystoreImportRaw(w http.ResponseWriter, r *http.Request, _ htt
func (s *Server) KeystoreDelete(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// Call the keystore handler with a callback to perform the deletion
s.keystoreHandler(w, r, func(k *crypto.Keystore, ptr *keystoreRequest) (any, error) {
k.DeleteKey(crypto.DeleteOpts{
if err := k.DeleteKey(ptr.Password, crypto.DeleteOpts{
Address: ptr.Address,
Nickname: ptr.Nickname,
})
}); err != nil {
return nil, err
}
// Update the keystore on disk and return the account address
return ptr.Address, k.SaveToFile(s.config.DataDirPath)
})
Expand Down
6 changes: 4 additions & 2 deletions cmd/rpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ type AddrOrNickname struct {
Nickname string
}

func (c *Client) KeystoreDelete(addrOrNickname AddrOrNickname) (returned crypto.AddressI, err lib.ErrorI) {
func (c *Client) KeystoreDelete(addrOrNickname AddrOrNickname, password string) (returned crypto.AddressI, err lib.ErrorI) {
returned = new(crypto.Address)

if addrOrNickname.Address != "" {
Expand All @@ -480,14 +480,16 @@ func (c *Client) KeystoreDelete(addrOrNickname AddrOrNickname) (returned crypto.
return
}
err = c.keystoreRequest(KeystoreDeleteRouteName, keystoreRequest{
addressRequest: addressRequest{bz},
addressRequest: addressRequest{bz},
passwordRequest: passwordRequest{password},
}, returned)
return
}

if addrOrNickname.Nickname != "" {
err = c.keystoreRequest(KeystoreDeleteRouteName, keystoreRequest{
nicknameRequest: nicknameRequest{addrOrNickname.Nickname},
passwordRequest: passwordRequest{password},
}, returned)
return
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/rpc/web/wallet/public/plugin/canopy/chain.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@
"method": "POST"
},
"body": {
"nickname": "{{nickname}}"
"address": "{{address}}",
"password": "{{password}}"
}
},
"validator": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
},
"keystoreDelete": {
"source": { "base": "admin", "path": "/v1/admin/keystore-delete", "method": "POST" },
"body": { "nickname": "{{nickname}}" }
"body": { "address": "{{address}}", "password": "{{password}}" }
},
"validator": {
"source": { "base": "rpc", "path": "/v1/query/validator", "method": "POST" },
Expand Down
4 changes: 2 additions & 2 deletions cmd/rpc/web/wallet/src/app/pages/KeyManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ export const KeyManagement = (): JSX.Element => {
className="max-h-[90vh] max-w-[min(96vw,56rem)] gap-0 overflow-hidden border-[#272729] bg-[#171717] p-0"
>
<div className="max-h-[90vh] overflow-y-auto p-5 sm:p-6">
{activeModal === 'import' ? <ImportWallet embedded /> : null}
{activeModal === 'create' ? <NewKey embedded /> : null}
{activeModal === 'import' ? <ImportWallet embedded onSuccess={() => setActiveModal(null)} /> : null}
{activeModal === 'create' ? <NewKey embedded onSuccess={() => setActiveModal(null)} /> : null}
</div>
</DialogContent>
</Dialog>
Expand Down
6 changes: 4 additions & 2 deletions cmd/rpc/web/wallet/src/app/providers/AccountsListProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type AccountsListContextValue = {
isReady: boolean
refetch: () => Promise<any>
createNewAccount: (nickname: string, password: string) => Promise<string>
deleteAccount: (accountId: string, onDeleted?: (nextAccountId: string | null) => void) => Promise<void>
deleteAccount: (accountId: string, password: string, onDeleted?: (nextAccountId: string | null) => void) => Promise<void>
}

const AccountsListContext = createContext<AccountsListContextValue | undefined>(undefined)
Expand Down Expand Up @@ -76,6 +76,7 @@ export function AccountsListProvider({ children }: { children: React.ReactNode }

const deleteAccount = useCallback(async (
accountId: string,
password: string,
onDeleted?: (nextAccountId: string | null) => void
): Promise<void> => {
try {
Expand All @@ -85,7 +86,8 @@ export function AccountsListProvider({ children }: { children: React.ReactNode }
}

await dsFetch('keystoreDelete', {
nickname: account.nickname
address: account.address,
password,
})

// Notify caller about which account to switch to
Expand Down
6 changes: 3 additions & 3 deletions cmd/rpc/web/wallet/src/app/providers/AccountsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type AccountsContextValue = {

switchAccount: (id: string | null) => void
createNewAccount: (nickname: string, password: string) => Promise<string>
deleteAccount: (accountId: string) => Promise<void>
deleteAccount: (accountId: string, password: string) => Promise<void>
refetch: () => Promise<any>
}

Expand Down Expand Up @@ -48,8 +48,8 @@ export function useAccounts(): AccountsContextValue {
const selected = useSelectedAccount()

// Wrap deleteAccount to integrate with switchAccount
const deleteAccount = useCallback(async (accountId: string): Promise<void> => {
await list.deleteAccount(accountId, (nextAccountId) => {
const deleteAccount = useCallback(async (accountId: string, password: string): Promise<void> => {
await list.deleteAccount(accountId, password, (nextAccountId: string | null) => {
if (selected.selectedId === accountId && nextAccountId) {
selected.switchAccount(nextAccountId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ export const PollDetailsModal: React.FC<PollDetailsModalProps> = ({
onClose,
onVote,
}) => {
React.useEffect(() => {
if (!isOpen) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [isOpen, onClose]);

if (!poll) return null;

const normalizedHash = poll.proposalHash || poll.hash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ export const ProposalDetailsModal: React.FC<ProposalDetailsModalProps> = ({
onClose,
}) => {
const { symbol, factor } = useDenom();

React.useEffect(() => {
if (!isOpen) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [isOpen, onClose]);

if (!proposal) return null;

const getCategoryColor = (category: string) => {
Expand Down
48 changes: 27 additions & 21 deletions cmd/rpc/web/wallet/src/components/key-management/CurrentWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
const [passwordError, setPasswordError] = useState("");
const [isFetchingKey, setIsFetchingKey] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] = useState("");
const [deletePassword, setDeletePassword] = useState("");
const [deletePasswordError, setDeletePasswordError] = useState("");
const [isDeleting, setIsDeleting] = useState(false);
const [isRenameOpen, setIsRenameOpen] = useState(false);
const [renameNickname, setRenameNickname] = useState("");
Expand Down Expand Up @@ -217,7 +218,8 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
return;
}

setDeleteConfirmation("");
setDeletePassword("");
setDeletePasswordError("");
setShowDeleteModal(true);
};

Expand Down Expand Up @@ -271,20 +273,20 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
const handleConfirmDelete = async () => {
if (!selectedAccount) return;

const nickname = selectedKeyEntry?.keyNickname || selectedAccount.nickname;
if (deleteConfirmation !== nickname) {
toast.error({
title: "Confirmation Failed",
description: `Please type "${nickname}" to confirm deletion`,
});
if (!deletePassword) {
setDeletePasswordError("Password is required.");
return;
}

setIsDeleting(true);
setDeletePasswordError("");

try {
const nickname = selectedKeyEntry?.keyNickname || selectedAccount.nickname;

await dsFetch("keystoreDelete", {
nickname: nickname,
address: selectedKeyEntry?.keyAddress ?? selectedAccount.address,
password: deletePassword,
});

await invalidateKeystore();
Expand All @@ -295,9 +297,8 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
});

setShowDeleteModal(false);
setDeleteConfirmation("");
setDeletePassword("");

// Switch to another account
const otherAccounts = accounts.filter((acc) => acc.id !== selectedAccount.id);
if (otherAccounts.length > 0) {
setTimeout(() => {
Expand All @@ -307,6 +308,7 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
switchAccount(null);
}
} catch (error) {
setDeletePasswordError("Unable to delete with that password.");
toast.error({
title: "Delete Failed",
description: error instanceof Error ? error.message : String(error),
Expand Down Expand Up @@ -591,25 +593,29 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
</div>

<p className="text-sm text-muted-foreground mb-4">
Type <span className="font-semibold text-foreground">
Enter your wallet password to confirm deletion of <span className="font-semibold text-foreground">
{selectedKeyEntry?.keyNickname || selectedAccount?.nickname}
</span> to confirm deletion:
</span>:
</p>

<input
type="text"
value={deleteConfirmation}
onChange={(e) => setDeleteConfirmation(e.target.value)}
placeholder="Type wallet name to confirm"
className="w-full bg-[#0f0f0f] text-foreground border border-[#272729] rounded-lg px-3 py-2.5 mb-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff1845]/25"
type="password"
value={deletePassword}
onChange={(e) => setDeletePassword(e.target.value)}
placeholder="Password"
className="w-full bg-[#0f0f0f] text-foreground border border-[#272729] rounded-lg px-3 py-2.5 mb-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff1845]/25"
autoFocus
/>
{deletePasswordError && (
<div className="text-sm text-[#ff1845] mb-2">{deletePasswordError}</div>
)}

<div className="flex justify-end gap-2">
<div className="flex justify-end gap-2 mt-2">
<button
onClick={() => {
setShowDeleteModal(false);
setDeleteConfirmation("");
setDeletePassword("");
setDeletePasswordError("");
}}
className="px-4 py-2 rounded-lg border border-[#272729] bg-[#0f0f0f] text-white hover:bg-[#272729]"
disabled={isDeleting}
Expand All @@ -619,7 +625,7 @@ export const CurrentWallet = ({ embedded = false }: { embedded?: boolean }): JSX
<button
onClick={handleConfirmDelete}
className="px-4 py-2 rounded-lg bg-[#ff1845] text-white hover:bg-[#ff1845]/90 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isDeleting || deleteConfirmation !== (selectedKeyEntry?.keyNickname || selectedAccount?.nickname)}
disabled={isDeleting || !deletePassword}
>
{isDeleting ? "Deleting..." : "Delete Permanently"}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface EncryptedKeyFile {
keyNickname?: string;
}

export const ImportWallet = ({ embedded = false }: { embedded?: boolean }): JSX.Element => {
export const ImportWallet = ({ embedded = false, onSuccess }: { embedded?: boolean; onSuccess?: () => void }): JSX.Element => {
const toast = useToast();
const dsFetch = useDSFetcher();
const queryClient = useQueryClient();
Expand Down Expand Up @@ -101,6 +101,7 @@ export const ImportWallet = ({ embedded = false }: { embedded?: boolean }): JSX.
});

setImportForm({ privateKey: '', password: '', confirmPassword: '', nickname: '' });
onSuccess?.();
} catch (error) {
toast.dismiss(loadingToast);
toast.error({
Expand Down Expand Up @@ -195,6 +196,7 @@ export const ImportWallet = ({ embedded = false }: { embedded?: boolean }): JSX.
setKeystoreFile(null);
setKeystoreFileName('');
if (fileInputRef.current) fileInputRef.current.value = '';
onSuccess?.();
} catch (error) {
toast.dismiss(loadingToast);
toast.error({
Expand Down
3 changes: 2 additions & 1 deletion cmd/rpc/web/wallet/src/components/key-management/NewKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useToast } from '@/toast/ToastContext';
import { useDSFetcher } from '@/core/dsFetch';
import { useQueryClient } from '@tanstack/react-query';

export const NewKey = ({ embedded = false }: { embedded?: boolean }): JSX.Element => {
export const NewKey = ({ embedded = false, onSuccess }: { embedded?: boolean; onSuccess?: () => void }): JSX.Element => {
const { switchAccount } = useAccounts();
const toast = useToast();
const dsFetch = useDSFetcher();
Expand Down Expand Up @@ -59,6 +59,7 @@ export const NewKey = ({ embedded = false }: { embedded?: boolean }): JSX.Elemen
});

setNewKeyForm({ password: '', walletName: '' });
onSuccess?.();

const newAddress = typeof response === 'string' ? response : (response as any)?.address;
if (newAddress) {
Expand Down
15 changes: 13 additions & 2 deletions lib/crypto/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ type DeleteOpts struct {
Nickname string
}

// DeleteKey() removes a private key from the store given an address and/or nickname
func (ks *Keystore) DeleteKey(opts DeleteOpts) {
// DeleteKey() removes a private key from the store given an address and/or nickname after validating the password
func (ks *Keystore) DeleteKey(password string, opts DeleteOpts) error {
var stringAddress string
if opts.Address != nil {
stringAddress = hex.EncodeToString(opts.Address)
Expand All @@ -192,10 +192,21 @@ func (ks *Keystore) DeleteKey(opts DeleteOpts) {
}

pKey := ks.AddressMap[stringAddress]
if pKey == nil {
return fmt.Errorf("key not found")
}
if password == "" {
return fmt.Errorf("invalid password")
}
if _, err := DecryptPrivateKey(pKey, []byte(password)); err != nil {
return err
}

if pKey.KeyNickname != "" {
delete(ks.NicknameMap, pKey.KeyNickname)
}
delete(ks.AddressMap, pKey.KeyAddress)
return nil
}

// SaveToFile() persists the keystore to a filepath
Expand Down
Loading
Loading