@@ -60,6 +60,7 @@ const INFERNO_PULSE = {
6060 imageCid : "bafybeidm2pqh5ty5ltlt4zpl3osy2t27annf5zui5ur6j6uxwk2oco24zm" ,
6161 localImagePath : "/nfts/inferno-pulse.png" ,
6262 name : "StackUp: Inferno Pulse" ,
63+ kind : 101 ,
6364} as const ;
6465
6566type NftOverrideEntry = {
@@ -115,6 +116,7 @@ export default function ClientPage() {
115116 Record < number , number | null >
116117 > ( { } ) ;
117118 const [ milestones , setMilestones ] = useState < number [ ] | null > ( null ) ;
119+ const [ badgeUris , setBadgeUris ] = useState < Record < number , string | null > > ( { } ) ;
118120 const [ adminOpen , setAdminOpen ] = useState < boolean > ( false ) ;
119121 const [ adminUnlocked , setAdminUnlocked ] = useState < boolean > ( false ) ;
120122 const [ adminTapCount , setAdminTapCount ] = useState < number > ( 0 ) ;
@@ -131,6 +133,8 @@ export default function ClientPage() {
131133 const [ collectiblesStatus , setCollectiblesStatus ] = useState <
132134 "idle" | "loading" | "loaded" | "error"
133135 > ( "idle" ) ;
136+ const [ infernoFeeUstx , setInfernoFeeUstx ] = useState < number | null > ( null ) ;
137+ const [ infernoUri , setInfernoUri ] = useState < string | null > ( null ) ;
134138 const [ nftOverrides , setNftOverrides ] = useState < NftOverrides > ( {
135139 byTokenId : { } ,
136140 byKind : { } ,
@@ -346,10 +350,20 @@ export default function ClientPage() {
346350 ) ;
347351
348352 const badgeKinds = new Set < number > ( BADGE_MILESTONES . map ( ( m ) => m . kind ) ) ;
353+ const badgeTokenIdSet = new Set < number > (
354+ Object . values ( badgeTokenIds )
355+ . filter ( ( v ) : v is number => typeof v === "number" && Number . isFinite ( v ) )
356+ ) ;
357+ const badgeUriSet = new Set < string > (
358+ Object . values ( badgeUris )
359+ . filter ( ( v ) : v is string => typeof v === "string" && v . length > 0 )
360+ ) ;
349361
350362 const collectibleItems : OwnedCollectible [ ] = [ ] ;
351363 for ( const info of tokenInfo ) {
364+ if ( badgeTokenIdSet . has ( info . tokenId ) ) continue ;
352365 if ( info . kind !== null && badgeKinds . has ( info . kind ) ) continue ;
366+ if ( info . metadataUri && badgeUriSet . has ( info . metadataUri ) ) continue ;
353367 if ( info . kind === null ) {
354368 // If we can't resolve kind, treat it as non-badge but keep it safe in UI.
355369 }
@@ -420,7 +434,7 @@ export default function ClientPage() {
420434 setCollectiblesStatus ( "error" ) ;
421435 }
422436 } ,
423- [ stacksApiBase , getOverrideForToken , resolveLocalNftImage ]
437+ [ stacksApiBase , getOverrideForToken , resolveLocalNftImage , badgeTokenIds , badgeUris ]
424438 ) ;
425439
426440 useEffect ( ( ) => {
@@ -678,14 +692,32 @@ export default function ClientPage() {
678692
679693 try {
680694 const senderAddress = sender ;
681- const milestonesCV = await fetchCallReadOnlyFunction ( {
682- contractAddress : CONTRACT_ADDRESS ,
683- contractName : CONTRACT_NAME ,
684- functionName : "get-milestones" ,
685- functionArgs : [ ] ,
686- network : STACKS_NETWORK_OBJ ,
687- senderAddress,
688- } ) ;
695+ const [ milestonesCV , infernoFeeCV , infernoUriCV ] = await Promise . all ( [
696+ fetchCallReadOnlyFunction ( {
697+ contractAddress : CONTRACT_ADDRESS ,
698+ contractName : CONTRACT_NAME ,
699+ functionName : "get-milestones" ,
700+ functionArgs : [ ] ,
701+ network : STACKS_NETWORK_OBJ ,
702+ senderAddress,
703+ } ) ,
704+ fetchCallReadOnlyFunction ( {
705+ contractAddress : CONTRACT_ADDRESS ,
706+ contractName : CONTRACT_NAME ,
707+ functionName : "get-mint-fee-kind" ,
708+ functionArgs : [ uintCV ( INFERNO_PULSE . kind ) ] ,
709+ network : STACKS_NETWORK_OBJ ,
710+ senderAddress,
711+ } ) ,
712+ fetchCallReadOnlyFunction ( {
713+ contractAddress : CONTRACT_ADDRESS ,
714+ contractName : CONTRACT_NAME ,
715+ functionName : "get-badge-uri" ,
716+ functionArgs : [ uintCV ( INFERNO_PULSE . kind ) ] ,
717+ network : STACKS_NETWORK_OBJ ,
718+ senderAddress,
719+ } ) ,
720+ ] ) ;
689721
690722 const ms = cvToValue ( milestonesCV ) as unknown ;
691723
@@ -699,6 +731,43 @@ export default function ClientPage() {
699731 setMilestones ( null ) ;
700732 }
701733
734+ const feeUnwrapped = unwrapCvToValue ( cvToValue ( infernoFeeCV ) as unknown ) ;
735+ const fee =
736+ feeUnwrapped === null
737+ ? null
738+ : typeof feeUnwrapped === "bigint"
739+ ? Number ( feeUnwrapped )
740+ : typeof feeUnwrapped === "number"
741+ ? feeUnwrapped
742+ : null ;
743+ setInfernoFeeUstx ( fee ) ;
744+
745+ const uriUnwrapped = unwrapCvToValue ( cvToValue ( infernoUriCV ) as unknown ) ;
746+ setInfernoUri ( typeof uriUnwrapped === "string" ? uriUnwrapped : null ) ;
747+
748+ try {
749+ const kindsToLoad = BADGE_MILESTONES . map ( ( m ) => m . kind ) ;
750+ const uriPairs = await Promise . all (
751+ kindsToLoad . map ( async ( kind ) => {
752+ const v = await fetchCallReadOnlyFunction ( {
753+ contractAddress : CONTRACT_ADDRESS ,
754+ contractName : CONTRACT_NAME ,
755+ functionName : "get-badge-uri" ,
756+ functionArgs : [ uintCV ( kind ) ] ,
757+ network : STACKS_NETWORK_OBJ ,
758+ senderAddress,
759+ } ) ;
760+ const u = unwrapCvToValue ( cvToValue ( v ) as unknown ) ;
761+ return [ kind , typeof u === "string" ? u : null ] as const ;
762+ } )
763+ ) ;
764+ const nextMap : Record < number , string | null > = { } ;
765+ for ( const [ k , u ] of uriPairs ) nextMap [ k ] = u ;
766+ setBadgeUris ( nextMap ) ;
767+ } catch {
768+ // ignore
769+ }
770+
702771 } catch {
703772 // Older contracts might not expose these admin read-only endpoints.
704773 setMilestones ( null ) ;
@@ -1011,6 +1080,38 @@ export default function ClientPage() {
10111080 }
10121081 } ;
10131082
1083+ const mintInfernoPulse = async ( ) => {
1084+ if ( ! address ) {
1085+ setError ( "Connect wallet first." ) ;
1086+ return ;
1087+ }
1088+
1089+ setError ( "" ) ;
1090+ setStatus ( "Minting Inferno Pulse..." ) ;
1091+
1092+ try {
1093+ openContractCall ( {
1094+ contractAddress : CONTRACT_ADDRESS ,
1095+ contractName : CONTRACT_NAME ,
1096+ functionName : "mint-paid-kind" ,
1097+ functionArgs : [ uintCV ( INFERNO_PULSE . kind ) ] ,
1098+ network : STACKS_NETWORK_OBJ ,
1099+ appDetails : {
1100+ name : APP_NAME ,
1101+ icon : new URL ( APP_ICON_PATH , window . location . origin ) . toString ( ) ,
1102+ } ,
1103+ onFinish : ( ) => {
1104+ setStatus ( "Mint submitted" ) ;
1105+ scheduleRefresh ( address ) ;
1106+ } ,
1107+ onCancel : ( ) => setStatus ( "Mint cancelled" ) ,
1108+ } ) ;
1109+ } catch {
1110+ setError ( "Failed to open mint transaction." ) ;
1111+ setStatus ( "Mint failed" ) ;
1112+ }
1113+ } ;
1114+
10141115 return (
10151116 < div className = { styles . page } >
10161117 < div className = { styles . shell } >
@@ -1247,6 +1348,47 @@ export default function ClientPage() {
12471348 < span className = { styles . pill } > Owned</ span >
12481349 </ div >
12491350 < div className = { styles . stack } >
1351+ < div className = { styles . dropCard } >
1352+ < div className = { styles . dropLeft } >
1353+ < div className = { styles . dropThumb } >
1354+ < Image
1355+ src = { INFERNO_PULSE . localImagePath }
1356+ alt = { INFERNO_PULSE . name }
1357+ width = { 96 }
1358+ height = { 96 }
1359+ unoptimized
1360+ style = { { width : "100%" , height : "100%" , objectFit : "contain" } }
1361+ />
1362+ </ div >
1363+ < div className = { styles . dropMeta } >
1364+ < div className = { styles . dropTitle } > { INFERNO_PULSE . name } </ div >
1365+ < div className = { styles . dropLine } >
1366+ Kind < code > u{ INFERNO_PULSE . kind } </ code >
1367+ { " · " }
1368+ { infernoFeeUstx === null
1369+ ? "Price: —"
1370+ : `Price: ${ ( infernoFeeUstx / 1_000_000 ) . toFixed ( 2 ) } STX` }
1371+ </ div >
1372+ < div className = { styles . dropLine } >
1373+ URI{ " " }
1374+ < code >
1375+ { infernoUri ? infernoUri : "not set" }
1376+ </ code >
1377+ </ div >
1378+ </ div >
1379+ </ div >
1380+ < div className = { styles . dropRight } >
1381+ < button
1382+ className = { styles . button }
1383+ onClick = { mintInfernoPulse }
1384+ disabled = { ! address || ! infernoUri }
1385+ type = "button"
1386+ >
1387+ Mint
1388+ </ button >
1389+ </ div >
1390+ </ div >
1391+
12501392 { ! address ? (
12511393 < div className = { styles . emptyState } >
12521394 < div className = { styles . emptyTitle } > Connect to view NFTs.</ div >
0 commit comments