import React, { FormEvent } from 'react'; import { ContractModel } from '../Data/ContractManager'; import './Simulation.css'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { useDispatch, useSelector } from 'react-redux'; import { set_unreachable } from './SimulationSlice'; import { TXID } from '../util'; import { IconButton, Slider, TextField, Typography, Tooltip, useTheme, } from '@mui/material'; import MoreHorizOutlinedIcon from '@mui/icons-material/MoreHorizOutlined'; import { green, red, pink } from '@mui/material/colors'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; import { ChangeEvent } from 'react-transition-group/node_modules/@types/react'; import { selectNetwork } from '../Settings/SettingsSlice'; import Color from 'color'; export function SimulationController(props: { contract: ContractModel; engine: DiagramEngine; hide: () => void; }) { const theme = useTheme(); const dispatch = useDispatch(); const network = useSelector(selectNetwork); // Start at 0 to make scaling work riht away const [min_time_ms, setMinTimeMs] = React.useState(Date.now()); const [max_time_ms, setMaxTimeMs] = React.useState( Date.now() + 365 * 24 * 60 * 60 * 1000 ); const pct_to_value = (p: number, max: number, min: number) => Math.round((max - min) * (p / 100.0) + min); const [first_tx_time_ms, setFirstTxTime] = React.useState( pct_to_value(33, max_time_ms, min_time_ms) ); const [current_time_ms, setCurrentTxTime] = React.useState( pct_to_value(50, max_time_ms, min_time_ms) ); const is_regtest = network === 'Regtest' || network === 'Signet'; const current_year = Math.round( (new Date().getFullYear() - 2008) * 144 * 365 - (144 * 365) / 2 ); const [min_blocks, setMinBlocks] = React.useState( is_regtest ? 100 : current_year ); const [max_blocks, setMaxBlocks] = React.useState( is_regtest ? 1000 : current_year + 365 * 144 ); const [first_tx_block, setFirstTxBlockPct] = React.useState( pct_to_value(33, max_blocks, min_blocks) ); const [current_block, setCurrentBlockPct] = React.useState( pct_to_value(66, max_blocks, min_blocks) ); const clear = () => { dispatch(set_unreachable({})); }; const wrapper = (f: (input: HTMLInputElement) => void) => (e: FormEvent) => { const input = e.currentTarget as HTMLInputElement; f(input); }; const updateMinTime = (e: ChangeEvent<HTMLInputElement>) => { setMinTimeMs(Date.parse(e.currentTarget.value) ?? max_time_ms); }; const updateBlocks = ( e: Event, n: number | number[], activeThumb: number ) => { if (typeof n !== 'number') { if (n.length === 2) { setFirstTxBlockPct(n[0]!); setCurrentBlockPct(n[1]!); } } }; const updateTimes = ( e: Event, n: number | number[], activeThumb: number ) => { if (typeof n !== 'number') { if (n.length === 2) { setFirstTxTime(n[0]!); setCurrentTxTime(n[1]!); } } }; const updateMaxTime = (e: ChangeEvent<HTMLInputElement>) => { setMaxTimeMs(Date.parse(e.currentTarget.value) ?? max_time_ms); }; const updateMinBlocks = wrapper((input: HTMLInputElement) => { setMinBlocks(input.valueAsNumber); }); const updateMaxBlocks = wrapper((input: HTMLInputElement) => { setMaxBlocks(input.valueAsNumber); }); React.useEffect(() => { const unreachable = props.contract.reachable_at_time( current_time_ms / 1000, current_block, first_tx_time_ms / 1000, first_tx_block ); const r: Record<TXID, null> = {}; for (const model of unreachable) { r[model.get_txid()] = null; } dispatch(set_unreachable(r)); }, [ first_tx_block, first_tx_time_ms, max_blocks, min_blocks, max_time_ms, min_time_ms, current_time_ms, current_block, ]); const snapBlocks = () => { const new_first_tx_block = first_tx_block; const new_current_block = current_block; if (new_first_tx_block === new_current_block) return; let new_start = Math.min(new_first_tx_block, new_current_block); // at least one day... let new_end = Math.max( new_first_tx_block, new_current_block, new_start + 144 ); const delta = Math.abs(new_current_block - new_first_tx_block); new_start = Math.max(new_start - delta, 0); new_end += delta; setMinBlocks(Math.round(new_start)); setMaxBlocks(Math.round(new_end)); setFirstTxBlockPct( pct_to_value( new_current_block > new_first_tx_block ? 33 : 66, Math.round(new_end), Math.round(new_start) ) ); setCurrentBlockPct( pct_to_value( new_current_block > new_first_tx_block ? 66 : 33, Math.round(new_end), Math.round(new_start) ) ); }; const snapTime = () => { // work in seconds const new_first_tx_time = first_tx_time_ms / 1000; const new_current_time = current_time_ms / 1000; if (new_first_tx_time === new_current_time) return; let new_start = Math.min(new_first_tx_time, new_current_time); // at least one day... let new_end = Math.max( new_first_tx_time, new_current_time, new_start + 24 * 60 * 60 ); const delta = Math.abs(new_current_time - new_first_tx_time); new_start -= delta; new_end += delta; setMinTimeMs(new_start * 1000); setMaxTimeMs(new_end * 1000); setCurrentTxTime( pct_to_value( new_current_time > new_first_tx_time ? 66 : 33, new_end * 1000, new_start * 1000 ) ); setFirstTxTime( pct_to_value( new_current_time > new_first_tx_time ? 33 : 66, new_end * 1000, new_start * 1000 ) ); }; const first_tx_time_str = new Date(first_tx_time_ms).toLocaleString( undefined, { timeZone: 'UTC', } ); const current_time_str = new Date(current_time_ms).toLocaleString( undefined, { timeZone: 'UTC', } ); const to_time_str = (t: Date) => `${t.getUTCFullYear()}-${t .getUTCMonth() .toString() .padStart(2, '0')}-${t.getUTCDay().toString().padStart(2, '0')}T${t .getUTCHours() .toString() .padStart(2, '0')}:${t .getUTCMinutes() .toString() .padStart(2, '0')}`; const max_time_str = to_time_str(new Date(max_time_ms)); const min_time_str = to_time_str(new Date(min_time_ms)); const ClockControl = ( <div className="Controler"> <div className="ControlerSliders"> <Slider value={[first_tx_time_ms, current_time_ms]} valueLabelFormat={(value: number, index: number) => { const d = new Date(value); return ( <div> <Typography> {d.toLocaleDateString()} </Typography> <p>{d.toLocaleTimeString()}</p> </div> ); }} step={1000} min={min_time_ms} max={max_time_ms} valueLabelDisplay="on" onChange={updateTimes} /> </div> <div className="ControlerSettings"> <h6> Date</h6> <TextField label="Start Time" type="datetime-local" defaultValue={min_time_str} onChange={updateMinTime} InputLabelProps={{ shrink: true, }} /> <TextField label="End Time" type="datetime-local" defaultValue={max_time_str} onChange={updateMaxTime} InputLabelProps={{ shrink: true, }} /> <Tooltip title="Click to Snap Time"> <IconButton aria-label="snap-time" onClick={snapTime}> <MoreHorizOutlinedIcon style={{ color: green[500] }} /> </IconButton> </Tooltip> </div> </div> ); const BlockControl = ( <div className="Controler"> <div className="ControlerSliders"> <Slider value={[first_tx_block, current_block]} min={min_blocks} max={max_blocks} valueLabelDisplay="on" onChange={updateBlocks} /> </div> <div className="ControlerSettings"> <h6> Height</h6> <div> <TextField label="Start Height" value={min_blocks} type="number" onChange={updateMinBlocks} /> </div> <div> <TextField label="End Height" value={max_blocks} type="number" onChange={updateMaxBlocks} /> </div> <Tooltip title="Click to Snap Blocks"> <IconButton aria-label="snap-blocks" onClick={snapBlocks}> <MoreHorizOutlinedIcon style={{ color: green[500] }} /> </IconButton> </Tooltip> </div> </div> ); return ( <form onSubmit={(e: React.FormEvent) => e.preventDefault()} className="Simulation" style={{ backgroundColor: Color(theme.palette.background.default) .fade(0.2) .toString(), }} > <Tooltip title="Close Simulator"> <IconButton aria-label="close-sim" onClick={props.hide}> <CancelOutlinedIcon style={{ color: red[500] }} /> </IconButton> </Tooltip> <Tooltip title="Hide Simulator Results"> <IconButton aria-label="hide-sim" onClick={clear}> <VisibilityOffOutlinedIcon style={{ color: pink[500] }} /> </IconButton> </Tooltip> <div className="Controlers"> {BlockControl} {ClockControl} </div> </form> ); }