Skip to content

Memo improvements#696

Open
feruzm wants to merge 2 commits intodevelopfrom
memo
Open

Memo improvements#696
feruzm wants to merge 2 commits intodevelopfrom
memo

Conversation

@feruzm
Copy link
Member

@feruzm feruzm commented Mar 9, 2026

Summary by CodeRabbit

  • New Features

    • Added memo encryption and decryption functionality for transfers, token operations, and savings transactions
    • Encrypted memos (marked with "#") now display in transaction history with built-in decryption capability
    • Introduced memo key management interface supporting secure encryption and decryption workflows
  • Bug Fixes

    • Poll voting now restricted to users with active sessions

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Introduces memo encryption functionality by adding preprocessing in transfer mutations to encrypt memos starting with "#", creating a new MemoDisplay component to render encrypted memos with decryption support, implementing a modal dialog flow for memo key input, developing crypto utilities for encryption/decryption across authentication methods, and extending key derivation to support memo keys.

Changes

Cohort / File(s) Summary
Transfer Memo Encryption
apps/web/src/api/mutations/sign-transfer.ts
Added preprocessing to encrypt memos starting with "#" using encryptMemo utility before dispatching transfer mutations across POINT, Hive Engine, standard transfer, savings, and interest claim paths.
Encrypted Memo Display
apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/{hbd,hive,hp}/_components/hive-transaction-row.tsx, apps/web/src/features/shared/transactions/transaction-row.tsx, apps/web/src/features/shared/transfer/transfer-step-2.tsx
Added conditional rendering of encrypted memos (prefixed with "#") using MemoDisplay component, with fallback to plain text rendering for standard memos and encrypted-label indicators.
Memo Key Dialog & Event System
apps/web/src/features/shared/memo-key/memo-key-dialog.tsx, apps/web/src/features/shared/memo-key/memo-key-events.ts, apps/web/src/features/shared/memo-key/index.ts
Implemented event-driven modal dialog for requesting memo private keys with purpose-based UI (encrypt/decrypt), including state management, timeout-based caching, and promise-based resolution API.
Memo Display Component
apps/web/src/features/shared/memo-display.tsx
New component that detects encrypted memos (heuristic: starts with "#", length > 50, no whitespace), renders decrypt button when user exists, handles async decryption with error logging, and displays decrypted text or plain memo fallback.
Transfer Form Updates
apps/web/src/features/shared/transfer/transfer-step-1.tsx
Added conditional memo help text to indicate encryption (lock icon + translated label) when memo starts with "#".
App Infrastructure
apps/web/src/app/client-providers.tsx, apps/web/src/features/ui/input/key-input.tsx, apps/web/src/features/polls/components/poll-widget.tsx
Integrated MemoKeyDialog into deferred rendering, extended KeyInput keyType to support "memo" with BIP44 and master-password derivation paths, and added activeUser check to poll widget button disabled state.
Crypto Utilities
apps/web/src/utils/memo-crypto.ts, apps/web/src/types/keychain-impl.ts
Added high-level encryptMemo/decryptMemo functions supporting keychain, hivesigner, and key-based flows; extended KeyChainImpl interface with requestEncodeMessage and requestVerifyKey methods.
SDK Memo Operations
packages/sdk/src/modules/operations/encrypt-memo.ts, packages/sdk/src/modules/operations/decrypt-memo.ts, packages/sdk/src/modules/operations/index.ts
New SDK modules providing low-level encryption/decryption utilities: encryptMemoWithKeys, encryptMemoWithAccounts, decryptMemoWithKeys with account fetching and validation.
Wallet Package Updates
packages/wallets/src/modules/wallets/utils/encrypt-memo.ts, packages/wallets/src/modules/wallets/utils/decrypt-memo.ts, packages/wallets/src/modules/wallets/utils/detect-hive-key-derivation.ts
Migrated memo encryption/decryption implementations to re-exports from SDK; extended detectHiveKeyDerivation to support "memo" key type with BIP44 and legacy derivation detection.
Translations & Configuration
apps/web/src/features/i18n/locales/en-US.json
Added 7 new translation keys for memo encryption UX: memo-encrypted, memo-decrypt, memo-decrypt-error, memo-encrypted-label, memo-key-title, memo-key-subtitle-encrypt, memo-key-subtitle-decrypt.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TransferForm as Transfer Form
    participant SignTransfer as sign-transfer.ts
    participant MemoKeyDialog as Memo Key Dialog
    participant CryptoUtils as Crypto Utils
    participant HiveAPI as Hive API

    User->>TransferForm: Enter memo starting with "#"
    User->>TransferForm: Submit transfer
    TransferForm->>SignTransfer: Dispatch with memo="#..."
    
    SignTransfer->>SignTransfer: Detect memo starts with "#"
    SignTransfer->>MemoKeyDialog: requestMemoKey("encrypt")
    MemoKeyDialog->>User: Show key input modal
    User->>MemoKeyDialog: Provide memo key
    MemoKeyDialog->>SignTransfer: Return signed key
    
    SignTransfer->>CryptoUtils: encryptMemo(loginType, username, recipient, memo)
    CryptoUtils->>HiveAPI: Get recipient account memo_key
    CryptoUtils->>CryptoUtils: Encrypt memo with keys
    CryptoUtils->>SignTransfer: Return "#encrypted..."
    
    SignTransfer->>HiveAPI: Transfer with encrypted memo
    HiveAPI->>User: Transaction confirmed
Loading
sequenceDiagram
    participant User
    participant TransactionRow as Transaction Row
    participant MemoDisplay as Memo Display
    participant MemoKeyDialog as Memo Key Dialog
    participant CryptoUtils as Crypto Utils
    participant HiveAPI as Hive API

    TransactionRow->>MemoDisplay: Display memo="#encrypted..."
    MemoDisplay->>MemoDisplay: Detect encryption (starts with #, length > 50)
    MemoDisplay->>User: Render decrypt button
    
    User->>MemoDisplay: Click decrypt
    MemoDisplay->>MemoKeyDialog: requestMemoKey("decrypt")
    MemoKeyDialog->>User: Show key input modal
    User->>MemoKeyDialog: Provide memo key
    MemoKeyDialog->>MemoDisplay: Return signed key
    
    MemoDisplay->>CryptoUtils: decryptMemo(loginType, username, encrypted)
    CryptoUtils->>HiveAPI: Get account private memo key (if keychain)
    CryptoUtils->>CryptoUtils: Decrypt memo with key
    CryptoUtils->>MemoDisplay: Return decrypted text
    
    MemoDisplay->>User: Display decrypted memo
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Engine/SPK to SDK #608: Modifies the same sign-transfer.ts file; the memo encryption preprocessing introduced in this PR directly affects the transfer mutation calls refactored in that PR.

Suggested labels

patch


🐰 Whispers encrypt so fine,
Memo keys now intertwine,
Transfer memos locked with care,
Decrypt when you dare,
Security's ours to define! 🔐✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Memo improvements' is vague and generic, using a non-descriptive term that does not convey specific information about the substantial changes in this comprehensive pull request. Consider using a more specific title that highlights a key feature, such as 'Add memo encryption and decryption support' or 'Implement encrypted memo handling with key management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch memo

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (2)
apps/web/src/utils/memo-crypto.ts (1)

60-66: HiveSigner fallback returns 0 when no access token exists.

Per getDecodedMemo (in hive-signer.ts), when no access token is available, it returns Promise.resolve(0). The current check result && result.result correctly rejects this case, but the error message "HiveSigner memo decode failed" may be misleading since the actual cause is a missing or expired token.

Consider adding a more specific check:

Suggested improvement
   if (loginType === "hivesigner") {
     const result = await getDecodedMemo(username, encryptedMemo);
+    if (result === 0) {
+      throw new Error("HiveSigner access token missing or expired");
+    }
     if (result && result.result) {
       return result.result;
     }
     throw new Error("HiveSigner memo decode failed");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/utils/memo-crypto.ts` around lines 60 - 66, The current
HiveSigner branch uses getDecodedMemo(username, encryptedMemo) and throws a
generic "HiveSigner memo decode failed" when result/result.result is falsy;
update the logic in the loginType === "hivesigner" block to explicitly detect
the sentinel Promise.resolve(0) case (i.e., if result === 0) and throw a
specific error like "HiveSigner access token missing or expired", otherwise if
result is present but result.result is falsy throw the existing generic decode
failure; reference getDecodedMemo, loginType, username, encryptedMemo and
result/result.result when implementing the checks.
apps/web/src/features/ui/input/key-input.tsx (1)

35-38: Add type annotation for the str parameter.

The capitalizeFirstLetter helper function is missing a TypeScript type annotation. This creates an implicit any type.

Suggested fix
-function capitalizeFirstLetter(str) {
+function capitalizeFirstLetter(str: string | undefined): string {
     if (typeof str !== 'string' || str.length === 0) return '';
     return str[0].toUpperCase() + str.slice(1);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/ui/input/key-input.tsx` around lines 35 - 38, The
helper function capitalizeFirstLetter has an implicit any for its parameter;
update its signature to explicitly type the parameter as a string and the return
type as string (e.g., capitalizeFirstLetter(str: string): string) so TypeScript
can type-check callers and avoid implicit any; keep the existing runtime guards
(typeof check and empty string) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/api/mutations/sign-transfer.ts`:
- Around line 85-91: The code currently encrypts memo unconditionally when it
starts with "#" inside mutateAsync (using getLoginType, encryptMemo, activeUser,
processedMemo), which can prompt for a memo key even for transfer paths that
never submit a memo; move the encryption logic into the specific branches that
actually send a memo (the branches that call the mutation code that includes
processedMemo) or add an explicit supportsMemo check before calling encryptMemo
so only paths that will include memo perform encryption and request keys.

In `@apps/web/src/app/client-providers.tsx`:
- Around line 47-49: MemoKeyDialog is currently wrapped by DeferredRender which
delays mounting and causes its ecency-memo-key listener (installed in
MemoKeyDialog's useEffect) to miss early requestMemoKey() calls; move
<MemoKeyDialog /> out of the <DeferredRender> block so it mounts eagerly (e.g.,
place MemoKeyDialog alongside AuthUpgradeDialog or above DeferredRender) so the
listener is registered immediately. Ensure you only change the component
placement (remove from DeferredRender) and do not alter MemoKeyDialog internals.

In `@apps/web/src/features/i18n/locales/en-US.json`:
- Around line 1470-1472: Update the three locale strings to explicitly reference
the private memo key to avoid confusion: change "memo-key-title" from "Enter
Memo Key" to "Enter Private Memo Key" and change both
"memo-key-subtitle-encrypt" and "memo-key-subtitle-decrypt" to mention "private
memo key" (e.g., "Your private memo key is needed to encrypt this message." and
"Your private memo key is needed to decrypt this message."). Ensure the keys
"memo-key-title", "memo-key-subtitle-encrypt", and "memo-key-subtitle-decrypt"
are updated accordingly.

In `@apps/web/src/features/shared/memo-display.tsx`:
- Around line 22-27: The memo-detection heuristic in isLikelyEncryptedMemo
(checks startsWith("#") plus content.length > 50) is inconsistent with callers
that only use memo.startsWith("#"), causing short encrypted memos to be
misrouted; export isLikelyEncryptedMemo from memo-display and replace the simple
memo.startsWith("#") checks in the calling components (the code that routes to
MemoDisplay, e.g., where transaction-row and transfer-step-2 decide rendering)
to call isLikelyEncryptedMemo instead, or if you prefer shorter thresholds
adjust the length constant inside isLikelyEncryptedMemo and update all callers
to import it; also ensure MemoDisplay’s decryption path handles failures
gracefully (catch and fall back to safe plain-text rendering) so decryption
attempts don’t break the UI.

In `@apps/web/src/features/shared/memo-key/memo-key-events.ts`:
- Around line 18-35: The current requestMemoKey resolves any existing
pendingResolve with false on re-entry, which cancels the first waiter; instead,
change requestMemoKey so it does not call pendingResolve(false) and instead
returns the existing pending promise when a request is already in flight.
Implement a shared pendingPromise alongside pendingResolve (or promote the
existing new Promise to a module-level pendingPromise) in requestMemoKey, have
requestMemoKey check if pendingPromise exists and return it, and ensure
clearTempMemoKey clears both pendingResolve and pendingPromise so subsequent
calls behave correctly; relevant symbols: requestMemoKey, pendingResolve,
pendingPromise (new), and clearTempMemoKey (encryptMemo/decryptMemo call
requestMemoKey).

In `@apps/web/src/features/shared/transfer/transfer-step-1.tsx`:
- Around line 473-480: The UI currently advertises encrypted memos when
memo.startsWith("#"), which is unsafe for exchange recipients; change the
rendering and validation so that TransferFormText only shows the encrypted-memo
hint when the recipient (prop `to` / `recipient`) is NOT in EXCHANGE_ACCOUNTS,
and ensure submission validation in the transfer flow (the memo validation used
by the form submit handler) rejects memos starting with "#" if the recipient is
in EXCHANGE_ACCOUNTS; update the condition around TransferFormText (the
memo.startsWith("#") branch) to check EXCHANGE_ACCOUNTS membership and add a
server/client-side validation in the transfer submit/validate function to block
encrypted-memo paths for exchange accounts.

In `@packages/sdk/src/modules/operations/encrypt-memo.ts`:
- Around line 1-3: The import is pulling Memo from the internal path
"@hiveio/dhive/lib/memo"; update the import statement to import Memo from the
public package export instead (i.e., import { Memo } from "@hiveio/dhive"), so
replace the current internal-path import with a public API import for Memo
alongside the existing PrivateKey and Client imports in encrypt-memo.ts to avoid
relying on internal module paths.

---

Nitpick comments:
In `@apps/web/src/features/ui/input/key-input.tsx`:
- Around line 35-38: The helper function capitalizeFirstLetter has an implicit
any for its parameter; update its signature to explicitly type the parameter as
a string and the return type as string (e.g., capitalizeFirstLetter(str:
string): string) so TypeScript can type-check callers and avoid implicit any;
keep the existing runtime guards (typeof check and empty string) intact.

In `@apps/web/src/utils/memo-crypto.ts`:
- Around line 60-66: The current HiveSigner branch uses getDecodedMemo(username,
encryptedMemo) and throws a generic "HiveSigner memo decode failed" when
result/result.result is falsy; update the logic in the loginType ===
"hivesigner" block to explicitly detect the sentinel Promise.resolve(0) case
(i.e., if result === 0) and throw a specific error like "HiveSigner access token
missing or expired", otherwise if result is present but result.result is falsy
throw the existing generic decode failure; reference getDecodedMemo, loginType,
username, encryptedMemo and result/result.result when implementing the checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85258c0d-8824-4a99-a27b-d13bfe3eff60

📥 Commits

Reviewing files that changed from the base of the PR and between 5ffc14c and e330fb7.

⛔ Files ignored due to path filters (15)
  • packages/sdk/dist/browser/index.d.ts is excluded by !**/dist/**
  • packages/sdk/dist/browser/index.js is excluded by !**/dist/**
  • packages/sdk/dist/browser/index.js.map is excluded by !**/dist/**, !**/*.map
  • packages/sdk/dist/node/index.cjs is excluded by !**/dist/**
  • packages/sdk/dist/node/index.cjs.map is excluded by !**/dist/**, !**/*.map
  • packages/sdk/dist/node/index.mjs is excluded by !**/dist/**
  • packages/sdk/dist/node/index.mjs.map is excluded by !**/dist/**, !**/*.map
  • packages/sdk/src/modules/operations/__snapshots__/memo-crypto.spec.ts.snap is excluded by !**/*.snap
  • packages/wallets/dist/browser/index.d.ts is excluded by !**/dist/**
  • packages/wallets/dist/browser/index.js is excluded by !**/dist/**
  • packages/wallets/dist/browser/index.js.map is excluded by !**/dist/**, !**/*.map
  • packages/wallets/dist/node/index.cjs is excluded by !**/dist/**
  • packages/wallets/dist/node/index.cjs.map is excluded by !**/dist/**, !**/*.map
  • packages/wallets/dist/node/index.mjs is excluded by !**/dist/**
  • packages/wallets/dist/node/index.mjs.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (24)
  • apps/web/src/api/mutations/sign-transfer.ts
  • apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hbd/_components/hive-transaction-row.tsx
  • apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hive/_components/hive-transaction-row.tsx
  • apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hp/_components/hive-transaction-row.tsx
  • apps/web/src/app/client-providers.tsx
  • apps/web/src/features/i18n/locales/en-US.json
  • apps/web/src/features/polls/components/poll-widget.tsx
  • apps/web/src/features/shared/memo-display.tsx
  • apps/web/src/features/shared/memo-key/index.ts
  • apps/web/src/features/shared/memo-key/memo-key-dialog.tsx
  • apps/web/src/features/shared/memo-key/memo-key-events.ts
  • apps/web/src/features/shared/transactions/transaction-row.tsx
  • apps/web/src/features/shared/transfer/transfer-step-1.tsx
  • apps/web/src/features/shared/transfer/transfer-step-2.tsx
  • apps/web/src/features/ui/input/key-input.tsx
  • apps/web/src/types/keychain-impl.ts
  • apps/web/src/utils/memo-crypto.ts
  • packages/sdk/src/modules/operations/decrypt-memo.ts
  • packages/sdk/src/modules/operations/encrypt-memo.ts
  • packages/sdk/src/modules/operations/index.ts
  • packages/sdk/src/modules/operations/memo-crypto.spec.ts
  • packages/wallets/src/modules/wallets/utils/decrypt-memo.ts
  • packages/wallets/src/modules/wallets/utils/detect-hive-key-derivation.ts
  • packages/wallets/src/modules/wallets/utils/encrypt-memo.ts

Comment on lines 85 to +91
mutateAsync: async ({ to, amount, memo }: SignTransferPayload) => {
// Encrypt memo if it starts with #
let processedMemo = memo;
if (memo.startsWith("#") && to) {
const loginType = getLoginType(activeUser?.username ?? "");
processedMemo = await encryptMemo(loginType, activeUser!.username, to, memo.slice(1));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only encrypt in branches that actually submit a memo.

This runs before the mode/asset switch, so # memos on paths like SPK, LARYNX, power-up, delegate, and convert will still request a memo key even though those mutations never send memo. That turns unsupported memo input into an avoidable prompt/failure path. Move the encryption into the branches that pass memo, or gate it with an explicit supportsMemo check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/api/mutations/sign-transfer.ts` around lines 85 - 91, The code
currently encrypts memo unconditionally when it starts with "#" inside
mutateAsync (using getLoginType, encryptMemo, activeUser, processedMemo), which
can prompt for a memo key even for transfer paths that never submit a memo; move
the encryption logic into the specific branches that actually send a memo (the
branches that call the mutation code that includes processedMemo) or add an
explicit supportsMemo check before calling encryptMemo so only paths that will
include memo perform encryption and request keys.

Comment on lines 47 to +49
<DeferredRender>
<AuthUpgradeDialog />
<MemoKeyDialog />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mount MemoKeyDialog eagerly; deferring it can drop the first memo-key request.

MemoKeyDialog installs its ecency-memo-key listener in useEffect (apps/web/src/features/shared/memo-key/memo-key-dialog.tsx:14-24). Putting it behind DeferredRender adds another async delay before that listener exists, so any early requestMemoKey() call is missed and the dialog never opens.

Suggested fix
         <UIManager>
           <ClientInit />
+          <MemoKeyDialog />
           {/* Defer non-critical components for LCP optimization */}
           <DeferredRender>
             <AuthUpgradeDialog />
-            <MemoKeyDialog />
             <EcencyConfigManager.Conditional
               condition={({ visionFeatures }) => visionFeatures.userActivityTracking.enabled}
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<DeferredRender>
<AuthUpgradeDialog />
<MemoKeyDialog />
<UIManager>
<ClientInit />
<MemoKeyDialog />
{/* Defer non-critical components for LCP optimization */}
<DeferredRender>
<AuthUpgradeDialog />
<EcencyConfigManager.Conditional
condition={({ visionFeatures }) => visionFeatures.userActivityTracking.enabled}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/client-providers.tsx` around lines 47 - 49, MemoKeyDialog is
currently wrapped by DeferredRender which delays mounting and causes its
ecency-memo-key listener (installed in MemoKeyDialog's useEffect) to miss early
requestMemoKey() calls; move <MemoKeyDialog /> out of the <DeferredRender> block
so it mounts eagerly (e.g., place MemoKeyDialog alongside AuthUpgradeDialog or
above DeferredRender) so the listener is registered immediately. Ensure you only
change the component placement (remove from DeferredRender) and do not alter
MemoKeyDialog internals.

Comment on lines +1470 to +1472
"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.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Call out that this is the private memo key.

"Enter Memo Key" / "Your memo key is needed" is ambiguous in Hive terminology and can easily lead users to paste the public memo key, which guarantees encrypt/decrypt failures. Please make the copy explicit, e.g. “Enter Private Memo Key”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/i18n/locales/en-US.json` around lines 1470 - 1472,
Update the three locale strings to explicitly reference the private memo key to
avoid confusion: change "memo-key-title" from "Enter Memo Key" to "Enter Private
Memo Key" and change both "memo-key-subtitle-encrypt" and
"memo-key-subtitle-decrypt" to mention "private memo key" (e.g., "Your private
memo key is needed to encrypt this message." and "Your private memo key is
needed to decrypt this message."). Ensure the keys "memo-key-title",
"memo-key-subtitle-encrypt", and "memo-key-subtitle-decrypt" are updated
accordingly.

Comment on lines +22 to +27
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent encrypted memo detection with calling components.

The isLikelyEncryptedMemo heuristic requires content.length > 50, but the components that route memos to MemoDisplay (e.g., transaction-row.tsx at lines 144-150, 183-189 and transfer-step-2.tsx at lines 56-65) use only memo.startsWith("#") without the length check.

This creates a UX problem: a short encrypted memo (≤50 chars) would be routed to MemoDisplay but then rendered as plain text (garbled base58). Consider either:

  1. Export and use isLikelyEncryptedMemo in calling components for consistent detection
  2. Lower the threshold (dhive's minimum encrypted output is shorter)
  3. Have MemoDisplay always attempt decryption for #-prefixed memos, handling failure gracefully
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/shared/memo-display.tsx` around lines 22 - 27, The
memo-detection heuristic in isLikelyEncryptedMemo (checks startsWith("#") plus
content.length > 50) is inconsistent with callers that only use
memo.startsWith("#"), causing short encrypted memos to be misrouted; export
isLikelyEncryptedMemo from memo-display and replace the simple
memo.startsWith("#") checks in the calling components (the code that routes to
MemoDisplay, e.g., where transaction-row and transfer-step-2 decide rendering)
to call isLikelyEncryptedMemo instead, or if you prefer shorter thresholds
adjust the length constant inside isLikelyEncryptedMemo and update all callers
to import it; also ensure MemoDisplay’s decryption path handles failures
gracefully (catch and fall back to safe plain-text rendering) so decryption
attempts don’t break the UI.

Comment on lines +18 to +35
export function requestMemoKey(
purpose: "encrypt" | "decrypt"
): Promise<string | false> {
if (pendingResolve) {
pendingResolve(false);
pendingResolve = null;
}

clearTempMemoKey();

return new Promise((resolve) => {
pendingResolve = resolve;
window.dispatchEvent(
new CustomEvent("ecency-memo-key", {
detail: { purpose }
})
);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t cancel the first waiter on re-entry.

Line 21 resolves the existing caller with false before the user answers the dialog. Since both encryptMemo() and decryptMemo() call this helper directly, concurrent memo actions can fail spuriously instead of sharing the same prompt.

Safer pattern
 let pendingResolve: ((key: string | false) => void) | null = null;
+let pendingRequest: Promise<string | false> | null = null;
 let tempMemoKey: string | null = null;
 let tempKeyTimeout: ReturnType<typeof setTimeout> | null = null;

 export function requestMemoKey(
   purpose: "encrypt" | "decrypt"
 ): Promise<string | false> {
-  if (pendingResolve) {
-    pendingResolve(false);
-    pendingResolve = null;
-  }
+  if (pendingRequest) return pendingRequest;

   clearTempMemoKey();

-  return new Promise((resolve) => {
-    pendingResolve = resolve;
+  pendingRequest = new Promise((resolve) => {
+    pendingResolve = (key) => {
+      pendingRequest = null;
+      resolve(key);
+    };
     window.dispatchEvent(
       new CustomEvent("ecency-memo-key", {
         detail: { purpose }
       })
     );
   });
+
+  return pendingRequest;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function requestMemoKey(
purpose: "encrypt" | "decrypt"
): Promise<string | false> {
if (pendingResolve) {
pendingResolve(false);
pendingResolve = null;
}
clearTempMemoKey();
return new Promise((resolve) => {
pendingResolve = resolve;
window.dispatchEvent(
new CustomEvent("ecency-memo-key", {
detail: { purpose }
})
);
});
let pendingResolve: ((key: string | false) => void) | null = null;
let pendingRequest: Promise<string | false> | null = null;
let tempMemoKey: string | null = null;
let tempKeyTimeout: ReturnType<typeof setTimeout> | null = null;
export function requestMemoKey(
purpose: "encrypt" | "decrypt"
): Promise<string | false> {
if (pendingRequest) return pendingRequest;
clearTempMemoKey();
pendingRequest = new Promise((resolve) => {
pendingResolve = (key) => {
pendingRequest = null;
resolve(key);
};
window.dispatchEvent(
new CustomEvent("ecency-memo-key", {
detail: { purpose }
})
);
});
return pendingRequest;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/shared/memo-key/memo-key-events.ts` around lines 18 -
35, The current requestMemoKey resolves any existing pendingResolve with false
on re-entry, which cancels the first waiter; instead, change requestMemoKey so
it does not call pendingResolve(false) and instead returns the existing pending
promise when a request is already in flight. Implement a shared pendingPromise
alongside pendingResolve (or promote the existing new Promise to a module-level
pendingPromise) in requestMemoKey, have requestMemoKey check if pendingPromise
exists and return it, and ensure clearTempMemoKey clears both pendingResolve and
pendingPromise so subsequent calls behave correctly; relevant symbols:
requestMemoKey, pendingResolve, pendingPromise (new), and clearTempMemoKey
(encryptMemo/decryptMemo call requestMemoKey).

Comment on lines +473 to +480
<TransferFormText
msg={
memo.startsWith("#")
? `🔒 ${i18next.t("transfer.memo-encrypted")}`
: i18next.t("transfer.memo-help")
}
type="muted"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Reject encrypted memos for exchange transfers.

This hint makes #... look valid even when to is in EXCHANGE_ACCOUNTS. Exchange deposits usually depend on an exact plain memo/tag; encrypting it can make the deposit unrecoverable. Please block the encrypted-memo path for exchange recipients instead of advertising it here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/shared/transfer/transfer-step-1.tsx` around lines 473 -
480, The UI currently advertises encrypted memos when memo.startsWith("#"),
which is unsafe for exchange recipients; change the rendering and validation so
that TransferFormText only shows the encrypted-memo hint when the recipient
(prop `to` / `recipient`) is NOT in EXCHANGE_ACCOUNTS, and ensure submission
validation in the transfer flow (the memo validation used by the form submit
handler) rejects memos starting with "#" if the recipient is in
EXCHANGE_ACCOUNTS; update the condition around TransferFormText (the
memo.startsWith("#") branch) to check EXCHANGE_ACCOUNTS membership and add a
server/client-side validation in the transfer submit/validate function to block
encrypted-memo paths for exchange accounts.

Comment on lines +1 to +3
import { PrivateKey } from "@hiveio/dhive";
import type { Client } from "@hiveio/dhive";
import { Memo } from "@hiveio/dhive/lib/memo";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does @hiveio/dhive publicly support importing Memo from "@hiveio/dhive/lib/memo", or is that an internal/private module path?

💡 Result:

@hiveio/dhive/lib/memo should be treated as an internal/deep import, not a publicly supported API surface.

What is publicly supported is importing Memo (and encode/decode) from the package entrypoint, since it’s part of the generated public API docs (Memo is a documented exported object in memo.ts). [1]

The only deep lib/* paths that are explicitly mentioned in dhive’s own docs are @hiveio/dhive/lib/index and @hiveio/dhive/lib/index-browser for bundling/deduplication purposes—there’s no similar documentation that blesses lib/memo. [2]

Recommended: import { Memo } from "@hiveio/dhive"; (or import { encode, decode } from "@hiveio/dhive";) rather than @hiveio/dhive/lib/memo. [1]

Sources:
[1] dhive API docs (exports include Memo, encode, decode) https://openhive-network.github.io/dhive/globals.html
[2] dhive docs (only documents deep imports for lib/index / lib/index-browser) https://openhive-network.github.io/dhive/index.html


Import Memo from the public entrypoint instead of the internal path.

Line 3 imports from @hiveio/dhive/lib/memo, which is an internal/non-documented module path. The public API surface exports Memo directly from @hiveio/dhive. Change to:

import { Memo } from "@hiveio/dhive";

This protects against breaking changes if dhive's internal structure shifts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk/src/modules/operations/encrypt-memo.ts` around lines 1 - 3, The
import is pulling Memo from the internal path "@hiveio/dhive/lib/memo"; update
the import statement to import Memo from the public package export instead
(i.e., import { Memo } from "@hiveio/dhive"), so replace the current
internal-path import with a public API import for Memo alongside the existing
PrivateKey and Client imports in encrypt-memo.ts to avoid relying on internal
module paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant