diff --git a/package.json b/package.json index ffce819..5a205ab 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "ts-node": "^10.9.2", "tw-animate-css": "^1.2.4", "uuid": "^11.1.0", - "viem": "^2.23.15", + "viem": "^2.25.0", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b38d8ac..4f08efd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,7 +138,7 @@ importers: specifier: ^11.1.0 version: 11.1.0 viem: - specifier: ^2.23.15 + specifier: ^2.25.0 version: 2.25.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.2) zustand: specifier: ^5.0.3 diff --git a/src/app/api/geturl/route.ts b/src/app/api/geturl/route.ts new file mode 100644 index 0000000..6c149fb --- /dev/null +++ b/src/app/api/geturl/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server'; + +// Route Segment Config - ensures this runs on the server only +export const runtime = 'nodejs'; + +// List of advertisement URLs +const AD_URLS = [ + 'https://self.xyz', + 'https://ethglobal.com', + 'https://aviral.software', + 'https://tinyurl.com/admojotap', + 'https://metal.build', +]; + +// Keep track of the current URL and when it was last updated +let currentUrl = AD_URLS[0]; +let lastUpdated = Date.now(); +const UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes in milliseconds + +/** + * GET handler for /api/geturl + * Returns a random advertisement URL that changes every 5 minutes + */ +export async function GET() { + const now = Date.now(); + + // Check if it's time to update the URL + if (now - lastUpdated > UPDATE_INTERVAL) { + // Get a random URL from the list (different from the current one) + let newUrlIndex; + do { + newUrlIndex = Math.floor(Math.random() * AD_URLS.length); + } while (AD_URLS[newUrlIndex] === currentUrl && AD_URLS.length > 1); + + currentUrl = AD_URLS[newUrlIndex]; + lastUpdated = now; + } + + return NextResponse.json({ url: currentUrl }); +} \ No newline at end of file diff --git a/src/app/api/registerTap/route.ts b/src/app/api/registerTap/route.ts new file mode 100644 index 0000000..5ec9c7f --- /dev/null +++ b/src/app/api/registerTap/route.ts @@ -0,0 +1,145 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { recordTap } from '@/lib/thingspeak'; +import { updateTaps } from '@/lib/blockchain'; + +// Route Segment Config - ensures this runs on the server only +export const runtime = 'nodejs'; + +/** + * Default device ID to use if none is provided + * In a real application, this would be dynamically determined + */ +const DEFAULT_DEVICE_ID = 1; + +/** + * Get URL from the geturl API + */ +async function getAdUrl(origin: string): Promise { + try { + const adResponse = await fetch(`${origin}/api/geturl`); + if (!adResponse.ok) { + throw new Error(`Failed to get ad URL: ${adResponse.status}`); + } + const data = await adResponse.json(); + return data.url; + } catch (error) { + console.error('Error getting ad URL:', error); + return 'https://example.com/fallback'; + } +} + +/** + * POST handler for /api/registerTap + * Records a tap in ThingSpeak and updates the blockchain + */ +export async function POST(request: NextRequest) { + // Get the device ID from the request + let deviceId: number; + + try { + const body = await request.json(); + deviceId = body.deviceId || DEFAULT_DEVICE_ID; + } catch { + // If there's an error parsing the body, use the default device ID + deviceId = DEFAULT_DEVICE_ID; + } + + // Get the ad URL first - we'll need this regardless of success/failure + const url = await getAdUrl(request.nextUrl.origin); + + try { + // Step 1: Record the tap in ThingSpeak + console.log(`Recording tap for device ${deviceId} in ThingSpeak...`); + try { + await recordTap(deviceId); + console.log('Successfully recorded tap in ThingSpeak'); + } catch (thingspeakError) { + console.error('ThingSpeak error:', thingspeakError); + // Continue to blockchain step, don't fail completely + } + + // Step 2: Update the tap on the blockchain + console.log(`Updating tap for device ${deviceId} on blockchain...`); + let txHash; + try { + txHash = await updateTaps(deviceId, 1); + console.log('Successfully updated tap on blockchain, txHash:', txHash); + } catch (blockchainError) { + console.error('Blockchain error:', blockchainError); + // This is server-side only, so it's more likely to fail in development + // For production, consider handling this more gracefully + txHash = 'blockchain-update-skipped'; + } + + // Return success with the advertisement URL and transaction hash + return NextResponse.json({ + success: true, + url, + txHash, + deviceId + }); + } catch (error) { + console.error('Error registering tap:', error); + + // Return an error response with the ad URL + return NextResponse.json({ + success: false, + url, + error: error instanceof Error ? error.message : 'Failed to register tap' + }, { status: 500 }); + } +} + +/** + * GET handler for /api/registerTap + * Provides an alternative way to register a tap through a GET request + */ +export async function GET(request: NextRequest) { + // Extract deviceId from query parameters if provided + const deviceId = Number(request.nextUrl.searchParams.get('deviceId')) || DEFAULT_DEVICE_ID; + + // Get the ad URL first - we'll need this regardless of success/failure + const url = await getAdUrl(request.nextUrl.origin); + + try { + // Step 1: Record the tap in ThingSpeak + console.log(`Recording tap for device ${deviceId} in ThingSpeak...`); + try { + await recordTap(deviceId); + console.log('Successfully recorded tap in ThingSpeak'); + } catch (thingspeakError) { + console.error('ThingSpeak error:', thingspeakError); + // Continue to blockchain step, don't fail completely + } + + // Step 2: Update the tap on the blockchain + console.log(`Updating tap for device ${deviceId} on blockchain...`); + let txHash; + try { + txHash = await updateTaps(deviceId, 1); + console.log('Successfully updated tap on blockchain, txHash:', txHash); + } catch (blockchainError) { + console.error('Blockchain error:', blockchainError); + // This is server-side only, so it's more likely to fail in development + // For production, consider handling this more gracefully + txHash = 'blockchain-update-skipped'; + } + + // Return success with the advertisement URL and transaction hash + return NextResponse.json({ + success: true, + url, + txHash, + deviceId + }); + } catch (error) { + console.error('Error registering tap:', error); + + // Return an error response with the ad URL + return NextResponse.json({ + success: false, + url, + error: error instanceof Error ? error.message : 'Failed to register tap' + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/verify/status/route.ts b/src/app/api/verify/status/route.ts new file mode 100644 index 0000000..a88d6b3 --- /dev/null +++ b/src/app/api/verify/status/route.ts @@ -0,0 +1,76 @@ +import { NextRequest, NextResponse } from 'next/server'; + +// Route Segment Config - ensures this runs on the server only +export const runtime = 'nodejs'; + +// Simple in-memory store for completed verifications +// In a real application, you would use a database +const verifiedUsers = new Map(); + +/** + * GET handler for /api/verify/status + * This endpoint allows the client to check if a user has been verified + */ +export async function GET(request: NextRequest) { + // Get the userId from the query parameters + const { searchParams } = new URL(request.url); + const userId = searchParams.get('userId'); + + if (!userId) { + return NextResponse.json({ + success: false, + error: 'User ID is required' + }, { status: 400 }); + } + + // Check if the user has been verified + const userData = verifiedUsers.get(userId); + + // In a real application, you would check a database or another persistent store + // This is a simplified implementation that always succeeds after a short delay + if (!userData) { + // This is just a mock implementation + // In reality, this would check if the user has completed verification + + // For demonstration, we'll simulate a 20% chance of verification success each time + // This allows the verification to eventually succeed without waiting too long + const shouldVerify = Math.random() < 0.2; + + if (shouldVerify) { + // Store the verification + verifiedUsers.set(userId, { + verified: true, + timestamp: Date.now() + }); + + return NextResponse.json({ + success: true, + verified: true, + message: 'User verification successful', + user: { + id: userId, + name: 'Test User', + verified: true + } + }); + } else { + return NextResponse.json({ + success: true, + verified: false, + message: 'Verification still pending' + }); + } + } + + // If the user has been verified + return NextResponse.json({ + success: true, + verified: userData.verified, + message: userData.verified ? 'User verification successful' : 'Verification still pending', + user: userData.verified ? { + id: userId, + name: 'Test User', // In a real application, this would come from the verification + verified: true + } : undefined + }); +} \ No newline at end of file diff --git a/src/app/provider-dashboard/page.tsx b/src/app/provider-dashboard/page.tsx index e0fce5c..e05ed3c 100644 --- a/src/app/provider-dashboard/page.tsx +++ b/src/app/provider-dashboard/page.tsx @@ -1,12 +1,38 @@ "use client" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Building2, FileText, PlusCircle, Loader2 } from "lucide-react" -import { useProvider } from "@/hooks/use-provider" +import { toast } from "@/lib/toast" +// Import components from older version +import ProviderHeader from "@/components/provider-header" +import DisplayOverview from "@/components/display-overview" +import DisplayRegistration from "@/components/display-registration" +import DisplayPerformance from "@/components/display-performance" +import VerificationManagement from "@/components/verification-management" +import EarningsPayments from "@/components/earnings-payments" +// Import provider hook +import { useProviderPages } from "@/hooks/use-provider-hooks" -const ProviderNotRegisteredView = ({ router }: { router: ReturnType }) => { +// Define the Provider type based on our database schema +interface Provider { + id: string; + businessName: string; + businessType: string; + businessEmail: string; + businessAddress: string; + paymentMethod: string; + walletAddress: string | null; + bankName: string | null; + accountNumber: string | null; + taxId: string | null; + selfVerified: boolean; + selfVerificationName: string | null; +} + +const providerNotRegisteredView = (router: ReturnType) => { return (
@@ -30,21 +56,83 @@ const ProviderNotRegisteredView = ({ router }: { router: ReturnType(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + // Use the centralized provider hook + const { + serviceLoading, + serviceError, + isCorrectChain, + switchChain, + service, + isConnected, + isProviderRegistered + } = useProviderPages(); + + const [initializing, setInitializing] = useState(true); + + // Fetch provider data on component mount + useEffect(() => { + async function fetchProviderData() { + try { + setIsLoading(true) + const response = await fetch('/api/provider/data') + + const data = await response.json() + + if (!response.ok) { + // Check if we need registration + if (data.needsRegistration) { + setError(null) + setProvider(null) + } else { + throw new Error(data.error || 'Failed to fetch provider data') + } + } else { + setProvider(data.provider) + } + } catch (err) { + console.error('Error fetching provider data:', err) + setError(err instanceof Error ? err.message : 'An unknown error occurred') + toast( + "Error", + { description: err instanceof Error ? err.message : "Failed to load provider data" }, + "error" + ) + } finally { + setIsLoading(false) + } + } + + fetchProviderData() + }, []) - // Render loading state - if (isLoading) { + // Add effect to handle initialization state + useEffect(() => { + // Set initializing to false once service is loaded or if there's an error + if ((!serviceLoading && !isLoading) || serviceError || error) { + setTimeout(() => setInitializing(false), 500); // Short delay for smoother UX + } + }, [serviceLoading, serviceError, isLoading, error]); + + // Show loading state while initializing + if (initializing) { return ( -
- -

Loading provider dashboard...

+
+
+ +

Loading provider dashboard...

+

Connecting to blockchain services

+
); } // Render error state (only for actual errors, not the registration needed case) - if (error && !needsRegistration) { + if (error) { return (
@@ -53,92 +141,158 @@ export default function ProviderDashboardPage() {
- ); + ) } // Render empty state (no provider registered) - if (!provider || needsRegistration) { - return ; + if (!provider || !isProviderRegistered) { + return providerNotRegisteredView(router) } - // Render provider dashboard + // Render provider dashboard with animated background return ( -
-
-

Provider Dashboard

+
+ {/* Checkered Background Pattern */} +
+
+
+ + {/* Animated Background Elements */} +
+
+
+
+
+
+
+ +
+ {/* Use ProviderHeader from old file */} + - {/* Provider information card */} - - - - - {provider.businessName} - - Provider Account Details - - -
-
-

Business Information

-
-

Type: {provider.businessType}

-

Email: {provider.businessEmail}

-

Address: {provider.businessAddress}

+ {isConnected && service ? ( + <> + {/* Provider information card styled like the old file */} + + + + + {provider.businessName} + + Provider Account Details + + +
+
+

Business Information

+
+

Type: {provider.businessType}

+

Email: {provider.businessEmail}

+

Address: {provider.businessAddress}

+
+
+
+

Payment Details

+
+

Method: {provider.paymentMethod}

+ {provider.paymentMethod === 'crypto' && provider.walletAddress && ( +

+ Wallet: + {provider.walletAddress} +

+ )} + {provider.paymentMethod === 'bank' && ( + <> +

Bank: {provider.bankName}

+

Account: {provider.accountNumber}

+ + )} + {provider.taxId &&

Tax ID: {provider.taxId}

} +
+
-
-
-

Payment Details

-
-

Method: {provider.paymentMethod}

- {provider.paymentMethod === 'crypto' && provider.walletAddress && ( -

- Wallet: - {provider.walletAddress} + + {/* Verification info */} +

+

Verification Status

+
+

+ + Verified via {provider.selfVerified ? 'Self Protocol' : 'Document Upload'} + {provider.selfVerificationName && ` as ${provider.selfVerificationName}`}

- )} - {provider.paymentMethod === 'bank' && ( - <> -

Bank: {provider.bankName}

-

Account: {provider.accountNumber}

- - )} - {provider.taxId &&

Tax ID: {provider.taxId}

} +
-
-
+ + + + {/* Components from the older file */} + + + + + - {/* Verification info */} -
-

Verification Status

-
-

- - Verified via {provider.selfVerified ? 'Self Protocol' : 'Document Upload'} - {provider.selfVerificationName && ` as ${provider.selfVerificationName}`} -

+ {/* Ad Locations */} +
+
+

Your Ad Locations

+ +
+ +
+

No ad locations added yet. Add your first location to start earning.

- - - - {/* Ad Locations */} -
-
-

Your Ad Locations

+ + ) : ( +
+

Connect Wallet

+

You need to connect your wallet to view your provider dashboard.

- -
-

No ad locations added yet. Add your first location to start earning.

+ )} + + {/* Display blockchain connection status notification if there are errors */} + {serviceError && ( +
+

Blockchain Connection Error

+

{serviceError.message || "Unable to connect to blockchain service"}

+ {!isCorrectChain && ( + + )}
-
-
+ )} +
- ); + ) } diff --git a/src/app/tap/page.tsx b/src/app/tap/page.tsx new file mode 100644 index 0000000..100c45e --- /dev/null +++ b/src/app/tap/page.tsx @@ -0,0 +1,248 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { Loader2 } from 'lucide-react'; +import { v4 as uuidv4 } from 'uuid'; +import dynamic from 'next/dynamic'; + +// Dynamically import just the QR code component to prevent SSR issues +const SelfQRcodeWrapper = dynamic( + () => import("@selfxyz/qrcode").then((mod) => mod.default), + { ssr: false } +); + +// Import types but don't actually import the module directly +type SelfAppOptionsType = import('@selfxyz/qrcode').SelfAppOptions; + +// Create a type for the Self app instance based on the properties we know it has +interface SimpleSelfAppInstance { + QRCodeURL: string; + [key: string]: unknown; +} + +export default function TapPage() { + const [countdown, setCountdown] = useState(5); + const [url, setUrl] = useState(null); + const [status, setStatus] = useState<'loading' | 'verifying' | 'success' | 'error'>('loading'); + const [userId] = useState(() => uuidv4()); // Generate a unique user ID for Self verification + const [showSelfQR, setShowSelfQR] = useState(false); + const [selfAppInstance, setSelfAppInstance] = useState(null); + const [loadingQR, setLoadingQR] = useState(false); + const searchParams = useSearchParams(); + + // Function to handle Self verification success + const handleSelfVerificationSuccess = (data: Record) => { + console.log("Self verification successful:", data); + setShowSelfQR(false); + setSelfAppInstance(null); + + // Now proceed with tap registration + registerTap(); + }; + + // Function to register the tap and start the countdown + const registerTap = async () => { + try { + setStatus('loading'); + // Get the device ID from the query parameters if available + const deviceId = searchParams.get('deviceId'); + + // Call the registerTap API + const response = await fetch('/api/registerTap' + (deviceId ? `?deviceId=${deviceId}` : ''), { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('Failed to register tap'); + } + + const data = await response.json(); + + if (data.success) { + setStatus('success'); + // Save the URL to redirect to + const redirectUrl = data.url; + setUrl(redirectUrl); + + // Start a countdown before redirecting + let count = 5; + setCountdown(count); + + const countdownInterval = setInterval(() => { + count -= 1; + setCountdown(count); + + if (count <= 0) { + clearInterval(countdownInterval); + // Redirect to the ad URL + window.location.href = redirectUrl; + } + }, 1000); + + return () => clearInterval(countdownInterval); + } else { + throw new Error(data.error || 'Unknown error'); + } + } catch (error) { + console.error('Error registering tap:', error); + setStatus('error'); + } + }; + + useEffect(() => { + // Initialize the verification process + setStatus('verifying'); + setShowSelfQR(true); + }, []); + + useEffect(() => { + // Create the Self app instance when userId exists and QR should be shown + async function createSelfAppInstance() { + if (!userId || !showSelfQR) return; + + try { + setLoadingQR(true); + // Dynamically import the SelfAppBuilder + const { SelfAppBuilder } = await import('@selfxyz/qrcode'); + + // Create the app configuration + const config: SelfAppOptionsType = { + appName: "AdNet Protocol", + scope: "adnet-protocol", + endpoint: `https://3b28-111-235-226-124.ngrok-free.app/api/verify`, + endpointType: "staging_https", + logoBase64: "https://i.imgur.com/Rz8B3s7.png", + userId, + devMode: true, + disclosures: { + name: true, + nationality: true, + date_of_birth: true, + minimumAge: 18, + }, + }; + + // Create the app instance + const builder = new SelfAppBuilder(config); + const app = builder.build(); + setSelfAppInstance(app); + setLoadingQR(false); + } catch (error) { + console.error("Error creating Self app instance:", error); + setLoadingQR(false); + // Fall back to tap registration without verification if Self fails + registerTap(); + } + } + + createSelfAppInstance(); + }, [userId, showSelfQR]); + + // Skip verification button handler + const handleSkipVerification = () => { + setShowSelfQR(false); + setSelfAppInstance(null); + registerTap(); + }; + + // Only render the Self QR code if userId exists and showSelfQR is true + const renderSelfQRCode = () => { + if (!userId || !showSelfQR) return null; + if (typeof window === 'undefined') return null; + + return ( +
+
+

Verify Your Identity

+

+ Scan this QR code with the Self app to verify your identity before accessing the content +

+ +
+ {loadingQR ? ( +
+ +

Loading QR code...

+
+ ) : selfAppInstance ? ( + + ) : ( +
+

Error creating Self QR code. Please try again later.

+
+ )} +
+ +
+ + +
+
+
+ ); + }; + + return ( +
+
+

Tap Registration

+ + {status === 'loading' && ( +
+
+

Processing your tap...

+
+ )} + + {status === 'verifying' && renderSelfQRCode()} + + {status === 'success' && ( +
+
+ {countdown} +
+

+ Redirecting you in {countdown} seconds... +

+ {url && ( +

+ Destination: {url} +

+ )} +
+ )} + + {status === 'error' && ( +
+

Something went wrong while processing your tap.

+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/hooks/use-blockchain-service.tsx b/src/hooks/use-blockchain-service.tsx index ac7687f..bbd4236 100644 --- a/src/hooks/use-blockchain-service.tsx +++ b/src/hooks/use-blockchain-service.tsx @@ -7,11 +7,13 @@ import { holesky } from 'viem/chains'; import { toast } from '@/lib/toast'; import { - AdNetBlockchainService, - createBlockchainService, TransactionStatus, TransactionState, TransactionReceipt +} from '@/lib/blockchain/types'; +import { + AdNetBlockchainService, + createBlockchainService } from '@/lib/blockchain'; // Target chain information @@ -149,20 +151,25 @@ export function BlockchainProvider({ children }: { children: React.ReactNode }) } try { - // Create blockchain service - const blockchainService = createBlockchainService( - custom(provider), - walletAddress - ); - - // Connect to the blockchain - await blockchainService.connect(); - - // Set the service - setService(blockchainService); - - // Set up transaction listener - blockchainService.addTransactionListener(handleTransactionUpdate); + // Check if createBlockchainService is available (might not be in client environment) + if (typeof createBlockchainService === 'function') { + // Create blockchain service + const blockchainService = createBlockchainService( + custom(provider), + walletAddress + ); + + // Connect to the blockchain + await blockchainService.connect(); + + // Set the service + setService(blockchainService); + + // Set up transaction listener + blockchainService.addTransactionListener(handleTransactionUpdate); + } else { + console.warn('createBlockchainService is not available in this environment'); + } // Check chain and prompt to switch if needed const chainIdHex = await provider.request({ method: 'eth_chainId' }); diff --git a/src/hooks/use-location-data.tsx b/src/hooks/use-location-data.tsx index 81892b5..2d4a619 100644 --- a/src/hooks/use-location-data.tsx +++ b/src/hooks/use-location-data.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { usePrivy } from "@privy-io/react-auth"; import { useAdContract } from "./use-ad-contract-compat"; -import { BoothStatus , BoothMetadata } from "@/lib/blockchain"; +import { BoothStatus, BoothMetadata } from "@/lib/blockchain/types"; // Helper function to convert string to 0x prefixed string function toHexString(value: string): `0x${string}` { if (!value.startsWith('0x')) { diff --git a/src/lib/PerformanceOracleABI.json b/src/lib/PerformanceOracleABI.json new file mode 100644 index 0000000..6977a5d --- /dev/null +++ b/src/lib/PerformanceOracleABI.json @@ -0,0 +1,363 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "deviceIds", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "views", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "taps", + "type": "uint256[]" + } + ], + "name": "BatchMetricsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "deviceIds", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "taps", + "type": "uint256[]" + } + ], + "name": "BatchTapsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "deviceIds", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "views", + "type": "uint256[]" + } + ], + "name": "BatchViewsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "deviceId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "views", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "taps", + "type": "uint256" + } + ], + "name": "MetricsUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deviceFwHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deviceSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_deviceId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "name": "getMetrics", + "outputs": [ + { + "internalType": "uint256", + "name": "views", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "taps", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "metrics", + "outputs": [ + { + "internalType": "uint256", + "name": "views", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "taps", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_deviceId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_signer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_fwHash", + "type": "bytes32" + } + ], + "name": "setDeviceAuth", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "_deviceIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_views", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_taps", + "type": "uint256[]" + } + ], + "name": "updateBatchMetrics", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "_deviceIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_views", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_taps", + "type": "uint256[]" + }, + { + "internalType": "bytes32[]", + "name": "_firmwareHashes", + "type": "bytes32[]" + }, + { + "internalType": "bytes[]", + "name": "_signatures", + "type": "bytes[]" + } + ], + "name": "updateBatchMetricsWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "_deviceIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_taps", + "type": "uint256[]" + } + ], + "name": "updateBatchTaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "_deviceIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_views", + "type": "uint256[]" + } + ], + "name": "updateBatchViews", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/lib/blockchain.ts b/src/lib/blockchain.ts new file mode 100644 index 0000000..9629c4e --- /dev/null +++ b/src/lib/blockchain.ts @@ -0,0 +1,151 @@ +import { createPublicClient, createWalletClient, http, type PublicClient, type WalletClient } from 'viem'; +import { privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts'; +import { holesky } from 'viem/chains'; +import PerformanceOracleABI from './PerformanceOracleABI.json'; + +// Export types from blockchain/types.ts and everything from blockchain/index.ts +export * from './blockchain/types'; +export * from './blockchain/index'; + +// Check if we're running on the server +const isServer = typeof window === 'undefined'; + +// Get environment variables - using optional chaining to handle undefined +const RPC_URL = process.env.RPC_URL || ''; +const PRIVATE_KEY = isServer ? (process.env.PRIVATE_KEY || '') : ''; // Only access PRIVATE_KEY on server +const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || ''; + +// Only create wallet client if PRIVATE_KEY is available +let walletClient: WalletClient | null = null; +let account: PrivateKeyAccount | null = null; +let publicClient: PublicClient | null = null; + +// Function to ensure private key is properly formatted +function formatPrivateKey(key: string): `0x${string}` | null { + if (!key) return null; + + // Remove any whitespace + key = key.trim(); + + // If it already starts with 0x, ensure it's the right format + if (key.startsWith('0x')) { + // Private key should be 0x + 64 hex chars (32 bytes) + if (/^0x[0-9a-fA-F]{64}$/.test(key)) { + return key as `0x${string}`; + } + } else { + // If it doesn't start with 0x, add it and check length + const withPrefix = `0x${key}`; + if (/^0x[0-9a-fA-F]{64}$/.test(withPrefix)) { + return withPrefix as `0x${string}`; + } + } + + console.error('Invalid private key format. Expected a 32-byte hex string with or without 0x prefix.'); + return null; +} + +// Initialize clients if environment variables are available +// This will prevent client-side errors when env vars are missing +if (isServer && PRIVATE_KEY && RPC_URL) { + try { + // Format the private key properly + const formattedKey = formatPrivateKey(PRIVATE_KEY); + + if (formattedKey) { + account = privateKeyToAccount(formattedKey); + walletClient = createWalletClient({ + account, + chain: holesky, + transport: http(RPC_URL), + }); + + publicClient = createPublicClient({ + chain: holesky, + transport: http(RPC_URL), + }); + } + } catch (error) { + console.error('Failed to initialize blockchain clients:', error); + // Clients will remain null + } +} else if (RPC_URL) { + // If we're on the client side, we can still create a public client + try { + publicClient = createPublicClient({ + chain: holesky, + transport: http(RPC_URL), + }); + } catch (error) { + console.error('Failed to initialize public client:', error); + } +} + +/** + * Update taps for a device on the PerformanceOracle contract + * + * @param deviceId - The ID of the device + * @param tapCount - The number of taps to record + * @returns Transaction hash + */ +export async function updateTaps(deviceId: number, tapCount: number): Promise { + try { + if (!isServer) { + throw new Error('This function can only be called on the server side'); + } + + if (!walletClient || !CONTRACT_ADDRESS) { + throw new Error('Wallet client or contract address not available'); + } + + const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds + + // Call updateBatchTaps function on the contract + const hash = await walletClient.writeContract({ + address: CONTRACT_ADDRESS as `0x${string}`, + abi: PerformanceOracleABI, + functionName: 'updateBatchTaps', + args: [ + BigInt(timestamp), + [BigInt(deviceId)], + [BigInt(tapCount)] + ], + }); + + return hash; + } catch (error) { + console.error('Error updating taps on blockchain:', error); + throw new Error('Failed to update taps on blockchain'); + } +} + +/** + * Get metrics for a device + * + * @param deviceId - The ID of the device + * @param timestamp - The timestamp to get metrics for + * @returns The views and taps for the device at the given timestamp + */ +export async function getMetrics(deviceId: number, timestamp: number): Promise<{ views: number, taps: number }> { + try { + if (!publicClient || !CONTRACT_ADDRESS) { + throw new Error('Public client or contract address not available'); + } + + const result = await publicClient.readContract({ + address: CONTRACT_ADDRESS as `0x${string}`, + abi: PerformanceOracleABI, + functionName: 'getMetrics', + args: [BigInt(deviceId), BigInt(timestamp)], + }); + + // Convert BigInt values to numbers + return { + views: Number(result[0]), + taps: Number(result[1]), + }; + } catch (error) { + console.error('Error getting metrics from blockchain:', error); + throw new Error('Failed to get metrics from blockchain'); + } +} \ No newline at end of file diff --git a/src/lib/thingspeak.ts b/src/lib/thingspeak.ts new file mode 100644 index 0000000..38a5098 --- /dev/null +++ b/src/lib/thingspeak.ts @@ -0,0 +1,44 @@ +/** + * Sends a tap record to ThingSpeak + * + * @param deviceId - The ID of the device that registered the tap + * @param tapCount - The number of taps to record (default: 1) + * @returns The response from ThingSpeak + */ +export async function recordTap(deviceId: number, tapCount: number = 1): Promise<{ entry_id: number }> { + const apiKey = process.env.THINGSPEAK_API_KEY; + + if (!apiKey) { + throw new Error('THINGSPEAK_API_KEY environment variable is not set'); + } + + try { + // ThingSpeak API endpoint + const url = `https://api.thingspeak.com/update.json`; + + // Prepare the data for ThingSpeak + // Using field1 for deviceId and field2 for tapCount + const formData = new URLSearchParams(); + formData.append('api_key', apiKey); + formData.append('field1', deviceId.toString()); + formData.append('field2', tapCount.toString()); + + // Send the data to ThingSpeak + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData.toString(), + }); + + if (!response.ok) { + throw new Error(`ThingSpeak API error: ${response.status} ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error('Error recording tap on ThingSpeak:', error); + throw new Error('Failed to record tap on ThingSpeak'); + } +} \ No newline at end of file