/** * Component: LatestBlocks * Author: Nearblocks Pte Ltd * License: Business Source License 1.1 * Description: Latest Blocks on Near Protocol. */ /* INCLUDE: "includes/formats.jsx" */ function convertToMetricPrefix(number) { const prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; // Metric prefixes let count = 0; while (Math.abs(number) >= 1000 && count < prefixes.length - 1) { number /= 1000; count++; } return number.toFixed(2) + prefixes[count]; } function gasFee(gas, price) { const near = yoctoToNear(Big(gas).mul(Big(price)).toString(), true); return `${near} Ⓝ`; } function currency(number) { let absNumber = Math.abs(number); const suffixes = ['', 'K', 'M', 'B', 'T', 'Q']; let suffixIndex = 0; while (absNumber >= 1000 && suffixIndex < suffixes.length - 1) { absNumber /= 1000; suffixIndex++; } let shortNumber = parseFloat(absNumber.toFixed(2)); return (number < 0 ? '-' : '') + shortNumber + ' ' + suffixes[suffixIndex]; } function formatDate(dateString) { const inputDate = new Date(dateString); const days = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ]; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const dayOfWeek = days[inputDate.getDay()]; const month = months[inputDate.getMonth()]; const day = inputDate.getDate(); const year = inputDate.getFullYear(); const formattedDate = dayOfWeek + ', ' + month + ' ' + day + ', ' + year; return formattedDate; } function formatCustomDate(inputDate) { var date = new Date(inputDate); // Array of month names var monthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; // Get month and day var month = monthNames[date.getMonth()]; var day = date.getDate(); // Create formatted date string in "MMM DD" format var formattedDate = month + ' ' + (day < 10 ? '0' + day : day); return formattedDate; } function shortenHex(address) { return `${address && address.substr(0, 6)}...${address.substr(-4)}`; } function yoctoToNear(yocto, format) { const YOCTO_PER_NEAR = Big(10).pow(24).toString(); const near = Big(yocto).div(YOCTO_PER_NEAR).toString(); return format ? localFormat(near) : near; } function getTimeAgoString(timestamp) { const currentUTC = Date.now(); const date = new Date(timestamp); const seconds = Math.floor((currentUTC - date.getTime()) / 1000); const intervals = { year: seconds / (60 * 60 * 24 * 365), month: seconds / (60 * 60 * 24 * 30), week: seconds / (60 * 60 * 24 * 7), day: seconds / (60 * 60 * 24), hour: seconds / (60 * 60), minute: seconds / 60, }; if (intervals.year > 1) { return Math.floor(intervals.year) + ' years ago'; } else if (intervals.month > 1) { return Math.floor(intervals.month) + ' months ago'; } else if (intervals.week > 1) { return Math.floor(intervals.week) + ' weeks ago'; } else if (intervals.day > 1) { return Math.floor(intervals.day) + ' days ago'; } else if (intervals.hour > 1) { return Math.floor(intervals.hour) + ' hours ago'; } else if (intervals.minute > 1) { return Math.floor(intervals.minute) + ' minutes ago'; } else { return 'a few seconds ago'; } } function formatWithCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function formatTimestampToString(timestamp) { const date = new Date(timestamp); // Format the date to 'YYYY-MM-DD HH:mm:ss' format const formattedDate = date.toISOString().replace('T', ' ').split('.')[0]; return formattedDate; } function convertToMetricPrefix(number) { const prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; // Metric prefixes let count = 0; while (Math.abs(number) >= 1000 && count < prefixes.length - 1) { number /= 1000; count++; } return number.toFixed(2) + prefixes[count]; } function gasFee(gas, price) { const near = yoctoToNear(Big(gas).mul(Big(price)).toString(), true); return `${near} Ⓝ`; } function currency(number) { let absNumber = Math.abs(number); const suffixes = ['', 'K', 'M', 'B', 'T', 'Q']; let suffixIndex = 0; while (absNumber >= 1000 && suffixIndex < suffixes.length - 1) { absNumber /= 1000; suffixIndex++; } let shortNumber = parseFloat(absNumber.toFixed(2)); return (number < 0 ? '-' : '') + shortNumber + ' ' + suffixes[suffixIndex]; } function formatDate(dateString) { const inputDate = new Date(dateString); const days = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ]; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const dayOfWeek = days[inputDate.getDay()]; const month = months[inputDate.getMonth()]; const day = inputDate.getDate(); const year = inputDate.getFullYear(); const formattedDate = dayOfWeek + ', ' + month + ' ' + day + ', ' + year; return formattedDate; } function formatCustomDate(inputDate) { var date = new Date(inputDate); // Array of month names var monthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; // Get month and day var month = monthNames[date.getMonth()]; var day = date.getDate(); // Create formatted date string in "MMM DD" format var formattedDate = month + ' ' + (day < 10 ? '0' + day : day); return formattedDate; } function shortenHex(address) { return `${address && address.substr(0, 6)}...${address.substr(-4)}`; } function yoctoToNear(yocto, format) { const YOCTO_PER_NEAR = Big(10).pow(24).toString(); const near = Big(yocto).div(YOCTO_PER_NEAR).toString(); return format ? localFormat(near) : near; } function localFormat(number) { const formattedNumber = Number(number).toLocaleString('en', { minimumFractionDigits: 0, maximumFractionDigits: 5, }); return formattedNumber; } function dollarFormat(number) { const formattedNumber = Number(number).toLocaleString('en', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); return formattedNumber; } function weight(number) { const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; let suffixIndex = 0; while (number >= 1000 && suffixIndex < suffixes.length - 1) { number /= 1000; suffixIndex++; } return number.toFixed(2) + ' ' + suffixes[suffixIndex]; } function convertToUTC(timestamp, hour) { const date = new Date(timestamp); // Get UTC date components const utcYear = date.getUTCFullYear(); const utcMonth = ('0' + (date.getUTCMonth() + 1)).slice(-2); // Adding 1 because months are zero-based const utcDay = ('0' + date.getUTCDate()).slice(-2); const utcHours = ('0' + date.getUTCHours()).slice(-2); const utcMinutes = ('0' + date.getUTCMinutes()).slice(-2); const utcSeconds = ('0' + date.getUTCSeconds()).slice(-2); // Array of month abbreviations const monthAbbreviations = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; const monthIndex = Number(utcMonth) - 1; // Format the date as required (Jul-25-2022 16:25:37) const formattedDate = monthAbbreviations[monthIndex] + '-' + utcDay + '-' + utcYear + ' ' + utcHours + ':' + utcMinutes + ':' + utcSeconds; if (hour) { const currentDate = new Date(); const differenceInSeconds = Math.floor( (currentDate.getTime() - date.getTime()) / 1000, ); if (differenceInSeconds < 60) { return 'a few seconds ago'; } else if (differenceInSeconds < 3600) { const minutes = Math.floor(differenceInSeconds / 60); return minutes + ' minute' + (minutes !== 1 ? 's' : '') + ' ago'; } else if (differenceInSeconds < 86400) { const hours = Math.floor(differenceInSeconds / 3600); return hours + ' hour' + (hours !== 1 ? 's' : '') + ' ago'; } return formattedDate; } return formattedDate; } function getTimeAgoString(timestamp) { const currentUTC = Date.now(); const date = new Date(timestamp); const seconds = Math.floor((currentUTC - date.getTime()) / 1000); const intervals = { year: seconds / (60 * 60 * 24 * 365), month: seconds / (60 * 60 * 24 * 30), week: seconds / (60 * 60 * 24 * 7), day: seconds / (60 * 60 * 24), hour: seconds / (60 * 60), minute: seconds / 60, }; if (intervals.year > 1) { return Math.floor(intervals.year) + ' years ago'; } else if (intervals.month > 1) { return Math.floor(intervals.month) + ' months ago'; } else if (intervals.week > 1) { return Math.floor(intervals.week) + ' weeks ago'; } else if (intervals.day > 1) { return Math.floor(intervals.day) + ' days ago'; } else if (intervals.hour > 1) { return Math.floor(intervals.hour) + ' hours ago'; } else if (intervals.minute > 1) { return Math.floor(intervals.minute) + ' minutes ago'; } else { return 'a few seconds ago'; } } function formatWithCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function formatTimestampToString(timestamp) { const date = new Date(timestamp); // Format the date to 'YYYY-MM-DD HH:mm:ss' format const formattedDate = date.toISOString().replace('T', ' ').split('.')[0]; return formattedDate; } function convertToMetricPrefix(number) { const prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; // Metric prefixes let count = 0; while (Math.abs(number) >= 1000 && count < prefixes.length - 1) { number /= 1000; count++; } return number.toFixed(2) + prefixes[count]; } function gasFee(gas, price) { const near = yoctoToNear(Big(gas).mul(Big(price)).toString(), true); return `${near} Ⓝ`; } function currency(number) { let absNumber = Math.abs(number); const suffixes = ['', 'K', 'M', 'B', 'T', 'Q']; let suffixIndex = 0; while (absNumber >= 1000 && suffixIndex < suffixes.length - 1) { absNumber /= 1000; suffixIndex++; } let shortNumber = parseFloat(absNumber.toFixed(2)); return (number < 0 ? '-' : '') + shortNumber + ' ' + suffixes[suffixIndex]; } function formatDate(dateString) { const inputDate = new Date(dateString); const days = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ]; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const dayOfWeek = days[inputDate.getDay()]; const month = months[inputDate.getMonth()]; const day = inputDate.getDate(); const year = inputDate.getFullYear(); const formattedDate = dayOfWeek + ', ' + month + ' ' + day + ', ' + year; return formattedDate; } function formatCustomDate(inputDate) { var date = new Date(inputDate); // Array of month names var monthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; // Get month and day var month = monthNames[date.getMonth()]; var day = date.getDate(); // Create formatted date string in "MMM DD" format var formattedDate = month + ' ' + (day < 10 ? '0' + day : day); return formattedDate; } function shortenHex(address) { return `${address && address.substr(0, 6)}...${address.substr(-4)}`; } function yoctoToNear(yocto, format) { const YOCTO_PER_NEAR = Big(10).pow(24).toString(); const near = Big(yocto).div(YOCTO_PER_NEAR).toString(); return format ? localFormat(near) : near; } /* END_INCLUDE: "includes/formats.jsx" */ /* INCLUDE: "includes/libs.jsx" */ function getConfig(network) { switch (network) { case 'mainnet': return { ownerId: 'nearblocks.near', nodeUrl: 'https://rpc.mainnet.near.org', backendUrl: 'https://api-beta.nearblocks.io/v1/', rpcUrl: 'https://archival-rpc.testnet.near.org', appUrl: 'https://nearblocks.io/', }; case 'testnet': return { ownerId: 'nearblocks.testnet', nodeUrl: 'https://rpc.testnet.near.org', backendUrl: 'https://api-testnet-beta.nearblocks.io/v1/', rpcUrl: 'https://archival-rpc.testnet.near.org', appUrl: 'https://testnet.nearblocks.io/', }; default: return {}; } } function nanoToMilli(nano) { return Big(nano).div(Big(10).pow(6)).round().toNumber(); } function shortenAddress(address) { const string = String(address); if (string.length <= 20) return string; return `${string.substr(0, 10)}...${string.substr(-7)}`; } function truncateString(str, maxLength, suffix) { if (str.length <= maxLength) { return str; } return str.substring(0, maxLength - suffix.length) + suffix; } function getConfig(network) { switch (network) { case 'mainnet': return { ownerId: 'nearblocks.near', nodeUrl: 'https://rpc.mainnet.near.org', backendUrl: 'https://api-beta.nearblocks.io/v1/', rpcUrl: 'https://archival-rpc.testnet.near.org', appUrl: 'https://nearblocks.io/', }; case 'testnet': return { ownerId: 'nearblocks.testnet', nodeUrl: 'https://rpc.testnet.near.org', backendUrl: 'https://api-testnet-beta.nearblocks.io/v1/', rpcUrl: 'https://archival-rpc.testnet.near.org', appUrl: 'https://testnet.nearblocks.io/', }; default: return {}; } } /* END_INCLUDE: "includes/libs.jsx" */ function MainComponent() { const [isLoading, setIsLoading] = useState(false); const [blocks, setBlocks] = useState([]); const config = getConfig(context.networkId); const Loader = (props) => { return ( <div className={`bg-gray-200 h-5 rounded shadow-sm animate-pulse ${props.className}`} ></div> ); }; useEffect(() => { function fetchLatestBlocks() { setIsLoading(true); asyncFetch(`${config.backendUrl}blocks/latest`).then( (data ) => { const resp = data?.body?.blocks; setBlocks(resp); }, ); setIsLoading(false); } fetchLatestBlocks(); const interval = setInterval(() => { fetchLatestBlocks(); }, 5000); return () => clearInterval(interval); }, [config.backendUrl]); return ( <> <div className="relative"> <ScrollArea.Root> <ScrollArea.Viewport> {!blocks && ( <div className="flex items-center h-16 mx-3 py-2 text-gray-400 text-xs"> Error! </div> )} {!isLoading && blocks.length === 0 && ( <div className="flex items-center h-16 mx-3 py-2 text-gray-400 text-xs"> No blocks found </div> )} {isLoading && ( <div className="px-3 divide-y h-80"> {[...Array(10)].map((_, i) => ( <div className="grid grid-cols-2 md:grid-cols-3 gap-3 py-3" key={i} > <div className="flex items-center"> <div className="flex-shrink-0 rounded-lg h-10 w-10 bg-blue-900/10 flex items-center justify-center text-sm"> BK </div> <div className="px-2"> <div className="text-green-500 text-sm"> <Loader className="h-4" wrapperClassName="h-5 w-14" /> </div> <div className="text-gray-400 text-xs"> <Loader className="h-3" wrapperClassName="h-4 w-24" /> </div> </div> </div> <div className="col-span-2 md:col-span-1 px-2 order-2 md:order-1 text-sm"> <Loader className="h-4" wrapperClassName="h-5 w-36" /> <div className="text-gray-400 text-sm"> <Loader className="h-4" wrapperClassName="h-5 w-14" /> </div> </div> <div className="text-right order-1 md:order-2"> <Loader wrapperClassName="ml-auto w-32" /> </div> </div> ))} </div> )} {blocks.length > 0 && ( <div className="px-3 divide-y h-80"> {blocks.map((block) => { return ( <div className="grid grid-cols-2 md:grid-cols-3 gap-2 lg:gap-3 py-3" key={block.block_hash} > <div className=" flex items-center"> <div className="flex-shrink-0 rounded-lg h-10 w-10 bg-blue-900/10 flex items-center justify-center text-sm"> BK </div> <div className="overflow-hidden pl-2"> <div className="text-green-500 text-sm font-medium "> <a href={`/blocks/${block.block_hash}`}> <a className="text-green-500"> {localFormat(block.block_height)} </a> </a> </div> <div className="text-gray-400 text-xs truncate"> {getTimeAgoString( nanoToMilli(block.block_timestamp), )} </div> </div> </div> <div className="col-span-2 md:col-span-1 px-2 order-2 md:order-1 text-sm whitespace-nowrap truncate"> Author{' '} <a href={`/address/${block.author_account_id}`}> <a className="text-green-500 font-medium"> {block.author_account_id} </a> </a> <div className="text-gray-400 text-sm "> {localFormat(block?.transactions_agg.count || 0)} txns{' '} </div> </div> <div className="text-right order-1 md:order-2 overflow-hidden"> <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <span className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 text-gray-400 truncate"> {block.chunks_agg.gas_used ? convertToMetricPrefix( block.chunks_agg.gas_used, ) : '0 '} gas </span> </Tooltip.Trigger> <Tooltip.Content className="h-auto max-w-xs bg-black bg-opacity-90 z-10 text-white text-xs p-2" sideOffset={5} > Gas used </Tooltip.Content> </Tooltip.Root> </Tooltip.Provider> </div> </div> ); })} </div> )} </ScrollArea.Viewport> <ScrollArea.Scrollbar className="flex select-none touch-none p-0.5 bg-gray-400 transition-colors duration-[160ms] ease-out hover:bg-blend-darken data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5" orientation="vertical" > <ScrollArea.Thumb className="flex-1 bg-gray-400 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" /> </ScrollArea.Scrollbar> <ScrollArea.Scrollbar className="flex select-none touch-none p-0.5 bg-gray-400 transition-colors duration-[160ms] ease-out hover:bg-blend-darken data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5" orientation="horizontal" > <ScrollArea.Thumb className="flex-1 bg-gray-400 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" /> </ScrollArea.Scrollbar> <ScrollArea.Corner className="bg-black-500" /> </ScrollArea.Root> </div> {isLoading && ( <div className="border-t px-2 py-3 text-gray-700"> <Loader className="h-10" /> </div> )} {blocks && blocks.length > 0 && ( <div className="border-t px-2 py-3 text-gray-700"> <a href="/blocks"> <a className="block text-center border border-green-900/10 bg-green-500 hover:bg-green-400 font-thin text-white text-xs py-3 rounded w-full focus:outline-none hover:no-underline"> View all blocks </a> </a> </div> )} </> ); } return MainComponent(props, context);