Create a Webapp using Viem.js to display sensor data from smart contracts on JIBCHAIN L1.
Connection Details:
- Chain ID: 8899
- RPC URL: https://rpc-l1.jibchain.net
- Block Explorer: https://exp.jibchain.net
Contract Addresses:
- Factory Contract: 0x63bB41b79b5aAc6e98C7b35Dcb0fE941b85Ba5Bb
- FloodBoy001 Store: 0xCd3Ec17ddFDa24f8F97131fa0FDf20e7cbd1A8Bb
- FloodBoy016 Store: 0x0994Bc66b2863f8D58C8185b1ed6147895632812
- Universal Signer: 0xcB0e58b011924e049ce4b4D62298Edf43dFF0BDd (authorized for all stores)
Final UI Design (Latest Sensor Data Card):
Header Section:
- Title: "Latest Sensor Data"
- Store Nickname: Display nickname from factory contract (e.g., "FloodBoy001", "FloodBoy016")
- Store Description: Display description from factory contract (e.g., "Northern Thailand Flood Monitor", "FloodBoy016")
- Current Block: "Current Block: 5944625" (display current block number)
- Last Updated timestamp: "Last Updated: 7:25:08 PM"
- Store address (truncated): "0xCd3Ec17d...d1A8Bb" or "0x0994Bc66...632812" with external link icon
Chart Section (Full Width):
- Display a full-width line chart with toggle controls
- Toggle between "Water Depth" and "Battery Voltage" views
- Chart titles: "Water Depth Over Time" / "Battery Voltage Over Time"
- Y-axis: Water depth in meters (scaled from x10000) OR Battery voltage in volts (scaled from x100)
- X-axis: Timestamp (last 24 hours or available data range)
- Chart colors: Blue (#3B82F6) for water depth, Green (#10B981) for voltage
- Full container width with proper responsive scaling
- Show data points and connect with smooth lines
- Apply data smoothing for better chart visualization (moving averages, interpolation)
- Include hover tooltips showing exact values and timestamps
- Toggle buttons above chart: [Water Depth] [Battery Voltage]
- Active toggle button highlighted with matching chart color
- Responsive design that works on mobile devices
- If no historical data available, show "No historical data available" message
Data Table:
Columns: Metric | Current | Min | Max
Rows (CORRECT FORMAT - use proper capitalization and sample counts):
- Battery Voltage: [voltage] V | [min] V | [max] V
- Installation Height: [height] m | [height] m | [height] m
- Water Depth ([X] samples): [depth] m | [min] m | [max] m
Real Examples (FloodBoy001 vs FloodBoy016):
FloodBoy001: Water Depth (9 samples): 0.4454 m | 0.4450 m | 0.4460 m
FloodBoy016: Water Depth (3 samples): 0.5433 m | 0.5400 m | 0.5490 m
WRONG FORMAT (DO NOT USE):
❌ battery voltage: [voltage] V (lowercase field name)
❌ installation height: 302.000 m (incorrect unit conversion from x10000 - should be ~3.02 m)
❌ water depth: 44.540 m (wrong conversion from x10000 - should be ~0.44-0.54 m)
❌ water depth count: [X] count (should be "Water Depth ([X] samples)" format instead)
Footer:
- Last Updated: 7/22/2025, 7:25:08 PM
- Store Owner: 0x943E41e4cc22f971284ae957A380D3DbeA1Dc481 (truncated with link)
- Deployed Block: #5944625 (with block explorer link)
- Sensor Count: 1 authorized sensor
Data Processing Requirements:
Unit Scaling:
- x100 → divide by 100 (3 decimal places for voltage) - Example: 1291 → 12.910 V
- x1000 → divide by 1000 (3 decimal places)
- x10000 → divide by 10000 (2-4 decimal places for meters) - Examples: 30200 → 3.02 m, 2700 → 0.27 m
- Extract base unit from unit string (e.g., "V" from "V x100", "m" from "m x10000")
- Format timestamps in human-readable format (MM/dd/yyyy, h:mm:ss AM/PM)
- Use appropriate decimal precision: voltage (3 decimals), meters (2 decimals for readability)
Visual Design:
- Clean white card with rounded corners and subtle shadow
- Store nickname prominently displayed as main heading
- Store description as subtitle below nickname
- Alternating row colors (white/light gray)
- Green status indicator for current block
- Truncated addresses with external link icons
- Sample count badges where applicable
- Responsive table layout
- Store metadata section with owner and deployment information
Features:
- Display store nickname and description from factory contract
- Full-width chart with toggle between Water Depth and Battery Voltage views
- Toggle buttons with active state highlighting (matching chart colors)
- Loading indicators during data fetching
- Error handling with user-friendly messages
- Always use the universal signer (0xcB0e58b011924e049ce4b4D62298Edf43dFF0BDd) for data retrieval
- Show "No data" if no records exist
- Responsive chart scaling for all screen sizes
- Store metadata integration (nickname, description, owner, deployed block)
Historical Data Requirements:
- Use RecordStored events to build both water depth and battery voltage timelines
- Handle RPC limits with pagination (max 2000 blocks per request)
- Cache event data to reduce blockchain calls
- Dynamically find water depth AND battery voltage field indexes using getAllFields()
- Process both data types: water depth (x10000 scaling) and battery voltage (x100 scaling)
- Sort events by timestamp for proper chart ordering
- Support toggle between datasets without refetching data
- Include error handling for missing or corrupted event data
- Show loading state while fetching historical events
- Display "No historical data" message if events array is empty
Required ABIs for Webapp
Factory Contract ABI (Key Functions):
[
{
"name": "getStoreInfo",
"inputs": [{"name": "store", "type": "address"}],
"outputs": [
{"name": "nickname", "type": "string"},
{"name": "owner", "type": "address"},
{"name": "authorizedSensorCount", "type": "uint256"},
{"name": "deployedBlock", "type": "uint128"},
{"name": "description", "type": "string"}
],
"stateMutability": "view",
"type": "function"
}
]
CatLabSecureSensorStore ABI (Key Functions):
[
{
"name": "getAllFields",
"outputs": [{
"components": [
{"name": "name", "type": "string"},
{"name": "unit", "type": "string"},
{"name": "dtype", "type": "string"}
],
"type": "tuple[]"
}],
"stateMutability": "view",
"type": "function"
},
{
"name": "getLatestRecord",
"inputs": [{"name": "sensor", "type": "address"}],
"outputs": [
{"name": "", "type": "uint256"},
{"name": "", "type": "int256[]"}
],
"stateMutability": "view",
"type": "function"
}
]
Note: Full ABIs are provided in /abis/ directory.
Include complete ABIs in your implementation.
Implementation Example
import { createPublicClient, http } from 'viem';
import FactoryABI from '@/abis/CatLabFactory.json';
import StoreABI from '@/abis/CatLabSecureSensorStore.abi.json';
// JIBCHAIN L1 Configuration
const jibchain = {
id: 8899,
name: 'JIBCHAIN L1',
rpcUrls: {
default: { http: ['https://rpc-l1.jibchain.net'] }
}
};
const client = createPublicClient({
chain: jibchain,
transport: http()
});
// Constants
const FACTORY_ADDRESS = '0x63bB41b79b5aAc6e98C7b35Dcb0fE941b85Ba5Bb';
const FLOODBOY001_STORE = '0xCd3Ec17ddFDa24f8F97131fa0FDf20e7cbd1A8Bb';
const FLOODBOY016_STORE = '0x0994Bc66b2863f8D58C8185b1ed6147895632812';
const UNIVERSAL_SIGNER = '0xcB0e58b011924e049ce4b4D62298Edf43dFF0BDd';
// Step 1: Get store information from factory contract
const [nickname, owner, sensorCount, deployedBlock, description] = await client.readContract({
address: FACTORY_ADDRESS,
abi: FactoryABI,
functionName: 'getStoreInfo',
args: [FLOODBOY016_STORE] // Using FloodBoy016 as example
});
// Display store metadata in UI
console.log('Store Info:', { nickname, description, owner, sensorCount, deployedBlock });
// Step 2: Get field configurations
const fields = await client.readContract({
address: FLOODBOY016_STORE,
abi: StoreABI,
functionName: 'getAllFields'
});
// Step 3: Get latest sensor data (using universal signer)
const [timestamp, values] = await client.readContract({
address: FLOODBOY016_STORE,
abi: StoreABI,
functionName: 'getLatestRecord',
args: [UNIVERSAL_SIGNER]
});
// Step 4: Get historical data using event logs
// RecordStored event signature from CatLabSecureSensorStore:
// event RecordStored(address indexed sensor, uint256 timestamp, int256[] values)
// Method 1: Get recent events (recommended for charts)
const currentBlockNumber = await client.getBlockNumber();
const fromBlock = currentBlockNumber - BigInt(28800); // ~24 hours (assuming 3sec blocks)
const historicalEvents = await client.getContractEvents({
address: FLOODBOY016_STORE,
abi: StoreABI,
eventName: 'RecordStored',
fromBlock: fromBlock,
toBlock: 'latest',
args: {
sensor: UNIVERSAL_SIGNER // Filter by sensor address
}
});
// Process chart data for toggle functionality
const waterDepthIndex = fields.findIndex(field =>
field.name.toLowerCase().includes('water_depth') && !field.name.includes('min') && !field.name.includes('max')
);
const batteryVoltageIndex = fields.findIndex(field =>
field.name.toLowerCase().includes('battery_voltage') && !field.name.includes('min') && !field.name.includes('max')
);
const chartData = historicalEvents.map(event => ({
timestamp: Number(event.args.timestamp) * 1000, // Convert to milliseconds
waterDepth: waterDepthIndex >= 0 ? Number(event.args.values[waterDepthIndex]) / 10000 : null,
batteryVoltage: batteryVoltageIndex >= 0 ? Number(event.args.values[batteryVoltageIndex]) / 100 : null,
blockNumber: Number(event.blockNumber)
})).sort((a, b) => a.timestamp - b.timestamp);
// Chart toggle state management
const [activeChart, setActiveChart] = useState('waterDepth'); // 'waterDepth' or 'batteryVoltage'
// Data processing function with CORRECT x10000 conversion - MOST CRITICAL FUNCTION
function processValue(value, unit) {
const baseUnit = unit.replace(/ x\d+/, '');
if (unit.includes('x100')) return (Number(value) / 100).toFixed(3) + ' ' + baseUnit; // Voltage: 3 decimals
if (unit.includes('x1000')) return (Number(value) / 1000).toFixed(3) + ' ' + baseUnit;
// ⚠️ CRITICAL: x10000 MUST divide by 10000, NOT 100!
if (unit.includes('x10000')) {
// CORRECT: divide by 10000
return (Number(value) / 10000).toFixed(4) + ' ' + baseUnit;
// WRONG examples that AI often generates:
// return (Number(value) / 100).toFixed(3) + ' ' + baseUnit; // ❌ Wrong divisor!
// return Number(value).toFixed(3) + ' ' + baseUnit; // ❌ No division!
}
return value + ' ' + unit;
}
// Field name formatting - CRITICAL FORMATTING RULES
function formatFieldName(fieldName) {
// Convert snake_case to Title Case
return fieldName
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
// Examples of CORRECT field name formatting:
// "battery_voltage" → "Battery Voltage" (NOT "battery voltage")
// "installation_height" → "Installation Height" (NOT "installation height")
// "water_depth" → "Water Depth" (NOT "water depth")
// "water_depth_count" → append sample count as "Water Depth ([X] samples)" (NOT "Water Depth Count: [X] count")
// Note: [X] is dynamic - FloodBoy001: 9 samples, FloodBoy016: 3 samples, etc.
// Examples of correct processing:
// Battery voltage: 1291 (x100) → 12.91 V (showing as 12.910 V with 3 decimals)
// Installation height: 30200 (x10000) → 3.02 m
// Water depth: 2700 (x10000) → 0.27 m
Network Info:
Chain ID: 8899 (JIBCHAIN L1)
RPC URL: https://rpc-l1.jibchain.net
Block Explorer: https://exp.jibchain.net
FloodBoy016 Store: 0x0994Bc66b2863f8D58C8185b1ed6147895632812
** CRITICAL UNIT CONVERSION - MOST COMMON ERROR **
⚠️ x10000 fields MUST divide by 10000, NOT display raw values!
Unit Conversion Rules:
- x100 → divide by 100 (example: 1384 → 13.840 V) ✅
- x1000 → divide by 1000
- x10000 → divide by 10000 ⚠️ CRITICAL:
- Raw value 30200 → 3.0200 m ✅ (divide by 10000)
- Raw value 4383 → 0.4383 m ✅ (divide by 10000)
- NOT 302.000 m ❌ (wrong: displaying raw/100 instead of raw/10000)
- NOT 43.830 m ❌ (wrong: displaying raw/100 instead of raw/10000)
Field Name Formatting:
- ALWAYS use Title Case: "Battery Voltage", "Installation Height", "Water Depth"
- NEVER use lowercase: "battery voltage", "installation height", "water depth"
- For count fields: Use "Water Depth ([X] samples)" format, NOT "Water Depth Count: [X] count"
- Sample count X is dynamic per store (FloodBoy001: 9, FloodBoy016: 3, etc.)
Common Mistakes to Avoid:
❌ "installation height: 302.000 m" (WRONG: lowercase + x10000 displayed as raw/100)
✅ "Installation Height: 3.0200 m" (CORRECT: Title Case + raw÷10000)
❌ "Water Depth: 43.830 m" (WRONG: x10000 displayed as raw/100)
✅ "Water Depth: 0.4383 m" (CORRECT: raw 4383÷10000)
❌ "water depth count: [X] count" (wrong: separate count field)
✅ "Water Depth ([X] samples): [0.44-0.54] m" (correct: embedded sample count)
Generic Pattern for All Stores:
- Sample count varies by store: FloodBoy001 (9 samples), FloodBoy016 (3 samples)
- Water depth values vary: ~0.44-0.46m (001) vs ~0.54-0.55m (016)
- Installation height varies: 3.0200m (001) vs 3.0000m (016)
- Always use dynamic sample count from actual data
Data Smoothing Requirements:
- Apply smoothing algorithms when rendering charts to reduce noise and improve readability
- Use moving averages (window size: 3-5 data points) for real-time data visualization
- Implement interpolation for gaps in historical data
- Provide option to toggle between raw data and smoothed data views
- Maintain original data precision while smoothing display values
Real-World Sensor Data Examples:
The webapp should handle data volumes from various production FloodBoy sensors:
Sensor Data Records Overview (varies by store):
- FloodBoy001: 7,379 total → 1,569 grouped (30-min intervals)
- FloodBoy016: 3,907 total → 917 grouped (30-min intervals)
- Data Grouping Options: 30 minutes, 1 hour, 6 hours, 24 hours
- Static data display (no real-time updates needed)
Sample Sensor Output Formats:
FloodBoy001 (0xCd3Ec17d...d1A8Bb):
| Metric | Current | Min | Max |
|--------|---------|-----|-----|
| Battery Voltage | 13.780 V | 13.540 V | 14.130 V |
| Installation Height | 3.0200 m | 3.0200 m | 3.0200 m |
| Water Depth (9 samples) | 0.4454 m | 0.4450 m | 0.4460 m |
FloodBoy016 (0x0994Bc66...632812):
| Metric | Current | Min | Max |
|--------|---------|-----|-----|
| Battery Voltage | 14.140 V | 14.140 V | 14.150 V |
| Installation Height | 3.0000 m | 3.0000 m | 3.0000 m |
| Water Depth (3 samples) | 0.5433 m | 0.5400 m | 0.5490 m |
UI Controls Required:
- 📊 Data Table view toggle
- 📈 Charts view toggle
- ⛶ Maximize/fullscreen option
- Show/hide fields selector
- Data grouping interval selector (30 min default)
- Simple static data display (no refresh controls needed)
Chart Data Processing:
- Handle variable dataset sizes efficiently (FloodBoy001: 7K+ records, FloodBoy016: 4K+ records)
- Group data by time intervals to reduce chart complexity (30min intervals typical)
- Apply smoothing to grouped data for cleaner visualization
- Show dynamic sample counts for grouped metrics (varies by store: "9 samples", "3 samples", etc.)
- Maintain responsive performance across all store sizes
- Adapt to different water depth ranges (001: ~0.44m, 016: ~0.54m) and sample frequencies
For direct blockchain access commands (cast/curl), see the dedicated Open Data page: /opendata Chain ID: 8899 (JIBCHAIN L1)
RPC URL: https://rpc-l1.jibchain.net
Block Explorer: https://exp.jibchain.net
FloodBoy001 Store: 0xCd3Ec17ddFDa24f8F97131fa0FDf20e7cbd1A8Bb