Skip to content

Umul MVP Technical Design

Written 2026-07-26.

Status: approved.

Target networks: XMTP dev, GIWA Sepolia.

1. Goal

The Umul MVP is a mobile-shaped web messenger that uses a wallet address as identity. Sign-in happens through wallet connection and a signature, with no email, phone number, or password. 1:1 and group messages travel over the XMTP dev network.

Dojang verification does not restrict messaging permissions. Verified addresses get a Verified badge, and unverified addresses can still create conversations and exchange messages. Dojang verification for every participant is required only when recording conversation evidence on GIWA Sepolia.

The P0 completion scope is:

  1. Wallet sign-in and basic chat UX
  2. XMTP-based 1:1 and group E2EE messaging
  3. Dojang status display and inbox routing
  4. On-chain conversation evidence signed by every participant
  5. Verified real XMTP messaging for the three supplied test addresses

2. Out of scope

The following are deferred to P1 and beyond.

  • CLAIR-based risk detection
  • Token transfers, purchases, KRW payments
  • Gas sponsorship and on/off ramps
  • A feature for choosing a different messaging protocol
  • Judgements and advice on legal weight
  • A native mobile app implementation

The test ERC-20 address is for the P1 payments feature. No payment flow enters the P0 code.

3. Technology choices

Area Technology Why
Frontend React, TypeScript, Vite Mobile-shaped web UI and browser XMTP SDK integration
Messaging XMTP Browser SDK, XMTP dev Wallet signatures and MLS E2EE handled in the browser
Wallet / chain EIP-1193, wagmi, viem Wallet connection, GIWA Sepolia lookups and signing
Backend Node.js, TypeScript, Fastify The XMTP Node SDK is available and the API is simple to implement
Database PostgreSQL Consistency for evidence request and signature state
Contracts Solidity, Foundry EIP-712 signature verification and GIWA Sepolia deployment
Testing Vitest, Testing Library, Playwright, Foundry Unit, integration, and real user flow verification

The official XMTP chat SDK list has no Go SDK. The backend uses Node.js and TypeScript. User messaging is handled in the browser, and the XMTP Node SDK is used only where E2E tooling or server-side integration requires it.

4. Repository layout

Top-level directories are split by language and deployment unit. There is no packages directory for shared product code.

umul/
├── frontend/
│   ├── src/
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   └── presentation/
│   └── tests/
├── backend/
│   ├── src/
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   └── interfaces/
│   ├── migrations/
│   └── tests/
├── contracts/
│   ├── src/
│   ├── test/
│   ├── script/
│   └── deployments/
├── mobile/
│   └── README.md
├── docs/
│   ├── plans/
│   ├── architecture/
│   ├── api/
│   └── runbooks/
├── compose.yaml
└── README.md

The root holds only integrated run configuration and whole-project documentation.

The backend owns the API contract. The generated frontend API client lives inside frontend. contracts owns the canonical contract ABIs, and the frontend and backend each keep a generated copy in their own infrastructure directory.

5. Component boundaries

5.1 Frontend

The frontend is the only Umul component that touches message plaintext.

  • domain: addresses, conversations, inbox routing, Dojang status, evidence manifest rules
  • application: sign-in, conversation sync, sending, routing, evidence creation and signing use cases
  • infrastructure: wallet, XMTP Browser SDK, Dojang RPC, backend API, contract connection
  • presentation: screens, components, screen state and styling

presentation never calls an SDK directly. It uses only interfaces from the application layer. XMTP, Dojang, API, and contract implementations are injected at application startup.

5.2 Backend

The backend stores neither ordinary message plaintext nor XMTP decryption keys.

  • Wallet login nonce and session management
  • Evidence request identifiers and state management
  • Signature collection per participant
  • Transaction hash and confirmation state management
  • API contract and input validation

5.3 Contracts

The evidence contract does only the following.

  • Verify EIP-712 signatures on the evidence request
  • Check participant address ordering, duplicates, and omissions
  • Query Dojang verification status for every participant
  • Prevent evidence ID reuse
  • Record the evidence hash and metadata
  • Emit an evidence-recorded event

The contract stores neither the conversation nor individual message plaintext.

5.4 Mobile

No native mobile code is written at this stage. mobile/README.md records the boundary for porting the frontend use cases and screen model to mobile.

6. User flows

6.1 Wallet sign-in

  1. The user chooses GIWA Wallet, MetaMask, or WalletConnect.
  2. The frontend checks the GIWA Sepolia connection state.
  3. The backend issues a nonce.
  4. The user signs the login message.
  5. The backend verifies the signature and issues an HttpOnly session cookie.
  6. The XMTP Browser SDK creates or recovers the Inbox.
  7. The app moves to the conversation list.

Declining wallet connection or signing returns to the onboarding screen. If the session is valid and a local XMTP database exists, a relaunch goes straight to the conversation list.

6.2 Conversation list and Dojang routing

The frontend syncs XMTP conversations, then checks participants' wallet addresses. DojangScroll.isVerified() results are kept in memory per address for one hour. Relaunching the app clears the cache.

A 1:1 conversation uses the peer's status.

  • Verified peer: main inbox
  • Unverified peer: request inbox
  • Dojang RPC error: unconfirmable state
  • Marked spam by the user: spam inbox

A group lands in the main inbox when every participant other than yourself is Verified. If even one is Unverified it goes to the request inbox. The group header shows Verified N/M.

The request inbox is not a read-only quarantine. Users can start conversations, send messages, and reply to Unverified peers.

A Dojang RPC error is not converted into Unverified. Even in an unconfirmable state, messaging is allowed and only evidence is blocked.

6.3 New conversations

  1. The user enters a wallet address.
  2. The frontend checks the address format and XMTP reachability.
  3. It queries Dojang status and shows a badge.
  4. The user can create the conversation regardless of Verified status.
  5. Groups take multiple addresses and are created as XMTP MLS groups.

6.4 Sending and receiving

  • 1:1 uses XMTP DMs.
  • Groups use XMTP MLS groups.
  • The app subscribes to the message stream for every conversation.
  • Failed messages stay on screen with a retry option.
  • If the stream drops, connection state is displayed and it reconnects with bounded exponential backoff.
  • An XMTP initialization error never auto-deletes the local database.

7. On-chain evidence

7.1 The evidence manifest

The frontend builds a normalized manifest from the selected messages.

  • Format version
  • Hash of the XMTP conversation ID
  • Participant addresses in ascending order
  • Message IDs
  • Sender Inbox IDs
  • Send times
  • Hash of each message's plaintext
  • The overall content hash
  • A reuse-prevention nonce

Messages are sorted by send time and message ID. The same conversation and message set must produce the same content hash.

7.2 Proposal and signing

  1. The requester selects a message range.
  2. The frontend builds the manifest and content hash.
  3. It re-queries every participant's Dojang status without the cache.
  4. If any participant is unverified or unconfirmable, evidence does not start.
  5. The evidence proposal is sent to participants as an XMTP encrypted control message.
  6. Participants recompute the content hash from their own local messages.
  7. On a match, they sign the identical EIP-712 data with their wallet.
  8. The backend stores the content hash, participants, signature state, and signature values.
  9. Once every participant has signed, the request state becomes READY.

The backend never receives the plaintext of the selected messages.

7.3 Transaction submission

One participant in the READY state submits a GIWA Sepolia transaction. The contract checks, in order:

  1. That the evidence ID is unused.
  2. That the participant list is sorted with no duplicates.
  3. Each participant's EIP-712 signature.
  4. DojangScroll.isVerified() for each participant.
  5. Records the evidence hash and metadata.
  6. Emits an event including participants and evidence details.

Frontend pre-checks and contract checks are used together. If verification is revoked or expires after signing, the contract rejects the submission.

7.4 On-chain data

The contract stores or emits the following.

  • evidence ID
  • content hash
  • conversation ID hash
  • participants hash
  • Participant addresses
  • Message count
  • Conversation start and end times
  • Record time
  • Submitter

The S6 evidence detail screen treats chain data as canonical. Backend data is derived, for progress state and query convenience.

The contract has no way to independently confirm the XMTP group's participant set. Every participant signing the same participants hash is how they agree to the declared set. This limit and the undetermined legal weight are stated in the user documentation.

8. Backend state and data

Evidence request states:

PROPOSED -> SIGNING -> READY -> SUBMITTED -> CONFIRMED
     |          |         |          |
     +------> REJECTED    +------> FAILED
     +------> EXPIRED

State transitions are checked inside a transaction. API retries use an idempotency key to block duplicate requests and duplicate signatures.

Only these categories are stored in PostgreSQL.

  • Wallet sessions and nonces
  • Evidence requests and participants
  • Content hash and manifest version
  • Participant signatures and signing times
  • Transaction hash, block number, confirmation state
  • Creation, expiry, and rejection times

Ordinary message plaintext, XMTP private keys, and wallet private keys are not stored.

9. Error handling

Error User handling System handling
Wallet connection or signature declined Return to onboarding, retry No session created
Chain mismatch Prompt to switch networks Transaction blocked
XMTP initialization failure Show cause and retry Local DB preserved
XMTP stream interruption Show connection state Bounded backoff reconnect
Dojang RPC failure Show unconfirmable Messaging allowed, evidence blocked
Message send failure Failure bubble and retry Duplicate send prevented
Evidence hash mismatch Signing refused Require conversation resync
Participant declines to sign Show rejected state Transition request to REJECTED
Dojang verification revoked Show submission failure reason Final block at the contract
Transaction failure Offer resubmission Collected signatures preserved
Duplicate evidence Show already recorded Rejected by evidence ID

10. Test strategy

New behaviour gets a test written first, confirmed failing, then implemented.

10.1 Frontend

  • Address normalization and abbreviation
  • 1:1 and group Dojang routing
  • Handling of unconfirmable Dojang state
  • Evidence manifest normalization and hashing
  • Screen state and user input
  • SDK port success, failure, and retry

10.2 Backend

  • Login nonce single use
  • Session ownership checks
  • Evidence state transitions
  • Duplicate request and duplicate signature prevention
  • Blocking access from non-participant addresses
  • Repository integration tests against real PostgreSQL

10.3 Contracts

  • Success with every participant signing
  • Rejection of missing, duplicate, or invalid signatures
  • Rejection of unverified participants
  • Rejection of evidence ID reuse
  • Rejection of unsorted participants
  • Fuzz testing and state invariant checks
  • GIWA Sepolia fork or live-deployment integration checks

10.4 E2E

Playwright runs three independent user sessions. Test private keys are injected only from the execution environment and never left in source, fixtures, snapshots, or logs.

A test that stubs out XMTP calls does not count as evidence of real messaging.

11. Completion criteria

11.1 Real messaging for the three test addresses

The following addresses serve as A, B, and C.

  • A: 0xEf792c4973Cc8ef6aF433B3bd9f4461B701301f6
  • B: 0x532dA989489eaB825d484EAd09f117018da47778
  • C: 0x2F9713FB44eE7A15EF393eA694959324Be7a6479

Completion criteria:

  1. A, B, and C sign in with wallet signatures.
  2. All three create or recover an XMTP dev Inbox.
  3. A and B exchange 1:1 messages both ways.
  4. B and C exchange 1:1 messages both ways.
  5. A creates a group containing A, B, and C.
  6. All three send a message to the group.
  7. The other two receive each message over a real XMTP stream.
  8. Conversations and messages resync after a refresh.
  9. Before verification is issued, A shows Verified under TESTNET_FAUCET while B and C show Unverified.
  10. In that mixed state all three can still send messages.
  11. To verify evidence success, C stays Unverified and continues messaging even after B is verified.

The verification record keeps XMTP message IDs, sender and recipient addresses, and timestamps — but no private keys.

11.2 Full P0

  • The mobile web flow works from S1 through S7.
  • Real 1:1 and group XMTP messaging succeeds for the three test addresses.
  • Dojang lookups and the Verified, Unverified, and unconfirmable states work.
  • Main, request, and spam inbox routing works.
  • Unverified users can exchange messages.
  • Evidence including an unverified participant is blocked.
  • EIP-712 multi-signature and GIWA Sepolia evidence succeed for a conversation where every participant is verified.
  • The evidence detail view can open a GIWA Explorer link.
  • Frontend, backend, and contract tests and builds pass.
  • No private keys or local environment variables exist in Git history.
  • How to run, the API, the DB, the contract, and verification results are documented.

12. Testnet dependencies

As measured on 2026-07-26, only A is TESTNET_FAUCET Verified; B and C are unverified. Under UPBIT_KOREA all three are unverified.

The current state allows verifying these flows on the live network.

  • Verified and Unverified badges
  • Main and request inbox routing
  • 1:1 and group messaging independent of verification
  • Blocking evidence that includes an unverified participant

Verifying multi-signature evidence success on GIWA Sepolia requires every address in the conversation to be Verified under the project's configured Dojang attester. Mixed-state messaging and routing for A, B, and C are recorded first, then only B receives testnet verification. Evidence success is confirmed on the A–B 1:1 conversation, while the three-person group including C confirms evidence blocking alongside messaging still being allowed. The testnet verification procedure is written as a runbook during implementation, and a real successful transaction is confirmed before reporting completion.

13. Security baseline

  • The supplied test private keys are never committed to Git.
  • Private keys never enter the frontend bundle, API responses, or logs.
  • The backend never receives users' message plaintext or XMTP decryption keys.
  • Session cookies carry HttpOnly, Secure, and SameSite settings.
  • Address and signature inputs are re-validated at the server and the contract.
  • Evidence hashes are computed under versioned normalization rules.
  • The contract deployment address and chain ID are included in the signing domain to prevent reuse.
  • Error logs mask raw signatures and environment variables.

14. Implementation order

Each stage finishes implementation, tests, real-flow verification, and a commit before moving to the next.

  1. Repository foundation and development environment
  2. Wallet sign-in and the mobile shell
  3. XMTP 1:1 messaging
  4. XMTP group messaging
  5. Dojang status and inbox routing
  6. The evidence contract
  7. Evidence backend and frontend flow
  8. Real three-user E2E and documentation

P1 features are not implemented before P0 verification finishes.