@ -1,14 +1,15 @@
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
import { useWallet } from '@txnlab/use-wallet-react'
import { sha512_256 } from 'js-sha512'
import { useSnackbar } from 'notistack'
import { useEffect , useMemo , useState } from 'react'
import { AiOutlineInfoCircle , AiOutlineLoading3Quarters } from 'react-icons/ai'
import { ChangeEvent , useEffect, useMemo , useRef , useState } from 'react'
import { AiOutlineCloudUpload , AiOutlineInfoCircle, AiOutlineLoading3Quarters } from 'react-icons/ai'
import { BsCoin } from 'react-icons/bs'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
/**
* Type for created assets stored in browser localStorage
* Captures all ASA configuration including compliance fields
* Captures ASA configuration including compliance fields
*/
type CreatedAsset = {
assetId : number
@ -27,6 +28,23 @@ type CreatedAsset = {
const STORAGE_KEY = 'tokenize_assets'
const LORA_BASE = 'https://lora.algokit.io/testnet'
function resolveBackendBase ( ) : string {
// 1) Respect explicit env (Vercel or custom)
const env = import . meta . env . VITE_API_URL ? . trim ( )
if ( env ) return env . replace ( /\/$/ , '' )
// 2) Codespaces: convert current host to port 3001
// e.g. https://abc-5173.app.github.dev -> https://abc-3001.app.github.dev
const host = window . location . host
if ( host . endsWith ( '.app.github.dev' ) ) {
const base = host . replace ( /-\d+\.app\.github\.dev$/ , '-3001.app.github.dev' )
return ` https:// ${ base } `
}
// 3) Plain local fallback
return 'http://localhost:3001'
}
/**
* Load created assets from browser localStorage
*/
@ -53,10 +71,11 @@ function persistAsset(asset: CreatedAsset): CreatedAsset[] {
/**
* TokenizeAsset Component
* Main form for creating Algorand Standard Assets (ASAs)
* Collects basic and advanced compliance metadata
* + NFT minting panel (ASA mint with IPFS metadata)
* Persists created assets to localStorage for tracking
*/
export default function TokenizeAsset() {
// ===== ASA (original) state =====
const [ assetName , setAssetName ] = useState < string > ( 'Tokenized Coffee Membership' )
const [ unitName , setUnitName ] = useState < string > ( 'COFFEE' )
const [ total , setTotal ] = useState < string > ( '1000' )
@ -72,15 +91,37 @@ export default function TokenizeAsset() {
const [ loading , setLoading ] = useState < boolean > ( false )
const [ createdAssets , setCreatedAssets ] = useState < CreatedAsset [ ] > ( [ ] )
// NEW: Transfer state (added)
// ===== Transfer state =====
const [ transferAssetId , setTransferAssetId ] = useState < string > ( '' )
const [ receiverAddress , setReceiverAddress ] = useState < string > ( '' )
const [ transferAmount , setTransferAmount ] = useState < string > ( '1' )
const [ transferLoading , setTransferLoading ] = useState < boolean > ( false )
// ===== NFT mint state (new) =====
const [ selectedFile , setSelectedFile ] = useState < File | null > ( null )
const [ previewUrl , setPreviewUrl ] = useState < string > ( '' )
const [ nftLoading , setNftLoading ] = useState < boolean > ( false )
const fileInputRef = useRef < HTMLInputElement > ( null )
// NFT mint configurable fields
const [ nftName , setNftName ] = useState < string > ( 'MasterPass Ticket' )
const [ nftUnit , setNftUnit ] = useState < string > ( 'MTK' )
const [ nftSupply , setNftSupply ] = useState < string > ( '1' )
const [ nftDecimals , setNftDecimals ] = useState < string > ( '0' )
const [ nftDefaultFrozen , setNftDefaultFrozen ] = useState < boolean > ( false )
// NFT advanced (addresses)
const [ nftShowAdvanced , setNftShowAdvanced ] = useState < boolean > ( false )
const [ nftManager , setNftManager ] = useState < string > ( '' )
const [ nftReserve , setNftReserve ] = useState < string > ( '' )
const [ nftFreeze , setNftFreeze ] = useState < string > ( '' )
const [ nftClawback , setNftClawback ] = useState < string > ( '' )
// ===== Wallet + notifications =====
const { transactionSigner , activeAddress } = useWallet ( )
const { enqueueSnackbar } = useSnackbar ( )
// ===== Algorand client =====
const algodConfig = getAlgodConfigFromViteEnvironment ( )
const algorand = useMemo ( ( ) = > AlgorandClient . fromConfig ( { algodConfig } ) , [ algodConfig ] )
@ -92,7 +133,12 @@ export default function TokenizeAsset() {
if ( activeAddress && ! manager ) setManager ( activeAddress )
} , [ activeAddress , manager ] )
// NEW: Prefill transfer asset id from latest created asset (optional QoL )
// NFT: default manager to connected address (same UX as ASA )
useEffect ( ( ) = > {
if ( activeAddress && ! nftManager ) setNftManager ( activeAddress )
} , [ activeAddress , nftManager ] )
// Prefill transfer asset id from latest created asset (QoL)
useEffect ( ( ) = > {
if ( ! transferAssetId && createdAssets . length > 0 ) {
setTransferAssetId ( String ( createdAssets [ 0 ] . assetId ) )
@ -112,20 +158,36 @@ export default function TokenizeAsset() {
setClawback ( '' )
}
// NEW: Copy helper (added)
const resetNftDefaults = ( ) = > {
setSelectedFile ( null )
setPreviewUrl ( '' )
if ( fileInputRef . current ) fileInputRef . current . value = ''
setNftName ( 'MasterPass Ticket' )
setNftUnit ( 'MTK' )
setNftSupply ( '1' )
setNftDecimals ( '0' )
setNftDefaultFrozen ( false )
setNftShowAdvanced ( false )
setNftManager ( activeAddress ? ? '' )
setNftReserve ( '' )
setNftFreeze ( '' )
setNftClawback ( '' )
}
const isWholeNumber = ( v : string ) = > /^\d+$/ . test ( v )
const copyToClipboard = async ( text : string ) = > {
try {
await navigator . clipboard . writeText ( text )
enqueueSnackbar ( 'Asset ID copied to clipboard' , { variant : 'success' } )
// Also fill transfer field to reduce friction
setTransferAssetId ( text )
} catch {
enqueueSnackbar ( 'Copy failed. Please copy manually.' , { variant : 'warning' } )
}
}
const isWholeNumber = ( v : string ) = > /^\d+$/ . test ( v )
/**
* Handle ASA creation with validation and on-chain transaction
* Adjusts total supply by decimals and saves asset to localStorage
@ -219,7 +281,10 @@ export default function TokenizeAsset() {
}
}
const handleTransferAsset = async ( ) = > {
/**
* Transfer ASA (works for NFTs too, since NFTs are ASAs)
*/
const handleTransferAsset = async ( ) = > {
if ( ! transactionSigner || ! activeAddress ) {
enqueueSnackbar ( 'Please connect your wallet first.' , { variant : 'warning' } )
return
@ -252,7 +317,6 @@ export default function TokenizeAsset() {
amount : BigInt ( transferAmount ) ,
} )
// AlgoKit commonly returns txId here
const txId = ( result as { txId? : string } ) . txId
enqueueSnackbar ( '✅ Transfer complete!' , {
@ -280,24 +344,181 @@ export default function TokenizeAsset() {
}
}
/**
* NFT mint helpers
*/
const handleFileChange = ( e : ChangeEvent < HTMLInputElement > ) = > {
const file = e . target . files ? . [ 0 ] || null
setSelectedFile ( file )
setPreviewUrl ( file ? URL . createObjectURL ( file ) : '' )
}
const handleDivClick = ( ) = > fileInputRef . current ? . click ( )
const handleMintNFT = async ( ) = > {
if ( ! transactionSigner || ! activeAddress ) {
enqueueSnackbar ( 'Please connect wallet first' , { variant : 'warning' } )
return
}
if ( ! selectedFile ) {
enqueueSnackbar ( 'Please select an image file to mint.' , { variant : 'warning' } )
return
}
// Validate NFT fields
if ( ! nftName || ! nftUnit ) {
enqueueSnackbar ( 'Please enter an NFT name and unit/symbol.' , { variant : 'warning' } )
return
}
if ( ! nftSupply || ! isWholeNumber ( nftSupply ) ) {
enqueueSnackbar ( 'Supply must be a whole number.' , { variant : 'warning' } )
return
}
if ( ! nftDecimals || ! isWholeNumber ( nftDecimals ) ) {
enqueueSnackbar ( 'Decimals must be a whole number (0– 19).' , { variant : 'warning' } )
return
}
const d = Number ( nftDecimals )
if ( Number . isNaN ( d ) || d < 0 || d > 19 ) {
enqueueSnackbar ( 'NFT decimals must be between 0 and 19.' , { variant : 'warning' } )
return
}
setNftLoading ( true )
enqueueSnackbar ( 'Uploading and preparing NFT...' , { variant : 'info' } )
let metadataUrl = ''
try {
const backendBase = resolveBackendBase ( )
const backendApiUrl = ` ${ backendBase . replace ( /\/$/ , '' ) } /api/pin-image `
const formData = new FormData ( )
formData . append ( 'file' , selectedFile )
const response = await fetch ( backendApiUrl , {
method : 'POST' ,
body : formData ,
mode : 'cors' ,
} )
if ( ! response . ok ) {
const errorText = await response . text ( )
throw new Error ( ` Backend request failed: ${ response . status } - ${ errorText } ` )
}
const data = await response . json ( )
metadataUrl = data . metadataUrl
if ( ! metadataUrl ) throw new Error ( 'Backend did not return a valid metadata URL' )
} catch ( e : any ) {
console . error ( e )
enqueueSnackbar ( 'Error uploading to backend. If in Codespaces, make port 3001 Public.' , { variant : 'error' } )
setNftLoading ( false )
return
}
try {
enqueueSnackbar ( 'Minting NFT on Algorand...' , { variant : 'info' } )
// Demo shortcut: hash the metadata URL string (ARC-3 would hash JSON bytes)
const metadataHash = Uint8Array . from ( sha512_256 . digest ( metadataUrl ) )
const onChainTotal = BigInt ( nftSupply ) * 10 n * * BigInt ( d )
const createNFTResult = await algorand . send . assetCreate ( {
sender : activeAddress ,
signer : transactionSigner ,
total : onChainTotal ,
decimals : d ,
assetName : nftName ,
unitName : nftUnit ,
url : metadataUrl ,
metadataHash ,
defaultFrozen : nftDefaultFrozen ,
manager : nftManager || undefined ,
reserve : nftReserve || undefined ,
freeze : nftFreeze || undefined ,
clawback : nftClawback || undefined ,
} )
const assetId = createNFTResult . assetId
// ✅ Persist minted NFT into SAME history list (NFTs are ASAs)
const nftEntry : CreatedAsset = {
assetId : Number ( assetId ) ,
assetName : String ( nftName ) ,
unitName : String ( nftUnit ) ,
total : String ( nftSupply ) ,
decimals : String ( nftDecimals ) ,
url : metadataUrl ? String ( metadataUrl ) : undefined ,
manager : nftManager ? String ( nftManager ) : undefined ,
reserve : nftReserve ? String ( nftReserve ) : undefined ,
freeze : nftFreeze ? String ( nftFreeze ) : undefined ,
clawback : nftClawback ? String ( nftClawback ) : undefined ,
createdAt : new Date ( ) . toISOString ( ) ,
}
const next = persistAsset ( nftEntry )
setCreatedAssets ( next )
// QoL: prefill transfer section with minted asset id
setTransferAssetId ( String ( assetId ) )
enqueueSnackbar ( ` ✅ Success! NFT Asset ID: ${ assetId } ` , {
variant : 'success' ,
action : ( ) = >
assetId ? (
< a
href = { ` ${ LORA_BASE } /asset/ ${ assetId } ` }
target = "_blank"
rel = "noopener noreferrer"
style = { { textDecoration : 'underline' , marginLeft : 8 } }
>
View on Lora ↗
< / a >
) : null ,
} )
// Reset just the file picker + preview (keep fields, so they can mint many quickly)
setSelectedFile ( null )
setPreviewUrl ( '' )
if ( fileInputRef . current ) fileInputRef . current . value = ''
} catch ( e : any ) {
console . error ( e )
enqueueSnackbar ( ` Failed to mint NFT: ${ e ? . message || 'Unknown error' } ` , { variant : 'error' } )
} finally {
setNftLoading ( false )
}
}
const canSubmit = ! ! assetName && ! ! unitName && ! ! total && ! loading && ! ! activeAddress
const canMintNft =
! ! nftName &&
! ! nftUnit &&
! ! nftSupply &&
! ! nftDecimals &&
! ! selectedFile &&
! ! activeAddress &&
! nftLoading
return (
< div className = "bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-lg p-6 sm:p-8" >
{ /* ===== ORIGINAL TOKENIZE FORM (UNCHANGED) ===== */ }
{ /* Top header */ }
< div className = "flex items-start justify-between gap-3" >
< div className = "flex items-start gap-3" >
< span className = "inline-flex h-12 w-12 items-center justify-center rounded-lg bg-teal-100 dark:bg-teal-900/30" >
< BsCoin className = "text-2xl text-teal-600 dark:text-teal-400" / >
< / span >
< div >
< h2 className = "text-xl sm:text-2xl font-bold tracking-tight text-slate-900 dark:text-white" > Tokenize a n Asset ( Mint ASA ) < / h2 >
< p className = "text-sm text-slate-600 dark:text-slate-400 mt-1" > Create a standard ASA on TestNet . Perfect for RWA POCs . < / p >
< h2 className = "text-xl sm:text-2xl font-bold tracking-tight text-slate-900 dark:text-white" > Tokenize o n Algorand < / h2 >
< p className = "text-sm text-slate-600 dark:text-slate-400 mt-1" > Mint standard ASAs or mint an NFT - style ASA on TestNet . < / p >
< / div >
< / div >
< / div >
{ /* ASA loading bar */ }
{ loading && (
< div className = "relative h-1 w-full mt-5 overflow-hidden rounded bg-slate-200 dark:bg-slate-700" >
< div className = "absolute inset-y-0 left-0 w-1/3 animate-[loading_1.2s_ease-in-out_infinite] bg-teal-600 dark:bg-teal-500" / >
@ -311,167 +532,394 @@ export default function TokenizeAsset() {
< / div >
) }
< div className = { ` mt-6 ${ loading ? 'opacity-50' : '' } ` } >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-5 " >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Asset Name < / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { assetName }
onChange = { ( e ) = > setAssetName ( e . target . value ) }
/ >
< / div >
{ /* MAIN: 2-column panel (ASA left, NFT right) */ }
< div className = "mt-6 " >
< div className = "grid grid-cols-1 lg:grid-cols-2 gap-8" >
{ /* ===== LEFT: ASA TOKENIZE FORM ===== */ }
< div className = { ` ${ loading ? 'opacity-50 pointer-events-none' : '' } ` } >
< div className = "mb-4" >
< h3 className = "text-lg font-bold text-slate-900 dark:text-white" > Tokenize an Asset ( Mint ASA ) < / h3 >
< p className = "text-sm text-slate-600 dark:text-slate-400 mt-1" > Create a standard ASA on TestNet . Perfect for RWA POCs . < / p >
< / div >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Symbol < / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { unitName }
onChange = { ( e ) = > setUnitName ( e . target . value ) }
/ >
< / div >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Total Supply < / label >
< input
type = "number"
min = { 1 }
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { total }
onChange = { ( e ) = > setTotal ( e . target . value ) }
/ >
< / div >
< div >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > Decimals < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
Decimals controls fractional units . 0 = whole units only .
< / div >
< div className = "rounded-2xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm p-5 sm:p-6" >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-5" >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Asset Name < / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { assetName }
onChange = { ( e ) = > setAssetName ( e . target . value ) }
/ >
< / div >
< / label >
< input
type = "number"
min = { 0 }
max = { 19 }
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { decimals }
onChange = { ( e ) = > setDecimals ( e . target . value ) }
/ >
< / div >
< div className = "md:col-span-2" >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > Metadata URL ( optional ) < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = " text-slate-4 00 cursor-help hov er:text-slate-6 00 dark:hov er:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
A public link describing the as set ( JSON , webpa ge, or doc ) .
< / div >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Symbol < / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-9 00 dark:text-white placehold er:text-slate-4 00 dark:placehold er:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { unitName }
onChange = { ( e ) = > setUnitName ( e . tar get . value ) }
/ >
< / div >
< / label >
< input
type = "url"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
placeholder = "https://example.com/metadata.json "
value = { url }
onChange = { ( e ) = > setUrl ( e . target . value ) }
/ >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Total Supply < / label >
< input
type = "number "
min = { 1 }
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { total }
onChange = { ( e ) = > setTotal ( e . target . value ) }
/ >
< / div >
< div >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > Decimals < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
Decimals controls fractional units . 0 = whole units only .
< / div >
< / div >
< / label >
< input
type = "number"
min = { 0 }
max = { 19 }
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { decimals }
onChange = { ( e ) = > setDecimals ( e . target . value ) }
/ >
< / div >
< div className = "md:col-span-2" >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > Metadata URL ( optional ) < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
A public link describing the asset ( JSON , webpage , or doc ) .
< / div >
< / div >
< / label >
< input
type = "url"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
placeholder = "https://example.com/metadata.json"
value = { url }
onChange = { ( e ) = > setUrl ( e . target . value ) }
/ >
< / div >
< / div >
< div className = "mt-6" >
< button
type = "button"
onClick = { ( ) = > setShowAdvanced ( ( s ) = > ! s ) }
className = "flex items-center gap-2 text-sm font-medium text-primary hover:underline transition"
>
< span > { showAdvanced ? 'Hide advanced options' : 'Show advanced options' } < / span >
< span className = { ` transition-transform ${ showAdvanced ? 'rotate-180' : '' } ` } > ▾ < / span >
< / button >
{ showAdvanced && (
< div className = "mt-4 grid grid-cols-1 md:grid-cols-2 gap-5" >
{ [
{
label : 'Manager' ,
tip : 'The manager can update or reconfigure asset settings. Often set to the issuer wallet.' ,
value : manager ,
setValue : setManager ,
placeholder : 'Defaults to your wallet address' ,
} ,
{
label : 'Reserve' ,
tip : 'Reserve may hold non-circulating supply depending on your design. Leave blank to disable.' ,
value : reserve ,
setValue : setReserve ,
placeholder : 'Optional address' ,
} ,
{
label : 'Freeze' ,
tip : 'Freeze can freeze/unfreeze holdings (useful for compliance). Leave blank to disable.' ,
value : freeze ,
setValue : setFreeze ,
placeholder : 'Optional address' ,
} ,
{
label : 'Clawback' ,
tip : 'Clawback can revoke tokens from accounts (recovery/compliance). Leave blank to disable.' ,
value : clawback ,
setValue : setClawback ,
placeholder : 'Optional address' ,
} ,
] . map ( ( f ) = > (
< div key = { f . label } >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > { f . label } < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
{ f . tip }
< / div >
< / div >
< / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
placeholder = { f . placeholder }
value = { f . value }
onChange = { ( e ) = > f . setValue ( e . target . value ) }
/ >
< / div >
) ) }
< / div >
) }
< / div >
< div className = "mt-8 flex flex-col sm:flex-row gap-3 sm:justify-end" >
< button
type = "button"
className = { ` px-6 py-3 rounded-lg font-semibold transition ${
canSubmit
? 'bg-teal-600 hover:bg-teal-700 text-white shadow-md'
: 'bg-slate-300 text-slate-500 cursor-not-allowed dark:bg-slate-700 dark:text-slate-400'
} ` }
onClick = { handleTokenize }
disabled = { ! canSubmit }
>
{ loading ? (
< span className = "flex items-center gap-2" >
< AiOutlineLoading3Quarters className = "animate-spin" / >
Creating …
< / span >
) : (
'Tokenize Asset'
) }
< / button >
< / div >
< / div >
< / div >
< / div >
< div className = "mt-6" >
< button
type = "button"
onClick = { ( ) = > setShowAdvanced ( ( s ) = > ! s ) }
className = "flex items-center gap-2 text-sm font-medium text-primary hover:underline transition"
>
< span > { showAdvanced ? 'Hide advanced options' : 'Show advanced options' } < / span >
< span className = { ` transition-transform ${ showAdvanced ? 'rotate-180' : '' } ` } > ▾ < / span >
< / button >
{ /* ===== RIGHT: NFT MINT PANEL ===== */ }
< div className = { ` ${ nftLoading ? 'opacity-90' : '' } ` } >
< div className = "mb-4" >
< h3 className = "text-lg font-bold text-slate-900 dark:text-white" > Tokenize an NFT ( Mint ASA ) < / h3 >
< p className = "text-sm text-slate-600 dark:text-slate-400 mt-1" >
Upload an image → backend pins to IPFS → mint an ASA with metadata on Algorand TestNet .
< / p >
< / div >
{ showAdvanced && (
< div className = "mt-4 grid grid-cols-1 md:grid-cols-2 gap-5" >
{ [
{
label: 'Manager' ,
tip : 'The manager can update or reconfigure asset settings. Often set to the issuer wallet.' ,
value : manager ,
setValue : setManager ,
placeholder : 'Defaults to your wallet address' ,
},
{
label : 'Reserve' ,
tip : 'Reserve may hold non-circulating supply depending on your design. Leave blank to disable.' ,
value : reserve ,
setValue : setReserve ,
placeholder : 'Optional address' ,
} ,
{
label : 'Freeze' ,
tip : 'Freeze can freeze/unfreeze holdings (useful for compliance). Leave blank to disable.' ,
value : freeze ,
setValue : setFreeze ,
placeholder : 'Optional address' ,
} ,
{
label : 'Clawback' ,
tip : 'Clawback can revoke tokens from accounts (recovery/compliance). Leave blank to disable.' ,
value : clawback ,
setValue : setClawback ,
placeholder : 'Optional address' ,
},
] . map ( ( f ) = > (
< div key = { f . label } >
< div className = { ` rounded-2xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm p-5 sm:p-6 ${ nftLoading ? 'pointer-events-none opacity-70' : '' } ` } >
{ /* NFT fields */ }
< div className = "grid grid-cols-1 md:grid-cols-2 gap-5" >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Name < / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { nftName }
onChange = { ( e ) = > setNftName ( e . target . value ) }
/ >
< / div >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Unit / Symbol < / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { nftUnit }
onChange = { ( e ) = > setNftUnit ( e . target . value ) }
/ >
< / div >
< div >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Supply < / label >
< input
type = "number"
min = { 1 }
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
value = { nftSupply }
onChange = { ( e ) = > setNftSupply ( e . target . value ) }
/ >
< p className = "mt-1 text-[11px] text-slate-500 dark:text-slate-400" > For a true 1 / 1 NFT , set supply = 1 . < / p >
< / div >
< div >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > { f . label } < / span >
< span > Decimals < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
{ f . tip }
Decimals controls fractional units . For a typical NFT , use 0 .
< / div >
< / div >
< / label >
< input
type = "text "
type = "number "
min = { 0 }
max = { 19 }
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
placeholder = { f . placeholder }
valu e= { f . value }
onChange = { ( e ) = > f . setValue ( e . target . value ) }
value = { nftDecimals }
onChang e= { ( e ) = > setNftDecimals ( e . target . value ) }
/ >
< / div >
) ) }
< div className = "md:col-span-2" >
< label className = "flex items-center gap-3 text-sm font-semibold text-slate-700 dark:text-slate-300" >
< input
type = "checkbox"
checked = { nftDefaultFrozen }
onChange = { ( e ) = > setNftDefaultFrozen ( e . target . checked ) }
className = "h-4 w-4 rounded border border-slate-300 dark:border-slate-600"
/ >
< span > Default Frozen < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
If enabled , new holdings start frozen until unfrozen by the Freeze account .
< / div >
< / div >
< / label >
< / div >
< / div >
{ /* NFT advanced options toggle */ }
< div className = "mt-6" >
< button
type = "button"
onClick = { ( ) = > setNftShowAdvanced ( ( s ) = > ! s ) }
className = "flex items-center gap-2 text-sm font-medium text-primary hover:underline transition"
>
< span > { nftShowAdvanced ? 'Hide advanced options' : 'Show advanced options' } < / span >
< span className = { ` transition-transform ${ nftShowAdvanced ? 'rotate-180' : '' } ` } > ▾ < / span >
< / button >
{ nftShowAdvanced && (
< div className = "mt-4 grid grid-cols-1 md:grid-cols-2 gap-5" >
{ [
{
label : 'Manager' ,
tip : 'Manager can update or reconfigure asset settings. Often set to the issuer wallet.' ,
value : nftManager ,
setValue : setNftManager ,
placeholder : 'Defaults to your wallet address' ,
} ,
{
label : 'Reserve' ,
tip : 'Reserve can hold non-circulating supply depending on design. Leave blank to disable.' ,
value : nftReserve ,
setValue : setNftReserve ,
placeholder : 'Optional address' ,
} ,
{
label : 'Freeze' ,
tip : 'Freeze can freeze/unfreeze holdings (useful for compliance). Leave blank to disable.' ,
value : nftFreeze ,
setValue : setNftFreeze ,
placeholder : 'Optional address' ,
} ,
{
label : 'Clawback' ,
tip : 'Clawback can revoke tokens from accounts (recovery/compliance). Leave blank to disable.' ,
value : nftClawback ,
setValue : setNftClawback ,
placeholder : 'Optional address' ,
} ,
] . map ( ( f ) = > (
< div key = { f . label } >
< label className = "flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" >
< span > { f . label } < / span >
< div className = "group relative" >
< AiOutlineInfoCircle className = "text-slate-400 cursor-help hover:text-slate-600 dark:hover:text-slate-300" / >
< div className = "invisible group-hover:visible bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 text-xs rounded px-2 py-1 whitespace-nowrap absolute bottom-full left-0 mb-1 z-10" >
{ f . tip }
< / div >
< / div >
< / label >
< input
type = "text"
className = "w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 border border-slate-300 dark:border-slate-600 focus:outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200 dark:focus:ring-teal-900/30 px-4 py-2 transition"
placeholder = { f . placeholder }
value = { f . value }
onChange = { ( e ) = > f . setValue ( e . target . value ) }
/ >
< / div >
) ) }
< / div >
) }
< / div >
{ /* Image upload */ }
< div className = "mt-6" >
< label className = "block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2" > Select an image < / label >
< div
className = "flex flex-col items-center justify-center p-6 border-2 border-dashed border-slate-200 dark:border-slate-700 rounded-xl cursor-pointer bg-slate-50 dark:bg-slate-800/40 hover:border-teal-200 dark:hover:border-teal-700 transition-colors"
onClick = { handleDivClick }
>
{ previewUrl ? (
< img src = { previewUrl } alt = "NFT preview" className = "rounded-lg max-h-48 object-contain shadow-sm bg-white dark:bg-slate-900" / >
) : (
< div className = "text-center" >
< AiOutlineCloudUpload className = "mx-auto h-12 w-12 text-slate-400" / >
< p className = "mt-2 text-sm text-slate-700 dark:text-slate-200" > Drag and drop or click to upload < / p >
< p className = "text-xs text-slate-500 dark:text-slate-400" > PNG , JPG , GIF up to 10 MB < / p >
< / div >
) }
< input
type = "file"
ref = { fileInputRef }
className = "sr-only"
onChange = { handleFileChange }
accept = "image/png, image/jpeg, image/gif"
/ >
< / div >
< / div >
{ /* Buttons */ }
< div className = "mt-6 flex flex-col sm:flex-row gap-3 sm:justify-end" >
< button
type = "button"
className = "px-6 py-3 rounded-lg font-semibold transition bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-700"
onClick = { resetNftDefaults }
disabled = { nftLoading }
>
Reset
< / button >
< button
type = "button"
onClick = { handleMintNFT }
disabled = { ! canMintNft }
className = { ` px-6 py-3 rounded-lg font-semibold transition ${
canMintNft ? 'bg-teal-600 hover:bg-teal-700 text-white shadow-md' : 'bg-slate-300 text-slate-500 cursor-not-allowed dark:bg-slate-700 dark:text-slate-400'
} ` }
>
{ nftLoading ? (
< span className = "flex items-center gap-2" >
< AiOutlineLoading3Quarters className = "animate-spin" / >
Minting …
< / span >
) : (
'Mint NFT'
) }
< / button >
< / div >
< p className = "mt-3 text-xs text-slate-500 dark:text-slate-400 flex items-center gap-2" >
< AiOutlineInfoCircle / >
Uses backend < span className = "font-mono" > / api / pin - image < / span > . In Codespaces , make port 3001 Public .
< / p >
< / div >
) }
< / div >
< / div >
< div className = "mt-8 flex flex-col sm:flex-row gap-3 sm:justify-end" >
< button
type = "button"
className = { ` px-6 py-3 rounded-lg font-semibold transition ${
canSubmit
? 'bg-teal-600 hover:bg-teal-700 text-white shadow-md'
: 'bg-slate-300 text-slate-500 cursor-not-allowed dark:bg-slate-700 dark:text-slate-400'
} ` }
onClick = { handleTokenize }
disabled = { ! canSubmit }
>
{ loading ? (
< span className = "flex items-center gap-2" >
< AiOutlineLoading3Quarters className = "animate-spin" / >
Creating …
< / span >
) : (
'Tokenize Asset'
) }
< / button >
< / div >
{ /* ===== MY CREATED ASSETS ===== */ }
{ /* ===== MY CREATED ASSETS ===== */ }
< div className = "mt-10" >
< div className = "flex items-center justify-between mb-4" >
< h3 className = "text-lg font-bold text-slate-900 dark:text-white" > My Created Assets < / h3 >
@ -549,9 +997,7 @@ export default function TokenizeAsset() {
{ /* ===== TRANSFER ASSET ===== */ }
< div className = "mt-12 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-lg p-6 sm:p-8" >
< h3 className = "text-lg font-bold text-slate-900 dark:text-white mb-2" > Transfer Asset < / h3 >
< p className = "text-sm text-slate-600 dark:text-slate-400 mb-6" >
Send units of an Algorand Standard Asset ( ASA ) to another wallet .
< / p >
< p className = "text-sm text-slate-600 dark:text-slate-400 mb-6" > Send units of an Algorand Standard Asset ( ASA ) to another wallet . < / p >
< div className = "grid grid-cols-1 md:grid-cols-3 gap-4" >
< div >