@stellar-expert/ui-framework 1.9.0

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 (124) hide show
  1. package/.npmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -0
  4. package/account/account-address.js +127 -0
  5. package/account/account-address.scss +20 -0
  6. package/account/available-balance.js +21 -0
  7. package/account/identicon.js +91 -0
  8. package/account/identicon.scss +6 -0
  9. package/account/signer-key.js +65 -0
  10. package/api/api-cache.js +5 -0
  11. package/api/explorer-api-call.js +37 -0
  12. package/api/explorer-api-hooks.js +202 -0
  13. package/api/explorer-api-paginated-list-hooks.js +439 -0
  14. package/api/explorer-batch-info-loader.js +85 -0
  15. package/api/explorer-tx-api.js +29 -0
  16. package/asset/amount.js +53 -0
  17. package/asset/asset-icon.js +34 -0
  18. package/asset/asset-issuer.js +22 -0
  19. package/asset/asset-link.js +93 -0
  20. package/asset/asset-link.scss +26 -0
  21. package/asset/asset-list-hooks.js +60 -0
  22. package/asset/asset-meta-hooks.js +78 -0
  23. package/asset/asset-selector.js +72 -0
  24. package/asset/asset-selector.scss +41 -0
  25. package/basic-styles/base.scss +53 -0
  26. package/basic-styles/form.scss +90 -0
  27. package/basic-styles/grid.scss +388 -0
  28. package/basic-styles/link.scss +17 -0
  29. package/basic-styles/lists.scss +68 -0
  30. package/basic-styles/loader.scss +22 -0
  31. package/basic-styles/responsive.scss +29 -0
  32. package/basic-styles/scrollbars.scss +19 -0
  33. package/basic-styles/spacing.scss +50 -0
  34. package/basic-styles/table.scss +104 -0
  35. package/basic-styles/themes.scss +59 -0
  36. package/basic-styles/typography.scss +191 -0
  37. package/basic-styles/variables.scss +26 -0
  38. package/claimable-balance/claimable-balance-claimants.js +12 -0
  39. package/contract/sc-val.js +70 -0
  40. package/contract/wasm-section-parser.js +159 -0
  41. package/contract/wasm-section-reader.js +79 -0
  42. package/contract/xdr-reader.js +148 -0
  43. package/controls/active-icon.scss +23 -0
  44. package/controls/button-group.js +20 -0
  45. package/controls/button-group.scss +48 -0
  46. package/controls/button.js +74 -0
  47. package/controls/button.scss +148 -0
  48. package/controls/code-block.js +35 -0
  49. package/controls/code-block.scss +74 -0
  50. package/controls/dropdown.js +281 -0
  51. package/controls/dropdown.scss +161 -0
  52. package/controls/external-link.js +5 -0
  53. package/controls/info-tooltip.js +17 -0
  54. package/controls/info-tooltip.scss +4 -0
  55. package/controls/slider.js +20 -0
  56. package/controls/slider.scss +22 -0
  57. package/controls/tabs.js +94 -0
  58. package/controls/tabs.scss +73 -0
  59. package/controls/tooltip.js +214 -0
  60. package/controls/tooltip.scss +97 -0
  61. package/controls/update-highlighter.js +28 -0
  62. package/controls/update-highlighter.scss +10 -0
  63. package/date/date-selector.js +44 -0
  64. package/date/elapsed-time.js +22 -0
  65. package/date/utc-timestamp.js +23 -0
  66. package/dex/price-dynamic.js +45 -0
  67. package/dex/price-dynamic.scss +36 -0
  68. package/directory/directory-hooks.js +89 -0
  69. package/effect/effect-description.js +289 -0
  70. package/effect/op-effects-view.js +11 -0
  71. package/effect/op-effects.scss +8 -0
  72. package/errors/error-boundary.js +98 -0
  73. package/fonts/font.scss +23 -0
  74. package/fonts/icons/config.json +1858 -0
  75. package/fonts/icons/icons-embedded.scss +644 -0
  76. package/horizon/horizon-account-helpers.js +104 -0
  77. package/horizon/horizon-client-helpers.js +52 -0
  78. package/horizon/horizon-ledger-helpers.js +36 -0
  79. package/horizon/horizon-orderbook-helpers.js +14 -0
  80. package/horizon/horizon-trades-helper.js +89 -0
  81. package/horizon/horizon-transaction-helpers.js +36 -0
  82. package/index.js +85 -0
  83. package/index.scss +16 -0
  84. package/interaction/autofocus.js +9 -0
  85. package/interaction/block-select.js +54 -0
  86. package/interaction/block-select.scss +24 -0
  87. package/interaction/copy-to-clipboard.js +19 -0
  88. package/interaction/inline-progress.js +16 -0
  89. package/interaction/qr-code.js +45 -0
  90. package/interaction/responsive.js +21 -0
  91. package/interaction/spoiler.js +40 -0
  92. package/interaction/spoiler.scss +9 -0
  93. package/interaction/theme-selector.js +11 -0
  94. package/ledger/ledger-entry-href-formatter.js +22 -0
  95. package/ledger/ledger-entry-link.js +59 -0
  96. package/meta/page-meta-tags.js +229 -0
  97. package/module/dynamic-module.js +48 -0
  98. package/package.json +41 -0
  99. package/state/on-screen-hooks.js +23 -0
  100. package/state/page-visibility-helpers.js +17 -0
  101. package/state/page-visibility-hooks.js +12 -0
  102. package/state/screen-orientation-hooks.js +16 -0
  103. package/state/state-hooks.js +77 -0
  104. package/state/stellar-network-hooks.js +45 -0
  105. package/state/theme.js +29 -0
  106. package/stellar/key-type.js +92 -0
  107. package/stellar/ledger-generic-id.js +40 -0
  108. package/stellar/signature-hint-utils.js +65 -0
  109. package/toast/toast-notification-instance.js +43 -0
  110. package/toast/toast-notification.js +46 -0
  111. package/toast/toast-notifications-block.js +55 -0
  112. package/toast/toast-notifications.scss +166 -0
  113. package/tx/op-accounting-changes.js +27 -0
  114. package/tx/op-description-view.js +925 -0
  115. package/tx/op-description.scss +98 -0
  116. package/tx/op-icon.js +93 -0
  117. package/tx/parser/op-balance-changes.js +62 -0
  118. package/tx/parser/op-descriptor.js +49 -0
  119. package/tx/parser/tx-details-parser.js +75 -0
  120. package/tx/parser/tx-matcher.js +372 -0
  121. package/tx/parser/type-filter-matcher.js +127 -0
  122. package/tx/tx-fee-effect.js +14 -0
  123. package/tx/tx-list-hooks.js +19 -0
  124. package/tx/tx-operations-list.js +105 -0
package/.npmrc ADDED
@@ -0,0 +1 @@
1
+ auto-install-peers=true
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 StellarExpert Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @stellar-expert/ui-framework
2
+
3
+ ### StellarExpert shared UI components library
@@ -0,0 +1,127 @@
1
+ import React from 'react'
2
+ import cn from 'classnames'
3
+ import {shortenString} from '@stellar-expert/formatter'
4
+ import {AccountIdenticon} from './identicon'
5
+ import {InfoTooltip} from '../controls/info-tooltip'
6
+ import {useDirectory} from '../directory/directory-hooks'
7
+ import {formatExplorerLink} from '../ledger/ledger-entry-href-formatter'
8
+ import {useStellarNetwork} from '../state/stellar-network-hooks'
9
+ import {decodeKeyType} from '../stellar/key-type'
10
+ import './account-address.scss'
11
+
12
+
13
+ function isPublicKeyOrContract(type) {
14
+ return type === 'ed25519' || type === 'contract' || type === 'muxed'
15
+ }
16
+
17
+ function getAccountPredefinedDisplayName(address) {
18
+ if (!window.predefinedAccountDisplayNames)
19
+ return undefined
20
+ return window.predefinedAccountDisplayNames[address]
21
+ }
22
+
23
+ function AccountDisplayName({type, address, name}) {
24
+ const predefined = getAccountPredefinedDisplayName(address)
25
+ let directoryInfo = useDirectory(!predefined && !name && isPublicKeyOrContract(type) && address),
26
+ warning
27
+ if (name === false)
28
+ return null
29
+ if (predefined)
30
+ return `[${predefined}] `
31
+ if (directoryInfo) {
32
+ name = directoryInfo.name
33
+ if (directoryInfo.tags.includes('malicious')) {
34
+ warning = <i className="icon icon-warning color-warning"
35
+ title="This account was reported for illicit or fraudulent activity. Do not send funds to this address and do not trust any person affiliated with it."/>
36
+ }
37
+ }
38
+ if (!name && !warning) return null
39
+ return <>
40
+ {name ? `[${name}] ` : ''}
41
+ {warning}
42
+ </>
43
+
44
+ }
45
+
46
+ /**
47
+ * Explorer link for Account/MuxedAccount/Sha256Hash/PreAuthTx/SignedPayload
48
+ * @param {String} account - StrKey-encoded Account/MuxedAccount/Sha256Hash/PreAuthTx/SignedPayload
49
+ * @param {Number|'all'} [chars] - Visible address characters count
50
+ * @param {Bool|String} [name] - Explicit account name that overrides the name from Directory; if false, friendly name is ignored
51
+ * @param {Bool|String} [link] - Explicit link; if false, the component is rendered without a link
52
+ * @param {Boolean} [icon] - Whether to show/hide account identicon
53
+ * @param {String} [network] - Stellar network identifier
54
+ * @param {*} [prefix] - Link prefix
55
+ * @param {*} [suffix] - Link suffix
56
+ * @param {{}} [style] - Optional CSS inline style
57
+ * @param {String} [className] - Optional CSS class attribute
58
+ * @param {...*} [otherProps] - Optional container parameters
59
+ * @constructor
60
+ */
61
+ export const AccountAddress = React.memo(function AccountAddress({
62
+ account,
63
+ chars = 8,
64
+ name,
65
+ link,
66
+ style,
67
+ className,
68
+ icon,
69
+ prefix,
70
+ suffix,
71
+ network,
72
+ ...otherProps
73
+ }) {
74
+ useStellarNetwork()
75
+ const keyType = decodeKeyType(account)
76
+ if (!keyType)
77
+ return null //failed to decode address type
78
+ let {type, address, muxedId, publicKey, payload} = keyType
79
+
80
+ let innerStyle = !style ? undefined : style
81
+
82
+ let ed25519Address = address
83
+ if (chars && chars !== 'all') {
84
+ address = shortenString(account, chars)
85
+ }
86
+
87
+ const children = <>
88
+ {prefix}
89
+ {icon !== false && isPublicKeyOrContract(type) && <AccountIdenticon key="identicon" address={ed25519Address}/>}
90
+ <AccountDisplayName type={type} address={ed25519Address} name={name}/>
91
+ <span className="account-key">{address}</span>
92
+ {muxedId !== undefined && <InfoTooltip icon="icon-plus">
93
+ Subaccount of a custodial account<br/>
94
+ <AccountAddress account={ed25519Address} name={false} chars={12}/>
95
+ <div className="dimmed text-tiny micro-space">Multiplexed id: {muxedId.toString()}</div>
96
+ </InfoTooltip>}
97
+ {payload !== undefined && <InfoTooltip icon="icon-plus">
98
+ ED25519 payload signer<br/>
99
+ <AccountAddress account={publicKey} name={false} chars={12}/>
100
+ <div className="dimmed text-tiny micro-space">Payload: {payload}</div>
101
+ </InfoTooltip>}
102
+ {suffix}
103
+ </>
104
+
105
+ const containerProps = {
106
+ title: account,
107
+ 'aria-label': account,
108
+ className: cn('account-address', className),
109
+ style: innerStyle,
110
+ ...otherProps
111
+ }
112
+ let renderAs = 'span'
113
+
114
+ if (link !== false && isPublicKeyOrContract(type)) {
115
+ renderAs = 'a'
116
+ if (typeof link === 'string') {
117
+ containerProps.href = link
118
+ } else {
119
+ containerProps.href = formatExplorerLink(type === 'contract' ? 'contract' : 'account', account, network)
120
+ if (window.origin !== explorerFrontendOrigin) {
121
+ containerProps.target = '_blank'
122
+ }
123
+ }
124
+ }
125
+
126
+ return React.createElement(renderAs, containerProps, children)
127
+ })
@@ -0,0 +1,20 @@
1
+ .account-address {
2
+ white-space: nowrap;
3
+
4
+ .account-key {
5
+ white-space: normal;
6
+ word-break: break-word;
7
+ }
8
+
9
+ &.plain * {
10
+ font: inherit;
11
+ white-space: normal;
12
+ }
13
+ }
14
+
15
+ h2,
16
+ .table {
17
+ .account-address * {
18
+ letter-spacing: -.05em;
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ import {toStroops, fromStroops} from '@stellar-expert/formatter'
2
+
3
+ /**
4
+ * Calculate available balance for a given account balance trustline
5
+ * @param {AccountResponse} account
6
+ * @param {Horizon.BalanceLine} balance
7
+ * @param {Number} [additionalReserves]
8
+ * @return {String}
9
+ */
10
+ export function calculateAvailableBalance(account, balance, additionalReserves = null) {
11
+ let available = toStroops(balance.balance) - toStroops(balance.selling_liabilities || 0)
12
+ if (balance.asset_type === 'native') {
13
+ const reserves = 2 + account.subentry_count + account.num_sponsoring - account.num_sponsored
14
+ available = available - (BigInt(reserves) * 5000000n)
15
+ //TODO: fetch base_reserve from the Horizon
16
+ }
17
+ if (additionalReserves !== null) {
18
+ available = available - toStroops(additionalReserves)
19
+ }
20
+ return fromStroops(available)
21
+ }
@@ -0,0 +1,91 @@
1
+ import React from 'react'
2
+ import './identicon.scss'
3
+
4
+ const RESOLUTION = 7
5
+ const SIZE = 448
6
+ const base32Alphabet = {}
7
+
8
+ //map base32 alphabet
9
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.split('').map((c, i) => base32Alphabet[c] = i)
10
+
11
+ function decodeBase32(input) {
12
+ const buf = []
13
+ let shift = 8
14
+ let carry = 0
15
+
16
+ input.toUpperCase().split('').forEach(char => {
17
+ const symbol = base32Alphabet[char] & 0xff
18
+ shift -= 5
19
+ if (shift > 0) {
20
+ carry |= symbol << shift
21
+ } else if (shift < 0) {
22
+ buf.push(carry | (symbol >> -shift))
23
+ shift += 8
24
+ carry = (symbol << shift) & 0xff
25
+ } else {
26
+ buf.push(carry | symbol)
27
+ shift = 8
28
+ carry = 0
29
+ }
30
+ })
31
+
32
+ if (shift !== 8 && carry !== 0) {
33
+ buf.push(carry)
34
+ shift = 8
35
+ carry = 0
36
+ }
37
+
38
+ return buf
39
+ }
40
+
41
+ /**
42
+ * Draw Stellar address identicon on the given canvas
43
+ * @param {String} address - StrKey-encoded account address
44
+ * @param {Number} size? - Identicon size
45
+ * @return {String}
46
+ */
47
+ export function drawIdenticon(address, size = SIZE) {
48
+ //take 16 meaningful bytes from the raw pub key
49
+ const decoded = decodeBase32(address).slice(2, 16)
50
+ const width = RESOLUTION
51
+ const height = RESOLUTION
52
+ const columns = Math.ceil(width / 2)
53
+ const cellSize = size / width
54
+ const addressBytes = decoded.slice(1)
55
+ const fillStyle = `hsl(${360 * decoded[0] / 256 | 0},58%,52%)`
56
+ const dots = []
57
+ const rsize = ` width="${cellSize}" height="${cellSize}"`
58
+ for (let row = 0; row < height; row++) {
59
+ for (let column = 0; column < columns; column++) {
60
+ const position = column + row * columns
61
+ const bitSet = (addressBytes[position / 8 | 0] & (1 << (7 - position % 8))) !== 0
62
+ if (bitSet) {
63
+ dots.push(`<rect x="${cellSize * column}" y="${cellSize * row}"${rsize}/>`)
64
+ dots.push(`<rect x="${cellSize * (width - column - 1)}" y="${cellSize * row}"${rsize}/>`)
65
+ }
66
+ }
67
+ }
68
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" fill="${fillStyle}">${dots.join('')}</svg>`
69
+ }
70
+
71
+ /**
72
+ * Renders account-specific identicon
73
+ * @param {String} address - StrKey-encoded account address
74
+ * @param {Number} size - Identicon size
75
+ * @constructor
76
+ */
77
+ export const AccountIdenticon = React.memo(function AccountIdenticon({address, size}) {
78
+ const props = {
79
+ className: 'identicon',
80
+ src: 'data:image/svg+xml;charset=utf-8,' + drawIdenticon(address),
81
+ width: SIZE,
82
+ height: SIZE
83
+ }
84
+ if (size) {
85
+ props.style = {
86
+ width: size,
87
+ height: size
88
+ }
89
+ }
90
+ return <img {...props}/>
91
+ })
@@ -0,0 +1,6 @@
1
+ .identicon {
2
+ display: inline-block;
3
+ width: .72em;
4
+ height: .72em;
5
+ margin: 0 .1em;
6
+ }
@@ -0,0 +1,65 @@
1
+ import React from 'react'
2
+ import {StrKey} from '@stellar/stellar-base'
3
+ import {AccountAddress} from './account-address'
4
+
5
+ /**
6
+ * @param {Signer} signer
7
+ * @return {{key: String, weight: Number}|null}
8
+ * @internal
9
+ */
10
+ function decodeSigner(signer) {
11
+ if (!signer)
12
+ return null
13
+ let weight
14
+ let key
15
+ if (typeof signer.arm === 'function') { //raw xdr
16
+ const type = signer.arm()
17
+ weight = signer.weight()
18
+ switch (type) {
19
+ case 'ed25519':
20
+ key = StrKey.encodeEd25519PublicKey(signer.ed25519())
21
+ break
22
+ case 'preAuthTx':
23
+ key = StrKey.encodePreAuthTx(signer.preAuthTx())
24
+ break
25
+ case 'hashX':
26
+ key = StrKey.encodeSha256Hash(signer.hashX())
27
+ break
28
+ case 'ed25519SignedPayload':
29
+ key = StrKey.encodeSignedPayload(signer.ed25519SignedPayload())
30
+ break
31
+ }
32
+ } else { //parsed
33
+ if (signer.ed25519PublicKey) {
34
+ key = signer.ed25519PublicKey
35
+ } else if (signer.sha256Hash) {
36
+ key = StrKey.encodeSha256Hash(signer.sha256Hash)
37
+ } else if (signer.preAuthTx) {
38
+ key = StrKey.encodePreAuthTx(signer.preAuthTx)
39
+ } else if (signer.ed25519SignedPayload) {
40
+ key = StrKey.encodeSignedPayload(signer.ed25519SignedPayload)
41
+ }
42
+ weight = signer.weight
43
+ }
44
+ if (!key)
45
+ throw new Error('Unknown signer type: ' + JSON.stringify(signer))
46
+ return {key, weight}
47
+ }
48
+
49
+ /**
50
+ * Account signer key description
51
+ * @param {Signer} signer - StrKey-encoded Account/MuxedAccount/Sha256Hash/PreAuthTx/SignedPayload
52
+ * @param {String} name? - Explicit account name that overrides the name from Directory; if false, friendly name is ignored
53
+ * @param {String} className? - Optional CSS class attribute
54
+ * @param {Boolean} showWeight? - Whether to display weight attribute
55
+ * @constructor
56
+ */
57
+ export const SignerKey = React.memo(function SignerKey({signer, name, className, showWeight = true}) {
58
+ if (!signer)
59
+ return null
60
+ const {key, weight} = decodeSigner(signer)
61
+ return <span className={className}>
62
+ <AccountAddress account={key} name={name}/>
63
+ {!!showWeight && <span className="dimmed text-tiny">&nbsp;(weight={weight})</span>}
64
+ </span>
65
+ })
@@ -0,0 +1,5 @@
1
+ import {InMemoryClientCache} from '@stellar-expert/client-cache'
2
+
3
+ const apiCache = new InMemoryClientCache({prefix: 'ac:'})
4
+
5
+ export default apiCache
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Retrieve data from the server API endpoint.
3
+ * @param {string} endpointWithQuery
4
+ * @return {Promise<Object>}
5
+ */
6
+ export async function fetchExplorerApi(endpointWithQuery) {
7
+ const url = `${explorerApiOrigin}/explorer/${endpointWithQuery}`
8
+ try {
9
+ const resp = await fetch(url)
10
+ if (!resp.ok) {
11
+ let errorExt
12
+ try {
13
+ errorExt = await resp.json()
14
+ } catch (parsingError) {
15
+ errorExt = {}
16
+ }
17
+ const err = new Error(errorExt?.error || resp.statusText || 'Failed to fetch data from the server')
18
+ err.status = resp.status
19
+ err.ext = errorExt
20
+ throw err
21
+ }
22
+ return await resp.json()
23
+ } catch (e) {
24
+ console.error(e)
25
+ if (e instanceof Error) {
26
+ e = {
27
+ error: e.message,
28
+ status: e.status || 500,
29
+ ext: e.ext
30
+ }
31
+ }
32
+ if (e.ext && e.ext.status) {
33
+ e.status = e.ext.status
34
+ }
35
+ return e
36
+ }
37
+ }
@@ -0,0 +1,202 @@
1
+ import {useEffect, useState} from 'react'
2
+ import isEqual from 'react-fast-compare'
3
+ import {stringifyQuery} from '@stellar-expert/navigation'
4
+ import {addVisibilityChangeListener, isDocumentVisible} from '../state/page-visibility-helpers'
5
+ import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
6
+ import {fetchExplorerApi} from './explorer-api-call'
7
+ import apiCache from './api-cache'
8
+
9
+ export class ExplorerApiResult {
10
+ constructor(apiEndpoint, data, ts) {
11
+ this.apiEndpoint = apiEndpoint
12
+ this.data = data
13
+ this.fetchedAt = ts
14
+ }
15
+
16
+ /**
17
+ * Relative API URL
18
+ * @type {String}
19
+ */
20
+ apiEndpoint
21
+
22
+ /**
23
+ * API response data
24
+ * @type {Object}
25
+ */
26
+ data = null
27
+
28
+ /**
29
+ * Response error if any
30
+ * @type {String}
31
+ */
32
+ error
33
+
34
+ /**
35
+ * @type {Number}
36
+ */
37
+ status = 200
38
+
39
+ /**
40
+ * Response timestamp
41
+ * @type {Number}
42
+ */
43
+ fetchedAt = 0
44
+
45
+ /**
46
+ * Response result
47
+ * @return {Boolean}
48
+ */
49
+ get loaded() {
50
+ return !!this.data || !!this.error
51
+ }
52
+
53
+ /**
54
+ * @private
55
+ */
56
+ update(data) {
57
+ this.data = data
58
+ }
59
+ }
60
+
61
+ function buildApiResult(apiEndpoint, data, ts) {
62
+ return new ExplorerApiResult(apiEndpoint, data, ts)
63
+ }
64
+
65
+ function buildApiError(apiEndpoint, error, status) {
66
+ const res = new ExplorerApiResult(apiEndpoint, null, Math.round(new Date().getTime() / 1000))
67
+ res.error = error
68
+ res.status = status
69
+ return res
70
+ }
71
+
72
+ function setupAutoRefresh(refreshInterval, fetchData) {
73
+ if (!refreshInterval) return () => null
74
+
75
+ let autoRefreshTimer,
76
+ refreshedAt = 0
77
+
78
+ function scheduleAutoRefresh() {
79
+ autoRefreshTimer = setInterval(() => {
80
+ //refresh only when the tab is active
81
+ if (isDocumentVisible()) {
82
+ const now = new Date().getTime()
83
+ if (refreshedAt + (refreshInterval - 1) * 1000 < now) {
84
+ refreshedAt = now
85
+ fetchData()
86
+ }
87
+ } else {
88
+ stopAutoRefresh()
89
+ }
90
+ }, refreshInterval * 1000)
91
+ }
92
+
93
+ function stopAutoRefresh() {
94
+ clearInterval(autoRefreshTimer)
95
+ autoRefreshTimer = undefined
96
+ }
97
+
98
+ const stopVisibilityTracking = addVisibilityChangeListener(visible => {
99
+ if (visible) {
100
+ scheduleAutoRefresh()
101
+ } else {
102
+ stopAutoRefresh()
103
+ }
104
+ })
105
+ if (isDocumentVisible()) {
106
+ scheduleAutoRefresh()
107
+ }
108
+ //return finalizer
109
+ return function () {
110
+ refreshInterval = 0
111
+ stopVisibilityTracking()
112
+ stopAutoRefresh()
113
+ }
114
+ }
115
+
116
+ const currentRequests = {}
117
+
118
+ export function fetchData(url, ttl, processResult) {
119
+ //try to retrieve data from the browser cache
120
+ const fromCache = apiCache.get(url)
121
+ //check if the cache is fresh enough
122
+ if (fromCache && !fromCache.isExpired) {
123
+ //if the cached data is up to date - just proceed with it
124
+ return Promise.resolve({data: fromCache.data, ts: fromCache.ts})
125
+ }
126
+ const existingRequest = currentRequests[url]
127
+ if (existingRequest) return existingRequest
128
+ //fetch from the server only if there is no data or it is expired
129
+ return currentRequests[url] = fetchExplorerApi(url)
130
+ .then(data => {
131
+ if (typeof processResult === 'function') {
132
+ data = processResult(data)
133
+ }
134
+ //in case of error set small ttl in order to try re-fetching in 4 seconds
135
+ const {ts} = apiCache.set(url, data, data.error ? 4 : ttl)
136
+ delete currentRequests[url]
137
+ return {data, ts}
138
+ })
139
+ }
140
+
141
+ /**
142
+ *
143
+ * @param {String|APIEndpointParams} apiEndpoint - Server API endpoint to use as a data source.
144
+ * @param {Number} [refreshInterval] - Auto-refresh interval in seconds for dynamic data.
145
+ * @param {Number} [ttl] - Cache time-to-live in seconds.
146
+ * @param {Function} [processResult] - Callback to process a fetch result.
147
+ * @param {Boolean} [allowStaleDataTransition] - Allow stale data to be returned when the url changed and the new data has not been loaded yet.
148
+ * @return {ExplorerApiResult}
149
+ */
150
+ export function useExplorerApi(apiEndpoint, {refreshInterval, ttl = 60, processResult, allowStaleDataTransition = false} = {}) {
151
+ const endpointWithQuery = `${getCurrentStellarNetwork()}/${apiEndpoint}`
152
+ const [apiResponseData, updateApiResponseData] = useState(buildApiResult(endpointWithQuery))
153
+ useEffect(() => {
154
+ let componentUnmounted = false
155
+ updateApiResponseData(buildApiResult(endpointWithQuery))
156
+
157
+ if (!apiEndpoint) {
158
+ updateApiResponseData(buildApiError(endpointWithQuery, 'Not found', 404))
159
+ return
160
+ }
161
+
162
+ if (typeof apiEndpoint !== 'string') {
163
+ apiEndpoint = apiEndpoint.path + stringifyQuery(apiEndpoint.query)
164
+ }
165
+
166
+ function load() {
167
+ fetchData(endpointWithQuery, ttl, processResult)
168
+ .then(({data, ts}) => {
169
+ if (componentUnmounted) return
170
+
171
+ const newData = buildApiResult(endpointWithQuery, data, ts)
172
+
173
+ if (!isEqual(newData, apiResponseData)) {
174
+ updateApiResponseData(newData)
175
+ }
176
+ })
177
+ }
178
+
179
+ if (!componentUnmounted) {
180
+ load()
181
+ }
182
+
183
+ //set up auto-refresh
184
+ const stopAutoRefresh = setupAutoRefresh(refreshInterval, load)
185
+
186
+ return () => {
187
+ //finalize
188
+ componentUnmounted = true
189
+ stopAutoRefresh()
190
+ }
191
+ }, [apiEndpoint])
192
+ if (apiResponseData.apiEndpoint !== endpointWithQuery && !allowStaleDataTransition)
193
+ return buildApiResult(endpointWithQuery) //return empty result on URL transition if allowStaleDataTransition not set
194
+
195
+ return apiResponseData
196
+ }
197
+
198
+ /**
199
+ * @typedef {Object} APIEndpointParams
200
+ * @property {String} path
201
+ * @property {Object} query
202
+ */