Skip to main content
TUNA enables users to tip articles with SUI tokens, creating a direct incentive mechanism for content creators. Tips are sent on-chain and tracked in the registry.

Tipping Flow

1

User Selects Amount

User chooses a tip amount in SUI (quick amounts or custom input)
2

Convert to MIST

Amount is converted from SUI to MIST (1 SUI = 1,000,000,000 MIST)
3

Create Transaction

Build a transaction that splits coins and calls the tip function
4

Sign and Execute

User signs the transaction with their wallet
5

Update Registry

On-chain registry records the tip and updates engagement metrics

Creating a Tip Transaction

The createTipArticleTransaction function builds the transaction:
src/lib/sui.ts
import { Transaction } from '@mysten/sui/transactions';
import { CONTRACT_CONFIG } from '../config';

export function createTipArticleTransaction(
  blobId: string, 
  amount: number
): Transaction {
  const tx = new Transaction();

  // Split coins for the tip
  const [tipCoin] = tx.splitCoins(tx.gas, [amount]);

  tx.moveCall({
    target: `${CONTRACT_CONFIG.PACKAGE_ID}::${CONTRACT_CONFIG.MODULE_NAME}::tip_article`,
    arguments: [
      tx.object(CONTRACT_CONFIG.REGISTRY_ID),
      tx.pure.string(blobId),
      tipCoin,
    ],
  });

  return tx;
}
The transaction splits the specified amount from the user’s gas coins, ensuring precise payment.

SUI/MIST Conversion

All amounts must be converted between display units (SUI) and on-chain units (MIST).

Conversion Functions

src/lib/sui.ts
/**
 * Convert SUI to MIST
 * 1 SUI = 1,000,000,000 MIST
 */
export function suiToMist(sui: number): number {
  return Math.floor(sui * 1_000_000_000);
}

/**
 * Convert MIST to SUI
 */
export function mistToSui(mist: number): number {
  return mist / 1_000_000_000;
}

/**
 * Format SUI amount for display
 */
export function formatSui(mist: number): string {
  const sui = mistToSui(mist);
  if (sui < 0.001) return '< 0.001 SUI';
  if (sui < 1) return `${sui.toFixed(3)} SUI`;
  return `${sui.toFixed(2)} SUI`;
}

Usage Example

const userInput = 0.5; // 0.5 SUI
const amountInMist = suiToMist(userInput); // 500,000,000 MIST

const tx = createTipArticleTransaction(blobId, amountInMist);

Validating Tip Amounts

Enforce minimum tip requirements:
src/lib/sui.ts
import { CONSTANTS } from '../config';

export function isValidTipAmount(amount: number): boolean {
  return amount >= CONSTANTS.MIN_TIP_AMOUNT;
}
src/config/index.ts
export const CONSTANTS = {
  MIN_TIP_AMOUNT: 1_000_000, // 0.001 SUI in MIST
} as const;
The minimum tip amount (0.001 SUI) prevents spam while keeping tipping accessible.

TipModal Component

The TipModal provides a user-friendly interface for tipping:
src/components/TipModal.tsx
import { useState } from 'react';
import { useSignAndExecuteTransaction } from '@mysten/dapp-kit';
import { createTipArticleTransaction, suiToMist, isValidTipAmount } from '../lib/sui';

interface TipModalProps {
  isOpen: boolean;
  articleId: string;
  articleTitle: string;
  onClose: () => void;
}

export default function TipModal({ 
  isOpen, 
  articleId, 
  articleTitle, 
  onClose 
}: TipModalProps) {
  const [amount, setAmount] = useState(0.01);
  const [customAmount, setCustomAmount] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const { mutate: signAndExecute } = useSignAndExecuteTransaction();

  const quickAmounts = [0.01, 0.05, 0.1, 0.5, 1];

  const handleTip = async () => {
    const tipAmount = customAmount ? parseFloat(customAmount) : amount;
    const amountInMist = suiToMist(tipAmount);

    if (!isValidTipAmount(amountInMist)) {
      alert('Minimum tip amount is 0.001 SUI');
      return;
    }

    setIsSubmitting(true);

    try {
      const tx = createTipArticleTransaction(articleId, amountInMist);

      signAndExecute(
        { transaction: tx },
        {
          onSuccess: () => {
            alert(`Successfully tipped ${tipAmount} SUI!`);
            setTimeout(() => onClose(), 2000);
          },
          onError: (error) => {
            console.error('Tip failed:', error);
            alert('Failed to send tip. Please try again.');
            setIsSubmitting(false);
          },
        }
      );
    } catch (error) {
      console.error('Error creating tip transaction:', error);
      setIsSubmitting(false);
    }
  };

  // ... render modal UI
}

Quick Amount Selection

Provide preset tip amounts for convenience:
src/components/TipModal.tsx
<div className="quick-amounts">
  <p>Quick amounts:</p>
  <div className="amount-buttons">
    {quickAmounts.map((amt) => (
      <button
        key={amt}
        className={`amount-btn ${amount === amt && !customAmount ? 'active' : ''}`}
        onClick={() => {
          setAmount(amt);
          setCustomAmount('');
        }}
      >
        {amt} SUI
      </button>
    ))}
  </div>
</div>

Custom Amount Input

Allow users to enter custom amounts:
src/components/TipModal.tsx
<div className="custom-amount">
  <p>Or enter custom amount:</p>
  <div className="input-group">
    <input
      type="number"
      min="0.001"
      step="0.001"
      value={customAmount}
      onChange={(e) => setCustomAmount(e.target.value)}
      placeholder="0.000"
    />
    <span className="input-suffix">SUI</span>
  </div>
  <p className="hint">Minimum: 0.001 SUI</p>
</div>

Transaction Summary

Show users what they’ll pay:
src/components/TipModal.tsx
<div className="tip-summary">
  <div className="summary-row">
    <span>Tip Amount:</span>
    <span>{customAmount || amount} SUI</span>
  </div>
  <div className="summary-row">
    <span>Gas Fee:</span>
    <span>~0.001 SUI</span>
  </div>
</div>
Gas fees are estimated. The actual fee depends on network conditions and transaction complexity.

Executing the Transaction

Use the @mysten/dapp-kit hook to sign and execute:
const { mutate: signAndExecute } = useSignAndExecuteTransaction();

signAndExecute(
  { transaction: tx },
  {
    onSuccess: (result) => {
      console.log('Transaction success:', result);
      // Update UI, invalidate queries
    },
    onError: (error) => {
      console.error('Transaction failed:', error);
      // Show error message
    },
  }
);

Displaying Tip Stats

Show engagement metrics on article cards:
src/components/NewsCard.tsx
<div style={{ display: 'flex', gap: '1rem' }}>
  <span>💬 {article.commentCount || 0}</span>
  <span>💰 {article.totalTips || 0} SUI</span>
</div>

Comment Tipping

Users can also tip individual comments:
src/lib/sui.ts
export function createTipCommentTransaction(
  commentId: string, 
  amount: number
): Transaction {
  const tx = new Transaction();

  const [tipCoin] = tx.splitCoins(tx.gas, [amount]);

  tx.moveCall({
    target: `${CONTRACT_CONFIG.PACKAGE_ID}::${CONTRACT_CONFIG.MODULE_NAME}::tip_comment`,
    arguments: [
      tx.object(commentId),
      tipCoin,
    ],
  });

  return tx;
}

Withdrawing Tips

Comment authors can withdraw accumulated tips:
src/lib/sui.ts
export function createWithdrawCommentTipsTransaction(
  commentId: string
): Transaction {
  const tx = new Transaction();

  tx.moveCall({
    target: `${CONTRACT_CONFIG.PACKAGE_ID}::${CONTRACT_CONFIG.MODULE_NAME}::withdraw_comment_tips`,
    arguments: [tx.object(commentId)],
  });

  return tx;
}

Best Practices

Validate Amounts

Always validate tip amounts before creating transactions to prevent errors

Handle Errors

Provide clear error messages when transactions fail

Show Feedback

Display success/error toasts after transaction execution

Cache Invalidation

Invalidate React Query caches after successful tips to update UI

Next Steps

Wallet Connection

Learn how to connect Sui wallets

Comments

Implement the commenting system

Build docs developers (and LLMs) love