@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.
- package/.npmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/account/account-address.js +127 -0
- package/account/account-address.scss +20 -0
- package/account/available-balance.js +21 -0
- package/account/identicon.js +91 -0
- package/account/identicon.scss +6 -0
- package/account/signer-key.js +65 -0
- package/api/api-cache.js +5 -0
- package/api/explorer-api-call.js +37 -0
- package/api/explorer-api-hooks.js +202 -0
- package/api/explorer-api-paginated-list-hooks.js +439 -0
- package/api/explorer-batch-info-loader.js +85 -0
- package/api/explorer-tx-api.js +29 -0
- package/asset/amount.js +53 -0
- package/asset/asset-icon.js +34 -0
- package/asset/asset-issuer.js +22 -0
- package/asset/asset-link.js +93 -0
- package/asset/asset-link.scss +26 -0
- package/asset/asset-list-hooks.js +60 -0
- package/asset/asset-meta-hooks.js +78 -0
- package/asset/asset-selector.js +72 -0
- package/asset/asset-selector.scss +41 -0
- package/basic-styles/base.scss +53 -0
- package/basic-styles/form.scss +90 -0
- package/basic-styles/grid.scss +388 -0
- package/basic-styles/link.scss +17 -0
- package/basic-styles/lists.scss +68 -0
- package/basic-styles/loader.scss +22 -0
- package/basic-styles/responsive.scss +29 -0
- package/basic-styles/scrollbars.scss +19 -0
- package/basic-styles/spacing.scss +50 -0
- package/basic-styles/table.scss +104 -0
- package/basic-styles/themes.scss +59 -0
- package/basic-styles/typography.scss +191 -0
- package/basic-styles/variables.scss +26 -0
- package/claimable-balance/claimable-balance-claimants.js +12 -0
- package/contract/sc-val.js +70 -0
- package/contract/wasm-section-parser.js +159 -0
- package/contract/wasm-section-reader.js +79 -0
- package/contract/xdr-reader.js +148 -0
- package/controls/active-icon.scss +23 -0
- package/controls/button-group.js +20 -0
- package/controls/button-group.scss +48 -0
- package/controls/button.js +74 -0
- package/controls/button.scss +148 -0
- package/controls/code-block.js +35 -0
- package/controls/code-block.scss +74 -0
- package/controls/dropdown.js +281 -0
- package/controls/dropdown.scss +161 -0
- package/controls/external-link.js +5 -0
- package/controls/info-tooltip.js +17 -0
- package/controls/info-tooltip.scss +4 -0
- package/controls/slider.js +20 -0
- package/controls/slider.scss +22 -0
- package/controls/tabs.js +94 -0
- package/controls/tabs.scss +73 -0
- package/controls/tooltip.js +214 -0
- package/controls/tooltip.scss +97 -0
- package/controls/update-highlighter.js +28 -0
- package/controls/update-highlighter.scss +10 -0
- package/date/date-selector.js +44 -0
- package/date/elapsed-time.js +22 -0
- package/date/utc-timestamp.js +23 -0
- package/dex/price-dynamic.js +45 -0
- package/dex/price-dynamic.scss +36 -0
- package/directory/directory-hooks.js +89 -0
- package/effect/effect-description.js +289 -0
- package/effect/op-effects-view.js +11 -0
- package/effect/op-effects.scss +8 -0
- package/errors/error-boundary.js +98 -0
- package/fonts/font.scss +23 -0
- package/fonts/icons/config.json +1858 -0
- package/fonts/icons/icons-embedded.scss +644 -0
- package/horizon/horizon-account-helpers.js +104 -0
- package/horizon/horizon-client-helpers.js +52 -0
- package/horizon/horizon-ledger-helpers.js +36 -0
- package/horizon/horizon-orderbook-helpers.js +14 -0
- package/horizon/horizon-trades-helper.js +89 -0
- package/horizon/horizon-transaction-helpers.js +36 -0
- package/index.js +85 -0
- package/index.scss +16 -0
- package/interaction/autofocus.js +9 -0
- package/interaction/block-select.js +54 -0
- package/interaction/block-select.scss +24 -0
- package/interaction/copy-to-clipboard.js +19 -0
- package/interaction/inline-progress.js +16 -0
- package/interaction/qr-code.js +45 -0
- package/interaction/responsive.js +21 -0
- package/interaction/spoiler.js +40 -0
- package/interaction/spoiler.scss +9 -0
- package/interaction/theme-selector.js +11 -0
- package/ledger/ledger-entry-href-formatter.js +22 -0
- package/ledger/ledger-entry-link.js +59 -0
- package/meta/page-meta-tags.js +229 -0
- package/module/dynamic-module.js +48 -0
- package/package.json +41 -0
- package/state/on-screen-hooks.js +23 -0
- package/state/page-visibility-helpers.js +17 -0
- package/state/page-visibility-hooks.js +12 -0
- package/state/screen-orientation-hooks.js +16 -0
- package/state/state-hooks.js +77 -0
- package/state/stellar-network-hooks.js +45 -0
- package/state/theme.js +29 -0
- package/stellar/key-type.js +92 -0
- package/stellar/ledger-generic-id.js +40 -0
- package/stellar/signature-hint-utils.js +65 -0
- package/toast/toast-notification-instance.js +43 -0
- package/toast/toast-notification.js +46 -0
- package/toast/toast-notifications-block.js +55 -0
- package/toast/toast-notifications.scss +166 -0
- package/tx/op-accounting-changes.js +27 -0
- package/tx/op-description-view.js +925 -0
- package/tx/op-description.scss +98 -0
- package/tx/op-icon.js +93 -0
- package/tx/parser/op-balance-changes.js +62 -0
- package/tx/parser/op-descriptor.js +49 -0
- package/tx/parser/tx-details-parser.js +75 -0
- package/tx/parser/tx-matcher.js +372 -0
- package/tx/parser/type-filter-matcher.js +127 -0
- package/tx/tx-fee-effect.js +14 -0
- package/tx/tx-list-hooks.js +19 -0
- 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,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,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"> (weight={weight})</span>}
|
|
64
|
+
</span>
|
|
65
|
+
})
|
package/api/api-cache.js
ADDED
|
@@ -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
|
+
*/
|