EtherFeed Documentation
EtherFeed Docs

Architecture

Contract: Single Solidity contract (pragma ^0.8.0) implementing users, posts, comments, reactions, and follows. Mutating functions are gated by onlyEOA (EOA‑only) to reduce automated abuse.

UI: A minimal web client that reads/writes directly to the contract using a standard provider. It reconstructs timelines via counters and contiguous IDs, batch‑loads in small slices on scroll, supports deep links (#post{ID} and #post{ID}/#comment{ID}), and refreshes the home feed on an interval.

Resilience: No backend/indexer; as long as Ethereum is reachable, the app renders truthfully.
Determinism: Global post IDs and per‑user jump tables guarantee predictable pagination.
UX: Instant UI toggles by reading on‑chain booleans/enums (follow, like, etc.).

Why this design is smart (and fast)

  • Single source of truth: Ethereum state → zero drift, zero “event lag”.
  • Zero ops: No indexer/DB/cron to run or maintain. Fewer moving parts; fewer outages.
  • Symmetric clients: Any UI makes the same calls and gets the same answers. No privileged backend.
  • Latency bounded by RPC: Optional Multicall compresses round‑trips without changing the model.
  • Forkable by design: Anyone can build a new client without permission; the chain is the API.

Data Model

// Users
struct UserBasic   { uint256 userId; string username; uint256 accountCreationTime; uint256 accountCreationBlock; bool isRegistered; }
struct UserProfile { string nickname; string about; string website; string location; string profilePicture; string coverPicture; uint256 pinnedPost; }
struct UserStats   { uint256 postCount; uint256 commentCount; uint256 followerCount; uint256 followingCount; }

// Content
enum Reaction { NONE, LIKE, DISLIKE }
struct Post {
  uint256 globalPostId; address author; uint256 authorPostId; uint256 postTime;
  string content; uint256 commentCount; uint256 likeCount; uint256 dislikeCount; uint256 repostCount;
  bool isHidden; bool isRepost; uint256 originalPostId; string reposterContent;
  mapping(address => Reaction) reactions;
}
struct Comment {
  uint256 commentId; address author; uint256 commentTime; string comment;
  uint256 likeCount; uint256 dislikeCount; bool isHidden; mapping(address => Reaction) reactions;
}

// Key indices
uint256 private totalUsers; uint256 private globalPostCount;
mapping(string => address) usernameToAddress;           // case-insensitive lookup
mapping(uint256 => address) userAddressById;            // userId → address
mapping(address => mapping(uint256 => uint256)) userPostId; // per-user idx → global post id
mapping(uint256 => Post) allPosts;                      // global posts
mapping(uint256 => mapping(uint256 => Comment)) postComments; // per-post contiguous comments

// UI booleans
mapping(uint256 => mapping(address => bool)) hasCommentedOnPost;
mapping(uint256 => mapping(address => bool)) hasRepostedOnPost;

// Social graph
mapping(address => mapping(address => bool)) isFollowing;

The key: contiguous, monotonic IDs and authoritative counters make reads trivial and deterministic.

Identity & Usernames

  • Registration: createAccount(nickname, username) saves UserBasic, UserProfile, and UserStats, and assigns a monotonic userId.
  • Case‑insensitive uniqueness: Usernames are validated (≥5 chars, lowercase letters/digits). The lowercase form indexes usernameToAddress; display casing is preserved in UserBasic.username.
  • Lookups: Resolve by username (getUserAddressByUsername) or by userId (getUserAddressById).

Posts & Reposts

  • Creation: createPost(content) increments globalPostCount and the author’s postCount; assigns globalPostId and authorPostId. Text must be non‑empty.
  • Editing & Hiding: Authors can editPost or hidePost. Hidden posts keep IDs/counters; getters return empty text when hidden.
  • Reposts: createRepost(originalId, reposterContent) links to the original and increments its repostCount. editRepost updates reposter note.
  • Pinning: pinPost(globalPostId) (or 0 to unpin) is enforced on‑chain for the author only.

Comments

  • Contiguous IDs: Each post tracks postCommentCount[postId] and stores comments as 1..N for deterministic pagination.
  • Create/Edit/Hide: createComment, editComment, hideComment mirror post semantics (soft‑hide returns empty text via getter).
  • User comment history: getUserCommentCount(user) with getUserComment(user, i) provides a per‑user jump table of (postId, commentId).

Reactions & Social

  • Reactions: Per‑item mapping(address => Reaction) supports constant‑time checks. The API accepts "like"/"dislike" strings and adjusts counters on change.
  • Follow graph: followUser(target, bool) flips isFollowing and maintains follower/following counters with underflow guards.
  • Interaction flags: hasCommentedOnPost and hasRepostedOnPost provide O(1) UI gating.

Visibility & Integrity

  • Soft hide: isHidden flags avoid breaking links or pagination; getters return empty text when hidden.
  • Repost integrity: Reposts record originalPostId and bump the original’s repostCount.
  • EOA guard: onlyEOA limits mutations to externally‑owned accounts.

Query Patterns (Indexer‑Free)

Global/Home Feed

// 1) Latest post id
N = getGlobalPostCount()
// 2) Fetch descending in batches
for id in [N, N-1, ..., N-k]: render(getPost(id))

User Feed

// 1) Per-user total
K = getUserStats(user).postCount
// 2) Jump table per-user idx → global id
for i in [K, K-1, ..., K-k]: id = getGlobalPostId(user, i); render(getPost(id))

Comments

// 1) Count
C = getPostCommentCount(postId)
// 2) Contiguous 1..C
for i in [1 .. C]: render(getComment(postId, i))

Instant UI Toggles

liked     = getUserReactionOnPost(postId, me)
following = getIsFollowing(me, user)
reposted  = getHasRepostedAPost(postId, me)

Tip: Batch read‑only calls with Multicall to reduce latency on mobile.

Why no indexer is needed: Every query here is a direct lookup or a bounded loop over contiguous IDs. There is no search, no off‑chain sorting, and no background job. The blockchain is the index.

Contract API (Selected)

Accounts

  • createAccount(string nickname, string username)
  • changeUsername(string newUsername)
  • isUserRegistered(address user) → bool
  • getUserAddressById(uint256 id) → address
  • getUserAddressByUsername(string username) → address
  • getUserBasic(address) → (userId, username, createdAt, createdBlock, isRegistered)
  • getUserProfile(address) → (nickname, about, website, location, profilePicture, coverPicture, pinnedPost)
  • getUserStats(address) → (postCount, commentCount, followerCount, followingCount)

Posts

  • createPost(string content), editPost(uint256 id, string newContent), hidePost(uint256 id, bool)
  • createRepost(uint256 originalId, string reposterContent), editRepost(uint256 id, string newReposterContent)
  • getGlobalPostCount() → uint256, getGlobalPostId(address, uint256) → uint256, getPost(uint256) → (id, author, authorPostId, time, text, commentCount, likeCount, dislikeCount, hidden, isRepost, originalPostId, repostCount)

Comments

  • createComment(uint256 postId, string text), editComment(uint256 postId, uint256 commentId, string newText), hideComment(uint256 postId, uint256 commentId, bool)
  • getPostCommentCount(uint256 postId) → uint256, getComment(uint256 postId, uint256 commentId) → (...)
  • getUserComment(address user, uint256 userCommentId) → (postId, commentId), getUserCommentCount(address) → uint256

Social & Reactions

  • followUser(address target, bool follow), getIsFollowing(address follower, address followed) → bool
  • reactToPost(uint256 postId, string reaction), reactToComment(uint256 postId, uint256 commentId, string reaction)
  • getUserReactionOnPost(uint256 postId, address user) → string, getUserReactionOnComment(uint256 postId, uint256 commentId, address user) → string
  • getHasCommentedOnPost(uint256 postId, address user) → bool, getHasRepostedAPost(uint256 postId, address user) → bool

UI Implementation Notes

  • Routing & Deep Links: The UI supports #post{ID} and #post{ID}/#comment{ID} routes for single‑post and single‑comment views; other hashes map to usernames or sections.
  • Feeds: Home feed walks backward from getGlobalPostCount() in batches on window.onscroll. User feed maps per‑user indices via getGlobalPostId(user,i). Comments load contiguously using getPostCommentCount().
  • Profile/Stats Widgets: “Recently Joined” reads getTotalUsers() then iterates downwards to fetch the latest addresses via getUserAddressById(). “Platform Stats” reads getTotalUsers() and getGlobalPostCount().
  • Reactions: Buttons call reactToPost/reactToComment. Highlight state is re‑derived via getUserReactionOnPost/...OnComment.
  • Markdown: Post/comment text is parsed with marked; @mentions are auto‑linked to #username. (If building your own client, add an HTML sanitizer after parsing.)
  • Caching: The UI caches user display names/profile pics per address to reduce repeated calls.
  • Auto Refresh: Home feed can auto‑reload every 60s when on the home route.
© 2025 EtherFeed Project. Crafted and developed by EtherFeed. All rights reserved.