-
Notifications
You must be signed in to change notification settings - Fork 6
Memo improvements #696
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
base: develop
Are you sure you want to change the base?
Memo improvements #696
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { getQueryClient } from "@/core/react-query"; | |||||||||||||||||||||||||
| import { Announcements } from "@/features/announcement"; | ||||||||||||||||||||||||||
| import { Tracker } from "@/features/monitoring"; | ||||||||||||||||||||||||||
| import { AuthUpgradeDialog } from "@/features/shared/auth-upgrade"; | ||||||||||||||||||||||||||
| import { MemoKeyDialog } from "@/features/shared/memo-key"; | ||||||||||||||||||||||||||
| import { PushNotificationsProvider } from "@/features/push-notifications"; | ||||||||||||||||||||||||||
| import { UserActivityRecorder } from "@/features/user-activity"; | ||||||||||||||||||||||||||
| import { QueryClientProvider } from "@tanstack/react-query"; | ||||||||||||||||||||||||||
|
|
@@ -45,6 +46,7 @@ export function ClientProviders(props: PropsWithChildren) { | |||||||||||||||||||||||||
| {/* Defer non-critical components for LCP optimization */} | ||||||||||||||||||||||||||
| <DeferredRender> | ||||||||||||||||||||||||||
| <AuthUpgradeDialog /> | ||||||||||||||||||||||||||
| <MemoKeyDialog /> | ||||||||||||||||||||||||||
|
Comment on lines
47
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mount
Suggested fix <UIManager>
<ClientInit />
+ <MemoKeyDialog />
{/* Defer non-critical components for LCP optimization */}
<DeferredRender>
<AuthUpgradeDialog />
- <MemoKeyDialog />
<EcencyConfigManager.Conditional
condition={({ visionFeatures }) => visionFeatures.userActivityTracking.enabled}
>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| <EcencyConfigManager.Conditional | ||||||||||||||||||||||||||
| condition={({ visionFeatures }) => visionFeatures.userActivityTracking.enabled} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1463,6 +1463,13 @@ | |
| "memo": "Memo", | ||
| "memo-placeholder": "enter your notes here", | ||
| "memo-help": "This memo is public", | ||
| "memo-encrypted": "This memo will be encrypted", | ||
| "memo-decrypt": "Decrypt", | ||
| "memo-decrypt-error": "Could not decrypt memo. Check your memo key.", | ||
| "memo-encrypted-label": "Encrypted memo", | ||
| "memo-key-title": "Enter Memo Key", | ||
| "memo-key-subtitle-encrypt": "Your memo key is needed to encrypt this message.", | ||
| "memo-key-subtitle-decrypt": "Your memo key is needed to decrypt this message.", | ||
|
Comment on lines
+1470
to
+1472
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call out that this is the private memo key.
🤖 Prompt for AI Agents |
||
| "memo-error": "Never put private keys into a memo field, it is publicly visible to everyone.", | ||
| "memo-required": "Transfer to an exchange must have a memo.", | ||
| "invalid-asset": "You can only send HIVE to an exchange account.", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| "use client"; | ||
|
|
||
| import { useState, useCallback } from "react"; | ||
| import { Button } from "@ui/button"; | ||
| import i18next from "i18next"; | ||
| import { useActiveAccount } from "@/core/hooks/use-active-account"; | ||
| import { getLoginType } from "@/utils/user-token"; | ||
| import { decryptMemo } from "@/utils/memo-crypto"; | ||
| import { error } from "@/features/shared"; | ||
|
|
||
| interface Props { | ||
| memo: string; | ||
| } | ||
|
|
||
| /** | ||
| * Heuristic to detect if a #-prefixed memo is actually encrypted. | ||
| * | ||
| * Encrypted memos on Hive are `#` followed by a long base58-encoded string | ||
| * (typically 150+ chars, no spaces, no punctuation outside base58 alphabet). | ||
| * Plain text memos starting with `#` will have spaces and regular text. | ||
| */ | ||
| function isLikelyEncryptedMemo(memo: string): boolean { | ||
| if (!memo.startsWith("#")) return false; | ||
| const content = memo.slice(1); | ||
| // Encrypted memos are long base58 strings with no whitespace | ||
| return content.length > 50 && !/\s/.test(content); | ||
| } | ||
|
Comment on lines
+22
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent encrypted memo detection with calling components. The This creates a UX problem: a short encrypted memo (≤50 chars) would be routed to
🤖 Prompt for AI Agents |
||
|
|
||
| export function MemoDisplay({ memo }: Props) { | ||
| const { activeUser } = useActiveAccount(); | ||
| const [decryptedText, setDecryptedText] = useState<string | null>(null); | ||
| const [isDecrypting, setIsDecrypting] = useState(false); | ||
|
|
||
| const encrypted = isLikelyEncryptedMemo(memo); | ||
|
|
||
| const handleDecrypt = useCallback(async () => { | ||
| if (!activeUser) return; | ||
|
|
||
| setIsDecrypting(true); | ||
| try { | ||
| const loginType = getLoginType(activeUser.username); | ||
| const result = await decryptMemo(loginType, activeUser.username, memo); | ||
| setDecryptedText(result); | ||
| } catch (e) { | ||
| error(i18next.t("transfer.memo-decrypt-error")); | ||
| } finally { | ||
| setIsDecrypting(false); | ||
| } | ||
| }, [activeUser, memo]); | ||
|
|
||
| if (decryptedText !== null) { | ||
| return ( | ||
| <div className="text-sm text-gray-600 dark:text-gray-400 break-words"> | ||
| <span className="text-xs text-gray-500">🔓 </span> | ||
| {decryptedText} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!encrypted) { | ||
| // Plain text memo that happens to start with # | ||
| return ( | ||
| <div className="text-sm text-gray-600 dark:text-gray-400 break-words">{memo}</div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"> | ||
| <span>🔒 {i18next.t("transfer.memo-encrypted-label")}</span> | ||
| {activeUser && ( | ||
| <Button | ||
| size="xs" | ||
| appearance="secondary" | ||
| outline={true} | ||
| disabled={isDecrypting} | ||
| onClick={handleDecrypt} | ||
| > | ||
| {i18next.t("transfer.memo-decrypt")} | ||
| </Button> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { requestMemoKey, getTempMemoKey, clearTempMemoKey } from "./memo-key-events"; | ||
| export { MemoKeyDialog } from "./memo-key-dialog"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| "use client"; | ||
|
|
||
| import { useCallback, useEffect, useState } from "react"; | ||
| import { Modal, ModalBody, ModalHeader, ModalTitle } from "@ui/modal"; | ||
| import { KeyInput } from "@ui/input"; | ||
| import i18next from "i18next"; | ||
| import { PrivateKey } from "@hiveio/dhive"; | ||
| import { resolveMemoKey } from "./memo-key-events"; | ||
|
|
||
| interface MemoKeyRequest { | ||
| purpose: "encrypt" | "decrypt"; | ||
| } | ||
|
|
||
| export function MemoKeyDialog() { | ||
| const [request, setRequest] = useState<MemoKeyRequest | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const handler = (e: Event) => { | ||
| const detail = (e as CustomEvent<MemoKeyRequest>).detail; | ||
| setRequest(detail); | ||
| }; | ||
| window.addEventListener("ecency-memo-key", handler); | ||
| return () => window.removeEventListener("ecency-memo-key", handler); | ||
| }, []); | ||
|
|
||
| const handleClose = useCallback(() => { | ||
| setRequest(null); | ||
| resolveMemoKey(false); | ||
| }, []); | ||
|
|
||
| const handleKeySign = useCallback((privateKey: PrivateKey) => { | ||
| setRequest(null); | ||
| resolveMemoKey(privateKey.toString()); | ||
| }, []); | ||
|
|
||
| if (!request) return null; | ||
|
|
||
| const subtitle = | ||
| request.purpose === "encrypt" | ||
| ? i18next.t("transfer.memo-key-subtitle-encrypt") | ||
| : i18next.t("transfer.memo-key-subtitle-decrypt"); | ||
|
|
||
| return ( | ||
| <Modal show={true} centered={true} onHide={handleClose}> | ||
| <ModalHeader closeButton={true}> | ||
| <ModalTitle>{i18next.t("transfer.memo-key-title")}</ModalTitle> | ||
| </ModalHeader> | ||
| <ModalBody> | ||
| <p className="text-sm text-gray-600 mb-4">{subtitle}</p> | ||
| <KeyInput onSign={handleKeySign} keyType="memo" /> | ||
| </ModalBody> | ||
| </Modal> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only encrypt in branches that actually submit a memo.
This runs before the mode/asset switch, so
#memos on paths likeSPK,LARYNX,power-up,delegate, andconvertwill still request a memo key even though those mutations never sendmemo. That turns unsupported memo input into an avoidable prompt/failure path. Move the encryption into the branches that passmemo, or gate it with an explicitsupportsMemocheck.🤖 Prompt for AI Agents