// SPDX-License-Identifier: MIT pragma solidity ^0.8.28; /// @title EventRegistry /// @notice A public library of upcoming real-world events. Anyone can list one /// (what, where, when), and tokens on the launchpad point at a listing with the /// event id "listing:". /// /// Listings are permanent records. The lister can cancel their own listing, and /// the owner can hide spam from the app; neither deletes anything. contract EventRegistry { struct ListingParams { string title; string description; // short pitch, shown in the library string category; // "sports", "tech", "space", "music", ... string location; // human-readable place, e.g. "SoFi Stadium, Inglewood" string url; // optional link to the official page or stream string imageURI; // optional uint64 startsAt; // unix seconds uint64 endsAt; // unix seconds, or 0 if unknown int32 latE6; // microdegrees int32 lngE6; } struct Listing { uint256 id; address lister; uint64 listedAt; uint64 startsAt; uint64 endsAt; int32 latE6; int32 lngE6; bool cancelled; bool hidden; string title; string description; string category; string location; string url; string imageURI; } uint256 public constant MAX_TITLE_LENGTH = 80; uint256 public constant MAX_DESCRIPTION_LENGTH = 280; uint256 public constant MAX_CATEGORY_LENGTH = 24; uint256 public constant MAX_LOCATION_LENGTH = 80; uint256 public constant MAX_URI_LENGTH = 256; /// @notice Events can be listed at most this far ahead. uint256 public constant MAX_LEAD_TIME = 730 days; address public owner; Listing[] private _listings; event Listed(uint256 indexed id, address indexed lister, uint64 startsAt, string title); event Cancelled(uint256 indexed id); event HiddenSet(uint256 indexed id, bool hidden); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); error BadTitle(); error BadCategory(); error TooLong(); error BadTime(); error BadCoordinates(); error UnknownListing(); error NotLister(); error NotOwner(); error ZeroAddress(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } constructor(address owner_) { if (owner_ == address(0)) revert ZeroAddress(); owner = owner_; emit OwnershipTransferred(address(0), owner_); } // ----------------------------------------------------------------- list /// @notice List an upcoming event. It must start in the future. function list(ListingParams calldata p) external returns (uint256 id) { uint256 titleLen = bytes(p.title).length; uint256 categoryLen = bytes(p.category).length; if (titleLen == 0 || titleLen > MAX_TITLE_LENGTH) revert BadTitle(); if (categoryLen == 0 || categoryLen > MAX_CATEGORY_LENGTH) revert BadCategory(); if ( bytes(p.description).length > MAX_DESCRIPTION_LENGTH || bytes(p.location).length > MAX_LOCATION_LENGTH || bytes(p.url).length > MAX_URI_LENGTH || bytes(p.imageURI).length > MAX_URI_LENGTH ) revert TooLong(); if (p.startsAt <= block.timestamp || p.startsAt > block.timestamp + MAX_LEAD_TIME) revert BadTime(); if (p.endsAt != 0 && p.endsAt < p.startsAt) revert BadTime(); if (p.latE6 < -90_000_000 || p.latE6 > 90_000_000 || p.lngE6 < -180_000_000 || p.lngE6 > 180_000_000) { revert BadCoordinates(); } id = _listings.length; _listings.push( Listing({ id: id, lister: msg.sender, listedAt: uint64(block.timestamp), startsAt: p.startsAt, endsAt: p.endsAt, latE6: p.latE6, lngE6: p.lngE6, cancelled: false, hidden: false, title: p.title, description: p.description, category: p.category, location: p.location, url: p.url, imageURI: p.imageURI }) ); emit Listed(id, msg.sender, p.startsAt, p.title); } /// @notice The lister can mark their event as cancelled (called off, or listed by mistake). function cancel(uint256 id) external { Listing storage l = _get(id); if (msg.sender != l.lister) revert NotLister(); l.cancelled = true; emit Cancelled(id); } // ------------------------------------------------------------ moderation /// @notice Hide (or unhide) a listing from the app, e.g. spam. The record stays onchain. function setHidden(uint256 id, bool hidden) external onlyOwner { _get(id).hidden = hidden; emit HiddenSet(id, hidden); } function transferOwnership(address newOwner) external onlyOwner { if (newOwner == address(0)) revert ZeroAddress(); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } // ----------------------------------------------------------------- views function listingCount() external view returns (uint256) { return _listings.length; } function getListing(uint256 id) external view returns (Listing memory) { return _get(id); } /// @notice Page through listings, oldest first. Returns fewer than `limit` at the end. function getListings(uint256 offset, uint256 limit) external view returns (Listing[] memory page) { uint256 total = _listings.length; if (offset >= total) return new Listing[](0); uint256 end = offset + limit > total ? total : offset + limit; page = new Listing[](end - offset); for (uint256 i = offset; i < end; i++) { page[i - offset] = _listings[i]; } } function _get(uint256 id) internal view returns (Listing storage) { if (id >= _listings.length) revert UnknownListing(); return _listings[id]; } }