Got existing frontend working with updated API

Currently only renders `"ethereum_blockchain"` events.

Moved all stream state into the `useStream` hook. This is how we started
and really this is what makes sense rather than putting some pretty
heavy logic into the component/view.
pull/105/head
Neeraj Kashyap 2021-08-22 08:43:25 -07:00
rodzic 6959b78daf
commit 686bfef49b
8 zmienionych plików z 424 dodań i 189 usunięć

Wyświetl plik

@ -44,7 +44,7 @@ const FILTER_TYPES = {
ADDRESS: 0,
GAS: 1,
GAS_PRICE: 2,
AMMOUNT: 3,
AMOUNT: 3,
HASH: 4,
DISABLED: 99,
};
@ -63,6 +63,7 @@ const EntriesNavigation = () => {
const ui = useContext(UIContext);
const { isOpen, onOpen, onClose } = useDisclosure();
const { subscriptionsCache } = useSubscriptions();
const [entries, setEntries] = useState([]);
const [newFilterState, setNewFilterState] = useState([
{
type: FILTER_TYPES.ADDRESS,
@ -75,93 +76,44 @@ const EntriesNavigation = () => {
const loadMoreButtonRef = useRef(null);
const [streamBoundary, setStreamBoundary] = useState({
start_time: null,
end_time: null,
include_start: false,
include_end: true,
next_event_time: null,
previous_event_time: null,
});
const updateStreamBoundaryWith = (pageBoundary) => {
if (!pageBoundary) {
return streamBoundary;
}
let newBoundary = { ...streamBoundary };
// We do not check if there is no overlap between the streamBoundary and the pageBoundary - we assume
// that there *is* an overlap and even if there isn't the stream should gracefully respect the
// pageBoundary because that was the most recent request the user made.
// TODO(zomglings): If there is no overlap in boundaries, replace streamBoundary with pageBoundary.
// No overlap logic:
// if (<no overlap>) {
// setStreamBoundary(pageBoundary)
// return pageBoundary
// }
if (
!newBoundary.start_time ||
(pageBoundary.start_time &&
pageBoundary.start_time <= newBoundary.start_time)
) {
newBoundary.start_time = pageBoundary.start_time;
newBoundary.include_start =
newBoundary.include_start || pageBoundary.include_start;
}
newBoundary.include_start =
newBoundary.include_start || pageBoundary.include_start;
if (
!newBoundary.end_time ||
(pageBoundary.end_time && pageBoundary.end_time >= newBoundary.end_time)
) {
newBoundary.end_time = pageBoundary.end_time;
newBoundary.include_end =
newBoundary.include_end || pageBoundary.include_end;
}
newBoundary.include_end =
newBoundary.include_end || pageBoundary.include_end;
if (
!newBoundary.next_event_time ||
!pageBoundary.next_event_time ||
(pageBoundary.next_event_time &&
pageBoundary.next_event_time > newBoundary.next_event_time)
) {
newBoundary.next_event_time = pageBoundary.next_event_time;
}
if (
!newBoundary.previous_event_time ||
!pageBoundary.previous_event_time ||
(pageBoundary.previous_event_time &&
pageBoundary.previous_event_time < newBoundary.previous_event_time)
) {
newBoundary.previous_event_time = pageBoundary.previous_event_time;
}
setStreamBoundary(newBoundary);
return newBoundary;
};
const { EntriesPages, isLoading, refetch, isFetching, remove } = useStream({
searchQuery: ui.searchTerm,
start_time: streamBoundary.start_time,
end_time: streamBoundary.end_time,
include_start: streamBoundary.include_start,
include_end: streamBoundary.include_end,
updateStreamBoundaryWith: updateStreamBoundaryWith,
streamBoundary: streamBoundary,
setStreamBoundary: setStreamBoundary,
isContent: false,
});
const {
events,
eventsIsLoading,
eventsRefetch,
eventsIsFetching,
eventsRemove,
latestEvents,
latestEventsIsLoading,
latestEventsRefetch,
latestEventsIsFetching,
nextEvent,
nextEventRefetch,
previousEvent,
previousEventRefetch,
streamBoundary,
setDefaultBoundary,
} = useStream(ui.searchTerm.q);
useEffect(() => {
if (!streamBoundary.start_time && !streamBoundary.end_time) {
refetch();
setDefaultBoundary();
} else {
eventsRefetch();
latestEventsRefetch();
nextEventRefetch();
previousEventRefetch();
}
}, [streamBoundary, refetch]);
}, [streamBoundary]);
useEffect(() => {
if (events) {
events.then((data) => {
if (data && data.events) {
setEntries(data.events);
}
});
}
}, [events]);
const setFilterProps = useCallback(
(filterIdx, props) => {
@ -183,13 +135,6 @@ const EntriesNavigation = () => {
}
}, [subscriptionsCache, newFilterState, setFilterProps]);
const entriesPagesData = EntriesPages
? EntriesPages.data.map((page) => {
return page;
})
: [""];
const entries = entriesPagesData.flat();
const canCreate = false;
const canDelete = false;
@ -265,7 +210,7 @@ const EntriesNavigation = () => {
direction="column"
flexGrow={1}
>
{entries && !isLoading ? (
{entries && !eventsIsLoading ? (
<>
<Drawer onClose={onClose} isOpen={isOpen} size="lg">
<DrawerOverlay />
@ -476,10 +421,10 @@ const EntriesNavigation = () => {
//onScroll={(e) => handleScroll(e)}
>
<Stack direction="row" justifyContent="space-between">
{!isFetching ? (
{!eventsIsFetching ? (
<Button
onClick={() => {
remove();
eventsRemove();
setStreamBoundary({
start_time: null,
end_time: null,
@ -505,7 +450,7 @@ const EntriesNavigation = () => {
{streamBoundary.next_event_time &&
streamBoundary.end_time != 0 &&
!isFetching ? (
!eventsIsFetching ? (
<Button
onClick={() => {
updateStreamBoundaryWith({
@ -523,24 +468,22 @@ const EntriesNavigation = () => {
"" // some strange behaivior without else condition return 0 wich can see on frontend page
)}
</Stack>
{entries
?.sort((a, b) => b.timestamp - a.timestamp) // TODO(Andrey) improve that for bi chunks of data sorting can take time
.map((entry, idx) => (
<StreamEntry
showOnboardingTooltips={false}
key={`entry-list-${idx}`}
entry={entry}
disableDelete={!canDelete}
disableCopy={!canCreate}
filterCallback={handleFilterStateCallback}
filterConstants={{ DIRECTIONS, CONDITION, FILTER_TYPES }}
/>
))}
{streamBoundary.previous_event_time && !isFetching ? (
{entries.map((entry, idx) => (
<StreamEntry
showOnboardingTooltips={false}
key={`entry-list-${idx}`}
entry={entry}
disableDelete={!canDelete}
disableCopy={!canCreate}
filterCallback={handleFilterStateCallback}
filterConstants={{ DIRECTIONS, CONDITION, FILTER_TYPES }}
/>
))}
{streamBoundary.previous_event_time && !eventsIsFetching ? (
<Center>
<Button
onClick={() => {
remove();
eventsRemove();
updateStreamBoundaryWith({
start_time: streamBoundary.previous_event_time - 5 * 60,
include_start: false,
@ -555,7 +498,7 @@ const EntriesNavigation = () => {
</Center>
) : (
<Center>
{!isFetching ? (
{!eventsIsFetching ? (
"Тransactions not found. You can subscribe to more addresses in Subscriptions menu."
) : (
<Button
@ -567,7 +510,7 @@ const EntriesNavigation = () => {
)}
</Center>
)}
{streamBoundary.previous_event_time && isLoading ? (
{streamBoundary.previous_event_time && eventsIsLoading ? (
<Center>
<Spinner
//hidden={!isFetchingMore}

Wyświetl plik

@ -34,7 +34,7 @@ const StreamEntry_ = ({ entry, showOnboardingTooltips, className }) => {
h="100%"
spacing={0}
>
{entry.subscription_type_id === "0" && (
{entry.event_type === "ethereum_blockchain" && (
<EthereumMempoolCard entry={entry} />
)}

Wyświetl plik

@ -24,6 +24,8 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
const { onCopy, hasCopied } = useClipboard(copyString, 1);
const toast = useToast();
console.log("GREEN RAIN:", subscriptionsCache.data);
useEffect(() => {
if (hasCopied && copyString) {
toast("Copied to clipboard", "success");
@ -38,14 +40,24 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
const from_color =
subscriptionsCache.data.subscriptions.find((obj) => {
return obj.address === entry.from_address;
return obj.address === entry.event_data.from;
})?.color ?? "gray.500";
const from_label =
subscriptionsCache.data.subscriptions.find((obj) => {
return obj.address === entry.event_data.from;
})?.label ?? entry.event_data.from;
const to_color =
subscriptionsCache.data.subscriptions.find((obj) => {
return obj.address === entry.to_address;
return obj.address === entry.event_data.to;
})?.color ?? "gray.500";
const to_label =
subscriptionsCache.data.subscriptions.find((obj) => {
return obj.address === entry.event_data.to;
})?.label ?? entry.event_data.to;
return (
<Stack className={className}>
<Tooltip
@ -76,8 +88,12 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
Hash
</Heading>
<Spacer />
<Text isTruncated onClick={() => setCopyString(entry.hash)} pr={12}>
{entry.hash}
<Text
isTruncated
onClick={() => setCopyString(entry.event_data.hash)}
pr={12}
>
{entry.event_data.hash}
</Text>
</Stack>
</Tooltip>
@ -125,7 +141,7 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
From:
</Text>
</Tooltip>
<Tooltip label={entry.from_address} aria-label="From:">
<Tooltip label={entry.event_data.from} aria-label="From:">
<Text
mx={0}
py="2px"
@ -134,9 +150,9 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
isTruncated
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.from_address)}
onClick={() => setCopyString(entry.event_data.from)}
>
{entry.from_label ?? entry.from_address}
{from_label}
</Text>
</Tooltip>
</Stack>
@ -162,15 +178,15 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
>
To:
</Text>
<Tooltip label={entry.to_address} aria-label="From:">
<Tooltip label={entry.event_data.to} aria-label="From:">
<Text
bgColor={to_color}
isTruncated
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.to_address)}
onClick={() => setCopyString(entry.event_data.to)}
>
{entry.to_label ?? entry.to_address}
{to_label}
</Text>
</Tooltip>
</Stack>
@ -188,16 +204,16 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
>
Gas Price:
</Text>
<Tooltip label={entry.gasPrice} aria-label="Gas Price:">
<Tooltip label={entry.event_data.gasPrice} aria-label="Gas Price:">
<Text
mx={0}
py="2px"
fontSize="sm"
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.gasPrice)}
onClick={() => setCopyString(entry.event_data.gasPrice)}
>
{entry.gasPrice}
{entry.event_data.gasPrice}
</Text>
</Tooltip>
</Flex>
@ -212,16 +228,16 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
>
Gas:
</Text>
<Tooltip label={entry.gas} aria-label="Gas:">
<Tooltip label={entry.event_data.gas} aria-label="Gas:">
<Text
mx={0}
py="2px"
fontSize="sm"
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.gas)}
onClick={() => setCopyString(entry.event_data.gas)}
>
{entry.gas}
{entry.event_data.gas}
</Text>
</Tooltip>
</Flex>
@ -236,16 +252,16 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
>
Value:
</Text>
<Tooltip label={entry.value} aria-label="Value:">
<Tooltip label={entry.event_data.value} aria-label="Value:">
<Text
mx={0}
py="2px"
fontSize="sm"
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.value)}
onClick={() => setCopyString(entry.event_data.value)}
>
{entry.value}
{entry.event_data.value}
</Text>
</Tooltip>
</Flex>
@ -261,20 +277,20 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
>
Nonce:
</Text>
<Tooltip label={entry.value} aria-label="Value:">
<Tooltip label={entry.event_data.value} aria-label="Value:">
<Text
mx={0}
py="2px"
fontSize="sm"
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.value)}
onClick={() => setCopyString(entry.event_data.value)}
>
{entry.nonce}
{entry.event_data.nonce}
</Text>
</Tooltip>
</Flex>
{entry.timestamp && (
{entry.event_timestamp && (
<Flex h="auto" minW="fit-content">
<Text
px={1}
@ -285,7 +301,9 @@ const EthereumMempoolCard_ = ({ entry, showOnboardingTooltips, className }) => {
h="100%"
borderColor="gray.700"
>
{moment(entry.timestamp * 1000).format("DD MMM, YYYY, HH:mm:ss")}{" "}
{moment(entry.event_timestamp * 1000).format(
"DD MMM, YYYY, HH:mm:ss"
)}{" "}
</Text>
</Flex>
)}

Wyświetl plik

@ -1,4 +1,4 @@
export const BUGOUT_API_URL = process.env.NEXT_PUBLIC_SIMIOTICS_SEARCH_URL;
export const MOONSTREAM_API_URL = process.env.NEXT_PUBLIC_MOONSTREAM_API_URL;
export const BUGOUT_ENDPOINTS = {
Usage: "usage",

Wyświetl plik

@ -1,61 +1,211 @@
import { useState } from "react";
import { StreamService } from "../services";
import { useQuery } from "react-query";
import { queryCacheProps } from "./hookCommon";
import { defaultStreamBoundary } from "../services/servertime.service.js";
const useJournalEntries = ({
searchQuery,
start_time,
end_time,
include_start,
include_end,
updateStreamBoundaryWith,
}) => {
// set our get method
const getStream =
(searchTerm, start_time, end_time, include_start, include_end) =>
async () => {
// Request with params to streams
const response = await StreamService.getStream({
searchTerm: searchTerm,
start_time: start_time,
end_time: end_time,
include_start: include_start,
include_end: include_end,
});
const useStream = (q) => {
const [streamQuery, setStreamQuery] = useState(q || "");
const [streamBoundary, setStreamBoundary] = useState({});
// new events from stream
const newEventsList = response.data.stream.map((event) => ({
...event,
}));
const isStreamBoundaryEmpty = () => {
return !streamBoundary.start_time && !streamBoundary.end_time;
};
return {
data: [...newEventsList],
boundaries: { ...response.data.boundaries, update: false },
};
};
const setDefaultBoundary = async () => {
const defaultBoundary = await defaultStreamBoundary();
setStreamBoundary(defaultBoundary);
};
const { data, isLoading, refetch, isFetching, remove } = useQuery(
["stream", searchQuery, start_time, end_time],
getStream(searchQuery, start_time, end_time, include_start, include_end),
const updateStreamBoundaryWith = (extensionBoundary) => {
if (!extensionBoundary) {
return streamBoundary;
}
let newBoundary = { ...streamBoundary };
// We do not check if there is no overlap between the streamBoundary and the pageBoundary - we assume
// that there *is* an overlap and even if there isn't the stream should gracefully respect the
// pageBoundary because that was the most recent request the user made.
// TODO(zomglings): If there is no overlap in boundaries, replace streamBoundary with pageBoundary.
// No overlap logic:
// if (<no overlap>) {
// setStreamBoundary(pageBoundary)
// return pageBoundary
// }
if (
!newBoundary.start_time ||
(extensionBoundary.start_time &&
extensionBoundary.start_time <= newBoundary.start_time)
) {
newBoundary.start_time = extensionBoundary.start_time;
newBoundary.include_start =
newBoundary.include_start || extensionBoundary.include_start;
}
newBoundary.include_start =
newBoundary.include_start || extensionBoundary.include_start;
if (
!newBoundary.end_time ||
(extensionBoundary.end_time &&
extensionBoundary.end_time >= newBoundary.end_time)
) {
newBoundary.end_time = extensionBoundary.end_time;
newBoundary.include_end =
newBoundary.include_end || extensionBoundary.include_end;
}
newBoundary.include_end =
newBoundary.include_end || extensionBoundary.include_end;
setStreamBoundary(newBoundary);
return newBoundary;
};
const getEvents = () => async () => {
const response = await StreamService.getEvents({
streamQuery,
...streamBoundary,
});
return response.data;
};
const {
data: events,
isLoading: eventsIsLoading,
refetch: eventsRefetch,
isFetching: eventsIsFetching,
remove: eventsRemove,
} = useQuery(
["stream-events", streamQuery],
() => {
if (isStreamBoundaryEmpty()) {
return null;
}
return getEvents();
},
{
//refetchInterval: refreshRate,
...queryCacheProps,
keepPreviousData: true,
retry: 3,
onSuccess: (response) => {
// response is object which return condition in getStream
// TODO(andrey): Response should send page parameters inside "boundary" object (can be null).
updateStreamBoundaryWith(response.boundaries);
retry: 2,
onSuccess: (newEvents) => {
if (newEvents) {
updateStreamBoundaryWith(newEvents.stream_boundary);
}
},
}
);
const getLatestEvents = async () => {
const response = await StreamService.latestEvents({ q: streamQuery });
return response.data;
};
const {
data: latestEvents,
isLoading: latestEventsIsLoading,
refetch: latestEventsRefetch,
isFetching: latestEventsIsFetching,
remove: latestEventsRemove,
} = useQuery(
["stream-latest", streamQuery],
() => {
if (isStreamBoundaryEmpty()) {
return null;
}
return getLatestEvents();
},
{
...queryCacheProps,
keepPreviousData: false,
retry: 2,
}
);
const getNextEvent = async () => {
const response = await StreamService.nextEvent({
q: streamQuery,
...streamBoundary,
});
return response.data;
};
const {
data: nextEvent,
isLoading: nextEventIsLoading,
refetch: nextEventRefetch,
isFetching: nextEventIsFetching,
remove: nextEventRemove,
} = useQuery(
["stream-next", streamQuery],
() => {
if (isStreamBoundaryEmpty()) {
return null;
}
return getNextEvent();
},
{
...queryCacheProps,
keepPreviousData: false,
retry: 2,
}
);
const getPreviousEvent = async () => {
const response = await StreamService.previousEvent({
q: streamQuery,
...streamBoundary,
});
return response.data;
};
const {
data: previousEvent,
isLoading: previousEventIsLoading,
refetch: previousEventRefetch,
isFetching: previousEventIsFetching,
remove: previousEventRemove,
} = useQuery(
["stream-previous", streamQuery],
() => {
if (isStreamBoundaryEmpty()) {
return null;
}
return getPreviousEvent();
},
{
...queryCacheProps,
keepPreviousData: false,
retry: 2,
}
);
return {
EntriesPages: data,
isLoading,
refetch,
isFetching,
remove,
streamBoundary,
setDefaultBoundary,
updateStreamBoundaryWith,
events,
eventsIsLoading,
eventsRefetch,
eventsIsFetching,
eventsRemove,
setStreamQuery,
latestEvents,
latestEventsIsLoading,
latestEventsRefetch,
latestEventsIsFetching,
latestEventsRemove,
nextEvent,
nextEventIsLoading,
nextEventRefetch,
nextEventIsFetching,
nextEventRemove,
previousEvent,
previousEventIsLoading,
previousEventRefetch,
previousEventIsFetching,
previousEventRemove,
};
};
export default useJournalEntries;
export default useStream;

Wyświetl plik

@ -1,4 +1,3 @@
import * as SearchService from "./search.service";
import * as AuthService from "./auth.service";
import * as JournalService from "./journal.service";
import * as EntryService from "./entry.service";
@ -11,7 +10,6 @@ import * as SubscriptionsService from "./subscriptions.service";
import * as StreamService from "./stream.service";
import * as TxInfoService from "./txinfo.service";
export {
SearchService,
AuthService,
JournalService,
EntryService,

Wyświetl plik

@ -0,0 +1,53 @@
import { http } from "../utils";
import { MOONSTREAM_API_URL } from "../constants";
export const serverTimeNow = async () => {
const response = await http({
method: "GET",
url: `${MOONSTREAM_API_URL}/now`,
});
const timestamp = response.data.epoch_time;
// Javascript Date objects are loaded from Unix timestamps at the level of precision of milliseconds
// since epoch start. The server returns microseconds since epoch, but the integer part of the response
// time is at second-level precision.
const jsTimestamp = Math.floor(timestamp * 1000);
const jsDate = new Date(jsTimestamp);
return jsDate;
};
// Returns the milliseconds of difference between the clocks of the client and the Moonstream API
// server. Since this involves a request to the server, it also includes the latency of making an HTTP
// request to the server's /now endpoint and getting a response.
export const clientServerOffsetMillis = async () => {
// TODO(zomglings): Time the request and use a simple estimate for the latency based on:
// 1. Size of request
// 2. Size of resposne
// 3. Profiling the body of the `/now` handler on the server.
// At least a naive estimate would be something like:
// const currentServerTime = serverTime + (responseAt - requestAt)*3/4
//
// This assumes that 3/4 of the latency was involved in sending the response back to the client.
//
// Unfortunately, it also assumes that the client clock and server clock are moving at the same
// speed. Of course, we could check the speeds against each other by repeated calls to this method,
// but I think we don't need that level of synchronizating yet.
const serverTime = await serverTimeNow();
const clientTime = new Date();
return clientTime - serverTime;
};
// Returns a stream boundary representing the past 10 minutes.
export const defaultStreamBoundary = async () => {
const endTime = await serverTimeNow();
// 1 hour ago (in milliseconds)
const startTimeMillis = endTime - 60 * 60 * 1000;
const streamBoundary = {
start_time: Math.floor(startTimeMillis / 1000),
end_time: Math.floor(endTime / 1000),
include_start: true,
include_end: true,
};
return streamBoundary;
};

Wyświetl plik

@ -3,16 +3,17 @@ import { http } from "../utils";
const API = process.env.NEXT_PUBLIC_MOONSTREAM_API_URL;
export const getStream = ({
searchTerm,
export const getEvents = ({
q,
start_time,
end_time,
include_start,
include_end,
}) => {
let params = {
q: searchTerm,
};
let params = {};
if (q) {
params.q = q;
}
if (start_time || start_time === 0) {
params.start_time = start_time;
}
@ -32,3 +33,75 @@ export const getStream = ({
params,
});
};
// params is expected to be an object defining the query parameters to the /streams/latest endpoint
// The /streams/latest endpoint accepts the following query parameters:
// - q: Query filters of the form type:<subscription type> or subscription:from:<address> or subscription:to:<address>
// This will change over time and the API documentation should be the source of truth for these parameters.
// TODO(zomglings): Link here once API docs are set up.
export const latestEvents = (params) => {
return http({ method: "GET", url: `${API}/streams/latest`, params });
};
export const nextEvent = ({
q,
start_time,
end_time,
include_start,
include_end,
}) => {
let params = {};
if (q) {
params.q = q;
}
if (start_time || start_time === 0) {
params.start_time = start_time;
}
if (end_time || end_time === 0) {
params.end_time = end_time;
}
if (include_start) {
params.include_start = include_start;
}
if (include_end) {
params.include_end = include_end;
}
return http({
method: "GET",
url: `${API}/streams/next`,
params,
});
};
export const previousEvent = ({
q,
start_time,
end_time,
include_start,
include_end,
}) => {
// TODO(zomglings): Factor this query parameter building code out into a separate function.
let params = {};
if (q) {
params.q = q;
}
if (start_time || start_time === 0) {
params.start_time = start_time;
}
if (end_time || end_time === 0) {
params.end_time = end_time;
}
if (include_start) {
params.include_start = include_start;
}
if (include_end) {
params.include_end = include_end;
}
return http({
method: "GET",
url: `${API}/streams/previous`,
params,
});
};