Skip to content

feat(WALM-264): permanent V1 memory deletion — flag-gated UI + backend#378

Open
harrymove-ctrl wants to merge 7 commits into
devfrom
feat/v1-memory-deletion
Open

feat(WALM-264): permanent V1 memory deletion — flag-gated UI + backend#378
harrymove-ctrl wants to merge 7 commits into
devfrom
feat/v1-memory-deletion

Conversation

@harrymove-ctrl

@harrymove-ctrl harrymove-ctrl commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What

Implements WALM-264 per the agreed plan: a UI-only delete feature for old V1 memories — the app programmatically builds the system::delete_blob PTB from the user's blobs, the user only signs, and the backend submits the tx + deletes the DB rows in the same call. Everything ships off by default behind ENABLE_MEMORY_DELETION (server) + VITE_ENABLE_MEMORY_DELETION (app) — nothing is user-visible until rollout is agreed.

Flow (plan §1)

FE enumerate owned Blob objects (walrusClient.getBlobType — no hardcoded package)
   ∩ relayer DB blob_id set (§5.2, via new authenticated /api/memory-blob-ids)
→ FE build delete PTB, batched 950/tx (Sui 1024-command cap §3)
→ POST /sponsor (Enoki) → user signs the sponsored bytes   ← the ONLY user action
→ POST /api/delete-memories { digest, signature, blobIds }
   → BE submits via the sidecar sponsor-execute path → waits for on-chain success
   → only then delete_by_blob_id(blob_id, auth.owner) per blob
   → { digest, deletedRows }

Tickets

  • T2 — backend: POST /api/delete-memories in protected_routes, owner from Extension(auth) (never the body). Submits first, aborts everything if the tx doesn't land; cleanup_expired_blob remains the self-heal backstop. sponsor.rs's sidecar call extracted into call_sidecar_sponsor_execute, shared with the existing /sponsor/execute proxy.
  • T2b — §5.2 scoping: new read-only POST /api/memory-blob-ids (authenticated, same flag gate) exposing the owner's vector_entries blob_id set — realizes "get the set from the V1 relayer" for the browser; mixed V1/V2 wallets never see non-memory blobs in the delete list.
  • T1 — frontend: CleanupSection card in the Dashboard (mirrors the Delegate keys card — same Card shell, table classes, 52px header pills). Transport-aware enumeration (gRPC listOwnedObjects / JSON-RPC getOwnedObjects), u256→base64url blob-id conversion matching the relayer DB, batch loop resumable mid-run, confirm dialog with the plain-words permanent-deletion warning (§0/§5.3).
  • T3 — banner: dismissible "You have N old memories — delete them" in the Dashboard shell, localStorage dismissal.
  • T4 — guide: docs/fundamentals/deleting-old-memories.md (registered in docs.json) + in-UI warning copy.

Also included (found while testing on mainnet)

  • fix: JSON-RPC nested UID shape in registry lookupfetchAccountIdForOwner read the Table id as a string; JSON-RPC returns { id: { id } }, so every lookup on the JSON-RPC path failed with Invalid params and SetupWizard tried create_account for wallets that already have accounts (aborts EAccountAlreadyExists). Regression from the feat(relayer): gRPC write path for the JSON-RPC sunset (2026-07-31) #355 compat layer, hidden until now because testnet runs gRPC. This one matters for any mainnet deploy.
  • fix: apiCall nonce-based authutils/api.ts still signed the legacy 4-part message with no x-nonce, so the auth middleware rejected it with 426; now signs the current 6-part message like SDK v0.4+.

Verified

  • cargo test: 296 pass (5 pre-existing failures on clean dev, unrelated); cargo check --tests clean
  • App: tsc -b + vite build + verify-dist-assets pass
  • Live on localhost against mainnet (tech-lead env) and testnet gRPC: enumeration, §5.2 scoping, selection, confirm, sponsor request all exercised end-to-end up to the Enoki allowlist boundary

Before enabling the flag (not blockers for this merge)

  1. SEC2-84: add system::delete_blob to the Enoki Portal sponsor allowlist (testnet + mainnet apps) — Enoki currently rejects the sponsor dry-run with "not part of an allow-listed move call target".
  2. E2E on the 189-blob wallet 0x158a78f0… per §6 once the allowlist is in.
  3. Decide reclaimed-Storage handling (§5.4) — currently transferred back to the owner.
image

hien-p and others added 6 commits July 10, 2026 19:03
User-facing delete for old V1 memories, off by default behind
ENABLE_MEMORY_DELETION (server) + VITE_ENABLE_MEMORY_DELETION (app).

Backend: POST /api/delete-memories (protected_routes) accepts the
user-signed sponsored delete PTB {digest, signature, blobIds}, submits
it via the sidecar sponsor-execute path, and only after on-chain
success deletes the matching vector_entries rows (delete_by_blob_id,
owner-scoped from verified auth — never the body). sponsor.rs's sidecar
call is extracted into call_sidecar_sponsor_execute, shared with the
existing /sponsor/execute proxy. cleanup_expired_blob remains the
self-heal backstop if row-delete fails after a landed tx.

Frontend: /cleanup page enumerates the wallet's on-chain Walrus Blob
objects (walrusClient.getBlobType — no hardcoded package), converts
blob_id u256→base64url to match the relayer DB, builds the delete PTB
(deleteBlob + transferObjects of reclaimed Storage) in batches of 950
per Sui PTB caps, sponsors via POST /sponsor, and stops after the
wallet signature — the backend owns submit + DB cleanup. Confirm
dialog and copy state plainly that deletion is permanent, never
recoverable, never migrated. Plus the dismissible old-memories banner
and a user guide (docs/fundamentals/deleting-old-memories).

Verified: cargo tests 296 pass (5 pre-existing failures on clean dev),
app tsc + vite build + asset verification pass.
fetchAccountIdForOwner read the registry Table id as registryJson.accounts.id,
which is a plain string on gRPC json but Move's nested UID shape
({ id: { id } }) on JSON-RPC even after field unwrapping. The JSON-RPC
path then passed an object as getDynamicFieldObject's parentId and every
account lookup failed with 'Invalid params' — SetupWizard treated real
accounts as missing and tried create_account, which aborts on-chain with
EAccountAlreadyExists. Regression from the PR #355 compat layer, hidden
until now because testnet runs the gRPC path.
utils/api.ts still signed the legacy '{ts}.{method}.{path}.{bodySha}'
message with no x-nonce header, so the relayer's auth middleware
rejected every call with 426 (unsupported legacy SDK). Sign the current
'{ts}.{method}.{path}.{bodySha}.{nonce}.{account_id}' message and send
x-nonce, matching SDK v0.4+ — unblocks the delete-memories UI's calls
to /api/memory-blob-ids and /api/delete-memories.
Move the banner from AppContent into the Dashboard page (below the
navbar, above the header) so it shows where the cleanup section lives
and inherits the dash-shell layout, with the standard alert icon.
…ce (#351)

Mainnet writes were failing 100% at the register_sponsor phase with an Enoki
dry_run_failed -> MoveAbort in 0x2::coin::destroy_zero (ENonZero). Root cause is
not Enoki: @mysten/walrus '#withWal' pre-funds an *exact* WAL payment computed
from the client's cached systemState price, then asserts the coin is empty via
coin::destroy_zero. When mainnet storage/write price drifts down between the
cached read and execution, the contract deducts less WAL than we split off and
the leftover trips destroy_zero. Verified on prod: on-chain price moved
71464->70922 within ~15 min, and a fresh relayer boot served 6 writes before the
same client flipped to 100% destroy_zero at the next price move.

Two gaps let this fail hard instead of self-healing:
- The sidecar's auto-refresh only fired on isMoveAbortBalanceSplit ('balance'
  + 'split'); this message says destroy_zero, so refreshWalrusClient never ran.
- The Rust worker swept it into the MoveAbort -> Permanent catch, so Apalis
  Dead-marked every write with no retry.

Fix:
- enoki.ts: add isMoveAbortWalDestroyZero detector.
- walrus-upload.ts: refreshWalrusClient() on the destroy_zero abort so the retry
  rebuilds against the live price.
- jobs.rs: classify the register destroy_zero abort as Transient (retryable),
  disjoint from the balance::split gas-budget path.
- config.ts: drop WALRUS_CLIENT_MAX_AGE_MS default 30m -> 60s so the cached
  price tracks a live-drifting mainnet price (still env-overridable).

Tests: new TS detector suite (7) + Rust classification test using the verbatim
prod error. Full suites green (85 TS, 40 jobs).
The sidecar hardcoded getJsonRpcFullnodeUrl(mainnet) and ignored the SUI_RPC_URL
env, so it could not be pointed at a dedicated RPC when the public fullnode pool
degrades. Observed today: the public pool served stale reads (a blob certified
at 00:49:14 read back as 'does not exist' at 00:49:49, 35s later), failing
uploads at get_blob/certify/metadata. Honor an explicit SUI_RPC_URL override so
ops can switch to a healthy RPC via env; fall back to the network default.
@jessiemongeon1

Copy link
Copy Markdown
Collaborator

Style Guide Audit

Audited 1 file(s) against the Sui Documentation Style Guide.

19 violation(s) found. All must be fixed before merge.

docs/fundamentals/deleting-old-memories.md (19 violation(s))

19 violation(s) (16 regex, 3 claude)

  • Line 14 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 16 — Use present tense, not future
    • Current: will
    • Fix: Use present tense verb
  • Line 17 — Use "basic" not "simple"
    • Current: simply
    • Fix: (remove)
  • Line 20 — "on-chain" is one word → "onchain"
    • Current: on-chain
    • Fix: onchain
  • Line 29 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 30 — "on-chain" is one word → "onchain"
    • Current: on-chain
    • Fix: onchain
  • Line 32 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 34 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 35 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 39 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 40 — "on-chain" is one word → "onchain"
    • Current: on-chain
    • Fix: onchain
  • Line 46 — "on-chain" is one word → "onchain"
    • Current: on-chain
    • Fix: onchain
  • Line 51 — No em dashes in prose
    • Current:
    • Fix: Rewrite with comma, parentheses, or split sentence
  • Line 52 — "on-chain" is one word → "onchain"
    • Current: on-chain
    • Fix: onchain
  • Line 1 — Frontmatter needs description
    • Current: (missing or placeholder)
    • Fix: Add a complete sentence description
  • Line 1 — Frontmatter needs keywords
    • Current: (missing or placeholder)
    • Fix: Add relevant keywords
  • Line 9 — No special symbols for emphasis
    • Current: ## ⚠️ Deletion is permanent
    • Fix: ## Deletion is permanent
  • Line 29 — No quotation marks in prose
    • Current: from the "old memories" banner or the "Delete old
    • Fix: from the old memoriesbanner or theDelete old memories section
  • Line 41 — Passive voice
    • Current: Reclaimed Walrus storage objects are returned to your wallet.
    • Fix: You receive the reclaimed Walrus storage objects in your wallet.

Automated audit using the Sui Documentation Style Guide.

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.

4 participants