Skip to main content

Overview

The NewsRegistry is a Sui Move smart contract that serves as the decentralized index for all news articles in TUNA. It’s deployed as a Shared Object, making it accessible to all users for reading and writing.
Contract Address (Sui Testnet)
  • Package: 0xadf0a6ce11dd75d3d44930ab5bf55781801dea2bfead056eb0bb59c1aa1e9e66
  • Registry Object: 0x68c01d2c08923d5257a5a9959d7c9250c4053dbe4641e229ccff2f35e6a3bb6d
  • Module: news_registry

Registry Structure

The NewsRegistry is a Move struct with the following fields:
struct NewsRegistry has key {
  id: UID,
  latest_blobs: vector<String>,
  engagement_map: VecMap<String, ArticleEngagement>,
  admin_cap: ID,
}
  • id: Unique identifier for the Sui object
  • latest_blobs: Vector (array) of Walrus blob IDs, ordered chronologically
  • engagement_map: Maps blob IDs to their engagement statistics
  • admin_cap: Reference to the AdminCap object that controls registry updates

Article Storage

Blob ID Array

Articles are indexed as Walrus blob IDs in the latest_blobs vector:
// Fetching from Sui
const registry = await suiClient.getObject({
  id: REGISTRY_ID,
  options: { showContent: true }
});

const fields = registry.data.content.fields as any;
const allBlobIds = fields.latest_blobs as string[];

// Example blob IDs:
// [
//   "Cjk7S3b6pLWqZ8vYx3KjHdR2Uw9eFgT8mNpQ1aBc4D",
//   "9xTmK7qR3wYz2sLvN4jHdP8uF6gE1bA5cX0oI9rV7",
//   "5wQpL2tF9xYv3jKm8hD1rE7gS4nU6bC0aI9oX5zT2",
//   ...
// ]

Chronological Ordering

Articles are appended to the vector, so:
  • First items = Oldest articles
  • Last items = Newest articles
// Get 20 most recent articles
const recentBlobs = allBlobIds.slice(-20).reverse();

// Get articles 100-80 (20 articles before the most recent 80)
const olderBlobs = allBlobIds.slice(-100, -80);

// Total article count
const totalArticles = allBlobIds.length;
The registry only stores blob IDs (pointers), not the actual article content. Content is fetched from Walrus using these IDs.

Engagement Tracking

ArticleEngagement Structure

Each article has associated engagement metrics:
struct ArticleEngagement has store {
  total_tips: u64,      // Total SUI tipped (in MIST)
  tip_count: u64,       // Number of tips received
  comment_count: u64,   // Number of comments
}

Fetching Engagement Data

The engagement_map uses a VecMap structure:
async function getArticleEngagement(blobId: string) {
  const registry = await suiClient.getObject({
    id: CONTRACT_CONFIG.REGISTRY_ID,
    options: { showContent: true },
  });
  
  const fields = registry.data.content.fields as any;
  const engagementMap = fields.engagement_map as any;
  
  // VecMap format: { contents: [{ key: string, value: object }] }
  const engagement = engagementMap.contents?.find(
    (item: any) => item.key === blobId
  );
  
  if (!engagement) {
    return { totalTips: 0, tipCount: 0, commentCount: 0 };
  }
  
  return {
    totalTips: parseInt(engagement.value.total_tips || '0'),
    tipCount: parseInt(engagement.value.tip_count || '0'),
    commentCount: parseInt(engagement.value.comment_count || '0'),
  };
}

Combined Article Data

TUNA’s useLatestNews hook merges registry data with Walrus content:
export function useLatestNews(limit: number = 100) {
  return useQuery({
    queryKey: ['latestNews', limit],
    queryFn: async () => {
      // 1. Get blob IDs from registry
      const registry = await suiClient.getObject(/*...*/);
      const blobIds = fields.latest_blobs.slice(-limit).reverse();
      
      // 2. Fetch content and engagement in parallel
      const articles = await Promise.all(
        blobIds.map(async (blobId) => {
          const walrusContent = await fetchFromWalrus(blobId);
          const engagement = await getArticleEngagement(blobId);
          
          return {
            id: blobId,
            blob_id: blobId,
            title: walrusContent.title,
            content: walrusContent.content,
            summary: walrusContent.summary,
            url: walrusContent.url,
            image: walrusContent.image,
            author: walrusContent.author,
            source: walrusContent.source,
            timestamp: walrusContent.timestamp,
            // Engagement data
            totalTips: engagement.totalTips,
            tipCount: engagement.tipCount,
            commentCount: engagement.commentCount,
          };
        })
      );
      
      return articles.filter(a => a !== null);
    },
  });
}

Contract Functions

The NewsRegistry module exposes several public functions:

1. Tip Article

public entry fun tip_article(
  registry: &mut NewsRegistry,
  blob_id: String,
  tip: Coin<SUI>,
  ctx: &mut TxContext
)
Purpose: Tip an article, incrementing its engagement stats TypeScript Usage:
import { createTipArticleTransaction, suiToMist } from '../lib/sui';

const tx = createTipArticleTransaction(
  blobId,
  suiToMist(0.1) // 0.1 SUI
);

const result = await signAndExecuteTransaction({ transaction: tx });

2. Post Comment

public entry fun post_comment(
  registry: &mut NewsRegistry,
  blob_id: String,
  comment_text: String,
  ctx: &mut TxContext
)
Purpose: Post a short comment (≤280 chars) on an article TypeScript Usage:
import { createPostCommentTransaction } from '../lib/sui';

const tx = createPostCommentTransaction(
  blobId,
  "Great article! Very informative."
);

const result = await signAndExecuteTransaction({ transaction: tx });

3. Post Comment with Blob

public entry fun post_comment_with_blob(
  registry: &mut NewsRegistry,
  blob_id: String,
  preview_text: String,
  content_blob_id: String,
  comment_type: String,
  ctx: &mut TxContext
)
Purpose: Post a long comment or comment with media TypeScript Usage:
import { createPostCommentWithBlobTransaction } from '../lib/sui';
import { uploadToWalrus } from '../lib/walrus';

// Upload full content to Walrus
const fullContent = {
  text: longCommentText,
  media: [{ type: 'image', url: imageUrl }],
  timestamp: Date.now(),
};
const contentBlobId = await uploadToWalrus(fullContent);

// Post comment with preview
const tx = createPostCommentWithBlobTransaction(
  blobId,
  previewText.slice(0, 280),
  contentBlobId,
  'media' // or 'text_long'
);

const result = await signAndExecuteTransaction({ transaction: tx });

4. Tip Comment

public entry fun tip_comment(
  comment: &mut Comment,
  tip: Coin<SUI>,
  ctx: &mut TxContext
)
Purpose: Tip a specific comment TypeScript Usage:
import { createTipCommentTransaction, suiToMist } from '../lib/sui';

const tx = createTipCommentTransaction(
  commentId, // Object ID of the comment
  suiToMist(0.05)
);

const result = await signAndExecuteTransaction({ transaction: tx });

5. Withdraw Comment Tips

public entry fun withdraw_comment_tips(
  comment: &mut Comment,
  ctx: &mut TxContext
)
Purpose: Withdraw accumulated tips from a comment (author only) TypeScript Usage:
import { createWithdrawCommentTipsTransaction } from '../lib/sui';

const tx = createWithdrawCommentTipsTransaction(commentId);

const result = await signAndExecuteTransaction({ transaction: tx });

Comment Objects

Comments are created as individual Sui objects:
struct Comment has key, store {
  id: UID,
  blob_id: String,           // Article blob ID
  author: address,           // Comment author
  preview_text: String,      // Short text (≤280 chars)
  content_blob_id: Option<String>,  // Full content on Walrus
  comment_type: String,      // "text", "text_long", or "media"
  timestamp: u64,
  tips_received: Balance<SUI>,  // Accumulated tips
}
Comments are owned objects transferred to the author, allowing them to withdraw tips independently.

Admin Capabilities

The registry is controlled by an AdminCap:
struct AdminCap has key {
  id: UID,
}
Admin-only functions (not exposed in TUNA frontend):
  • Adding new articles to the registry
  • Removing articles (moderation)
  • Updating registry configuration
The AdminCap object ID is 0x18d48d74bfddffbe3dc75025136722380f374baec942df2e0aef76cad1061496 on testnet. Only the holder can perform admin operations.

Data Flow Summary

1

Article Creation

Admin uploads content to Walrus, then adds blob ID to registry
2

User Reads

Query registry for blob IDs → Fetch content from Walrus → Display articles
3

User Tips

Execute tip transaction → Registry updates engagement_map → Tips sent to admin
4

User Comments

Upload comment to Walrus (if long/media) → Execute comment transaction → Comment object created → Registry increments comment_count
5

Tip Comment

Execute tip transaction on Comment object → Tips accumulate in comment.tips_received
6

Withdraw

Comment author executes withdraw transaction → Tips transferred to author

Constants and Limits

export const CONSTANTS = {
  SHORT_COMMENT_THRESHOLD: 280,        // Max chars for on-chain comments
  MIN_TIP_AMOUNT: 1_000_000,          // 0.001 SUI in MIST
  MAX_PREVIEW_LENGTH: 280,            // Max preview length
};
The 280-character limit mirrors Twitter’s original limit, making comments concise and gas-efficient.

Querying the Registry

Basic Query

import { SuiClient } from '@mysten/sui.js/client';
import { CONTRACT_CONFIG } from '../config';

const client = new SuiClient({ 
  url: 'https://fullnode.testnet.sui.io:443' 
});

const registry = await client.getObject({
  id: CONTRACT_CONFIG.REGISTRY_ID,
  options: { showContent: true }
});

console.log(registry.data?.content);

Filtering Articles

// Get articles from last 24 hours
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
const recentArticles = articles.filter(a => a.timestamp > oneDayAgo);

// Get articles by source
const twitterArticles = articles.filter(a => a.source === 'twitter');

// Get most tipped articles
const topTipped = articles
  .sort((a, b) => b.totalTips - a.totalTips)
  .slice(0, 10);

Next Steps

Architecture

See the big picture

Sui Integration

Learn transaction building

Walrus Storage

Understand content storage

Integration Guide

Build with TUNA

Build docs developers (and LLMs) love