envio 3.0.0-alpha.3 → 3.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +2 -2
  2. package/evm.schema.json +0 -1
  3. package/index.d.ts +333 -2
  4. package/index.js +4 -0
  5. package/package.json +13 -6
  6. package/rescript.json +4 -1
  7. package/src/ChainFetcher.res +25 -1
  8. package/src/ChainFetcher.res.mjs +19 -1
  9. package/src/Config.res +212 -19
  10. package/src/Config.res.mjs +228 -29
  11. package/src/{Indexer.res → Ctx.res} +1 -1
  12. package/src/Ecosystem.res +2 -2
  13. package/src/Ecosystem.res.mjs +1 -1
  14. package/src/Envio.gen.ts +1 -1
  15. package/src/Envio.res +1 -1
  16. package/src/EventProcessing.res +18 -18
  17. package/src/EventProcessing.res.mjs +14 -14
  18. package/src/GlobalState.res +29 -35
  19. package/src/GlobalState.res.mjs +47 -47
  20. package/src/GlobalStateManager.res +68 -0
  21. package/src/GlobalStateManager.res.mjs +75 -0
  22. package/src/GlobalStateManager.resi +7 -0
  23. package/src/Internal.res +41 -1
  24. package/src/LogSelection.res +33 -27
  25. package/src/LogSelection.res.mjs +6 -0
  26. package/src/Main.res +342 -0
  27. package/src/Main.res.mjs +289 -0
  28. package/src/PgStorage.gen.ts +10 -0
  29. package/src/PgStorage.res +24 -2
  30. package/src/PgStorage.res.d.mts +5 -0
  31. package/src/PgStorage.res.mjs +22 -1
  32. package/src/Types.ts +1 -1
  33. package/src/UserContext.res +0 -1
  34. package/src/UserContext.res.mjs +0 -2
  35. package/src/Utils.res +28 -0
  36. package/src/Utils.res.mjs +18 -0
  37. package/src/bindings/ClickHouse.res +31 -1
  38. package/src/bindings/ClickHouse.res.mjs +27 -1
  39. package/src/bindings/Ethers.res +27 -67
  40. package/src/bindings/Ethers.res.mjs +18 -70
  41. package/src/bindings/Postgres.gen.ts +8 -0
  42. package/src/bindings/Postgres.res +3 -0
  43. package/src/bindings/Postgres.res.d.mts +5 -0
  44. package/src/bindings/RescriptMocha.res +123 -0
  45. package/src/bindings/RescriptMocha.res.mjs +18 -0
  46. package/src/bindings/Yargs.res +8 -0
  47. package/src/bindings/Yargs.res.mjs +2 -0
  48. package/src/sources/FuelSDK.res +4 -3
  49. package/src/sources/HyperSyncHeightStream.res +28 -110
  50. package/src/sources/HyperSyncHeightStream.res.mjs +30 -63
  51. package/src/sources/HyperSyncSource.res +11 -13
  52. package/src/sources/HyperSyncSource.res.mjs +20 -20
  53. package/src/sources/Rpc.res +43 -0
  54. package/src/sources/Rpc.res.mjs +31 -0
  55. package/src/sources/RpcSource.res +9 -4
  56. package/src/sources/RpcSource.res.mjs +9 -4
  57. package/src/sources/Source.res +1 -0
  58. package/src/sources/SourceManager.res +164 -81
  59. package/src/sources/SourceManager.res.mjs +146 -83
  60. package/src/sources/{Solana.res → Svm.res} +4 -4
  61. package/src/sources/{Solana.res.mjs → Svm.res.mjs} +4 -4
  62. package/src/tui/Tui.res +266 -0
  63. package/src/tui/Tui.res.mjs +342 -0
  64. package/src/tui/bindings/Ink.res +376 -0
  65. package/src/tui/bindings/Ink.res.mjs +75 -0
  66. package/src/tui/bindings/Style.res +123 -0
  67. package/src/tui/bindings/Style.res.mjs +2 -0
  68. package/src/tui/components/BufferedProgressBar.res +40 -0
  69. package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
  70. package/src/tui/components/CustomHooks.res +114 -0
  71. package/src/tui/components/CustomHooks.res.mjs +162 -0
  72. package/src/tui/components/Messages.res +41 -0
  73. package/src/tui/components/Messages.res.mjs +75 -0
  74. package/src/tui/components/SyncETA.res +193 -0
  75. package/src/tui/components/SyncETA.res.mjs +269 -0
  76. package/src/tui/components/TuiData.res +46 -0
  77. package/src/tui/components/TuiData.res.mjs +29 -0
  78. package/src/bindings/Ethers.gen.ts +0 -14
  79. /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
@@ -0,0 +1,114 @@
1
+ open Belt
2
+ module InitApi = {
3
+ type ecosystem = | @as("evm") Evm | @as("fuel") Fuel | @as("svm") Svm
4
+ type body = {
5
+ envioVersion: string,
6
+ envioApiToken: option<string>,
7
+ ecosystem: ecosystem,
8
+ hyperSyncNetworks: array<int>,
9
+ rpcNetworks: array<int>,
10
+ }
11
+
12
+ let bodySchema = S.object(s => {
13
+ envioVersion: s.field("envioVersion", S.string),
14
+ envioApiToken: s.field("envioApiToken", S.option(S.string)),
15
+ ecosystem: s.field("ecosystem", S.enum([Evm, Fuel, Svm])),
16
+ hyperSyncNetworks: s.field("hyperSyncNetworks", S.array(S.int)),
17
+ rpcNetworks: s.field("rpcNetworks", S.array(S.int)),
18
+ })
19
+
20
+ let makeBody = (~envioVersion, ~envioApiToken, ~config: Config.t) => {
21
+ let hyperSyncNetworks = []
22
+ let rpcNetworks = []
23
+ config.chainMap
24
+ ->ChainMap.values
25
+ ->Array.forEach(({sources, id}) => {
26
+ switch sources->Js.Array2.some(s => s.poweredByHyperSync) {
27
+ | true => hyperSyncNetworks
28
+ | false => rpcNetworks
29
+ }
30
+ ->Js.Array2.push(id)
31
+ ->ignore
32
+ })
33
+
34
+ {
35
+ envioVersion,
36
+ envioApiToken,
37
+ ecosystem: (config.ecosystem.name :> ecosystem),
38
+ hyperSyncNetworks,
39
+ rpcNetworks,
40
+ }
41
+ }
42
+
43
+ type messageColor =
44
+ | @as("primary") Primary
45
+ | @as("secondary") Secondary
46
+ | @as("info") Info
47
+ | @as("danger") Danger
48
+ | @as("success") Success
49
+ | @as("white") White
50
+ | @as("gray") Gray
51
+
52
+ let toTheme = (color: messageColor): Style.chalkTheme =>
53
+ switch color {
54
+ | Primary => Primary
55
+ | Secondary => Secondary
56
+ | Info => Info
57
+ | Danger => Danger
58
+ | Success => Success
59
+ | White => White
60
+ | Gray => Gray
61
+ }
62
+
63
+ type message = {
64
+ color: messageColor,
65
+ content: string,
66
+ }
67
+
68
+ let messageSchema = S.object(s => {
69
+ color: s.field("color", S.enum([Primary, Secondary, Info, Danger, Success, White, Gray])),
70
+ content: s.field("content", S.string),
71
+ })
72
+
73
+ let client = Rest.client(Env.envioAppUrl ++ "/api")
74
+
75
+ let route = Rest.route(() => {
76
+ method: Post,
77
+ path: "/hyperindex/init",
78
+ input: s => s.body(bodySchema),
79
+ responses: [s => s.field("messages", S.array(messageSchema))],
80
+ })
81
+
82
+ let getMessages = async (~config) => {
83
+ let envioVersion = Utils.EnvioPackage.value.version
84
+ let body = makeBody(~envioVersion, ~envioApiToken=Env.envioApiToken, ~config)
85
+
86
+ switch await route->Rest.fetch(body, ~client) {
87
+ | exception exn => Error(exn->Obj.magic)
88
+ | messages => Ok(messages)
89
+ }
90
+ }
91
+ }
92
+
93
+ type request<'ok, 'err> = Data('ok) | Loading | Err('err)
94
+
95
+ let useMessages = (~config) => {
96
+ let (request, setRequest) = React.useState(_ => Loading)
97
+ React.useEffect0(() => {
98
+ InitApi.getMessages(~config)
99
+ ->Promise.thenResolve(res =>
100
+ switch res {
101
+ | Ok(data) => setRequest(_ => Data(data))
102
+ | Error(e) =>
103
+ Logging.error({
104
+ "msg": "Failed to load messages from envio server",
105
+ "err": e->Utils.prettifyExn,
106
+ })
107
+ setRequest(_ => Err(e))
108
+ }
109
+ )
110
+ ->ignore
111
+ None
112
+ })
113
+ request
114
+ }
@@ -0,0 +1,162 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Env from "../../Env.res.mjs";
4
+ import * as Rest from "../../vendored/Rest.res.mjs";
5
+ import * as Utils from "../../Utils.res.mjs";
6
+ import * as React from "react";
7
+ import * as Logging from "../../Logging.res.mjs";
8
+ import * as ChainMap from "../../ChainMap.res.mjs";
9
+ import * as Belt_Array from "rescript/lib/es6/belt_Array.js";
10
+ import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
11
+ import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js";
12
+
13
+ var bodySchema = S$RescriptSchema.object(function (s) {
14
+ return {
15
+ envioVersion: s.f("envioVersion", S$RescriptSchema.string),
16
+ envioApiToken: s.f("envioApiToken", S$RescriptSchema.option(S$RescriptSchema.string)),
17
+ ecosystem: s.f("ecosystem", S$RescriptSchema.$$enum([
18
+ "evm",
19
+ "fuel",
20
+ "svm"
21
+ ])),
22
+ hyperSyncNetworks: s.f("hyperSyncNetworks", S$RescriptSchema.array(S$RescriptSchema.$$int)),
23
+ rpcNetworks: s.f("rpcNetworks", S$RescriptSchema.array(S$RescriptSchema.$$int))
24
+ };
25
+ });
26
+
27
+ function makeBody(envioVersion, envioApiToken, config) {
28
+ var hyperSyncNetworks = [];
29
+ var rpcNetworks = [];
30
+ Belt_Array.forEach(ChainMap.values(config.chainMap), (function (param) {
31
+ (
32
+ param.sources.some(function (s) {
33
+ return s.poweredByHyperSync;
34
+ }) ? hyperSyncNetworks : rpcNetworks
35
+ ).push(param.id);
36
+ }));
37
+ return {
38
+ envioVersion: envioVersion,
39
+ envioApiToken: envioApiToken,
40
+ ecosystem: config.ecosystem.name,
41
+ hyperSyncNetworks: hyperSyncNetworks,
42
+ rpcNetworks: rpcNetworks
43
+ };
44
+ }
45
+
46
+ function toTheme(color) {
47
+ switch (color) {
48
+ case "primary" :
49
+ return "#9860E5";
50
+ case "secondary" :
51
+ return "#FFBB2F";
52
+ case "info" :
53
+ return "#6CBFEE";
54
+ case "danger" :
55
+ return "#FF8269";
56
+ case "success" :
57
+ return "#3B8C3D";
58
+ case "white" :
59
+ return "white";
60
+ case "gray" :
61
+ return "gray";
62
+
63
+ }
64
+ }
65
+
66
+ var messageSchema = S$RescriptSchema.object(function (s) {
67
+ return {
68
+ color: s.f("color", S$RescriptSchema.$$enum([
69
+ "primary",
70
+ "secondary",
71
+ "info",
72
+ "danger",
73
+ "success",
74
+ "white",
75
+ "gray"
76
+ ])),
77
+ content: s.f("content", S$RescriptSchema.string)
78
+ };
79
+ });
80
+
81
+ var client = Rest.client(Env.envioAppUrl + "/api", undefined);
82
+
83
+ function route() {
84
+ return {
85
+ method: "POST",
86
+ path: "/hyperindex/init",
87
+ input: (function (s) {
88
+ return s.body(bodySchema);
89
+ }),
90
+ responses: [(function (s) {
91
+ return s.field("messages", S$RescriptSchema.array(messageSchema));
92
+ })]
93
+ };
94
+ }
95
+
96
+ async function getMessages(config) {
97
+ var envioVersion = Utils.EnvioPackage.value.version;
98
+ var body = makeBody(envioVersion, Env.envioApiToken, config);
99
+ var messages;
100
+ try {
101
+ messages = await Rest.$$fetch(route, body, client);
102
+ }
103
+ catch (raw_exn){
104
+ var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
105
+ return {
106
+ TAG: "Error",
107
+ _0: exn
108
+ };
109
+ }
110
+ return {
111
+ TAG: "Ok",
112
+ _0: messages
113
+ };
114
+ }
115
+
116
+ var InitApi = {
117
+ bodySchema: bodySchema,
118
+ makeBody: makeBody,
119
+ toTheme: toTheme,
120
+ messageSchema: messageSchema,
121
+ client: client,
122
+ route: route,
123
+ getMessages: getMessages
124
+ };
125
+
126
+ function useMessages(config) {
127
+ var match = React.useState(function () {
128
+ return "Loading";
129
+ });
130
+ var setRequest = match[1];
131
+ React.useEffect((function () {
132
+ getMessages(config).then(function (res) {
133
+ if (res.TAG === "Ok") {
134
+ var data = res._0;
135
+ return setRequest(function (param) {
136
+ return {
137
+ TAG: "Data",
138
+ _0: data
139
+ };
140
+ });
141
+ }
142
+ var e = res._0;
143
+ Logging.error({
144
+ msg: "Failed to load messages from envio server",
145
+ err: Utils.prettifyExn(e)
146
+ });
147
+ setRequest(function (param) {
148
+ return {
149
+ TAG: "Err",
150
+ _0: e
151
+ };
152
+ });
153
+ });
154
+ }), []);
155
+ return match[0];
156
+ }
157
+
158
+ export {
159
+ InitApi ,
160
+ useMessages ,
161
+ }
162
+ /* bodySchema Not a pure module */
@@ -0,0 +1,41 @@
1
+ open Belt
2
+ open Ink
3
+ module Message = {
4
+ @react.component
5
+ let make = (~message: CustomHooks.InitApi.message) => {
6
+ <Text color={message.color->CustomHooks.InitApi.toTheme}>
7
+ {message.content->React.string}
8
+ </Text>
9
+ }
10
+ }
11
+
12
+ module Notifications = {
13
+ @react.component
14
+ let make = (~children) => {
15
+ <>
16
+ <Newline />
17
+ <Text bold=true> {"Notifications:"->React.string} </Text>
18
+ {children}
19
+ </>
20
+ }
21
+ }
22
+
23
+ @react.component
24
+ let make = (~config) => {
25
+ let messages = CustomHooks.useMessages(~config)
26
+ <>
27
+ {switch messages {
28
+ | Data([]) | Loading => React.null //Don't show anything while loading or no messages
29
+ | Data(messages) =>
30
+ <Notifications>
31
+ {messages
32
+ ->Array.mapWithIndex((i, message) => {<Message key={i->Int.toString} message />})
33
+ ->React.array}
34
+ </Notifications>
35
+ | Err(_) =>
36
+ <Notifications>
37
+ <Message message={color: Danger, content: "Failed to load messages from envio server"} />
38
+ </Notifications>
39
+ }}
40
+ </>
41
+ }
@@ -0,0 +1,75 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as $$Ink from "../bindings/Ink.res.mjs";
4
+ import * as $$Ink$1 from "ink";
5
+ import * as Belt_Array from "rescript/lib/es6/belt_Array.js";
6
+ import * as Caml_option from "rescript/lib/es6/caml_option.js";
7
+ import * as CustomHooks from "./CustomHooks.res.mjs";
8
+ import * as JsxRuntime from "react/jsx-runtime";
9
+
10
+ function Messages$Message(props) {
11
+ var message = props.message;
12
+ return JsxRuntime.jsx($$Ink$1.Text, {
13
+ children: message.content,
14
+ color: CustomHooks.InitApi.toTheme(message.color)
15
+ });
16
+ }
17
+
18
+ var Message = {
19
+ make: Messages$Message
20
+ };
21
+
22
+ function Messages$Notifications(props) {
23
+ return JsxRuntime.jsxs(JsxRuntime.Fragment, {
24
+ children: [
25
+ JsxRuntime.jsx($$Ink.Newline.make, {}),
26
+ JsxRuntime.jsx($$Ink$1.Text, {
27
+ children: "Notifications:",
28
+ bold: true
29
+ }),
30
+ props.children
31
+ ]
32
+ });
33
+ }
34
+
35
+ var Notifications = {
36
+ make: Messages$Notifications
37
+ };
38
+
39
+ function Messages(props) {
40
+ var messages = CustomHooks.useMessages(props.config);
41
+ var tmp;
42
+ if (typeof messages !== "object") {
43
+ tmp = null;
44
+ } else if (messages.TAG === "Data") {
45
+ var messages$1 = messages._0;
46
+ tmp = messages$1.length !== 0 ? JsxRuntime.jsx(Messages$Notifications, {
47
+ children: Belt_Array.mapWithIndex(messages$1, (function (i, message) {
48
+ return JsxRuntime.jsx(Messages$Message, {
49
+ message: message
50
+ }, String(i));
51
+ }))
52
+ }) : null;
53
+ } else {
54
+ tmp = JsxRuntime.jsx(Messages$Notifications, {
55
+ children: JsxRuntime.jsx(Messages$Message, {
56
+ message: {
57
+ color: "danger",
58
+ content: "Failed to load messages from envio server"
59
+ }
60
+ })
61
+ });
62
+ }
63
+ return JsxRuntime.jsx(JsxRuntime.Fragment, {
64
+ children: Caml_option.some(tmp)
65
+ });
66
+ }
67
+
68
+ var make = Messages;
69
+
70
+ export {
71
+ Message ,
72
+ Notifications ,
73
+ make ,
74
+ }
75
+ /* Ink Not a pure module */
@@ -0,0 +1,193 @@
1
+ open Ink
2
+ open Belt
3
+
4
+ let isIndexerFullySynced = (chains: array<TuiData.chain>) => {
5
+ chains->Array.reduce(true, (accum, current) => {
6
+ switch current.progress {
7
+ | Synced(_) => accum
8
+ | _ => false
9
+ }
10
+ })
11
+ }
12
+
13
+ let getTotalRemainingBlocks = (chains: array<TuiData.chain>) => {
14
+ chains->Array.reduce(0, (accum, {progress, knownHeight, latestFetchedBlockNumber, endBlock}) => {
15
+ let finalBlock = switch endBlock {
16
+ | Some(endBlock) => endBlock
17
+ | None => knownHeight
18
+ }
19
+ switch progress {
20
+ | Syncing({latestProcessedBlock})
21
+ | Synced({latestProcessedBlock}) =>
22
+ finalBlock - latestProcessedBlock + accum
23
+ | SearchingForEvents => finalBlock - latestFetchedBlockNumber + accum
24
+ }
25
+ })
26
+ }
27
+
28
+ let getLatestTimeCaughtUpToHead = (chains: array<TuiData.chain>, indexerStartTime: Js.Date.t) => {
29
+ let latesttimestampCaughtUpToHeadOrEndblockFloat = chains->Array.reduce(0.0, (accum, current) => {
30
+ switch current.progress {
31
+ | Synced({timestampCaughtUpToHeadOrEndblock}) =>
32
+ timestampCaughtUpToHeadOrEndblock->Js.Date.valueOf > accum
33
+ ? timestampCaughtUpToHeadOrEndblock->Js.Date.valueOf
34
+ : accum
35
+ | Syncing(_)
36
+ | SearchingForEvents => accum
37
+ }
38
+ })
39
+
40
+ DateFns.formatDistanceWithOptions(
41
+ indexerStartTime,
42
+ latesttimestampCaughtUpToHeadOrEndblockFloat->Js.Date.fromFloat,
43
+ {includeSeconds: true},
44
+ )
45
+ }
46
+
47
+ let getTotalBlocksProcessed = (chains: array<TuiData.chain>) => {
48
+ chains->Array.reduce(0, (accum, {progress, latestFetchedBlockNumber}) => {
49
+ switch progress {
50
+ | Syncing({latestProcessedBlock, firstEventBlockNumber})
51
+ | Synced({latestProcessedBlock, firstEventBlockNumber}) =>
52
+ latestProcessedBlock - firstEventBlockNumber + accum
53
+ | SearchingForEvents => latestFetchedBlockNumber + accum
54
+ }
55
+ })
56
+ }
57
+
58
+ let useShouldDisplayEta = (~chains: array<TuiData.chain>) => {
59
+ let (shouldDisplayEta, setShouldDisplayEta) = React.useState(_ => false)
60
+ React.useEffect(() => {
61
+ //Only compute this while it is not displaying eta
62
+ if !shouldDisplayEta {
63
+ //Each chain should have fetched at least one batch
64
+ let (allChainsHaveFetchedABatch, totalNumBatchesFetched) = chains->Array.reduce((true, 0), (
65
+ (allChainsHaveFetchedABatch, totalNumBatchesFetched),
66
+ chain,
67
+ ) => {
68
+ (
69
+ allChainsHaveFetchedABatch && chain.numBatchesFetched >= 1,
70
+ totalNumBatchesFetched + chain.numBatchesFetched,
71
+ )
72
+ })
73
+
74
+ //Min num fetched batches is num of chains + 2. All
75
+ // Chains should have fetched at least 1 batch. (They
76
+ // could then be blocked from fetching if they are past
77
+ //the max queue size on first batch)
78
+ // Only display once an additinal 2 batches have been fetched to allow
79
+ // eta to realistically stabalize
80
+ let numChains = chains->Array.length
81
+ let minTotalBatches = numChains + 2
82
+ let hasMinNumBatches = totalNumBatchesFetched >= minTotalBatches
83
+
84
+ let shouldDisplayEta = allChainsHaveFetchedABatch && hasMinNumBatches
85
+
86
+ if shouldDisplayEta {
87
+ setShouldDisplayEta(_ => true)
88
+ }
89
+ }
90
+
91
+ None
92
+ }, [chains])
93
+
94
+ shouldDisplayEta
95
+ }
96
+
97
+ let useEta = (~chains, ~indexerStartTime) => {
98
+ let shouldDisplayEta = useShouldDisplayEta(~chains)
99
+ let (secondsToSub, setSecondsToSub) = React.useState(_ => 0.)
100
+ let (timeSinceStart, setTimeSinceStart) = React.useState(_ => 0.)
101
+
102
+ React.useEffect2(() => {
103
+ setTimeSinceStart(_ => Js.Date.now() -. indexerStartTime->Js.Date.valueOf)
104
+ setSecondsToSub(_ => 0.)
105
+
106
+ let intervalId = Js.Global.setInterval(() => {
107
+ setSecondsToSub(prev => prev +. 1.)
108
+ }, 1000)
109
+
110
+ Some(() => Js.Global.clearInterval(intervalId))
111
+ }, (chains, indexerStartTime))
112
+
113
+ //blocksProcessed/remainingBlocks = timeSoFar/eta
114
+ //eta = (timeSoFar/blocksProcessed) * remainingBlocks
115
+
116
+ let blocksProcessed = getTotalBlocksProcessed(chains)->Int.toFloat
117
+ if shouldDisplayEta && blocksProcessed > 0. {
118
+ let nowDate = Js.Date.now()
119
+ let remainingBlocks = getTotalRemainingBlocks(chains)->Int.toFloat
120
+ let etaFloat = timeSinceStart /. blocksProcessed *. remainingBlocks
121
+ let millisToSub = secondsToSub *. 1000.
122
+ let etaFloat = Pervasives.max(etaFloat -. millisToSub, 0.0) //template this
123
+ let eta = (etaFloat +. nowDate)->Js.Date.fromFloat
124
+ let interval: DateFns.interval = {start: nowDate->Js.Date.fromFloat, end: eta}
125
+ let duration = DateFns.intervalToDuration(interval)
126
+ let formattedDuration = DateFns.formatDuration(
127
+ duration,
128
+ {format: ["hours", "minutes", "seconds"]},
129
+ )
130
+ let outputString = switch formattedDuration {
131
+ | "" => "less than 1 second"
132
+ | formattedDuration => formattedDuration
133
+ }
134
+ Some(outputString)
135
+ } else {
136
+ None
137
+ }
138
+ }
139
+
140
+ module Syncing = {
141
+ @react.component
142
+ let make = (~etaStr) => {
143
+ <Text bold=true>
144
+ <Text> {"Sync Time ETA: "->React.string} </Text>
145
+ <Text> {etaStr->React.string} </Text>
146
+ <Text> {" ("->React.string} </Text>
147
+ <Text color=Primary>
148
+ <Spinner />
149
+ </Text>
150
+ <Text color=Secondary> {" in progress"->React.string} </Text>
151
+ <Text> {")"->React.string} </Text>
152
+ </Text>
153
+ }
154
+ }
155
+
156
+ module Synced = {
157
+ @react.component
158
+ let make = (~latestTimeCaughtUpToHeadStr) => {
159
+ <Text bold=true>
160
+ <Text> {"Time Synced: "->React.string} </Text>
161
+ <Text> {`${latestTimeCaughtUpToHeadStr}`->React.string} </Text>
162
+ <Text> {" ("->React.string} </Text>
163
+ <Text color=Success> {"synced"->React.string} </Text>
164
+ <Text> {")"->React.string} </Text>
165
+ </Text>
166
+ }
167
+ }
168
+
169
+ module Calculating = {
170
+ @react.component
171
+ let make = () => {
172
+ <Text>
173
+ <Text color=Primary>
174
+ <Spinner />
175
+ </Text>
176
+ <Text bold=true> {" Calculating ETA..."->React.string} </Text>
177
+ </Text>
178
+ }
179
+ }
180
+
181
+ @react.component
182
+ let make = (~chains, ~indexerStartTime) => {
183
+ let optEta = useEta(~chains, ~indexerStartTime)
184
+ if isIndexerFullySynced(chains) {
185
+ let latestTimeCaughtUpToHeadStr = getLatestTimeCaughtUpToHead(chains, indexerStartTime)
186
+ <Synced latestTimeCaughtUpToHeadStr /> //TODO add real time
187
+ } else {
188
+ switch optEta {
189
+ | Some(etaStr) => <Syncing etaStr />
190
+ | None => <Calculating />
191
+ }
192
+ }
193
+ }