From 73943bc5d6ce0d2525b71b6fa018d1a7cc6218a1 Mon Sep 17 00:00:00 2001 From: BigBen-7 Date: Sat, 20 Sep 2025 14:09:49 +0100 Subject: [PATCH 1/4] feat/Develop a Token-Gated Content/Feature Hook --- app/profile/page.tsx | 15 +++++++++---- components/web3/VipBadge.tsx | 37 +++++++++++++++++++++++++++++++ hooks/useTokenGatedAccess.ts | 43 ++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 components/web3/VipBadge.tsx create mode 100644 hooks/useTokenGatedAccess.ts diff --git a/app/profile/page.tsx b/app/profile/page.tsx index d78d05f..d80a2e1 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -6,6 +6,9 @@ import { redirect } from 'next/navigation' import type { UserProfile } from '@/lib/api-schema' import ProfileForm from '@/components/profile/ProfileForm' import ContractDemo from '@/components/web3/ContractDemo' +import ContractDemo from '@/components/web3/ContractDemo' +import VipBadge from '@/components/web3/VipBadge' // Import the new badge + const fetchProfile = async (): Promise => { const res = await fetch('/api/profile') @@ -34,13 +37,17 @@ export default function ProfilePage() { ) } - return ( -
+ return ( +
-

Edit Your Profile

+
+

Edit Your Profile

+ {/* Add the VipBadge right next to the title */} + +
{profile && }
- +

Web3 Contract Interaction

diff --git a/components/web3/VipBadge.tsx b/components/web3/VipBadge.tsx new file mode 100644 index 0000000..b3de3dc --- /dev/null +++ b/components/web3/VipBadge.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useTokenGatedAccess } from '@/hooks/useTokenGatedAccess' +import { proofOfFandomContract } from '@/lib/contracts' + +export default function VipBadge() { + const { isLoading, hasAccess } = useTokenGatedAccess({ + contract: proofOfFandomContract, + }) + + if (isLoading) { + return
+ } + + if (!hasAccess) { + return null // If the user doesn't have the token, render nothing + } + + // If the user has access, show the exclusive badge + return ( +
+ + + + Fandom VIP +
+ ) +} \ No newline at end of file diff --git a/hooks/useTokenGatedAccess.ts b/hooks/useTokenGatedAccess.ts new file mode 100644 index 0000000..5ee181f --- /dev/null +++ b/hooks/useTokenGatedAccess.ts @@ -0,0 +1,43 @@ +'use client' + +import { useAccount, useReadContract } from 'wagmi' +import { Abi } from 'viem' + +interface UseTokenGatedAccessProps { + contract: { + address: `0x${string}` + abi: Abi + } + requiredBalance?: bigint +} + +/** + * A hook to check if the connected user has a sufficient balance of a token. + * @param {object} contract - The contract configuration { address, abi }. + * @param {bigint} [requiredBalance=1n] - The minimum balance required for access. Defaults to 1. + * @returns An object with { isLoading, hasAccess, balance }. + */ +export function useTokenGatedAccess({ + contract, + requiredBalance = 1n, // Using bigint for token amounts +}: UseTokenGatedAccessProps) { + const { address, isConnected } = useAccount() + + const { data: balance, isLoading } = useReadContract({ + ...contract, + functionName: 'balanceOf', + args: [address!], + query: { + enabled: isConnected && !!address, + }, + }) + + // Determine if the user has access + const hasAccess = balance !== undefined && balance >= requiredBalance + + return { + isLoading, + hasAccess, + balance, + } +} \ No newline at end of file From afdb598fbe50ffcc14b8a179cb51a1dfd2692b52 Mon Sep 17 00:00:00 2001 From: BigBen-7 Date: Sat, 20 Sep 2025 14:10:33 +0100 Subject: [PATCH 2/4] feat/Develop a Token-Gated Content/Feature Hook --- app/profile/page.tsx | 5 ++--- components/web3/VipBadge.tsx | 6 ++++-- hooks/useTokenGatedAccess.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/profile/page.tsx b/app/profile/page.tsx index d80a2e1..cff1a74 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -9,7 +9,6 @@ import ContractDemo from '@/components/web3/ContractDemo' import ContractDemo from '@/components/web3/ContractDemo' import VipBadge from '@/components/web3/VipBadge' // Import the new badge - const fetchProfile = async (): Promise => { const res = await fetch('/api/profile') if (!res.ok) throw new Error('Failed to fetch profile') @@ -37,7 +36,7 @@ export default function ProfilePage() { ) } - return ( + return (
@@ -47,7 +46,7 @@ export default function ProfilePage() {
{profile && }
- +

Web3 Contract Interaction

diff --git a/components/web3/VipBadge.tsx b/components/web3/VipBadge.tsx index b3de3dc..e39ebb9 100644 --- a/components/web3/VipBadge.tsx +++ b/components/web3/VipBadge.tsx @@ -9,7 +9,9 @@ export default function VipBadge() { }) if (isLoading) { - return
+ return ( +
+ ) } if (!hasAccess) { @@ -34,4 +36,4 @@ export default function VipBadge() { Fandom VIP
) -} \ No newline at end of file +} diff --git a/hooks/useTokenGatedAccess.ts b/hooks/useTokenGatedAccess.ts index 5ee181f..2018836 100644 --- a/hooks/useTokenGatedAccess.ts +++ b/hooks/useTokenGatedAccess.ts @@ -40,4 +40,4 @@ export function useTokenGatedAccess({ hasAccess, balance, } -} \ No newline at end of file +} From aa82707f3fd8b546c896535abf919a9ca7c25210 Mon Sep 17 00:00:00 2001 From: BigBen-7 Date: Sat, 20 Sep 2025 14:36:39 +0100 Subject: [PATCH 3/4] feat(web3): Integrate Flare Time Series Oracle (FTSO) for Price Feeds --- app/profile/page.tsx | 10 +++++- components/web3/FtsoPriceFeed.tsx | 45 ++++++++++++++++++++++++++ components/web3/TippingDemo.tsx | 53 +++++++++++++++++++++++++++++++ lib/abi/PriceSubmitter.json | 26 +++++++++++++++ lib/abi/contracts.ts | 11 +++++++ 5 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 components/web3/FtsoPriceFeed.tsx create mode 100644 components/web3/TippingDemo.tsx create mode 100644 lib/abi/PriceSubmitter.json diff --git a/app/profile/page.tsx b/app/profile/page.tsx index cff1a74..b1e7edd 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -7,7 +7,8 @@ import type { UserProfile } from '@/lib/api-schema' import ProfileForm from '@/components/profile/ProfileForm' import ContractDemo from '@/components/web3/ContractDemo' import ContractDemo from '@/components/web3/ContractDemo' -import VipBadge from '@/components/web3/VipBadge' // Import the new badge +import VipBadge from '@/components/web3/VipBadge' +import TippingDemo from '@/components/web3/TippingDemo' const fetchProfile = async (): Promise => { const res = await fetch('/api/profile') @@ -47,6 +48,13 @@ export default function ProfilePage() { {profile && }
+
+

+ Tipping Demo & FTSO Price Feed +

+ +
+

Web3 Contract Interaction

diff --git a/components/web3/FtsoPriceFeed.tsx b/components/web3/FtsoPriceFeed.tsx new file mode 100644 index 0000000..8d0fcfc --- /dev/null +++ b/components/web3/FtsoPriceFeed.tsx @@ -0,0 +1,45 @@ +'use client' + +import { priceSubmitterContractSGB } from '@/lib/contracts' +import { useReadContract } from 'wagmi' +import { formatUnits } from 'viem' + +interface FtsoPriceFeedProps { + symbol: 'SGB' | 'FLR' // Extendable for other tokens +} + +export default function FtsoPriceFeed({ symbol }: FtsoPriceFeedProps) { + const { data: priceData, isLoading, isError } = useReadContract({ + ...priceSubmitterContractSGB, + functionName: 'getPrice', + args: [symbol], + // Automatically refetch the price every 5 seconds to keep it live + query: { + refetchInterval: 5000, + }, + }) + + if (isLoading) { + return Loading price... + } + + if (isError || !priceData) { + return Price unavailable + } + + // The FTSO returns the price with 5 decimal places. + // We use formatUnits to correctly place the decimal point. + const price = Number(formatUnits(priceData[0], 5)) + const timestamp = new Date(Number(priceData[1]) * 1000) + + return ( +
+

+ Live Price: ${price.toFixed(5)} +

+

+ Last Updated: {timestamp.toLocaleTimeString()} +

+
+ ) +} \ No newline at end of file diff --git a/components/web3/TippingDemo.tsx b/components/web3/TippingDemo.tsx new file mode 100644 index 0000000..61351a2 --- /dev/null +++ b/components/web3/TippingDemo.tsx @@ -0,0 +1,53 @@ +'use client' + +import { priceSubmitterContractSGB } from '@/lib/contracts' +import { useState } from 'react' +import { useReadContract } from 'wagmi' +import { formatUnits } from 'viem' +import FtsoPriceFeed from './FtsoPriceFeed' + +export default function TippingDemo() { + const [tipAmount, setTipAmount] = useState('1000') + + // We fetch the price here as well to use in our calculation + const { data: priceData } = useReadContract({ + ...priceSubmitterContractSGB, + functionName: 'getPrice', + args: ['SGB'], + query: { + refetchInterval: 5000, + }, + }) + + let usdValue = 0 + if (priceData && tipAmount) { + const price = Number(formatUnits(priceData[0], 5)) + usdValue = Number(tipAmount) * price + } + + return ( +
+

Tip an Artist (Demo)

+

See the FTSO in action by calculating a tip's value in real-time.

+ +
+ setTipAmount(e.target.value)} + className='w-full p-2 rounded-md border-gray-300 shadow-sm sm:text-sm dark:bg-gray-800 dark:border-gray-600' + placeholder='SGB Amount' + /> + SGB +
+ +
+

Equivalent Value (USD)

+

${usdValue.toFixed(2)}

+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/lib/abi/PriceSubmitter.json b/lib/abi/PriceSubmitter.json new file mode 100644 index 0000000..cfbec6f --- /dev/null +++ b/lib/abi/PriceSubmitter.json @@ -0,0 +1,26 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "_symbol", + "type": "string" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/lib/abi/contracts.ts b/lib/abi/contracts.ts index 47bcfeb..3b38033 100644 --- a/lib/abi/contracts.ts +++ b/lib/abi/contracts.ts @@ -1,12 +1,23 @@ import proofOfFandomAbi from './abi/ProofOfFandom.json' +import priceSubmitterAbi from './PriceSubmitter.json' // Import the new ABI import { flare, songbird } from './chains' const proofOfFandomAddress = '0x1234567890123456789012345678901234567890' as const +const priceSubmitterAddressSGB = + '0x1000000000000000000000000000000000000003' as const + export const proofOfFandomContract = { address: proofOfFandomAddress, abi: proofOfFandomAbi, chains: [flare.id, songbird.id], } + +export const priceSubmitterContractSGB = { + address: priceSubmitterAddressSGB, + abi: priceSubmitterAbi, + // This contract is on the Songbird chain + chains: [songbird.id], +} From cbdd83611d091d138fee6b1a1a6265ba2ddf104e Mon Sep 17 00:00:00 2001 From: BigBen-7 Date: Sat, 20 Sep 2025 14:37:15 +0100 Subject: [PATCH 4/4] remaining --- components/web3/FtsoPriceFeed.tsx | 11 ++++++++--- components/web3/TippingDemo.tsx | 12 ++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/components/web3/FtsoPriceFeed.tsx b/components/web3/FtsoPriceFeed.tsx index 8d0fcfc..94693a0 100644 --- a/components/web3/FtsoPriceFeed.tsx +++ b/components/web3/FtsoPriceFeed.tsx @@ -9,7 +9,11 @@ interface FtsoPriceFeedProps { } export default function FtsoPriceFeed({ symbol }: FtsoPriceFeedProps) { - const { data: priceData, isLoading, isError } = useReadContract({ + const { + data: priceData, + isLoading, + isError, + } = useReadContract({ ...priceSubmitterContractSGB, functionName: 'getPrice', args: [symbol], @@ -35,11 +39,12 @@ export default function FtsoPriceFeed({ symbol }: FtsoPriceFeedProps) { return (

- Live Price: ${price.toFixed(5)} + Live Price:{' '} + ${price.toFixed(5)}

Last Updated: {timestamp.toLocaleTimeString()}

) -} \ No newline at end of file +} diff --git a/components/web3/TippingDemo.tsx b/components/web3/TippingDemo.tsx index 61351a2..09859f3 100644 --- a/components/web3/TippingDemo.tsx +++ b/components/web3/TippingDemo.tsx @@ -28,8 +28,10 @@ export default function TippingDemo() { return (

Tip an Artist (Demo)

-

See the FTSO in action by calculating a tip's value in real-time.

- +

+ See the FTSO in action by calculating a tip's value in real-time. +

+
-

Equivalent Value (USD)

+

+ Equivalent Value (USD) +

${usdValue.toFixed(2)}

@@ -50,4 +54,4 @@ export default function TippingDemo() {
) -} \ No newline at end of file +}