From 7e1a0de606f51b7089944b12aa4d69bf50b5ed12 Mon Sep 17 00:00:00 2001 From: ducnmm Date: Fri, 20 Mar 2026 00:52:33 +0700 Subject: [PATCH 01/34] rename memwal_v2 to memwal, redeploy testnet, add packageId metadata to blobs --- apps/app/.env.example | 16 ++-- apps/app/Dockerfile | 2 +- apps/app/src/App.tsx | 2 +- apps/app/src/config.ts | 7 +- apps/noter/docker-compose.yml | 2 +- .../feature/note/hook/use-pdw-client.ts | 2 +- .../package/feature/note/lib/pdw-client.ts | 2 +- apps/researcher/.env.example | 2 +- docs/examples/basic-usage.md | 4 +- packages/sdk/src/manual.ts | 2 +- packages/sdk/src/memwal.ts | 2 +- packages/sdk/src/types.ts | 2 +- packages/sdk/src/utils.ts | 2 +- services/contract/Move.lock | 10 +-- services/contract/Move.toml | 1 - services/contract/Published.toml | 6 +- services/contract/sources/account.move | 4 +- services/contract/tests/account_tests.move | 4 +- services/indexer/.env.example | 6 +- services/indexer/Dockerfile | 2 +- services/indexer/src/main.rs | 8 +- services/server/Dockerfile | 2 +- services/server/migrations/001_init.sql | 2 +- .../server/migrations/002_add_namespace.sql | 4 +- services/server/scripts/sidecar-server.ts | 84 ++++++++++++++++++- services/server/src/main.rs | 4 +- services/server/src/routes.rs | 19 ++++- services/server/src/walrus.rs | 10 +++ services/server/tests/e2e_test.py | 4 +- tmp-rememe | 1 + tmp-smashblob | 1 + 31 files changed, 160 insertions(+), 59 deletions(-) create mode 160000 tmp-rememe create mode 160000 tmp-smashblob diff --git a/apps/app/.env.example b/apps/app/.env.example index 0449ad89..bcb651fd 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -17,15 +17,15 @@ VITE_MEMWAL_SERVER_URL=http://localhost:8000 VITE_DOCS_URL=http://localhost:5174 # ══════════════════════════════════════════════════════════════ -# ▶ ACTIVE: MAINNET +# ▷ INACTIVE: MAINNET (uncomment below, comment out TESTNET above) # ══════════════════════════════════════════════════════════════ -VITE_SUI_NETWORK=mainnet -VITE_MEMWAL_PACKAGE_ID=0xcee7a6fd8de52ce645c38332bde23d4a30fd9426bc4681409733dd50958a24c6 -VITE_MEMWAL_REGISTRY_ID=0x0da982cefa26864ae834a8a0504b904233d49e20fcc17c373c8bed99c75a7edd +# VITE_SUI_NETWORK=mainnet +# VITE_MEMWAL_PACKAGE_ID=0xcee7a6fd8de52ce645c38332bde23d4a30fd9426bc4681409733dd50958a24c6 +# VITE_MEMWAL_REGISTRY_ID=0x0da982cefa26864ae834a8a0504b904233d49e20fcc17c373c8bed99c75a7edd # ══════════════════════════════════════════════════════════════ -# ▷ INACTIVE: TESTNET (uncomment below, comment out MAINNET above) +# ▶ ACTIVE: TESTNET # ══════════════════════════════════════════════════════════════ -# VITE_SUI_NETWORK=testnet -# VITE_MEMWAL_PACKAGE_ID=0x12b28adbe55c25341f08b8ad9ac69462aab917048c7cd5b736d951200090ee3f -# VITE_MEMWAL_REGISTRY_ID=0xfb8a1d298e2a73bdab353da3fcb3b16f68ab7d1f392f3a5c4944c747c026fc05 +VITE_SUI_NETWORK=testnet +VITE_MEMWAL_PACKAGE_ID=0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6 +VITE_MEMWAL_REGISTRY_ID=0xe80f2feec1c139616a86c9f71210152e2a7ca552b20841f2e192f99f75864437 diff --git a/apps/app/Dockerfile b/apps/app/Dockerfile index b31596bf..115c77c5 100644 --- a/apps/app/Dockerfile +++ b/apps/app/Dockerfile @@ -1,5 +1,5 @@ # ============================================================ -# MemWal V2 App — Dockerfile +# memwal App — Dockerfile # Vite React SPA — build + serve static files # Build context: repo root (Railway Root Directory = /) # ============================================================ diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 8eda35ae..e5c16123 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -1,5 +1,5 @@ /** - * MemWal V2 — Web App + * memwal — Web App * * Enoki zkLogin integration with @mysten/dapp-kit * Flow: Landing → Sign in with Google (Enoki) → Setup Wizard → Dashboard diff --git a/apps/app/src/config.ts b/apps/app/src/config.ts index ef1da461..4f510690 100644 --- a/apps/app/src/config.ts +++ b/apps/app/src/config.ts @@ -5,12 +5,13 @@ export const config = { enokiApiKey: import.meta.env.VITE_ENOKI_API_KEY as string || '', googleClientId: import.meta.env.VITE_GOOGLE_CLIENT_ID as string || '', memwalPackageId: import.meta.env.VITE_MEMWAL_PACKAGE_ID as string || - '0xcee7a6fd8de52ce645c38332bde23d4a30fd9426bc4681409733dd50958a24c6', + '0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6', memwalRegistryId: import.meta.env.VITE_MEMWAL_REGISTRY_ID as string || - '0x0da982cefa26864ae834a8a0504b904233d49e20fcc17c373c8bed99c75a7edd', + '0xe80f2feec1c139616a86c9f71210152e2a7ca552b20841f2e192f99f75864437', memwalServerUrl: import.meta.env.VITE_MEMWAL_SERVER_URL as string || 'http://localhost:8000', - suiNetwork: (import.meta.env.VITE_SUI_NETWORK as string || 'mainnet') as 'testnet' | 'mainnet', + suiNetwork: (import.meta.env.VITE_SUI_NETWORK as string || 'testnet') as 'testnet' | 'mainnet', sealKeyServers: (import.meta.env.VITE_SEAL_KEY_SERVERS as string || '') .split(',').map(s => s.trim()).filter(Boolean) as string[], + sidecarUrl: import.meta.env.VITE_SIDECAR_URL as string || 'http://localhost:9000', docsUrl: import.meta.env.VITE_DOCS_URL as string || '', } as const diff --git a/apps/noter/docker-compose.yml b/apps/noter/docker-compose.yml index a9eb31a2..e5c14397 100644 --- a/apps/noter/docker-compose.yml +++ b/apps/noter/docker-compose.yml @@ -1,4 +1,4 @@ -name: memwal-v2-noter +name: memwal-noter services: postgres: diff --git a/apps/noter/package/feature/note/hook/use-pdw-client.ts b/apps/noter/package/feature/note/hook/use-pdw-client.ts index d2ab086b..50d1d8ea 100644 --- a/apps/noter/package/feature/note/hook/use-pdw-client.ts +++ b/apps/noter/package/feature/note/hook/use-pdw-client.ts @@ -1,7 +1,7 @@ "use client"; /** - * MemWal V2 Status Hook + * memwal Status Hook * * Simple hook to check if MemWal is configured (MEMWAL_KEY set). * No client-side SDK needed — all operations go through server. diff --git a/apps/noter/package/feature/note/lib/pdw-client.ts b/apps/noter/package/feature/note/lib/pdw-client.ts index 619d370b..9fd5ab41 100644 --- a/apps/noter/package/feature/note/lib/pdw-client.ts +++ b/apps/noter/package/feature/note/lib/pdw-client.ts @@ -1,5 +1,5 @@ /** - * MEMWAL V2 CLIENT — Server-side MemWal SDK wrapper + * memwal CLIENT — Server-side MemWal SDK wrapper * Uses Ed25519 delegate key auth — no wallet/zkLogin needed. * Server handles: embed → encrypt → Walrus upload → store * diff --git a/apps/researcher/.env.example b/apps/researcher/.env.example index edacd3be..55f3f491 100644 --- a/apps/researcher/.env.example +++ b/apps/researcher/.env.example @@ -6,7 +6,7 @@ OPENROUTER_API_KEY= # Local: postgresql://user:pass@localhost:5432/researcher POSTGRES_URL= -# MemWal V2 SDK — Ed25519 delegate key for memory layer +# memwal SDK — Ed25519 delegate key for memory layer MEMWAL_KEY= MEMWAL_SERVER_URL=http://localhost:3001 diff --git a/docs/examples/basic-usage.md b/docs/examples/basic-usage.md index ab5ed07f..ff584bef 100644 --- a/docs/examples/basic-usage.md +++ b/docs/examples/basic-usage.md @@ -15,8 +15,8 @@ import { MemWal } from '@cmdoss/memwal' const memwal = new MemWal({ network: 'testnet', - packageId: '0x12b28adbe55c25341f08b8ad9ac69462aab917048c7cd5b736d951200090ee3f', - registryId: '0xfb8a1d298e2a73bdab353da3fcb3b16f68ab7d1f392f3a5c4944c747c026fc05', + packageId: '0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6', + registryId: '0xe80f2feec1c139616a86c9f71210152e2a7ca552b20841f2e192f99f75864437', }) ``` diff --git a/packages/sdk/src/manual.ts b/packages/sdk/src/manual.ts index f1c59e2d..b9a2e6e5 100644 --- a/packages/sdk/src/manual.ts +++ b/packages/sdk/src/manual.ts @@ -1,5 +1,5 @@ /** - * MemWal V2 — Manual Client (Full Client-Side) + * memwal — Manual Client (Full Client-Side) * * User-side flow where the SDK handles everything locally: * - SEAL encrypt/decrypt via @mysten/seal (user's own Sui wallet) diff --git a/packages/sdk/src/memwal.ts b/packages/sdk/src/memwal.ts index e6b552e5..167d8db8 100644 --- a/packages/sdk/src/memwal.ts +++ b/packages/sdk/src/memwal.ts @@ -1,5 +1,5 @@ /** - * MemWal V2 — SDK Client + * memwal — SDK Client * * Ed25519 delegate key based client that communicates with the MemWal * Rust server (TEE). All data processing (encryption, embedding, Walrus) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index e169932b..e3a9a34f 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,5 +1,5 @@ /** - * MemWal V2 — Core Types + * memwal — Core Types * * Ed25519 delegate key based SDK that communicates with * the MemWal Rust server (TEE). diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 05c14515..a5642802 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -1,5 +1,5 @@ /** - * MemWal V2 — Shared Utilities + * memwal — Shared Utilities * * Common crypto and encoding helpers used across the SDK. */ diff --git a/services/contract/Move.lock b/services/contract/Move.lock index fb8e8e67..8d5f6885 100644 --- a/services/contract/Move.lock +++ b/services/contract/Move.lock @@ -23,19 +23,19 @@ manifest_digest = "E41BBD67BE8940D26C79D78B028477EF5B33BA217A1282C78ACB344CF8A5E deps = { std = "MoveStdlib", sui = "Sui" } [pinned.testnet.MoveStdlib] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "90f1d8e92068a7fcb72af13f60df831deb2f2a65" } +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "b38bca86f0323b3fe8b6b7f4ca0cd7ae7faebe4b" } use_environment = "testnet" manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" deps = {} [pinned.testnet.Sui] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "90f1d8e92068a7fcb72af13f60df831deb2f2a65" } +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "b38bca86f0323b3fe8b6b7f4ca0cd7ae7faebe4b" } use_environment = "testnet" manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" deps = { MoveStdlib = "MoveStdlib" } -[pinned.testnet.memwal_v2] +[pinned.testnet.memwal] source = { root = true } use_environment = "testnet" -manifest_digest = "0224D9865DA34C6DCAAD6FFE99D0F4E20CED3C358E57A58B25B0677EE69DFC17" -deps = { Sui = "Sui" } +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/services/contract/Move.toml b/services/contract/Move.toml index 5c314d18..204db741 100644 --- a/services/contract/Move.toml +++ b/services/contract/Move.toml @@ -7,7 +7,6 @@ authors = ["CommandOSS"] [addresses] memwal = "0x0" -memwal_v2 = "0x0" [dev-addresses] diff --git a/services/contract/Published.toml b/services/contract/Published.toml index 023f18bf..b67aaacf 100644 --- a/services/contract/Published.toml +++ b/services/contract/Published.toml @@ -13,9 +13,9 @@ upgrade-capability = "0xd98341980569514d1c4038ca8a6689d7012c82f4667951a7e5b6bdb0 [published.testnet] chain-id = "4c78adac" -published-at = "0x12b28adbe55c25341f08b8ad9ac69462aab917048c7cd5b736d951200090ee3f" -original-id = "0x12b28adbe55c25341f08b8ad9ac69462aab917048c7cd5b736d951200090ee3f" +published-at = "0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6" +original-id = "0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6" version = 1 toolchain-version = "1.67.1" build-config = { flavor = "sui", edition = "2024" } -upgrade-capability = "0xfbe5823a12bd003f7215783a2c7bee7ab666c434f043d75fc9357fa83102d4e6" +upgrade-capability = "0x97f972776ad53c73f1e79b8f83681ddcc2c1b82bed08f699b4a86c44bcae54be" diff --git a/services/contract/sources/account.move b/services/contract/sources/account.move index 62aebbf8..3fd5f031 100644 --- a/services/contract/sources/account.move +++ b/services/contract/sources/account.move @@ -1,4 +1,4 @@ -/// MemWal V2 — Account & SEAL Access Control +/// MemWal — Account & SEAL Access Control /// /// Core on-chain module for managing MemWal accounts and delegate keys. /// Delegate keys are Ed25519 Sui keypairs that can sign transactions @@ -9,7 +9,7 @@ /// - MemWalAccount: owned object — stores owner + delegate_keys /// - DelegateKey: struct with public_key, sui_address, label, created_at /// - seal_approve: SEAL policy — authorizes owner OR delegate key holder to decrypt -module memwal_v2::account { +module memwal::account { use std::string::String; use sui::event; use sui::table::{Self, Table}; diff --git a/services/contract/tests/account_tests.move b/services/contract/tests/account_tests.move index 15a31503..b36b6237 100644 --- a/services/contract/tests/account_tests.move +++ b/services/contract/tests/account_tests.move @@ -1,10 +1,10 @@ #[test_only] #[allow(implicit_const_copy)] -module memwal_v2::account_tests { +module memwal::account_tests { use std::string; use sui::test_scenario; use sui::clock; - use memwal_v2::account::{Self, MemWalAccount, AccountRegistry}; + use memwal::account::{Self, MemWalAccount, AccountRegistry}; const OWNER: address = @0xCAFE; const OTHER: address = @0xBEEF; diff --git a/services/indexer/.env.example b/services/indexer/.env.example index 6e6679df..4e273f65 100644 --- a/services/indexer/.env.example +++ b/services/indexer/.env.example @@ -1,11 +1,11 @@ -# Database (same PostgreSQL as v2-server — shared instance) +# Database (same PostgreSQL as server — shared instance) DATABASE_URL=postgresql://memwal:memwal_secret@localhost:5432/memwal # Sui Network -SUI_RPC_URL=https://fullnode.mainnet.sui.io:443 +SUI_RPC_URL=https://fullnode.testnet.sui.io:443 # MemWal contract package ID (for event filtering) -MEMWAL_PACKAGE_ID=0xcee7a6fd8de52ce645c38332bde23d4a30fd9426bc4681409733dd50958a24c6 +MEMWAL_PACKAGE_ID=0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6 # Polling interval in seconds POLL_INTERVAL_SECS=5 diff --git a/services/indexer/Dockerfile b/services/indexer/Dockerfile index dcfe1bdc..9c836025 100644 --- a/services/indexer/Dockerfile +++ b/services/indexer/Dockerfile @@ -1,5 +1,5 @@ # ============================================================ -# MemWal V2 Indexer — Dockerfile +# memwal Indexer — Dockerfile # Pure Rust binary — indexes Sui on-chain events # ============================================================ diff --git a/services/indexer/src/main.rs b/services/indexer/src/main.rs index 98173d30..6c66273c 100644 --- a/services/indexer/src/main.rs +++ b/services/indexer/src/main.rs @@ -1,7 +1,7 @@ -/// MemWal V2 Indexer +/// MemWal Indexer /// /// Polls Sui blockchain events and indexes MemWal accounts into PostgreSQL. -/// This eliminates the need for the v2-server to scan the on-chain registry +/// This eliminates the need for the server to scan the on-chain registry /// during auth, providing O(1) account lookups instead. /// /// Indexed events: @@ -99,12 +99,12 @@ async fn main() { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "memwal_v2_indexer=debug".into()), + .unwrap_or_else(|_| "memwal_indexer=debug".into()), ) .init(); let config = Config::from_env(); - tracing::info!("starting memwal v2 indexer"); + tracing::info!("starting memwal indexer"); tracing::info!(" database: {}", redact_url(&config.database_url)); tracing::info!(" sui rpc: {}", config.sui_rpc_url); tracing::info!(" package: {}", config.package_id); diff --git a/services/server/Dockerfile b/services/server/Dockerfile index 2f6aa2dd..fe370e1f 100644 --- a/services/server/Dockerfile +++ b/services/server/Dockerfile @@ -1,5 +1,5 @@ # ============================================================ -# MemWal V2 Server — Dockerfile +# memwal Server — Dockerfile # Multi-stage build: Rust binary + Node.js sidecar scripts # ============================================================ diff --git a/services/server/migrations/001_init.sql b/services/server/migrations/001_init.sql index 266bcd8a..45267b74 100644 --- a/services/server/migrations/001_init.sql +++ b/services/server/migrations/001_init.sql @@ -1,4 +1,4 @@ --- MemWal V2 Server — PostgreSQL Schema +-- memwal Server — PostgreSQL Schema -- Requires: pgvector extension CREATE EXTENSION IF NOT EXISTS vector; diff --git a/services/server/migrations/002_add_namespace.sql b/services/server/migrations/002_add_namespace.sql index 367bb684..8c16ba79 100644 --- a/services/server/migrations/002_add_namespace.sql +++ b/services/server/migrations/002_add_namespace.sql @@ -1,8 +1,8 @@ --- MemWal V2 — Add namespace to vector_entries +-- memwal — Add namespace to vector_entries -- Supports multi-tenant/multi-app memory isolation ALTER TABLE vector_entries ADD COLUMN IF NOT EXISTS namespace TEXT NOT NULL DEFAULT 'default'; -- Composite index for namespace-scoped queries -CREATE INDEX IF NOT EXISTS idx_vector_entries_owner_ns ON vector_entries (owner, namespace); +CREATE INDEX IF NOT EXISTS idx_vector_entries_owner_ns ON vector_entries (owner, namespace); \ No newline at end of file diff --git a/services/server/scripts/sidecar-server.ts b/services/server/scripts/sidecar-server.ts index 166cce58..86cbafc4 100644 --- a/services/server/scripts/sidecar-server.ts +++ b/services/server/scripts/sidecar-server.ts @@ -394,7 +394,7 @@ app.post("/seal/decrypt-batch", async (req, res) => { // ============================================================ app.post("/walrus/upload", async (req, res) => { try { - const { data, privateKey, owner, namespace, epochs = 50 } = req.body; + const { data, privateKey, owner, namespace, packageId, epochs = 50 } = req.body; if (!data || !privateKey) { return res.status(400).json({ error: "Missing required fields: data, privateKey" }); } @@ -420,6 +420,7 @@ app.post("/walrus/upload", async (req, res) => { attributes: { ...(namespace ? { memwal_namespace: namespace } : {}), ...(owner ? { memwal_owner: owner } : {}), + ...(packageId ? { memwal_package_id: packageId } : {}), }, }); @@ -481,6 +482,19 @@ app.post("/walrus/upload", async (req, res) => { typeArguments: [], }); + // Set memwal_package_id + if (packageId) { + metaTx.moveCall({ + target: `${WALRUS_PKG}::blob::insert_or_update_metadata_pair`, + arguments: [ + blobArg, + metaTx.pure.string("memwal_package_id"), + metaTx.pure.string(packageId), + ], + typeArguments: [], + }); + } + // Transfer blob to user metaTx.transferObjects([blobArg], owner); @@ -509,7 +523,7 @@ app.post("/walrus/upload", async (req, res) => { // ============================================================ app.post("/walrus/query-blobs", async (req, res) => { try { - const { owner, namespace } = req.body; + const { owner, namespace, packageId } = req.body; if (!owner) { return res.status(400).json({ error: "Missing required field: owner" }); } @@ -519,7 +533,7 @@ app.post("/walrus/query-blobs", async (req, res) => { const WALRUS_BLOB_TYPE = `${WALRUS_PACKAGE_ID}::blob::Blob`; // Query all Walrus Blob objects owned by the user - const blobs: { blobId: string; objectId: string; namespace: string }[] = []; + const blobs: { blobId: string; objectId: string; namespace: string; packageId: string }[] = []; let cursor: string | null | undefined = undefined; let hasMore = true; @@ -549,6 +563,7 @@ app.post("/walrus/query-blobs", async (req, res) => { // Read metadata from dynamic field (Walrus stores metadata as dynamic field on Blob UID) let blobNamespace = "default"; let blobOwner = ""; + let blobPackageId = ""; try { // getDynamicFieldObject: key type is "vector", value is b"metadata" @@ -570,6 +585,7 @@ app.post("/walrus/query-blobs", async (req, res) => { const value = entry?.fields?.value; if (key === "memwal_namespace") blobNamespace = value; if (key === "memwal_owner") blobOwner = value; + if (key === "memwal_package_id") blobPackageId = value; } } } @@ -581,6 +597,9 @@ app.post("/walrus/query-blobs", async (req, res) => { // Filter by namespace if specified if (namespace && blobNamespace !== namespace) continue; + // Filter by packageId if specified + if (packageId && blobPackageId !== packageId) continue; + if (rawBlobId) { // blob_id from chain is a big integer (U256) — convert to base64url (little-endian!) let blobIdStr = String(rawBlobId); @@ -596,7 +615,7 @@ app.post("/walrus/query-blobs", async (req, res) => { // Keep as-is if conversion fails } } - blobs.push({ blobId: blobIdStr, objectId, namespace: blobNamespace }); + blobs.push({ blobId: blobIdStr, objectId, namespace: blobNamespace, packageId: blobPackageId }); } } @@ -611,6 +630,63 @@ app.post("/walrus/query-blobs", async (req, res) => { } }); +// ============================================================ +// POST /sponsor — Create Enoki-sponsored transaction for frontend +// Frontend sends TransactionKind bytes + sender → returns sponsored { bytes, digest } +// ============================================================ +app.post("/sponsor", async (req, res) => { + try { + const { transactionBlockKindBytes, sender } = req.body; + if (!transactionBlockKindBytes || !sender) { + return res.status(400).json({ error: "Missing required fields: transactionBlockKindBytes, sender" }); + } + if (!enokiApiKey) { + return res.status(503).json({ error: "Enoki sponsorship is not configured (ENOKI_API_KEY missing)" }); + } + + console.log(`[sponsor] creating sponsored tx for sender=${sender}`); + const sponsored = await callEnoki("/transaction-blocks/sponsor", { + network: enokiNetwork, + transactionBlockKindBytes, + sender, + }); + + console.log(`[sponsor] sponsored tx created, digest=${sponsored.digest}`); + res.json(sponsored); // { bytes, digest } + } catch (err: any) { + console.error(`[sponsor] error: ${err.message || err}`); + res.status(500).json({ error: err.message || String(err) }); + } +}); + +// ============================================================ +// POST /sponsor/execute — Execute signed sponsored transaction +// Frontend sends { digest, signature } after user wallet signs → returns { digest } +// ============================================================ +app.post("/sponsor/execute", async (req, res) => { + try { + const { digest, signature } = req.body; + if (!digest || !signature) { + return res.status(400).json({ error: "Missing required fields: digest, signature" }); + } + if (!enokiApiKey) { + return res.status(503).json({ error: "Enoki sponsorship is not configured (ENOKI_API_KEY missing)" }); + } + + console.log(`[sponsor/execute] executing sponsored tx digest=${digest}`); + const executed = await callEnoki( + `/transaction-blocks/sponsor/${digest}`, + { digest, signature } + ); + + console.log(`[sponsor/execute] tx executed, final digest=${executed.digest}`); + res.json(executed); // { digest } + } catch (err: any) { + console.error(`[sponsor/execute] error: ${err.message || err}`); + res.status(500).json({ error: err.message || String(err) }); + } +}); + // ============================================================ // Start server // ============================================================ diff --git a/services/server/src/main.rs b/services/server/src/main.rs index 98568f8f..7b7543b5 100644 --- a/services/server/src/main.rs +++ b/services/server/src/main.rs @@ -29,7 +29,7 @@ async fn main() { // Load config let config = Config::from_env(); - tracing::info!("starting memwal v2 server on port {}", config.port); + tracing::info!("starting memwal server on port {}", config.port); tracing::info!(" Sui RPC: {}", config.sui_rpc_url); tracing::info!(" package id: {}", config.package_id); tracing::info!(" registry id: {}", config.registry_id); @@ -140,7 +140,7 @@ async fn main() { .await .expect("Failed to bind address"); - tracing::info!("memwal v2 server listening on {}", addr); + tracing::info!("memwal server listening on {}", addr); tracing::info!(" health: http://localhost:{}/health", config.port); tracing::info!(" api: http://localhost:{}/api/{{remember,recall,analyze}}", config.port); diff --git a/services/server/src/routes.rs b/services/server/src/routes.rs index 9309ba23..8b7b544b 100644 --- a/services/server/src/routes.rs +++ b/services/server/src/routes.rs @@ -135,7 +135,7 @@ pub async fn remember( })?; let upload_result = walrus::upload_blob( &state.http_client, &state.config.sidecar_url, - &encrypted, 50, owner, sui_key, namespace, + &encrypted, 50, owner, sui_key, namespace, &state.config.package_id, ).await?; let blob_id = upload_result.blob_id; @@ -294,6 +294,7 @@ pub async fn remember_manual( owner, sui_key, namespace, + &state.config.package_id, ) .await?; @@ -407,7 +408,7 @@ pub async fn analyze( // Upload to Walrus (via sidecar HTTP) let upload_result = walrus::upload_blob( &state.http_client, &state.config.sidecar_url, - &encrypted, 50, &owner, &sui_key, &namespace, + &encrypted, 50, &owner, &sui_key, &namespace, &state.config.package_id, ).await?; // Store in Vector DB with namespace @@ -774,10 +775,18 @@ pub async fn restore( &state.config.sidecar_url, owner, Some(namespace), + Some(&state.config.package_id), ).await?; let all_blob_ids: Vec = on_chain_blobs.iter().map(|b| b.blob_id.clone()).collect(); let total = all_blob_ids.len(); + // Build blob_id → package_id lookup from on-chain metadata + // Each blob may have been encrypted with a different package_id (e.g. after contract upgrades) + let blob_package_ids: std::collections::HashMap = on_chain_blobs.iter() + .filter(|b| !b.package_id.is_empty()) + .map(|b| (b.blob_id.clone(), b.package_id.clone())) + .collect(); + if total == 0 { return Ok(Json(RestoreResponse { restored: 0, @@ -857,13 +866,17 @@ pub async fn restore( tracing::info!("restore: downloaded {}/{} blobs, decrypting (3 concurrent)...", downloaded.len(), missing_blob_ids.len()); // Step 4: SEAL decrypt with bounded concurrency (3 at a time) + // Use per-blob package_id from on-chain metadata, fall back to current server config use futures::stream::{self, StreamExt}; let decrypt_results: Vec> = stream::iter(downloaded.into_iter()) .map(|(blob_id, encrypted_data)| { let http_client = &state.http_client; let sidecar_url = state.config.sidecar_url.clone(); let private_key = private_key.clone(); - let package_id = state.config.package_id.clone(); + // Use the package_id that was stored with this blob (supports contract upgrades) + let package_id = blob_package_ids.get(&blob_id) + .cloned() + .unwrap_or_else(|| state.config.package_id.clone()); let account_id = auth.account_id.clone(); async move { match seal::seal_decrypt( diff --git a/services/server/src/walrus.rs b/services/server/src/walrus.rs index 960fa9d4..5c63a630 100644 --- a/services/server/src/walrus.rs +++ b/services/server/src/walrus.rs @@ -22,6 +22,9 @@ pub struct OnChainBlob { pub object_id: String, /// Namespace from on-chain metadata pub namespace: String, + /// MemWal package ID from on-chain metadata + #[serde(rename = "packageId", default)] + pub package_id: String, } /// Response from sidecar query-blobs endpoint @@ -39,6 +42,7 @@ struct WalrusUploadRequest { private_key: String, owner: String, namespace: String, + package_id: String, epochs: u64, } @@ -65,6 +69,7 @@ pub async fn upload_blob( owner_address: &str, sui_private_key: &str, namespace: &str, + package_id: &str, ) -> Result { let url = format!("{}/walrus/upload", sidecar_url); let data_b64 = BASE64.encode(data); @@ -76,6 +81,7 @@ pub async fn upload_blob( private_key: sui_private_key.to_string(), owner: owner_address.to_string(), namespace: namespace.to_string(), + package_id: package_id.to_string(), epochs, }) .send() @@ -120,6 +126,7 @@ pub async fn query_blobs_by_owner( sidecar_url: &str, owner_address: &str, namespace: Option<&str>, + package_id: Option<&str>, ) -> Result, AppError> { let url = format!("{}/walrus/query-blobs", sidecar_url); @@ -127,6 +134,9 @@ pub async fn query_blobs_by_owner( if let Some(ns) = namespace { body["namespace"] = serde_json::json!(ns); } + if let Some(pkg) = package_id { + body["packageId"] = serde_json::json!(pkg); + } let resp = client .post(&url) diff --git a/services/server/tests/e2e_test.py b/services/server/tests/e2e_test.py index fa7c7bac..883064f8 100644 --- a/services/server/tests/e2e_test.py +++ b/services/server/tests/e2e_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -E2E test for MemWal V2 Server — Full Ed25519 keypair flow. +E2E test for memwal Server — Full Ed25519 keypair flow. Tests: 1. Generate Ed25519 keypair @@ -208,7 +208,7 @@ def test_expired_timestamp(signing_key: SigningKey): if __name__ == "__main__": print("=" * 50) - print(" MemWal V2 Server — E2E Test Suite") + print(" memwal Server — E2E Test Suite") print("=" * 50) print() diff --git a/tmp-rememe b/tmp-rememe new file mode 160000 index 00000000..dab12f4c --- /dev/null +++ b/tmp-rememe @@ -0,0 +1 @@ +Subproject commit dab12f4c6bdff2da46b39aa2bb1a7824be8550bc diff --git a/tmp-smashblob b/tmp-smashblob new file mode 160000 index 00000000..70ed015b --- /dev/null +++ b/tmp-smashblob @@ -0,0 +1 @@ +Subproject commit 70ed015b53da20c0fb0529f18d8015c45e936eb0 From 200009ab91984d05c861ea9790523333bb890cca Mon Sep 17 00:00:00 2001 From: ducnmm Date: Fri, 20 Mar 2026 00:53:20 +0700 Subject: [PATCH 02/34] remove tmp-rememe and tmp-smashblob --- .gitignore | 2 ++ tmp-rememe | 1 - tmp-smashblob | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 160000 tmp-rememe delete mode 160000 tmp-smashblob diff --git a/.gitignore b/.gitignore index 0f00e2ff..c8a80f71 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,5 @@ CLAUDE.md .codacy .github/instructions x-wallet +tmp-rememe/ +tmp-smashblob/ diff --git a/tmp-rememe b/tmp-rememe deleted file mode 160000 index dab12f4c..00000000 --- a/tmp-rememe +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dab12f4c6bdff2da46b39aa2bb1a7824be8550bc diff --git a/tmp-smashblob b/tmp-smashblob deleted file mode 160000 index 70ed015b..00000000 --- a/tmp-smashblob +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 70ed015b53da20c0fb0529f18d8015c45e936eb0 From da00896cd4864862835003f1071115396b030844 Mon Sep 17 00:00:00 2001 From: ducnmm Date: Fri, 20 Mar 2026 09:34:18 +0700 Subject: [PATCH 03/34] feat: add account management to SDK, delegate key management UI in dashboard --- apps/app/src/pages/Dashboard.tsx | 351 ++++++++++++++++++++++++++++++- packages/sdk/src/account.ts | 350 ++++++++++++++++++++++++++++++ packages/sdk/src/index.ts | 9 + packages/sdk/src/types.ts | 71 +++++++ 4 files changed, 776 insertions(+), 5 deletions(-) create mode 100644 packages/sdk/src/account.ts diff --git a/apps/app/src/pages/Dashboard.tsx b/apps/app/src/pages/Dashboard.tsx index d772c99f..3e614348 100644 --- a/apps/app/src/pages/Dashboard.tsx +++ b/apps/app/src/pages/Dashboard.tsx @@ -1,24 +1,69 @@ /** - * Dashboard — Account info, delegate keys, SDK integration guide + * Dashboard — Account info, delegate keys management, SDK integration guide */ -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect, useMemo } from 'react' import { useCurrentAccount, useDisconnectWallet, + useSignAndExecuteTransaction, + useSignPersonalMessage, + useSuiClient, } from '@mysten/dapp-kit' +import { generateDelegateKey, addDelegateKey, removeDelegateKey } from '@cmdoss/memwal' +import type { WalletSigner } from '@cmdoss/memwal/manual' import { useDelegateKey } from '../App' import { config } from '../config' +// ============================================================ +// Types +// ============================================================ + +interface OnChainDelegateKey { + publicKey: string + suiAddress: string + label: string + createdAt: number +} + +// ============================================================ +// Dashboard Component +// ============================================================ + export default function Dashboard() { const currentAccount = useCurrentAccount() const { mutateAsync: disconnect } = useDisconnectWallet() - const { delegateKey, delegatePublicKey, clearDelegateKeys } = useDelegateKey() + const { mutateAsync: signAndExecuteTx } = useSignAndExecuteTransaction() + const { mutateAsync: signPersonalMsg } = useSignPersonalMessage() + const suiClient = useSuiClient() + const { delegateKey, delegatePublicKey, accountObjectId, clearDelegateKeys } = useDelegateKey() const address = currentAccount?.address || '' const [showKey, setShowKey] = useState(false) const [copied, setCopied] = useState(null) + // Delegate key management state + const [onChainKeys, setOnChainKeys] = useState([]) + const [loadingKeys, setLoadingKeys] = useState(false) + const [addingKey, setAddingKey] = useState(false) + const [removingKey, setRemovingKey] = useState(null) + const [showAddForm, setShowAddForm] = useState(false) + const [newKeyLabel, setNewKeyLabel] = useState('New Key') + const [keyError, setKeyError] = useState('') + const [newPrivateKey, setNewPrivateKey] = useState(null) + + // WalletSigner adapter — wraps dapp-kit hooks into SDK's WalletSigner interface + const walletSigner = useMemo(() => { + if (!currentAccount) return null + return { + address: currentAccount.address, + signAndExecuteTransaction: ({ transaction }) => + signAndExecuteTx({ transaction }), + signPersonalMessage: ({ message }) => + signPersonalMsg({ message }), + } + }, [currentAccount, signAndExecuteTx, signPersonalMsg]) + const copyToClipboard = useCallback(async (text: string, label: string) => { await navigator.clipboard.writeText(text) setCopied(label) @@ -30,6 +75,128 @@ export default function Dashboard() { await disconnect() }, [clearDelegateKeys, disconnect]) + // ============================================================ + // Fetch on-chain delegate keys + // ============================================================ + + const fetchOnChainKeys = useCallback(async () => { + if (!accountObjectId) return + setLoadingKeys(true) + try { + const obj = await suiClient.getObject({ + id: accountObjectId, + options: { showContent: true }, + }) + if (obj?.data?.content && 'fields' in obj.data.content) { + const fields = obj.data.content.fields as any + const keys = fields?.delegate_keys ?? [] + const parsed: OnChainDelegateKey[] = keys.map((k: any) => { + const f = k.fields ?? k + const pkBytes: number[] = f.public_key ?? [] + const pkHex = pkBytes.map((b: number) => b.toString(16).padStart(2, '0')).join('') + return { + publicKey: pkHex, + suiAddress: f.sui_address ?? '', + label: f.label ?? '', + createdAt: Number(f.created_at ?? 0), + } + }) + setOnChainKeys(parsed) + } + } catch (err) { + console.error('Failed to fetch on-chain keys:', err) + } finally { + setLoadingKeys(false) + } + }, [accountObjectId, suiClient]) + + useEffect(() => { + fetchOnChainKeys() + }, [fetchOnChainKeys]) + + // ============================================================ + // Generate + add a new delegate key (via SDK) + // ============================================================ + + const handleAddKey = useCallback(async () => { + if (!walletSigner) return + setAddingKey(true) + setKeyError('') + setNewPrivateKey(null) + try { + // Generate keypair via SDK + const delegate = await generateDelegateKey() + + // Register on-chain via SDK + await addDelegateKey({ + packageId: config.memwalPackageId, + accountId: accountObjectId!, + publicKey: delegate.publicKey, + label: newKeyLabel || 'New Key', + walletSigner, + suiClient, + suiNetwork: config.suiNetwork, + }) + + setNewPrivateKey(delegate.privateKey) + setShowAddForm(false) + setNewKeyLabel('New Key') + + // Copy private key to clipboard automatically + await navigator.clipboard.writeText(delegate.privateKey) + + // Refresh key list + await fetchOnChainKeys() + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'failed to add key' + setKeyError(msg) + } finally { + setAddingKey(false) + } + }, [walletSigner, accountObjectId, newKeyLabel, suiClient, fetchOnChainKeys]) + + // ============================================================ + // Remove a delegate key (via SDK) + // ============================================================ + + const handleRemoveKey = useCallback(async (publicKeyHex: string) => { + if (!walletSigner) return + if (!confirm('remove this delegate key? this cannot be undone.')) return + setRemovingKey(publicKeyHex) + setKeyError('') + setNewPrivateKey(null) + try { + await removeDelegateKey({ + packageId: config.memwalPackageId, + accountId: accountObjectId!, + publicKey: publicKeyHex, + walletSigner, + suiClient, + suiNetwork: config.suiNetwork, + }) + + // key removed successfully + + // If we removed our own key, clear local state + if (publicKeyHex === delegatePublicKey) { + clearDelegateKeys() + } + + // Refresh key list + await fetchOnChainKeys() + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'failed to remove key' + setKeyError(msg) + } finally { + + setRemovingKey(null) + } + }, [walletSigner, accountObjectId, delegatePublicKey, suiClient, fetchOnChainKeys, clearDelegateKeys]) + + // ============================================================ + // SDK code snippets + // ============================================================ + const sdkSnippet = `import { MemWal } from "@cmdoss/memwal" const memwal = MemWal.create({ @@ -100,11 +267,11 @@ const result = await generateText({ - {/* Delegate Key */} + {/* Current Delegate Key */}
-
delegate key
+
your delegate key
your Ed25519 key for SDK authentication
@@ -158,6 +325,180 @@ const result = await generateText({
+ {/* On-Chain Delegate Keys Management */} +
+
+
+
delegate keys (on-chain)
+
+ all Ed25519 keys registered on your MemWalAccount +
+
+
+ + +
+
+ + {/* Status messages */} + {keyError && ( +
+ {keyError} +
+ )} + {newPrivateKey && ( +
+
+

+ save this private key now! it has been copied to your clipboard. + store it securely — it cannot be recovered. +

+
+
+
new private key (keep secret)
+
{newPrivateKey}
+
+ + +
+
+
+ )} + + {/* Add Key Form */} + {showAddForm && ( +
+
+ + setNewKeyLabel(e.target.value)} + placeholder="e.g. MacBook Pro, Production Server" + style={{ + width: '100%', + padding: '8px 12px', + background: 'var(--bg-secondary)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-sm)', + color: 'var(--text-primary)', + fontSize: '0.85rem', + outline: 'none', + }} + /> +
+
+ + +
+

+ a new Ed25519 keypair will be generated. the private key will be copied to your clipboard. + save it securely — it cannot be recovered. +

+
+ )} + + {/* Key List */} + {loadingKeys ? ( +
+ loading keys... +
+ ) : onChainKeys.length === 0 ? ( +
+ no delegate keys found on-chain +
+ ) : ( +
+ {onChainKeys.map((k) => { + const isCurrentKey = k.publicKey === delegatePublicKey + const isRemoving = removingKey === k.publicKey + return ( +
+
+ {k.label || 'Untitled'} + {isCurrentKey && ' · current'} + + {new Date(k.createdAt).toLocaleDateString()} + +
+
+ {k.publicKey} +
+
+ + +
+
+ ) + })} +
+ )} +
+ {/* Quick Start: SDK */}
diff --git a/packages/sdk/src/account.ts b/packages/sdk/src/account.ts new file mode 100644 index 00000000..55d85c12 --- /dev/null +++ b/packages/sdk/src/account.ts @@ -0,0 +1,350 @@ +/** + * memwal — Account Management + * + * On-chain account operations: create account, add/remove delegate keys. + * Supports both wallet signing (browser) and private key signing (server-side). + * + * @example + * ```typescript + * import { createAccount, addDelegateKey, generateDelegateKey } from "@cmdoss/memwal" + * + * // Generate a delegate keypair + * const delegate = await generateDelegateKey() + * + * // Create account (wallet mode — browser) + * const account = await createAccount({ + * packageId: "0x...", + * registryId: "0x...", + * walletSigner, + * }) + * + * // Add the delegate key + * await addDelegateKey({ + * packageId: "0x...", + * accountId: account.accountId, + * publicKey: delegate.publicKey, + * label: "My Laptop", + * walletSigner, + * }) + * + * // Now use the delegate key with the SDK + * const memwal = MemWal.create({ key: delegate.privateKey }) + * ``` + */ + +import type { + WalletSigner, + CreateAccountOpts, + CreateAccountResult, + AddDelegateKeyOpts, + AddDelegateKeyResult, + RemoveDelegateKeyOpts, +} from "./types.js"; +import { bytesToHex, hexToBytes } from "./utils.js"; + +// ============================================================ +// SUI Clock object (shared, always 0x6) +// ============================================================ +const SUI_CLOCK = "0x0000000000000000000000000000000000000000000000000000000000000006"; + +// ============================================================ +// Internal: Build Sui client + signer +// ============================================================ + +interface TxContext { + suiClient: any; + signer: any; + address: string; + Transaction: any; +} + +async function buildTxContext(opts: { + suiPrivateKey?: string; + walletSigner?: WalletSigner; + suiClient?: any; + suiNetwork?: "testnet" | "mainnet"; +}): Promise { + if (!opts.suiPrivateKey && !opts.walletSigner) { + throw new Error("Provide either suiPrivateKey or walletSigner"); + } + if (opts.suiPrivateKey && opts.walletSigner) { + throw new Error("Provide suiPrivateKey OR walletSigner, not both"); + } + + const { Transaction } = await import("@mysten/sui/transactions"); + + // Build Sui client + let suiClient: any; + if (opts.suiClient) { + suiClient = opts.suiClient; + } else { + const mod = await import("@mysten/sui/client"); + const SuiClient = (mod as any).SuiClient; + if (typeof SuiClient !== "function") { + throw new Error( + "SuiClient not found. For @mysten/sui v2.6.0+, pass suiClient in opts." + ); + } + const network = opts.suiNetwork ?? "mainnet"; + const urls: Record = { + testnet: "https://fullnode.testnet.sui.io:443", + mainnet: "https://fullnode.mainnet.sui.io:443", + }; + suiClient = new SuiClient({ url: urls[network] ?? urls.mainnet }); + } + + // Build signer + if (opts.walletSigner) { + return { + suiClient, + signer: opts.walletSigner, + address: opts.walletSigner.address, + Transaction, + }; + } + + const { decodeSuiPrivateKey } = await import("@mysten/sui/cryptography"); + const { Ed25519Keypair } = await import("@mysten/sui/keypairs/ed25519"); + const { secretKey } = decodeSuiPrivateKey(opts.suiPrivateKey!); + const keypair = Ed25519Keypair.fromSecretKey(secretKey); + + return { + suiClient, + signer: keypair, + address: keypair.getPublicKey().toSuiAddress(), + Transaction, + }; +} + +async function signAndExecute( + ctx: TxContext, + tx: any, +): Promise<{ digest: string; effects: any }> { + if ("signAndExecuteTransaction" in ctx.signer && typeof ctx.signer.signAndExecuteTransaction === "function" && "address" in ctx.signer) { + // WalletSigner mode + const result = await ctx.signer.signAndExecuteTransaction({ transaction: tx }); + // Wait for transaction to be confirmed + const txResult = await ctx.suiClient.waitForTransaction({ + digest: result.digest, + options: { showEffects: true, showObjectChanges: true }, + }); + return { digest: result.digest, effects: txResult }; + } + + // Keypair mode + const result = await ctx.suiClient.signAndExecuteTransaction({ + signer: ctx.signer, + transaction: tx, + }); + const txResult = await ctx.suiClient.waitForTransaction({ + digest: result.digest, + options: { showEffects: true, showObjectChanges: true }, + }); + return { digest: result.digest, effects: txResult }; +} + +// ============================================================ +// createAccount +// ============================================================ + +/** + * Create a new MemWalAccount on-chain. + * + * Calls `{packageId}::account::create_account(registry, clock)`. + * Each address can only create ONE account (enforced by the contract). + * + * @returns CreateAccountResult with accountId, owner, and tx digest + */ +export async function createAccount( + opts: CreateAccountOpts, +): Promise { + const ctx = await buildTxContext(opts); + const { Transaction } = ctx; + + const tx = new Transaction(); + tx.moveCall({ + target: `${opts.packageId}::account::create_account`, + arguments: [ + tx.object(opts.registryId), + tx.object(SUI_CLOCK), + ], + }); + + const { digest, effects } = await signAndExecute(ctx, tx); + + // Extract the created MemWalAccount object ID from object changes + let accountId = ""; + const objectChanges = effects?.objectChanges ?? []; + for (const change of objectChanges) { + if ( + change.type === "created" && + change.objectType?.includes("::account::MemWalAccount") + ) { + accountId = change.objectId; + break; + } + } + + if (!accountId) { + // Fallback: try to find from effects + const created = effects?.effects?.created ?? []; + for (const obj of created) { + if (obj.owner?.Shared !== undefined) { + accountId = obj.reference?.objectId ?? ""; + break; + } + } + } + + return { + accountId, + owner: ctx.address, + digest, + }; +} + +// ============================================================ +// addDelegateKey +// ============================================================ + +/** + * Add a delegate key to a MemWalAccount. + * + * Calls `{packageId}::account::add_delegate_key(account, public_key, sui_address, label, clock)`. + * Only the account owner can add delegate keys. + * + * @param opts.publicKey - Ed25519 public key (32 bytes Uint8Array or hex string) + * @param opts.label - Human-readable label (e.g. "MacBook Pro", "Production Server") + * @returns AddDelegateKeyResult with digest, publicKey hex, and derived suiAddress + */ +export async function addDelegateKey( + opts: AddDelegateKeyOpts, +): Promise { + const ctx = await buildTxContext(opts); + const { Transaction } = ctx; + + // Normalize public key to Uint8Array + const pkBytes: Uint8Array = + typeof opts.publicKey === "string" + ? hexToBytes(opts.publicKey) + : opts.publicKey; + + if (pkBytes.length !== 32) { + throw new Error(`Invalid Ed25519 public key length: ${pkBytes.length} (expected 32)`); + } + + // Derive Sui address from the public key + const { blake2b } = await import("@noble/hashes/blake2.js"); + const input = new Uint8Array(33); + input[0] = 0x00; // Ed25519 scheme flag + input.set(pkBytes, 1); + const addressBytes = blake2b(input, { dkLen: 32 }); + const suiAddress = "0x" + bytesToHex(addressBytes); + + const tx = new Transaction(); + tx.moveCall({ + target: `${opts.packageId}::account::add_delegate_key`, + arguments: [ + tx.object(opts.accountId), + tx.pure("vector", Array.from(pkBytes)), + tx.pure("address", suiAddress), + tx.pure("string", opts.label), + tx.object(SUI_CLOCK), + ], + }); + + const { digest } = await signAndExecute(ctx, tx); + + return { + digest, + publicKey: bytesToHex(pkBytes), + suiAddress, + }; +} + +// ============================================================ +// removeDelegateKey +// ============================================================ + +/** + * Remove a delegate key from a MemWalAccount. + * + * Calls `{packageId}::account::remove_delegate_key(account, public_key)`. + * Only the account owner can remove delegate keys. + * + * @param opts.publicKey - Ed25519 public key to remove (32 bytes Uint8Array or hex string) + */ +export async function removeDelegateKey( + opts: RemoveDelegateKeyOpts, +): Promise<{ digest: string }> { + const ctx = await buildTxContext(opts); + const { Transaction } = ctx; + + const pkBytes: Uint8Array = + typeof opts.publicKey === "string" + ? hexToBytes(opts.publicKey) + : opts.publicKey; + + if (pkBytes.length !== 32) { + throw new Error(`Invalid Ed25519 public key length: ${pkBytes.length} (expected 32)`); + } + + const tx = new Transaction(); + tx.moveCall({ + target: `${opts.packageId}::account::remove_delegate_key`, + arguments: [ + tx.object(opts.accountId), + tx.pure("vector", Array.from(pkBytes)), + ], + }); + + const { digest } = await signAndExecute(ctx, tx); + return { digest }; +} + +// ============================================================ +// generateDelegateKey +// ============================================================ + +/** + * Generate a new Ed25519 delegate keypair. + * + * Returns the private key (hex), public key (bytes), and derived Sui address. + * The private key can be used with `MemWal.create({ key })`. + * + * @example + * ```typescript + * const delegate = await generateDelegateKey() + * console.log(delegate.privateKey) // hex string — store securely! + * console.log(delegate.suiAddress) // 0x... — use in addDelegateKey + * + * // Use with SDK + * const memwal = MemWal.create({ key: delegate.privateKey }) + * ``` + */ +export async function generateDelegateKey(): Promise<{ + privateKey: string; + publicKey: Uint8Array; + suiAddress: string; +}> { + const ed = await import("@noble/ed25519"); + const { blake2b } = await import("@noble/hashes/blake2.js"); + + // Generate random 32-byte private key + const privateKeyBytes = new Uint8Array(32); + globalThis.crypto.getRandomValues(privateKeyBytes); + const publicKey = await ed.getPublicKeyAsync(privateKeyBytes); + + // Derive Sui address + const input = new Uint8Array(33); + input[0] = 0x00; // Ed25519 scheme flag + input.set(publicKey, 1); + const addressBytes = blake2b(input, { dkLen: 32 }); + const suiAddress = "0x" + bytesToHex(addressBytes); + + return { + privateKey: bytesToHex(privateKeyBytes), + publicKey, + suiAddress, + }; +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 1f8cade6..e7112181 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -11,6 +11,9 @@ // Core client (server-mode: server handles SEAL + Walrus + embedding) export { MemWal } from "./memwal.js"; +// Account management (on-chain: create account, add/remove delegate keys) +export { createAccount, addDelegateKey, removeDelegateKey, generateDelegateKey } from "./account.js"; + // Delegate key utilities export { delegateKeyToSuiAddress, delegateKeyToPublicKey } from "./utils.js"; @@ -25,5 +28,11 @@ export type { AnalyzedFact, HealthResult, RestoreResult, + // Account management types + CreateAccountOpts, + CreateAccountResult, + AddDelegateKeyOpts, + AddDelegateKeyResult, + RemoveDelegateKeyOpts, } from "./types.js"; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index e3a9a34f..8790a085 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -199,3 +199,74 @@ export interface RecallManualResult { results: (RecallManualHit | RecallManualMemory)[]; total: number; } + +// ============================================================ +// Account Management Types +// ============================================================ + +/** Base options for on-chain account transactions */ +interface AccountTxOpts { + /** MemWal contract package ID on Sui */ + packageId: string; + /** + * Sui private key (bech32 suiprivkey1...) for signing. + * Provide EITHER this OR `walletSigner` — not both. + */ + suiPrivateKey?: string; + /** + * Connected wallet signer (e.g. from dapp-kit). + * Provide EITHER this OR `suiPrivateKey` — not both. + */ + walletSigner?: WalletSigner; + /** + * Pre-configured Sui client instance. + * If omitted, the SDK will create one internally. + */ + suiClient?: any; + /** Sui network (default: mainnet) */ + suiNetwork?: "testnet" | "mainnet"; +} + +/** Options for createAccount() */ +export interface CreateAccountOpts extends AccountTxOpts { + /** AccountRegistry shared object ID */ + registryId: string; +} + +/** Result from createAccount() */ +export interface CreateAccountResult { + /** Created MemWalAccount object ID */ + accountId: string; + /** Owner Sui address */ + owner: string; + /** Transaction digest */ + digest: string; +} + +/** Options for addDelegateKey() */ +export interface AddDelegateKeyOpts extends AccountTxOpts { + /** MemWalAccount object ID */ + accountId: string; + /** Ed25519 public key (32 bytes Uint8Array or hex string) */ + publicKey: Uint8Array | string; + /** Human-readable label (e.g. "MacBook Pro", "Production Server") */ + label: string; +} + +/** Result from addDelegateKey() */ +export interface AddDelegateKeyResult { + /** Transaction digest */ + digest: string; + /** Public key hex */ + publicKey: string; + /** Derived Sui address for this delegate key */ + suiAddress: string; +} + +/** Options for removeDelegateKey() */ +export interface RemoveDelegateKeyOpts extends AccountTxOpts { + /** MemWalAccount object ID */ + accountId: string; + /** Ed25519 public key to remove (32 bytes Uint8Array or hex string) */ + publicKey: Uint8Array | string; +} From 0e298554528c2dcc9ff1132766bc8406eb5fec5e Mon Sep 17 00:00:00 2001 From: ducnmm Date: Fri, 20 Mar 2026 12:20:13 +0700 Subject: [PATCH 04/34] feat: add Enoki sponsored transactions for gasless frontend UX + direct signing for Walrus uploads --- apps/app/src/hooks/useSponsoredTransaction.ts | 96 +++++++++++++++++++ apps/app/src/pages/Dashboard.tsx | 4 +- apps/app/src/pages/Playground.tsx | 4 +- apps/app/src/pages/SetupWizard.tsx | 4 +- services/server/scripts/sidecar-server.ts | 82 ++++++++++------ services/server/src/main.rs | 4 +- services/server/src/routes.rs | 58 +++++++++++ 7 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 apps/app/src/hooks/useSponsoredTransaction.ts diff --git a/apps/app/src/hooks/useSponsoredTransaction.ts b/apps/app/src/hooks/useSponsoredTransaction.ts new file mode 100644 index 00000000..43f740d7 --- /dev/null +++ b/apps/app/src/hooks/useSponsoredTransaction.ts @@ -0,0 +1,96 @@ +/** + * useSponsoredTransaction — Enoki-sponsored transaction hook + * + * Drop-in replacement for useSignAndExecuteTransaction from @mysten/dapp-kit. + * Routes transactions through Enoki sponsor via the sidecar server for gasless UX. + * + * Flow: + * 1. Build Transaction as TransactionKind bytes + * 2. POST to sidecar /sponsor → get { bytes, digest } + * 3. Sign sponsored bytes with user wallet + * 4. POST to sidecar /sponsor/execute → get { digest } + * + * Falls back to direct signAndExecute if sponsor fails. + */ + +import { useCurrentAccount, useSignTransaction, useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit' +import { Transaction } from '@mysten/sui/transactions' +import { config } from '../config' + +export function useSponsoredTransaction() { + const currentAccount = useCurrentAccount() + const suiClient = useSuiClient() + const { mutateAsync: signTransaction } = useSignTransaction() + const { mutateAsync: directSignAndExecute } = useSignAndExecuteTransaction() + + const mutateAsync = async ({ transaction }: { transaction: Transaction }): Promise<{ digest: string }> => { + const sender = currentAccount?.address + if (!sender) throw new Error('No wallet connected') + + try { + // 1. Build TransactionKind bytes (without gas data) + const kindBytes = await transaction.build({ + client: suiClient as any, + onlyTransactionKind: true, + }) + const kindBase64 = uint8ArrayToBase64(kindBytes) + + // 2. Sponsor via server (proxied to sidecar) + const sponsorRes = await fetch(`${config.memwalServerUrl}/sponsor`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + transactionBlockKindBytes: kindBase64, + sender, + }), + }) + + if (!sponsorRes.ok) { + const errText = await sponsorRes.text() + throw new Error(`Sponsor failed (${sponsorRes.status}): ${errText}`) + } + + const sponsored = await sponsorRes.json() + // sponsored = { bytes: base64, digest: string } + + // 3. Sign sponsored bytes with user wallet + const sponsoredTx = Transaction.from(sponsored.bytes) + const { signature } = await signTransaction({ transaction: sponsoredTx }) + + // 4. Execute via server (proxied to sidecar) + const execRes = await fetch(`${config.memwalServerUrl}/sponsor/execute`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + digest: sponsored.digest, + signature, + }), + }) + + if (!execRes.ok) { + const errText = await execRes.text() + throw new Error(`Sponsored execute failed (${execRes.status}): ${errText}`) + } + + const result = await execRes.json() + console.log(`[sponsored-tx] success, digest=${result.digest}`) + return { digest: result.digest } + } catch (err) { + // Fallback: try direct signing if sponsor fails + console.warn('[sponsored-tx] sponsor failed, falling back to direct signing:', err) + const result = await directSignAndExecute({ transaction }) + return { digest: result.digest } + } + } + + return { mutateAsync } +} + +// Helper: Uint8Array → base64 +function uint8ArrayToBase64(bytes: Uint8Array): string { + let binary = '' + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]) + } + return btoa(binary) +} diff --git a/apps/app/src/pages/Dashboard.tsx b/apps/app/src/pages/Dashboard.tsx index 3e614348..3fa05386 100644 --- a/apps/app/src/pages/Dashboard.tsx +++ b/apps/app/src/pages/Dashboard.tsx @@ -6,10 +6,10 @@ import { useState, useCallback, useEffect, useMemo } from 'react' import { useCurrentAccount, useDisconnectWallet, - useSignAndExecuteTransaction, useSignPersonalMessage, useSuiClient, } from '@mysten/dapp-kit' +import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' import { generateDelegateKey, addDelegateKey, removeDelegateKey } from '@cmdoss/memwal' import type { WalletSigner } from '@cmdoss/memwal/manual' import { useDelegateKey } from '../App' @@ -33,7 +33,7 @@ interface OnChainDelegateKey { export default function Dashboard() { const currentAccount = useCurrentAccount() const { mutateAsync: disconnect } = useDisconnectWallet() - const { mutateAsync: signAndExecuteTx } = useSignAndExecuteTransaction() + const { mutateAsync: signAndExecuteTx } = useSponsoredTransaction() const { mutateAsync: signPersonalMsg } = useSignPersonalMessage() const suiClient = useSuiClient() const { delegateKey, delegatePublicKey, accountObjectId, clearDelegateKeys } = useDelegateKey() diff --git a/apps/app/src/pages/Playground.tsx b/apps/app/src/pages/Playground.tsx index 4c19a6a0..b9054981 100644 --- a/apps/app/src/pages/Playground.tsx +++ b/apps/app/src/pages/Playground.tsx @@ -9,10 +9,10 @@ import { useState, useCallback, useMemo, type ReactNode } from 'react' import { useCurrentAccount, useDisconnectWallet, - useSignAndExecuteTransaction, useSignPersonalMessage, useSuiClient, } from '@mysten/dapp-kit' +import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' import { MemWal } from '@cmdoss/memwal' import { MemWalManual } from '@cmdoss/memwal/manual' import { useDelegateKey } from '../App' @@ -120,7 +120,7 @@ export default function Playground() { : '...' // Wallet signing hooks (for full client-side mode) - const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction() + const { mutateAsync: signAndExecuteTransaction } = useSponsoredTransaction() const { mutateAsync: signPersonalMessage } = useSignPersonalMessage() const suiClient = useSuiClient() diff --git a/apps/app/src/pages/SetupWizard.tsx b/apps/app/src/pages/SetupWizard.tsx index b0e814ea..50ee93b9 100644 --- a/apps/app/src/pages/SetupWizard.tsx +++ b/apps/app/src/pages/SetupWizard.tsx @@ -12,10 +12,10 @@ import { useState, useCallback } from 'react' import { useCurrentAccount, useDisconnectWallet, - useSignAndExecuteTransaction, useSuiClient, } from '@mysten/dapp-kit' import { Transaction } from '@mysten/sui/transactions' +import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' import { useDelegateKey } from '../App' import { config } from '../config' @@ -24,7 +24,7 @@ type Step = 'intro' | 'generating' | 'show-key' | 'onchain' | 'done' export default function SetupWizard() { const currentAccount = useCurrentAccount() const { mutateAsync: disconnect } = useDisconnectWallet() - const { mutateAsync: signAndExecute } = useSignAndExecuteTransaction() + const { mutateAsync: signAndExecute } = useSponsoredTransaction() const suiClient = useSuiClient() const { setDelegateKeys } = useDelegateKey() diff --git a/services/server/scripts/sidecar-server.ts b/services/server/scripts/sidecar-server.ts index 86cbafc4..18887d3f 100644 --- a/services/server/scripts/sidecar-server.ts +++ b/services/server/scripts/sidecar-server.ts @@ -118,30 +118,40 @@ async function executeWithEnokiSponsor(tx: Transaction, signer: Ed25519Keypair): return direct.digest; } - const txKindBytes = await tx.build({ - client: suiClient as any, - onlyTransactionKind: true, - }); + try { + const txKindBytes = await tx.build({ + client: suiClient as any, + onlyTransactionKind: true, + }); - const sponsored = await callEnoki("/transaction-blocks/sponsor", { - network: enokiNetwork, - transactionBlockKindBytes: Buffer.from(txKindBytes).toString("base64"), - sender: signer.toSuiAddress(), - }); + const sponsored = await callEnoki("/transaction-blocks/sponsor", { + network: enokiNetwork, + transactionBlockKindBytes: Buffer.from(txKindBytes).toString("base64"), + sender: signer.toSuiAddress(), + }); - const signature = await signer.signTransaction( - new Uint8Array(Buffer.from(sponsored.bytes, "base64")) - ); + const signature = await signer.signTransaction( + new Uint8Array(Buffer.from(sponsored.bytes, "base64")) + ); - const executed = await callEnoki( - `/transaction-blocks/sponsor/${sponsored.digest}`, - { - digest: sponsored.digest, - signature: signature.signature, - } - ); + const executed = await callEnoki( + `/transaction-blocks/sponsor/${sponsored.digest}`, + { + digest: sponsored.digest, + signature: signature.signature, + } + ); - return executed.digest; + return executed.digest; + } catch (err: any) { + // Fallback to direct signing if Enoki rejects (e.g. GasCoin usage in Walrus txs) + console.warn(`[enoki-sponsor] sponsor failed, falling back to direct signing: ${err.message}`); + const direct = await suiClient.signAndExecuteTransaction({ + signer, + transaction: tx, + }); + return direct.digest; + } } /** @@ -176,6 +186,17 @@ async function runExclusiveBySigner(signerAddress: string, task: () => Promis const app = express(); app.use(express.json({ limit: "50mb" })); +// CORS — allow frontend (any origin) to call sponsor endpoints +app.use((_req, res, next) => { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.header("Access-Control-Allow-Headers", "Content-Type, Authorization"); + if (_req.method === "OPTIONS") { + return res.sendStatus(204); + } + next(); +}); + // Health check app.get("/health", (_req, res) => { res.json({ status: "ok", uptime: process.uptime() }); @@ -425,19 +446,17 @@ app.post("/walrus/upload", async (req, res) => { }); // Wait until register tx is confirmed before starting upload/certify. - const registerDigest = await executeWithEnokiSponsor(registerTx, signer); - await suiClient.waitForTransaction({ - digest: registerDigest, - }); + // NOTE: Walrus register uses GasCoin internally — always direct sign (no Enoki sponsor) + const registerResult = await suiClient.signAndExecuteTransaction({ signer, transaction: registerTx }); + await suiClient.waitForTransaction({ digest: registerResult.digest }); - await flow.upload({ digest: registerDigest }); + await flow.upload({ digest: registerResult.digest }); const certifyTx = flow.certify(); // Wait until certify tx is confirmed before returning this upload. - const certifyDigest = await executeWithEnokiSponsor(certifyTx, signer); - await suiClient.waitForTransaction({ - digest: certifyDigest, - }); + // NOTE: Walrus certify uses unlisted system methods — always direct sign + const certifyResult = await suiClient.signAndExecuteTransaction({ signer, transaction: certifyTx }); + await suiClient.waitForTransaction({ digest: certifyResult.digest }); return flow.getBlob(); }); @@ -498,8 +517,9 @@ app.post("/walrus/upload", async (req, res) => { // Transfer blob to user metaTx.transferObjects([blobArg], owner); - const metaDigest = await executeWithEnokiSponsor(metaTx, signer); - await suiClient.waitForTransaction({ digest: metaDigest }); + // NOTE: Transfer to user address can't be Enoki-sponsored — always direct sign + const metaResult = await suiClient.signAndExecuteTransaction({ signer, transaction: metaTx }); + await suiClient.waitForTransaction({ digest: metaResult.digest }); console.error(`[walrus/upload] metadata set + transferred blob ${blobObjectId} to ${owner} (ns=${namespace})`); } catch (metaErr: any) { // Non-fatal: blob is uploaded but metadata/transfer failed diff --git a/services/server/src/main.rs b/services/server/src/main.rs index 7b7543b5..e90669d5 100644 --- a/services/server/src/main.rs +++ b/services/server/src/main.rs @@ -125,7 +125,9 @@ async fn main() { // Public routes let public_routes = Router::new() - .route("/health", get(routes::health)); + .route("/health", get(routes::health)) + .route("/sponsor", post(routes::sponsor_proxy)) + .route("/sponsor/execute", post(routes::sponsor_execute_proxy)); let app = Router::new() .merge(protected_routes) diff --git a/services/server/src/routes.rs b/services/server/src/routes.rs index 8b7b544b..ade981dc 100644 --- a/services/server/src/routes.rs +++ b/services/server/src/routes.rs @@ -1,4 +1,6 @@ use axum::{extract::State, Extension, Json}; +use axum::body::Body; +use axum::response::Response; use base64::Engine as _; use std::sync::Arc; @@ -949,3 +951,59 @@ pub async fn restore( owner: owner.clone(), })) } + +// ============================================================ +// Enoki Sponsor Proxy — forwards FE requests to internal sidecar +// ============================================================ + +/// POST /sponsor — proxy to sidecar POST /sponsor +pub async fn sponsor_proxy( + State(state): State>, + body: axum::body::Bytes, +) -> Result, AppError> { + let url = format!("{}/sponsor", state.config.sidecar_url); + let resp = state.http_client + .post(&url) + .header("Content-Type", "application/json") + .body(body.to_vec()) + .send() + .await + .map_err(|e| AppError::Internal(format!("Sponsor proxy failed: {}", e)))?; + + let status = axum::http::StatusCode::from_u16(resp.status().as_u16()) + .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR); + let resp_body = resp.bytes().await + .map_err(|e| AppError::Internal(format!("Sponsor proxy read failed: {}", e)))?; + + Ok(Response::builder() + .status(status) + .header("Content-Type", "application/json") + .body(Body::from(resp_body)) + .unwrap()) +} + +/// POST /sponsor/execute — proxy to sidecar POST /sponsor/execute +pub async fn sponsor_execute_proxy( + State(state): State>, + body: axum::body::Bytes, +) -> Result, AppError> { + let url = format!("{}/sponsor/execute", state.config.sidecar_url); + let resp = state.http_client + .post(&url) + .header("Content-Type", "application/json") + .body(body.to_vec()) + .send() + .await + .map_err(|e| AppError::Internal(format!("Sponsor execute proxy failed: {}", e)))?; + + let status = axum::http::StatusCode::from_u16(resp.status().as_u16()) + .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR); + let resp_body = resp.bytes().await + .map_err(|e| AppError::Internal(format!("Sponsor execute proxy read failed: {}", e)))?; + + Ok(Response::builder() + .status(status) + .header("Content-Type", "application/json") + .body(Body::from(resp_body)) + .unwrap()) +} From 15ed607b16677e99abff5a7890c3c95f789d139a Mon Sep 17 00:00:00 2001 From: ducnmm Date: Fri, 20 Mar 2026 15:00:31 +0700 Subject: [PATCH 05/34] feat: landing page buttons + fix build errors across apps --- apps/app/src/App.tsx | 12 +---- apps/app/src/index.css | 50 +++++++++++++++++++ apps/app/src/pages/Dashboard.tsx | 20 ++++++++ apps/app/src/pages/LandingPage.tsx | 20 ++++++-- apps/app/src/pages/Playground.tsx | 7 ++- apps/chatbot/app/(chat)/api/chat/route.ts | 4 +- apps/chatbot/app/(chat)/api/chat/schema.ts | 1 + apps/chatbot/components/artifact.tsx | 1 + apps/chatbot/components/chat.tsx | 19 +++++++ apps/chatbot/components/multimodal-input.tsx | 43 +++++++++++++++- apps/chatbot/lib/ai/providers.ts | 9 +++- apps/noter/app/api/chat/route.ts | 1 + apps/noter/app/api/memory/set-key/route.ts | 4 +- apps/noter/app/components/user-float.tsx | 25 ++++++++-- .../package/feature/note/lib/pdw-client.ts | 10 +++- apps/researcher/app/(auth)/login/page.tsx | 24 +++++++-- apps/researcher/app/(chat)/api/chat/route.ts | 2 + apps/researcher/app/api/auth/key/route.ts | 4 +- .../app/api/sprint/prepare/route.ts | 3 +- apps/researcher/app/api/sprint/save/route.ts | 2 + apps/researcher/lib/auth/session.ts | 8 +-- apps/researcher/lib/rag/tools/index.ts | 4 +- .../researcher/lib/rag/tools/recall-sprint.ts | 4 +- apps/researcher/lib/sprint/memwal.ts | 5 +- packages/sdk/src/account.ts | 4 +- packages/sdk/src/memwal.ts | 4 ++ packages/sdk/src/types.ts | 2 + scripts/test-namespace.ts | 1 + scripts/test-zero-restore.ts | 1 + 29 files changed, 252 insertions(+), 42 deletions(-) diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index e5c16123..498c156d 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -22,7 +22,7 @@ import LandingPage from './pages/LandingPage' import Dashboard from './pages/Dashboard' import SetupWizard from './pages/SetupWizard' import Playground from './pages/Playground' -import walrusIcon from './pages/walrus.jpg' + import '@mysten/dapp-kit/dist/index.css' @@ -137,16 +137,6 @@ function AppContent() { return ( <> - {/* Floating Docs Button - always visible */} - - Docs - {!currentAccount && } {currentAccount && !delegateKey && page !== 'playground' && } diff --git a/apps/app/src/index.css b/apps/app/src/index.css index 4c6e160f..4979794a 100644 --- a/apps/app/src/index.css +++ b/apps/app/src/index.css @@ -515,6 +515,18 @@ body { background: #101117; } +.lp-btn-docs { + background: transparent; + color: #191a23; + border: 2px solid #191a23; +} + +.lp-btn-docs:hover { + background: #191a23; + color: #f5f7fb; + transform: translateY(-1px); +} + .lp-connect-fallback { display: inline-flex; } @@ -523,6 +535,28 @@ body { text-transform: lowercase; } +.lp-cta-row { + display: flex; + align-items: stretch; + gap: 20px; +} + +.lp-cta-row > button.btn { + background: #191a23; + color: #f5f7fb; + border: 2px solid #191a23; + border-radius: 12px; + font-size: 0.94rem; + font-weight: 600; + padding: 8px 24px; + cursor: pointer; + text-transform: lowercase; + font-family: var(--font-sans); + line-height: 1.4; + box-sizing: border-box; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + .lp-illustration { position: relative; height: 320px; @@ -836,6 +870,22 @@ body { .lp-service-card--dark p { color: #f0f0f0; } + + .lp-btn-docs { + color: #f0f0f0; + border-color: #f0f0f0; + } + + .lp-btn-docs:hover { + background: #f0f0f0; + color: #191a23; + } + + .lp-cta-row > button.btn { + background: #444444; + color: #f0f0f0; + border-color: #555555; + } } /* ========== Dashboard ========== */ diff --git a/apps/app/src/pages/Dashboard.tsx b/apps/app/src/pages/Dashboard.tsx index 3fa05386..099d9f5a 100644 --- a/apps/app/src/pages/Dashboard.tsx +++ b/apps/app/src/pages/Dashboard.tsx @@ -201,6 +201,7 @@ export default function Dashboard() { const memwal = MemWal.create({ key: "${delegateKey?.slice(0, 8)}...${delegateKey?.slice(-8)}", + accountId: "${accountObjectId?.slice(0, 10)}...", serverUrl: "${config.memwalServerUrl}", }) @@ -217,6 +218,7 @@ import { openai } from "@ai-sdk/openai" const model = withMemWal(openai("gpt-4o"), { key: "${delegateKey?.slice(0, 8)}...${delegateKey?.slice(-8)}", + accountId: "${accountObjectId?.slice(0, 10)}...", serverUrl: "${config.memwalServerUrl}", }) @@ -276,6 +278,24 @@ const result = await generateText({
+ {/* Account ID */} + {accountObjectId && ( +
+
account ID
+
+ {accountObjectId} +
+
+ +
+
+ )} + {/* Public Key */}
public key
diff --git a/apps/app/src/pages/LandingPage.tsx b/apps/app/src/pages/LandingPage.tsx index b83baf6b..466e0a82 100644 --- a/apps/app/src/pages/LandingPage.tsx +++ b/apps/app/src/pages/LandingPage.tsx @@ -45,17 +45,29 @@ export default function LandingPage() {

{hasEnokiConfig && googleWallet ? ( -
+
+
) : ( -
- +
+ +
)} diff --git a/apps/app/src/pages/Playground.tsx b/apps/app/src/pages/Playground.tsx index b9054981..7c217b50 100644 --- a/apps/app/src/pages/Playground.tsx +++ b/apps/app/src/pages/Playground.tsx @@ -131,13 +131,14 @@ export default function Playground() { const [namespace, setNamespace] = useState('default') const memwal = useMemo(() => { - if (!delegateKey) return null + if (!delegateKey || !accountObjectId) return null return MemWal.create({ key: delegateKey, + accountId: accountObjectId, serverUrl, namespace: namespace || undefined, }) - }, [delegateKey, serverUrl, namespace]) + }, [delegateKey, accountObjectId, serverUrl, namespace]) // Step states @@ -487,6 +488,7 @@ export default function Playground() { const memwal = MemWal.create({ key: "${keyPreview}", + accountId: "${accountObjectId?.slice(0, 10)}...", serverUrl: "${serverUrl}", namespace: "${namespace || 'default'}", }) @@ -699,6 +701,7 @@ import { generateText } from "ai" // wrap your model with memwal — that's it const model = withMemWal(openai("gpt-4o-mini"), { key: delegateKeyHex, + accountId: "0x...", serverUrl: "${serverUrl}" }) diff --git a/apps/chatbot/app/(chat)/api/chat/route.ts b/apps/chatbot/app/(chat)/api/chat/route.ts index 971145db..b5115664 100644 --- a/apps/chatbot/app/(chat)/api/chat/route.ts +++ b/apps/chatbot/app/(chat)/api/chat/route.ts @@ -65,7 +65,7 @@ export async function POST(request: Request) { } try { - const { id, message, messages, selectedChatModel, selectedVisibilityType, useMemWal, memwalKey } = + const { id, message, messages, selectedChatModel, selectedVisibilityType, useMemWal, memwalKey, memwalAccountId } = requestBody; const [botResult, session] = await Promise.all([checkBotId(), auth()]); @@ -159,7 +159,7 @@ export async function POST(request: Request) { originalMessages: isToolApprovalFlow ? uiMessages : undefined, execute: async ({ writer: dataStream }) => { const result = streamText({ - model: useMemWal !== false ? getMemWalModel(selectedChatModel, memwalKey) : getLanguageModel(selectedChatModel), + model: useMemWal !== false ? getMemWalModel(selectedChatModel, memwalKey, memwalAccountId) : getLanguageModel(selectedChatModel), system: systemPrompt({ selectedChatModel, requestHints }), messages: modelMessages, stopWhen: stepCountIs(5), diff --git a/apps/chatbot/app/(chat)/api/chat/schema.ts b/apps/chatbot/app/(chat)/api/chat/schema.ts index e0fb0763..7b4bad2e 100644 --- a/apps/chatbot/app/(chat)/api/chat/schema.ts +++ b/apps/chatbot/app/(chat)/api/chat/schema.ts @@ -36,6 +36,7 @@ export const postRequestBodySchema = z.object({ selectedVisibilityType: z.enum(["public", "private"]), useMemWal: z.boolean().optional(), memwalKey: z.string().optional(), + memwalAccountId: z.string().optional(), }); export type PostRequestBody = z.infer; diff --git a/apps/chatbot/components/artifact.tsx b/apps/chatbot/components/artifact.tsx index 2c6b1d62..15a40e85 100644 --- a/apps/chatbot/components/artifact.tsx +++ b/apps/chatbot/components/artifact.tsx @@ -350,6 +350,7 @@ function PureArtifact({ stop={stop} useMemWal={true} memwalKey="" + memwalAccountId="" />
diff --git a/apps/chatbot/components/chat.tsx b/apps/chatbot/components/chat.tsx index 4b3efcd8..ea704003 100644 --- a/apps/chatbot/components/chat.tsx +++ b/apps/chatbot/components/chat.tsx @@ -92,9 +92,16 @@ export function Chat({ } return ''; }); + const [memwalAccountId, setMemwalAccountId] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('memwalAccountId') || ''; + } + return ''; + }); const currentModelIdRef = useRef(currentModelId); const useMemWalRef = useRef(useMemWal); const memwalKeyRef = useRef(memwalKey); + const memwalAccountIdRef = useRef(memwalAccountId); useEffect(() => { currentModelIdRef.current = currentModelId; @@ -114,6 +121,15 @@ export function Chat({ } }, [memwalKey]); + useEffect(() => { + memwalAccountIdRef.current = memwalAccountId; + if (memwalAccountId) { + localStorage.setItem('memwalAccountId', memwalAccountId); + } else { + localStorage.removeItem('memwalAccountId'); + } + }, [memwalAccountId]); + const { messages, setMessages, @@ -165,6 +181,7 @@ export function Chat({ selectedVisibilityType: visibilityType, useMemWal: useMemWalRef.current, memwalKey: memwalKeyRef.current || undefined, + memwalAccountId: memwalAccountIdRef.current || undefined, ...request.body, }, }; @@ -271,6 +288,8 @@ export function Chat({ onUseMemWalChange={setUseMemWal} memwalKey={memwalKey} onMemwalKeyChange={setMemwalKey} + memwalAccountId={memwalAccountId} + onMemwalAccountIdChange={setMemwalAccountId} /> )}
diff --git a/apps/chatbot/components/multimodal-input.tsx b/apps/chatbot/components/multimodal-input.tsx index 5d9de743..850b622d 100644 --- a/apps/chatbot/components/multimodal-input.tsx +++ b/apps/chatbot/components/multimodal-input.tsx @@ -72,6 +72,8 @@ function PureMultimodalInput({ onUseMemWalChange, memwalKey, onMemwalKeyChange, + memwalAccountId, + onMemwalAccountIdChange, }: { chatId: string; input: string; @@ -91,6 +93,8 @@ function PureMultimodalInput({ onUseMemWalChange?: (value: boolean) => void; memwalKey: string; onMemwalKeyChange?: (key: string) => void; + memwalAccountId: string; + onMemwalAccountIdChange?: (id: string) => void; }) { const textareaRef = useRef(null); const { width } = useWindowSize(); @@ -401,6 +405,8 @@ function PureMultimodalInput({ onUseMemWalChange={onUseMemWalChange} memwalKey={memwalKey} onMemwalKeyChange={onMemwalKeyChange} + memwalAccountId={memwalAccountId} + onMemwalAccountIdChange={onMemwalAccountIdChange} /> @@ -446,6 +452,9 @@ export const MultimodalInput = memo( if (prevProps.memwalKey !== nextProps.memwalKey) { return false; } + if (prevProps.memwalAccountId !== nextProps.memwalAccountId) { + return false; + } return true; } @@ -582,14 +591,19 @@ function PureMemWalButton({ onUseMemWalChange, memwalKey, onMemwalKeyChange, + memwalAccountId, + onMemwalAccountIdChange, }: { useMemWal: boolean; onUseMemWalChange?: (value: boolean) => void; memwalKey: string; onMemwalKeyChange?: (key: string) => void; + memwalAccountId: string; + onMemwalAccountIdChange?: (id: string) => void; }) { const [showKeyInput, setShowKeyInput] = useState(false); const [keyInput, setKeyInput] = useState(memwalKey); + const [accountIdInput, setAccountIdInput] = useState(memwalAccountId); const buttonRef = useRef(null); const dropdownRef = useRef(null); @@ -613,6 +627,10 @@ function PureMemWalButton({ setKeyInput(memwalKey); }, [memwalKey]); + useEffect(() => { + setAccountIdInput(memwalAccountId); + }, [memwalAccountId]); + const hasKey = !!memwalKey; return ( @@ -665,7 +683,7 @@ function PureMemWalButton({ memwal key (ed25519 private key hex) -
+
{ if (e.key === 'Enter') { onMemwalKeyChange?.(keyInput); + onMemwalAccountIdChange?.(accountIdInput); + setShowKeyInput(false); + } + }} + /> +
+
+ account ID (MemWalAccount object ID) +
+
+ setAccountIdInput(e.target.value)} + placeholder="0x..." + className="flex-1 rounded-md border border-border bg-muted px-2 py-1.5 text-xs font-mono outline-none focus:border-primary" + onKeyDown={(e) => { + if (e.key === 'Enter') { + onMemwalKeyChange?.(keyInput); + onMemwalAccountIdChange?.(accountIdInput); setShowKeyInput(false); } }} @@ -684,6 +722,7 @@ function PureMemWalButton({ variant="default" onClick={() => { onMemwalKeyChange?.(keyInput); + onMemwalAccountIdChange?.(accountIdInput); setShowKeyInput(false); }} > @@ -696,7 +735,9 @@ function PureMemWalButton({ className="mt-2 text-xs text-red-400 hover:text-red-300 transition-colors" onClick={() => { onMemwalKeyChange?.(''); + onMemwalAccountIdChange?.(''); setKeyInput(''); + setAccountIdInput(''); }} > remove key diff --git a/apps/chatbot/lib/ai/providers.ts b/apps/chatbot/lib/ai/providers.ts index b95646cf..226b7e03 100644 --- a/apps/chatbot/lib/ai/providers.ts +++ b/apps/chatbot/lib/ai/providers.ts @@ -73,19 +73,26 @@ export function getArtifactModel() { * Wrap a language model with MemWal memory layer. * Requires MEMWAL_KEY env var. Falls back to base model if not configured. */ -export function getMemWalModel(modelId: string, memwalKey?: string) { +export function getMemWalModel(modelId: string, memwalKey?: string, memwalAccountId?: string) { const baseModel = getLanguageModel(modelId); const key = memwalKey || process.env.MEMWAL_KEY; const memwalServerUrl = process.env.MEMWAL_SERVER_URL; + const accountId = memwalAccountId || process.env.MEMWAL_ACCOUNT_ID; if (!key) { console.warn("[MemWal] MEMWAL_KEY not set — memory layer disabled"); return baseModel; } + if (!accountId) { + console.warn("[MemWal] MEMWAL_ACCOUNT_ID not set — memory layer disabled"); + return baseModel; + } + return withMemWal(baseModel, { key, + accountId, serverUrl: memwalServerUrl || "http://localhost:8000", maxMemories: 5, autoSave: true, diff --git a/apps/noter/app/api/chat/route.ts b/apps/noter/app/api/chat/route.ts index fb11cab4..c812df36 100644 --- a/apps/noter/app/api/chat/route.ts +++ b/apps/noter/app/api/chat/route.ts @@ -28,6 +28,7 @@ function getModel(modelId: string) { return withMemWal(baseModel, { key: memwalKey, + accountId: process.env.MEMWAL_ACCOUNT_ID || "", serverUrl: process.env.MEMWAL_SERVER_URL || "http://localhost:8000", maxMemories: 5, autoSave: true, diff --git a/apps/noter/app/api/memory/set-key/route.ts b/apps/noter/app/api/memory/set-key/route.ts index 4631b70e..362fbcac 100644 --- a/apps/noter/app/api/memory/set-key/route.ts +++ b/apps/noter/app/api/memory/set-key/route.ts @@ -6,13 +6,13 @@ import { setMemWalKey } from "@/feature/note/lib/pdw-client"; export async function POST(req: Request) { try { - const { key } = await req.json(); + const { key, accountId } = await req.json(); if (typeof key !== "string") { return Response.json({ error: "key must be a string" }, { status: 400 }); } - setMemWalKey(key || null); + setMemWalKey(key || null, accountId || null); if (!key) { return Response.json({ status: "cleared" }); diff --git a/apps/noter/app/components/user-float.tsx b/apps/noter/app/components/user-float.tsx index 66aa56a1..cff12506 100644 --- a/apps/noter/app/components/user-float.tsx +++ b/apps/noter/app/components/user-float.tsx @@ -21,6 +21,7 @@ export function UserFloatPanel({ className, onClose }: UserFloatPanelProps) { const { user, suiAddress, logout } = useAuth(); const [copied, setCopied] = useState(false); const [memwalKey, setMemwalKey] = useState(""); + const [memwalAccountId, setMemwalAccountId] = useState(""); const [memwalStatus, setMemwalStatus] = useState<"idle" | "checking" | "connected" | "error">("idle"); // Always check MemWal status on mount (key may come from .env or localStorage) @@ -29,6 +30,10 @@ export function UserFloatPanel({ className, onClose }: UserFloatPanelProps) { if (savedKey) { setMemwalKey(savedKey); } + const savedAccountId = localStorage.getItem("memwal_account_id"); + if (savedAccountId) { + setMemwalAccountId(savedAccountId); + } // Always check health — server may have key from .env checkMemwalConnection(); }, []); @@ -47,13 +52,16 @@ export function UserFloatPanel({ className, onClose }: UserFloatPanelProps) { const handleSaveKey = async () => { if (!memwalKey.trim()) return; localStorage.setItem("memwal_key", memwalKey.trim()); + if (memwalAccountId.trim()) { + localStorage.setItem("memwal_account_id", memwalAccountId.trim()); + } // Save key to server-side via API try { const res = await fetch("/api/memory/set-key", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key: memwalKey.trim() }), + body: JSON.stringify({ key: memwalKey.trim(), accountId: memwalAccountId.trim() || undefined }), }); if (res.ok) { setMemwalStatus("connected"); @@ -67,12 +75,14 @@ export function UserFloatPanel({ className, onClose }: UserFloatPanelProps) { const handleClearKey = () => { setMemwalKey(""); + setMemwalAccountId(""); localStorage.removeItem("memwal_key"); + localStorage.removeItem("memwal_account_id"); setMemwalStatus("idle"); fetch("/api/memory/set-key", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key: "" }), + body: JSON.stringify({ key: "", accountId: "" }), }); }; @@ -177,7 +187,16 @@ export function UserFloatPanel({ className, onClose }: UserFloatPanelProps) { onChange={(e) => setMemwalKey(e.target.value)} className="flex-1 text-xs bg-secondary px-2 py-2 rounded border border-border outline-none focus:ring-1 focus:ring-ring font-mono" /> - {memwalKey ? ( +
+
+ setMemwalAccountId(e.target.value)} + className="flex-1 text-xs bg-secondary px-2 py-2 rounded border border-border outline-none focus:ring-1 focus:ring-ring font-mono" + /> + {(memwalKey || memwalAccountId) ? (
diff --git a/apps/noter/package/feature/note/lib/pdw-client.ts b/apps/noter/package/feature/note/lib/pdw-client.ts index 9fd5ab41..eae9ff2b 100644 --- a/apps/noter/package/feature/note/lib/pdw-client.ts +++ b/apps/noter/package/feature/note/lib/pdw-client.ts @@ -10,13 +10,15 @@ import { MemWal } from "@cmdoss/memwal"; let _memwal: MemWal | null = null; let _runtimeKey: string | null = null; +let _runtimeAccountId: string | null = null; /** * Set the MemWal key at runtime (from user input in profile panel). * Clears the existing client so it gets re-created with the new key. */ -export const setMemWalKey = (key: string | null) => { +export const setMemWalKey = (key: string | null, accountId?: string | null) => { _runtimeKey = key; + _runtimeAccountId = accountId ?? null; _memwal = null; // Force re-create on next call }; @@ -32,8 +34,14 @@ export const getMemWalClient = (): MemWal => { throw new Error("[MemWal] No key configured — set MEMWAL_KEY in Profile or .env"); } + const accountId = _runtimeAccountId || process.env.MEMWAL_ACCOUNT_ID; + if (!accountId) { + throw new Error("[MemWal] No accountId configured — set MEMWAL_ACCOUNT_ID in Profile or .env"); + } + _memwal = MemWal.create({ key, + accountId, serverUrl: process.env.MEMWAL_SERVER_URL || "http://localhost:8000", }); diff --git a/apps/researcher/app/(auth)/login/page.tsx b/apps/researcher/app/(auth)/login/page.tsx index c02ad295..b10b7804 100644 --- a/apps/researcher/app/(auth)/login/page.tsx +++ b/apps/researcher/app/(auth)/login/page.tsx @@ -11,6 +11,7 @@ import { Label } from "@/components/ui/label"; export default function Page() { const router = useRouter(); const [privateKey, setPrivateKey] = useState(""); + const [accountId, setAccountId] = useState(""); const [isLoading, setIsLoading] = useState(false); const [showKey, setShowKey] = useState(false); @@ -18,6 +19,11 @@ export default function Page() { e.preventDefault(); const trimmed = privateKey.trim(); + const trimmedAccountId = accountId.trim(); + if (!trimmedAccountId) { + toast({ type: "error", description: "Please enter your account ID." }); + return; + } if (!trimmed) { toast({ type: "error", description: "Please enter your private key." }); return; @@ -29,7 +35,7 @@ export default function Page() { const res = await fetch("/api/auth/key", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ privateKey: trimmed }), + body: JSON.stringify({ privateKey: trimmed, accountId: trimmedAccountId }), }); if (!res.ok) { @@ -77,11 +83,23 @@ export default function Page() { {/* Card */}
+
+ + setAccountId(e.target.value)} + placeholder="0x..." + required + value={accountId} + /> +
+
setPrivateKey(e.target.value)} @@ -107,7 +125,7 @@ export default function Page() { - -
- ) : ( -
- - -
- )} +
+ {hasEnokiConfig && googleWallet ? ( + + ) : ( + + )} +
+
+ - - + {/* ── Hero ── */} +
+
+
+

Privacy-Preserving
AI Memory

+

+ Store memories on Walrus, encrypt with SEAL, and recall with + semantic search. memwal gives agents long-term context while + users keep ownership. +

-
-
-
-

encrypted storage

-

SEAL encryption, persisted to Walrus blobs.

-
-
-

semantic recall

-

embedding search for relevant memories in milliseconds.

-
-
-

delegate keys

-

low-risk keys for apps, revocable anytime onchain.

-
-
-

AI middleware

-

wrap models with memory context using one SDK.

-
+
+ {hasEnokiConfig && googleWallet ? ( + + ) : ( + + )} + + View on GitHub +
-
- +
+
+ + MemWal mascot +
-
- + +
) } From 6161ead4eae6637378b177e75a3bcbf5f0d264cd Mon Sep 17 00:00:00 2001 From: ducnmm Date: Mon, 23 Mar 2026 21:23:37 +0700 Subject: [PATCH 24/34] ci: switch walrus deploy trigger to dev branch for testing --- .github/workflows/deploy-app-walrus.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-app-walrus.yml b/.github/workflows/deploy-app-walrus.yml index 0a4629f1..296845cc 100644 --- a/.github/workflows/deploy-app-walrus.yml +++ b/.github/workflows/deploy-app-walrus.yml @@ -3,7 +3,7 @@ name: Deploy App to Walrus Site on: push: branches: - - main + - dev paths: - 'apps/app/**' - '.github/workflows/deploy-app-walrus.yml' @@ -11,7 +11,6 @@ on: permissions: contents: write - pull-requests: write jobs: deploy-app: @@ -27,9 +26,11 @@ jobs: with: node-version: '20' + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install dependencies - working-directory: ./apps/app - run: npm ci + run: pnpm install --frozen-lockfile - name: Validate ws-resources.json exists run: | @@ -49,7 +50,7 @@ jobs: - name: Build app for Mainnet working-directory: ./apps/app run: | - npm run build + pnpm build if [ -f "ws-resources.json" ]; then cp ws-resources.json dist/ fi @@ -70,7 +71,7 @@ jobs: SUI_KEYSTORE: ${{ secrets.SUI_KEYSTORE }} DIST: ./apps/app/dist SUI_NETWORK: mainnet - EPOCHS: 3 + EPOCHS: ${{ vars.WALRUS_EPOCHS || '3' }} CHECK_EXTEND: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SITES_CONFIG: | From cec0f24bfbdce3a8efd46e753a72e1e1c8939a28 Mon Sep 17 00:00:00 2001 From: ducnmm Date: Tue, 24 Mar 2026 07:45:39 +0700 Subject: [PATCH 25/34] ci: switch walrus deploy trigger to main branch and update action --- .github/workflows/deploy-app-walrus.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-app-walrus.yml b/.github/workflows/deploy-app-walrus.yml index 296845cc..c1c9692f 100644 --- a/.github/workflows/deploy-app-walrus.yml +++ b/.github/workflows/deploy-app-walrus.yml @@ -3,7 +3,7 @@ name: Deploy App to Walrus Site on: push: branches: - - dev + - main paths: - 'apps/app/**' - '.github/workflows/deploy-app-walrus.yml' @@ -47,6 +47,9 @@ jobs: fi echo "ws-resources.json found!" + - name: Build SDK + run: pnpm build:sdk + - name: Build app for Mainnet working-directory: ./apps/app run: | @@ -65,7 +68,7 @@ jobs: - name: Deploy to Walrus Site (Mainnet) id: deploy-mainnet - uses: MystenLabs/walrus-sites/.github/actions/deploy@main + uses: MystenLabs/walrus-sites-github-actions/deploy@v1 with: SUI_ADDRESS: ${{ vars.SUI_ADDRESS }} SUI_KEYSTORE: ${{ secrets.SUI_KEYSTORE }} From 3f9049fd2e1f9091d7e954f8c79515432d0fe9a9 Mon Sep 17 00:00:00 2001 From: ducnmm Date: Tue, 24 Mar 2026 10:45:22 +0700 Subject: [PATCH 26/34] chore: reduce default Walrus storage epochs from 50 to 2 --- services/server/scripts/sidecar-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/server/scripts/sidecar-server.ts b/services/server/scripts/sidecar-server.ts index 18887d3f..b0fc727a 100644 --- a/services/server/scripts/sidecar-server.ts +++ b/services/server/scripts/sidecar-server.ts @@ -415,7 +415,7 @@ app.post("/seal/decrypt-batch", async (req, res) => { // ============================================================ app.post("/walrus/upload", async (req, res) => { try { - const { data, privateKey, owner, namespace, packageId, epochs = 50 } = req.body; + const { data, privateKey, owner, namespace, packageId, epochs = 2 } = req.body; if (!data || !privateKey) { return res.status(400).json({ error: "Missing required fields: data, privateKey" }); } From 32b88831c639505bb6e102554764d421ac8180dc Mon Sep 17 00:00:00 2001 From: ducnmm Date: Tue, 24 Mar 2026 11:35:54 +0700 Subject: [PATCH 27/34] feat: updating deployment environment variables. --- .github/workflows/deploy-app-walrus.yml | 4 +- .../feature/auth/ui/auth-button-group.tsx | 65 +++++-------------- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/.github/workflows/deploy-app-walrus.yml b/.github/workflows/deploy-app-walrus.yml index c1c9692f..ff5c5d3f 100644 --- a/.github/workflows/deploy-app-walrus.yml +++ b/.github/workflows/deploy-app-walrus.yml @@ -59,11 +59,10 @@ jobs: fi env: VITE_SUI_NETWORK: mainnet - VITE_ENOKI_API_KEY: ${{ secrets.VITE_ENOKI_API_KEY }} - VITE_GOOGLE_CLIENT_ID: ${{ vars.VITE_GOOGLE_CLIENT_ID }} VITE_MEMWAL_SERVER_URL: ${{ vars.VITE_MEMWAL_SERVER_URL }} VITE_MEMWAL_PACKAGE_ID: ${{ vars.VITE_MEMWAL_PACKAGE_ID }} VITE_MEMWAL_REGISTRY_ID: ${{ vars.VITE_MEMWAL_REGISTRY_ID }} + VITE_SEAL_KEY_SERVERS: ${{ vars.VITE_SEAL_KEY_SERVERS }} VITE_DOCS_URL: ${{ vars.VITE_DOCS_URL }} - name: Deploy to Walrus Site (Mainnet) @@ -75,7 +74,6 @@ jobs: DIST: ./apps/app/dist SUI_NETWORK: mainnet EPOCHS: ${{ vars.WALRUS_EPOCHS || '3' }} - CHECK_EXTEND: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SITES_CONFIG: | contexts: diff --git a/apps/noter/package/feature/auth/ui/auth-button-group.tsx b/apps/noter/package/feature/auth/ui/auth-button-group.tsx index d71a9831..113839b7 100644 --- a/apps/noter/package/feature/auth/ui/auth-button-group.tsx +++ b/apps/noter/package/feature/auth/ui/auth-button-group.tsx @@ -74,53 +74,24 @@ export function AuthButtonGroup() { return (
- {/* Primary Button - Continue with Google */} - - - {/* Dropdown Trigger */} - - - - - - - Other sign-in options - - - {/* Wallet Options */} - handleWalletSelect("slush")} - disabled={isWalletConnecting} - className="cursor-pointer" - > - {isWalletConnecting ? ( - - ) : ( - - )} - Slush Wallet - {!slushInstalled && ( - Install - )} - - - {/* Future: Add more OAuth providers here */} - {/* - - Facebook - */} - - + {/* Primary Button - Connect with Slush Wallet */} +
{/* Error Message */} From bdf9c6d3da5a91b8ff90f6373dce8fa4ce023171 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 24 Mar 2026 12:15:11 +0700 Subject: [PATCH 28/34] update landing page: reorganize nav and hero CTAs - Replace doc links in nav with single Playground CTA button - Move Documentation and GitHub buttons to hero section - Add GitHub icon via lucide-react - Add neo-brutalist hover animation to ConnectButton override - Add .lp-nav-github style for GitHub nav link --- apps/app/src/index.css | 22 ++++++++++++++++++++ apps/app/src/pages/LandingPage.tsx | 32 +++++++++++++----------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/app/src/index.css b/apps/app/src/index.css index 74f8657e..0168e853 100644 --- a/apps/app/src/index.css +++ b/apps/app/src/index.css @@ -514,6 +514,21 @@ h1, h2, h3 { opacity: 0.7; } +.lp-nav-github { + display: flex; + align-items: center; + gap: 6px; + color: #000000; + text-decoration: none; + font-size: 0.92rem; + font-weight: 500; + transition: opacity 0.2s; +} + +.lp-nav-github:hover { + opacity: 0.7; +} + .lp-nav-cta { background: #E8FF75; color: #000000; @@ -704,6 +719,13 @@ h1, h2, h3 { font-weight: 700 !important; font-family: var(--font-sans) !important; box-shadow: 3px 3px 0 #000000 !important; + transition: transform 0.15s, box-shadow 0.15s !important; +} + +.lp-nav-links [class*="ConnectButton"] button:hover, +.lp-nav-links button[data-dapp-kit]:hover { + transform: translate(-2px, -2px) !important; + box-shadow: 5px 5px 0 #000000 !important; } /* ── Responsive ── */ diff --git a/apps/app/src/pages/LandingPage.tsx b/apps/app/src/pages/LandingPage.tsx index 6fce9538..4a183337 100644 --- a/apps/app/src/pages/LandingPage.tsx +++ b/apps/app/src/pages/LandingPage.tsx @@ -9,6 +9,7 @@ import { useWallets, } from '@mysten/dapp-kit' import { isEnokiWallet, type EnokiWallet, type AuthProvider } from '@mysten/enoki' +import { Github } from 'lucide-react' import { config } from '../config' import memwalLogo from '../assets/memwal-logo.svg' @@ -46,20 +47,12 @@ export default function LandingPage() {
- {config.docsUrl && ( - <> - Guide - Examples - SDK - - )} - {hasEnokiConfig && googleWallet ? ( ) : ( - + )}
@@ -77,20 +70,23 @@ export default function LandingPage() {

- {hasEnokiConfig && googleWallet ? ( - - ) : ( - + {config.docsUrl && ( + + Documentation + )} - View on GitHub + GitHub
From 69e86f04bd27b86ae5c2ca499899e3189c3f8e63 Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 24 Mar 2026 12:19:35 +0700 Subject: [PATCH 29/34] switch to react-router-dom and put playground behind /playground - Replace hash-based routing with BrowserRouter pathname routing - Navigate to /playground after wallet connection - Unauthenticated users on non-root paths redirect to / --- apps/app/src/App.tsx | 50 +++++++++++++++--------------- apps/app/src/pages/LandingPage.tsx | 3 ++ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 498c156d..ebbcdf17 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -16,6 +16,7 @@ import { import { isEnokiNetwork, registerEnokiWallets } from '@mysten/enoki' import { getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom' import { config } from './config' import LandingPage from './pages/LandingPage' @@ -127,22 +128,19 @@ function RegisterEnokiWallets() { function AppContent() { const currentAccount = useCurrentAccount() const { delegateKey } = useDelegateKey() - const [page, setPage] = useState(() => window.location.hash.replace('#', '') || '') + const location = useLocation() - useEffect(() => { - const onHash = () => setPage(window.location.hash.replace('#', '') || '') - window.addEventListener('hashchange', onHash) - return () => window.removeEventListener('hashchange', onHash) - }, []) + if (!currentAccount) { + return location.pathname === '/' ? : + } return ( - <> - - {!currentAccount && } - {currentAccount && !delegateKey && page !== 'playground' && } - {currentAccount && page === 'playground' && } - {currentAccount && delegateKey && page !== 'playground' && } - + + } /> + : + } /> + ) } @@ -152,17 +150,19 @@ function AppContent() { export default function App() { return ( - - - - - -
- -
-
-
-
-
+ + + + + + +
+ +
+
+
+
+
+
) } diff --git a/apps/app/src/pages/LandingPage.tsx b/apps/app/src/pages/LandingPage.tsx index 4a183337..ca813e01 100644 --- a/apps/app/src/pages/LandingPage.tsx +++ b/apps/app/src/pages/LandingPage.tsx @@ -10,6 +10,7 @@ import { } from '@mysten/dapp-kit' import { isEnokiWallet, type EnokiWallet, type AuthProvider } from '@mysten/enoki' import { Github } from 'lucide-react' +import { useNavigate } from 'react-router-dom' import { config } from '../config' import memwalLogo from '../assets/memwal-logo.svg' @@ -26,6 +27,7 @@ export default function LandingPage() { ) const googleWallet = walletsByProvider.get('google') + const navigate = useNavigate() const hasEnokiConfig = config.enokiApiKey && config.googleClientId // If somehow already connected, this page shouldn't show @@ -34,6 +36,7 @@ export default function LandingPage() { const handleConnect = () => { if (hasEnokiConfig && googleWallet) { connect({ wallet: googleWallet }) + navigate('/playground') } } From 309381ea3a5851c82779433ea54133731a3cf724 Mon Sep 17 00:00:00 2001 From: Aaron1924 Date: Tue, 24 Mar 2026 12:19:56 +0700 Subject: [PATCH 30/34] docs: refresh onboarding and relayer docs --- README.md | 18 ++++++------ docs/docs.json | 2 +- docs/getting-started/quick-start.md | 9 ++++-- docs/getting-started/what-is-memwal.md | 6 ++-- docs/relayer/overview.md | 39 ++++++++++++++------------ docs/relayer/public-relayer.md | 16 ++++++----- docs/relayer/self-hosting.md | 17 +++++++---- packages/sdk/README.md | 14 ++++----- 8 files changed, 67 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 3b26f4af..bcce43d4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,9 @@ # MemWal -Privacy-first AI memory SDK and protocol stack for storing encrypted memories on Walrus and +Privacy-first AI memory layer for storing encrypted memories on Walrus and retrieving them with semantic search. -> MemWal is currently in beta. It is usable today, but the protocol, SDK, and operational -> surfaces may still evolve as we harden the system. The docs site is the primary source of -> truth for the supported integration path and current architecture, and contributions are very -> welcome as we improve the stack. +> MemWal is currently in beta and actively evolving. While fully usable today, we continue to refine the developer experience and operational guidance. We welcome feedback from early builders as we continue to improve the product. ## Install @@ -27,7 +24,8 @@ import { MemWal } from "@mysten/memwal"; const memwal = MemWal.create({ key: "your-delegate-key-hex", - serverUrl: "https://your-memwal-server.com", + accountId: "your-memwal-account-id", + serverUrl: "https://your-relayer-url.com", namespace: "demo", }); @@ -40,11 +38,11 @@ await memwal.restore("demo"); - Docs source of truth: `docs/` - Docs site entry points: - - [Overview](docs/about/what-is-memwal.md) - - [Build Your First Integration](docs/getting-started/for-developers.md) - - [SDK Overview](docs/sdk/overview.md) + - [What is MemWal?](docs/getting-started/what-is-memwal.md) + - [Quick Start](docs/getting-started/quick-start.md) + - [SDK Quick Start](docs/sdk/quick-start.md) - [Relayer Overview](docs/relayer/overview.md) - - [SDK API Reference](docs/reference/sdk-api.md) + - [SDK API Reference](docs/sdk/api-reference.md) ## Contributing diff --git a/docs/docs.json b/docs/docs.json index e4583cde..bf64dd91 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -17,7 +17,7 @@ "default": "system" }, "banner": { - "content": "**MemWal is in active beta.** APIs and features may evolve between releases.", + "content": "MemWal is in beta with ongoing improvements to APIs and features based on developer feedback.", "dismissible": false }, "navbar": { diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index eaf4a050..1527227a 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -8,7 +8,8 @@ The fastest way to get MemWal running is through the TypeScript SDK. - [Node.js](https://nodejs.org/) v18+ or [Bun](https://bun.sh/) v1+ - A delegate key (Ed25519 private key in hex) -- A relayer URL — use the [public relayer](/relayer/public-relayer) to get started +- A MemWalAccount object ID on Sui +- A relayer URL - use a public relayer if one is available to you, or your own local or self-hosted relayer ## Quick Start @@ -95,7 +96,7 @@ The fastest way to get MemWal running is through the TypeScript SDK. ### Configure the SDK - Set up the SDK with your delegate key and relayer URL: + Set up the SDK with your delegate key, account ID, and relayer URL: ```ts import { MemWal } from "@mysten/memwal"; @@ -107,6 +108,8 @@ The fastest way to get MemWal running is through the TypeScript SDK. namespace: "my-app", }); ``` + + If you are self-hosting the relayer and do not have `MEMWAL_ACCOUNT_ID` yet, see [Self-Hosting](/relayer/self-hosting) for the account-creation and delegate-key setup flow after the relayer starts. @@ -129,6 +132,6 @@ The fastest way to get MemWal running is through the TypeScript SDK. console.log(result.results); ``` - That's it — you're up and running. + That's it - you're up and running. diff --git a/docs/getting-started/what-is-memwal.md b/docs/getting-started/what-is-memwal.md index 84668573..15454af2 100644 --- a/docs/getting-started/what-is-memwal.md +++ b/docs/getting-started/what-is-memwal.md @@ -1,9 +1,9 @@ --- title: "What is MemWal?" -description: "Privacy-preserving, decentralized memory protocol for humans and AI agents — powered by Walrus and Sui." +description: "Privacy-preserving, decentralized memory layer for humans and AI agents — powered by Walrus and Sui." --- -MemWal is a privacy-preserving, decentralized memory protocol for humans and AI agents — powered by Walrus and Sui. Store, recall, and share memory across apps, sessions, and agents. +MemWal is a privacy-preserving, decentralized memory layer for humans and AI agents — powered by Walrus and Sui. Store, recall, and share memory across apps, sessions, and agents. @@ -86,7 +86,7 @@ AI agents today lose context between sessions — every conversation starts from ## Use Cases -MemWal fits any app or protocol that needs to store, retrieve, and update memory persistently: +MemWal fits any app that needs to store, retrieve, and update memory persistently: - **AI chat apps** — capture valuable knowledge from conversations so agents remember context across sessions - **Note-taking and knowledge tools** — save user insights, summaries, and references as persistent, encrypted memory diff --git a/docs/relayer/overview.md b/docs/relayer/overview.md index 54bb37d9..00ab16f7 100644 --- a/docs/relayer/overview.md +++ b/docs/relayer/overview.md @@ -19,24 +19,27 @@ The relayer is the backend that turns SDK calls into memory operations. Using a The relayer is a Rust service (Axum) that manages a TypeScript sidecar process for SEAL and Walrus operations that require the `@mysten/seal` and `@mysten/walrus` SDKs. -``` -┌──────────────────────────────────┐ -│ Rust Relayer (Axum) │ -│ ┌────────────┐ ┌─────────────┐ │ ┌──────────────┐ -│ │ Auth │ │ Routes │ │────▶│ PostgreSQL │ -│ │ Middleware │ │ (remember, │ │ │ + pgvector │ -│ │ (Ed25519) │ │ recall,...)│ │ └──────────────┘ -│ └────────────┘ └─────┬───────┘ │ -│ │ │ ┌──────────────┐ -│ ┌────────▼───────┐ │────▶│ Walrus │ -│ │ TS Sidecar │ │ │ (download) │ -│ │ (localhost:9000)│ │ └──────────────┘ -│ │ SEAL encrypt/ │ │ -│ │ decrypt, Walrus│ │ ┌──────────────┐ -│ │ upload, blob │ │────▶│ Sui RPC │ -│ │ query │ │ │ (auth verify) │ -│ └────────────────┘ │ └──────────────┘ -└──────────────────────────────────┘ +```mermaid +flowchart LR + Client["SDK / App"] + subgraph Host["Relayer Host"] + direction LR + Axum["Rust Relayer (Axum)
Auth + routes"] + Sidecar["TypeScript Sidecar
SEAL + Walrus"] + end + DB["PostgreSQL + pgvector"] + Sui["Sui RPC"] + AI["Embedding / LLM API"] + Seal["SEAL key servers"] + Walrus["Walrus"] + + Client --> Axum + Axum --> DB + Axum --> Sui + Axum --> AI + Axum --> Sidecar + Sidecar --> Seal + Sidecar --> Walrus ``` The sidecar is started automatically when the Rust server boots and communicates over HTTP on `localhost:9000` (configurable via `SIDECAR_URL`). If the sidecar fails to start, the relayer exits immediately. diff --git a/docs/relayer/public-relayer.md b/docs/relayer/public-relayer.md index 206213d4..5d02bb50 100644 --- a/docs/relayer/public-relayer.md +++ b/docs/relayer/public-relayer.md @@ -2,14 +2,16 @@ title: "Public Relayer" --- -The public relayer is a MemWal instance hosted and operated by the Mysten team. It's the fastest way to start building — no infrastructure to set up. +The public relayer is a managed MemWal deployment for teams that want to get started without running infrastructure. If a public relayer endpoint is available for your environment, it gives you the fastest path to integration. ## Endpoints | | Value | |---|---| -| **Relayer URL** | `TBD` | -| **Package ID** | `TBD` | +| **Relayer URL** | Provided by the relayer operator | +| **Package ID** | Provided by the relayer operator | + +If you do not have these values yet, use a local or [self-hosted relayer](/relayer/self-hosting) instead. ## Minimal Config @@ -26,9 +28,9 @@ const memwal = MemWal.create({ ## What to Know -- **Shared App ID** — all users of the public relayer share the same MemWal package ID. Your data is isolated by your own `owner + namespace` (Memory Space), but the underlying deployment is shared. -- **Trust assumption** — the relayer sees plaintext during encryption and embedding. By using the public relayer, you're trusting the Mysten-hosted instance with that data. See [Trust & Security Model](/fundamentals/architecture/data-flow-security-model) for details. -- **Availability** — the public relayer is a managed beta service. There are no SLA guarantees. -- **Storage costs** — the server wallet covers Walrus storage fees. Usage limits may apply during beta. +- **Shared App ID** - all users of the public relayer share the same MemWal package ID. Your data is isolated by your own `owner + namespace` (Memory Space), but the underlying deployment is shared. +- **Trust assumption** - the relayer sees plaintext during encryption and embedding. By using the public relayer, you're trusting the Mysten-hosted instance with that data. See [Trust & Security Model](/fundamentals/architecture/data-flow-security-model) for details. +- **Availability** - the public relayer is a managed beta service. There are no SLA guarantees. +- **Storage costs** - the server wallet covers Walrus storage fees. Usage limits may apply during beta. If you need full control over the trust boundary or your own dedicated instance, see [Self-Hosting](/relayer/self-hosting). diff --git a/docs/relayer/self-hosting.md b/docs/relayer/self-hosting.md index 77114c03..04abfd03 100644 --- a/docs/relayer/self-hosting.md +++ b/docs/relayer/self-hosting.md @@ -4,7 +4,7 @@ title: "Self-Hosting" Self-hosting means running your own relayer — either pointing at an existing MemWal package ID or deploying an entirely new MemWal instance with your own contract, database, and server wallet. -The public relayer provided by Mysten is a reference implementation. You can also build your own implementation that fits the protocol interface with custom logic. This guide covers how to run the reference implementation as your own self-hosted relayer. +The public relayer provided by Mysten is a reference implementation. You can also build your own implementation that fits the same API surface with custom logic. This guide covers how to run the reference implementation as your own self-hosted relayer. ## When to Self-Host @@ -30,13 +30,20 @@ The Rust relayer starts the TypeScript sidecar as a child process on boot. They ## Quick Start -If you already have PostgreSQL + pgvector running: +If you do not already have PostgreSQL + pgvector running, start it with: + +```bash +docker compose -f services/server/docker-compose.yml up -d postgres +``` + +Then run the relayer: ```bash cp services/server/.env.example services/server/.env -cd services/server/scripts && npm ci -cd ../ && cargo run -cd ../indexer && cargo run +cd services/server/scripts +npm ci +cd .. +cargo run ``` Then check: diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 4a19ae5b..e932990a 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,11 +1,8 @@ # @mysten/memwal -Privacy-first AI memory SDK. Stores encrypted memories on Walrus (decentralized storage) and -retrieves them via semantic search. +Privacy-first AI memory SDK for storing encrypted memories on Walrus and retrieving them with semantic search. -> MemWal is currently in beta. It works today, but rough edges and operational guidance may still -> evolve. Feedback and contributions are welcome while we harden the protocol and developer -> experience. +> MemWal is currently in beta and actively evolving. While fully usable today, we continue to refine the developer experience and operational guidance. We welcome feedback from early builders as we continue to improve the product. ## Install @@ -26,7 +23,8 @@ import { MemWal } from "@mysten/memwal"; const memwal = MemWal.create({ key: "your-delegate-key-hex", - serverUrl: "https://your-memwal-server.com", + accountId: "your-memwal-account-id", + serverUrl: "https://your-relayer-url.com", namespace: "demo", }); @@ -35,6 +33,8 @@ const memories = await memwal.recall("What are the user's preferences?"); await memwal.restore("demo"); ``` +If you are self-hosting the relayer and do not have an account ID yet, see [Self-Hosting](../../docs/relayer/self-hosting.md) for the account creation and delegate key setup flow. + ## Exports | Entry | Description | @@ -52,4 +52,4 @@ await memwal.restore("demo"); ## License -MIT +Apache 2.0 From 7a60bf77d5fd4c5f2ea88fea1e2d7bbedf184f3a Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 24 Mar 2026 13:08:33 +0700 Subject: [PATCH 31/34] restyle playground, dashboard & setup wizard to match landing page - Remove all dark mode CSS (light-only theme) - Apply violet-to-yellow gradient background across all pages - Update nav, cards, buttons, inputs, code blocks to use brand palette - Add lucide-react icons to all action buttons (copy, remove, reveal, etc.) - Use yellow CTA for primary actions (sign out, add key, generate key) - Use red btn-danger for destructive actions (remove key) - Replace hash-based navigation with react-router Link components - Update SetupWizard done text to reference playground --- apps/app/src/App.tsx | 6 +- apps/app/src/index.css | 322 +++++------------------------ apps/app/src/pages/Dashboard.tsx | 42 ++-- apps/app/src/pages/Playground.tsx | 20 +- apps/app/src/pages/SetupWizard.tsx | 15 +- 5 files changed, 88 insertions(+), 317 deletions(-) diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index ebbcdf17..88ca1dd0 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -136,9 +136,11 @@ function AppContent() { return ( - } /> + : + } /> : + delegateKey ? : } /> ) diff --git a/apps/app/src/index.css b/apps/app/src/index.css index 0168e853..98e602c6 100644 --- a/apps/app/src/index.css +++ b/apps/app/src/index.css @@ -63,49 +63,6 @@ --nav-bg: #F0EDE8; } -@media (prefers-color-scheme: dark) { - :root { - --bg-primary: #0a0a0a; - --bg-secondary: #141414; - --bg-card: #1a1a1a; - --bg-card-hover: #242424; - --bg-input: #141414; - - - - --text-primary: #FAF8F5; - --text-secondary: #b0b0bc; - --text-muted: #70707e; - - --accent: #FAF8F5; - --accent-hover: #ffffff; - --accent-glow: rgba(255, 255, 255, 0.06); - --accent-subtle: rgba(255, 255, 255, 0.04); - - --success: #4ade80; - --success-border: #22c55e; - --warning: #fbbf24; - --warning-border: #f59e0b; - --danger: #f87171; - --danger-border: #ef4444; - - --border: 1px solid rgba(255, 255, 255, 0.10); - --border-accent: 1px solid rgba(255, 255, 255, 0.18); - --border-light: 1px solid rgba(255, 255, 255, 0.06); - - --neo-shadow: 6px 6px 0 #888888; - --neo-shadow-sm: 5px 5px 0 #888888; - --neo-shadow-xs: 3px 3px 0 #888888; - - --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.5); - --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.6); - --shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.7); - --shadow-glow: 0 0 20px rgba(255, 255, 255, 0.05); - - --nav-bg: #0f0f0f; - } -} - * { margin: 0; padding: 0; @@ -142,6 +99,8 @@ h1, h2, h3 { display: flex; flex-direction: column; position: relative; + background: linear-gradient(180deg, #CAB1FF 0%, #E8FF75 100%); + min-height: 100vh; } /* Floating Docs Button */ @@ -183,17 +142,6 @@ h1, h2, h3 { box-shadow: 3px 3px 0 #000000; } -/* Dark mode */ -@media (prefers-color-scheme: dark) { - .floating-docs-btn { - background: #000000; - box-shadow: 4px 4px 0 rgba(255, 255, 255, 0.15); - } - .floating-docs-btn:hover { - box-shadow: 6px 6px 0 rgba(255, 255, 255, 0.2); - } -} - .container { max-width: 960px; margin: 0 auto; @@ -207,18 +155,22 @@ h1, h2, h3 { position: sticky; top: 0; z-index: 50; - background: var(--nav-bg); - border-bottom: 1px solid var(--border); - padding: 0 24px; + background: #CAB1FF; + border-bottom: none; + padding: 0 48px; + height: 72px; + display: flex; + align-items: center; } .nav-inner { - max-width: 960px; + max-width: 1280px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; - height: 64px; + height: 72px; + width: 100%; } .nav-brand { @@ -242,12 +194,12 @@ h1, h2, h3 { .nav-address { font-size: 0.8rem; - color: var(--text-secondary); + color: #000000; font-family: var(--font-mono); - background: var(--bg-card); + background: rgba(0, 0, 0, 0.06); padding: 6px 12px; border-radius: 999px; - border: 1px solid var(--border); + border: none; } /* ========== Buttons ========== */ @@ -274,15 +226,14 @@ h1, h2, h3 { .btn-primary { background: #000000; - color: var(--color-yellow); + color: #E8FF75; border: 2px solid #000000; - box-shadow: 3px 3px 0 var(--color-violet); + box-shadow: 3px 3px 0 #CAB1FF; } .btn-primary:hover:not(:disabled) { - background: #101117; - box-shadow: none; transform: translate(3px, 3px); + box-shadow: none; } .btn-primary:disabled { @@ -293,16 +244,15 @@ h1, h2, h3 { } .btn-secondary { - background: var(--color-violet); + background: #CAB1FF; color: #000000; - border: var(--border); - box-shadow: var(--neo-shadow-xs); + border: 2px solid #000000; + box-shadow: 2px 2px 0 #000000; } .btn-secondary:hover:not(:disabled) { - background: var(--bg-card-hover); - box-shadow: none; transform: translate(2px, 2px); + box-shadow: none; } .btn-danger { @@ -355,11 +305,11 @@ h1, h2, h3 { /* ========== Cards ========== */ .card { - background: var(--bg-card); - border: var(--border); - border-radius: var(--radius-md); + background: #FAF8F5; + border: 2px solid #000000; + border-radius: 12px; padding: 24px; - box-shadow: var(--neo-shadow); + box-shadow: 4px 4px 0 #000000; } .card:hover { @@ -789,31 +739,9 @@ h1, h2, h3 { .dashboard { padding: 40px 0; -} - -.dashboard > .card { - background: var(--color-yellow-soft); -} - -@media (prefers-color-scheme: dark) { - .dashboard > .card { - background: #C0D85C; - color: #000000; - } - - .dashboard > .card .card { - background: rgba(0, 0, 0, 0.08); - border-color: rgba(0, 0, 0, 0.15); - } - - .dashboard > .card .card-label { - color: #000000; - } - - .dashboard > .card .card-title, - .dashboard > .card .card-subtitle { - color: #000000; - } + max-width: 960px; + margin: 0 auto; + padding: 40px 24px; } .dashboard-header { @@ -822,13 +750,14 @@ h1, h2, h3 { .dashboard-header h2 { font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.02em; + font-weight: 800; + letter-spacing: -0.03em; margin-bottom: 6px; + color: #000000; } .dashboard-header p { - color: var(--text-secondary); + color: #2d2d3a; font-size: 0.9rem; } @@ -1066,19 +995,19 @@ h1, h2, h3 { .input { width: 100%; padding: 10px 14px; - background: var(--bg-primary); - border: var(--border); - border-radius: var(--radius-sm); - color: var(--text-primary); + background: #FAF8F5; + border: 2px solid #000000; + border-radius: 10px; + color: #000000; font-family: var(--font-sans); font-size: 0.875rem; - box-shadow: var(--neo-shadow-xs); + box-shadow: 2px 2px 0 #000000; transition: box-shadow 0.15s ease; } .input:focus { outline: none; - box-shadow: var(--neo-shadow-xs); + box-shadow: 2px 2px 0 #000000; } /* ========== Spinner ========== */ @@ -1174,45 +1103,17 @@ h1, h2, h3 { color: var(--text-primary); } -@media (prefers-color-scheme: dark) { - .demo-step-badge { - background: var(--bg-card); - border: var(--border-accent); - color: var(--text-primary); - } - - .demo-step-badge--highlight { - background: var(--color-violet); - color: #000000; - border: none; - } - - .demo-step .card { - background: #1a1a1a; - color: #FAF8F5; - } - - .demo-step h3 { - color: #000000; - font-weight: 800; - } - - .demo-step-subtitle { - color: #333333; - } -} - .demo-code-block { - background: #ffffff; - border: var(--border); - border-radius: var(--radius-md); + background: #FAF8F5; + border: 2px solid #000000; + border-radius: 12px; padding: 16px; overflow: auto; font-size: 0.78rem; line-height: 1.7; font-family: var(--font-mono); - color: var(--text-secondary); - box-shadow: var(--neo-shadow-xs); + color: #2d2d3a; + box-shadow: 2px 2px 0 #000000; } .install-command { @@ -1327,14 +1228,14 @@ h1, h2, h3 { } .demo-server-tag { - background: var(--color-violet); - border: var(--border); - border-radius: var(--radius-sm); + background: #E8FF75; + border: 2px solid #000000; + border-radius: 10px; padding: 8px 16px; font-size: 0.8rem; font-family: var(--font-mono); color: #000000; - box-shadow: var(--neo-shadow-xs); + box-shadow: 2px 2px 0 #000000; } .demo-tag-value { @@ -1368,135 +1269,6 @@ h1, h2, h3 { text-decoration: none; } -@media (prefers-color-scheme: dark) { - .install-command { - color: var(--text-primary); - font-weight: 700; - } - - .key-display .key-label { - color: var(--text-muted); - } - - .key-display { - background: var(--bg-card); - border: var(--border-accent); - } - - .key-display--white { - background: var(--bg-card); - border: var(--border-accent); - } - - .demo-code-block { - background: #141418; - border: var(--border); - color: var(--text-secondary); - } - - .demo-result-panel { - background: #162e1e; - border-color: rgba(34, 197, 94, 0.4); - box-shadow: 3px 3px 0 rgba(34, 197, 94, 0.25); - } - - .demo-error-panel { - background: #2e1616; - border-color: rgba(239, 68, 68, 0.4); - box-shadow: 3px 3px 0 rgba(239, 68, 68, 0.25); - } - - .dashboard-cta-arrow { - color: #000000; - } - - .warning-box { - background: rgba(251, 191, 36, 0.08); - border-color: rgba(245, 158, 11, 0.5); - } - - .nav { - border-bottom: 1px solid rgba(255, 255, 255, 0.07); - } - - .demo-info-label { - color: var(--text-muted); - } - - .demo-tag-value--server { - color: #555555; - } - - .demo-tag-value--sdk { - color: #555555; - } - - .demo-tag-value--key { - color: #555555; - } - - /* Cards in dark: neo-brutalist with black offset shadow */ - .card { - border: var(--border); - box-shadow: var(--neo-shadow); - } - - .stat-card { - border: var(--border); - box-shadow: var(--neo-shadow); - } - - .dashboard-cta { - background: var(--color-violet); - border-color: var(--color-violet); - } - - .dashboard-cta-title { - color: #000000; - } - - .dashboard-cta-subtitle { - color: rgba(0, 0, 0, 0.65); - } - - .demo-server-tag { - background: var(--color-violet); - color: #000000; - border: var(--border); - box-shadow: 3px 3px 0 #888888; - } - - .btn-secondary { - border: var(--border-accent); - background: var(--color-violet); - color: #000000; - } - - .btn-primary { - background: var(--color-violet); - color: #000000; - border: 2px solid #000000; - box-shadow: var(--neo-shadow-sm); - } - - .btn-primary:hover:not(:disabled) { - background: var(--color-yellow); - transform: translate(5px, 5px); - box-shadow: none; - } - - .nav-address { - background: var(--bg-card); - border: var(--border); - } - - .modal { - background: var(--bg-card); - border: var(--border-accent); - box-shadow: 0 24px 60px rgba(0,0,0,0.7); - } -} - /* ========== Responsive ========== */ @media (max-width: 640px) { diff --git a/apps/app/src/pages/Dashboard.tsx b/apps/app/src/pages/Dashboard.tsx index 3ac13705..52fd2884 100644 --- a/apps/app/src/pages/Dashboard.tsx +++ b/apps/app/src/pages/Dashboard.tsx @@ -12,6 +12,8 @@ import { import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' import { generateDelegateKey, addDelegateKey, removeDelegateKey } from '@mysten/memwal/account' import type { WalletSigner } from '@mysten/memwal/manual' +import { Link } from 'react-router-dom' +import { Copy, Eye, EyeOff, Trash2, RefreshCw, Plus, LogOut } from 'lucide-react' import { useDelegateKey } from '../App' import { config } from '../config' import memwalLogo from '../assets/memwal-logo.svg' @@ -242,8 +244,8 @@ const result = await generateText({ {address.slice(0, 6)}...{address.slice(-4)} - @@ -257,7 +259,7 @@ const result = await generateText({ {/* Try Demo CTA */} - +
try interactive demo @@ -267,7 +269,7 @@ const result = await generateText({
-
+ {/* Current Delegate Key */} @@ -291,7 +293,7 @@ const result = await generateText({ className="btn btn-secondary btn-sm" onClick={() => copyToClipboard(accountObjectId, 'acct')} > - {copied === 'acct' ? 'copied!' : 'copy'} + {copied === 'acct' ? 'copied!' : 'copy'} @@ -308,7 +310,7 @@ const result = await generateText({ className="btn btn-secondary btn-sm" onClick={() => copyToClipboard(delegatePublicKey!, 'pub')} > - {copied === 'pub' ? 'copied!' : 'copy'} + {copied === 'pub' ? 'copied!' : 'copy'} @@ -324,10 +326,10 @@ const result = await generateText({ className="btn btn-secondary btn-sm" onClick={() => copyToClipboard(delegateKey!, 'priv')} > - {copied === 'priv' ? 'copied!' : 'copy'} + {copied === 'priv' ? 'copied!' : 'copy'} @@ -338,7 +340,7 @@ const result = await generateText({
@@ -361,14 +363,14 @@ const result = await generateText({ onClick={fetchOnChainKeys} disabled={loadingKeys} > - {loadingKeys ? '...' : 'refresh'} + {loadingKeys ? '...' : 'refresh'} @@ -403,7 +405,7 @@ const result = await generateText({ className="btn btn-secondary btn-sm" onClick={() => copyToClipboard(newPrivateKey, 'new-priv')} > - {copied === 'new-priv' ? 'copied!' : 'copy'} + {copied === 'new-priv' ? 'copied!' : 'copy'} @@ -531,10 +533,10 @@ const result = await generateText({
                             {sdkSnippet}
@@ -553,10 +555,10 @@ const result = await generateText({
                     
                             {aiSnippet}
diff --git a/apps/app/src/pages/Playground.tsx b/apps/app/src/pages/Playground.tsx
index 842115e6..bbbbc709 100644
--- a/apps/app/src/pages/Playground.tsx
+++ b/apps/app/src/pages/Playground.tsx
@@ -6,6 +6,8 @@
  */
 
 import { useState, useCallback, useMemo, type ReactNode } from 'react'
+import { Link } from 'react-router-dom'
+import { LogOut } from 'lucide-react'
 import {
     useCurrentAccount,
     useDisconnectWallet,
@@ -422,25 +424,17 @@ export default function Playground() {
                         MemWal
                     
- { - e.preventDefault() - window.location.hash = '' - window.scrollTo({ top: 0, behavior: 'smooth' }) - }} - > - ← dashboard - + + ← Dashboard + {address.slice(0, 6)}...{address.slice(-4)}
diff --git a/apps/app/src/pages/SetupWizard.tsx b/apps/app/src/pages/SetupWizard.tsx index 1a615ea4..64eaaf5f 100644 --- a/apps/app/src/pages/SetupWizard.tsx +++ b/apps/app/src/pages/SetupWizard.tsx @@ -17,6 +17,7 @@ import { import { Transaction } from '@mysten/sui/transactions' import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' import { useDelegateKey } from '../App' +import { LogOut, Copy } from 'lucide-react' import { config } from '../config' import memwalLogo from '../assets/memwal-logo.svg' @@ -212,8 +213,8 @@ export default function SetupWizard() { {address.slice(0, 6)}...{address.slice(-4)} - @@ -262,7 +263,7 @@ export default function SetupWizard() { - @@ -298,7 +299,7 @@ export default function SetupWizard() {
{privateKeyHex}
@@ -342,8 +343,8 @@ export default function SetupWizard() { -
-                            {sdkSnippet}
-                        
+ + {sdkSnippet} + @@ -560,9 +583,9 @@ const result = await generateText({ > {copied === 'ai' ? 'done' : 'copy'} -
-                            {aiSnippet}
-                        
+ + {aiSnippet} + @@ -571,9 +594,23 @@ const result = await generateText({
install
-
-                        npm install @mysten/memwal
-                    
+
+ {(['npm', 'pnpm', 'yarn', 'bun'] as const).map((pm) => ( + + ))} +
+ + {pkgManager === 'npm' ? 'npm install @mysten/memwal' : + pkgManager === 'pnpm' ? 'pnpm add @mysten/memwal' : + pkgManager === 'yarn' ? 'yarn add @mysten/memwal' : + 'bun add @mysten/memwal'} + diff --git a/apps/app/src/pages/LandingPage.tsx b/apps/app/src/pages/LandingPage.tsx index ca813e01..2b9b6296 100644 --- a/apps/app/src/pages/LandingPage.tsx +++ b/apps/app/src/pages/LandingPage.tsx @@ -30,13 +30,12 @@ export default function LandingPage() { const navigate = useNavigate() const hasEnokiConfig = config.enokiApiKey && config.googleClientId - // If somehow already connected, this page shouldn't show - if (currentAccount) return null - const handleConnect = () => { - if (hasEnokiConfig && googleWallet) { + if (currentAccount) { + navigate('/dashboard') + } else if (hasEnokiConfig && googleWallet) { connect({ wallet: googleWallet }) - navigate('/playground') + navigate('/dashboard') } } @@ -50,7 +49,11 @@ export default function LandingPage() {
- {hasEnokiConfig && googleWallet ? ( + {currentAccount ? ( + + ) : hasEnokiConfig && googleWallet ? ( diff --git a/apps/app/src/pages/Playground.tsx b/apps/app/src/pages/Playground.tsx index fbaf562d..852bad78 100644 --- a/apps/app/src/pages/Playground.tsx +++ b/apps/app/src/pages/Playground.tsx @@ -8,6 +8,11 @@ import { useState, useCallback, useMemo, type ReactNode } from 'react' import { Link } from 'react-router-dom' import { LogOut } from 'lucide-react' +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter' +import js from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript' +import { githubGist } from 'react-syntax-highlighter/dist/esm/styles/hljs' + +SyntaxHighlighter.registerLanguage('javascript', js) import { useCurrentAccount, useDisconnectWallet, @@ -84,9 +89,16 @@ function DemoStep({ {children} {/* Code block */} -
-                {code}
-            
+
+ + {code} + +
{/* Success result */} {result && ( @@ -420,9 +432,9 @@ export default function Playground() { <>
-
-                        {`// memwal doesn't include an LLM — you choose your own.
+                    
+{`// memwal doesn't include an LLM — you choose your own.
 // steps 7–9 use this key for:
 //   • ask AI: recalls memories → injects into your LLM prompt
 //   • full client-side: embeds text via your OpenAI / OpenRouter key
 //
-// your key is never sent to memwal servers.`}
-                    
+// your key is never sent to memwal servers.`} + {/* Step 6: Ask AI — true middleware pattern */} @@ -688,8 +703,9 @@ const result = await memwal.restore("${namespace || 'default'}") /> -
-                        {`import { withMemWal } from "@mysten/memwal/ai"
+                    
+ +{`import { withMemWal } from "@mysten/memwal/ai" import { openai } from "@ai-sdk/openai" import { generateText } from "ai" @@ -705,8 +721,9 @@ const { text } = await generateText({ model, prompt: "${askQuestion.slice(0, 50)}" }) -// → AI answers using your encrypted memories as context`} -
+// → AI answers using your encrypted memories as context`} + + {/* Loading phase */} {askPhase && ( @@ -818,8 +835,9 @@ const { text } = await generateText({ /> -
-                        {`import { MemWalManual } from "@mysten/memwal/manual"
+                    
+ +{`import { MemWalManual } from "@mysten/memwal/manual" const memwal = MemWalManual.create({ key: delegateKeyHex, @@ -840,8 +858,9 @@ const memwal = MemWalManual.create({ // server then: // 3. upload encrypted bytes to Walrus (server pays gas) // 4. store vector + blob_id in DB -await memwal.rememberManual("${fullRememberText.slice(0, 40)}...")`} -
+await memwal.rememberManual("${fullRememberText.slice(0, 40)}...")`} + + {fullRememberPhase && (
@@ -899,8 +918,9 @@ await memwal.rememberManual("${fullRememberText.slice(0, 40)}...")`} />
-
-                        {`// client does:
+                    
+ +{`// client does: // 1. embed query via OpenAI // 2. SEAL decrypt each result (wallet popup) // server then: @@ -908,8 +928,9 @@ await memwal.rememberManual("${fullRememberText.slice(0, 40)}...")`} // 4. download encrypted blobs from Walrus // 5. return encrypted results to client const result = await memwal.recallManual("${fullRecallQuery}", 5) -// → { results: [{ blob_id, text, distance }], total }`} -
+// → { results: [{ blob_id, text, distance }], total }`} + + {fullRecallPhase && (
diff --git a/apps/app/src/pages/SetupWizard.tsx b/apps/app/src/pages/SetupWizard.tsx index 32803b5e..25df17f5 100644 --- a/apps/app/src/pages/SetupWizard.tsx +++ b/apps/app/src/pages/SetupWizard.tsx @@ -17,6 +17,7 @@ import { import { Transaction } from '@mysten/sui/transactions' import { useSponsoredTransaction } from '../hooks/useSponsoredTransaction' import { useDelegateKey } from '../App' +import { Link } from 'react-router-dom' import { LogOut, Copy } from 'lucide-react' import { config } from '../config' import memwalLogo from '../assets/memwal-logo.svg' @@ -206,9 +207,9 @@ export default function SetupWizard() { <>