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
User Selects Amount
User chooses a tip amount in SUI (quick amounts or custom input)
Convert to MIST
Amount is converted from SUI to MIST (1 SUI = 1,000,000,000 MIST)
Create Transaction
Build a transaction that splits coins and calls the tip function
Sign and Execute
User signs the transaction with their wallet
Update Registry
On-chain registry records the tip and updates engagement metrics
Creating a Tip Transaction
The createTipArticleTransaction function builds the transaction:
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
/**
* 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:
import { CONSTANTS } from '../config' ;
export function isValidTipAmount ( amount : number ) : boolean {
return amount >= CONSTANTS . MIN_TIP_AMOUNT ;
}
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 >
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 >
Users can also tip individual comments:
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:
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