@stellar-expert/ui-framework 1.16.8 → 1.17.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 (78) hide show
  1. package/CLAUDE.md +35 -0
  2. package/README.md +1125 -3
  3. package/account/account-address.js +127 -127
  4. package/account/available-balance.js +22 -22
  5. package/account/identicon.js +90 -90
  6. package/account/signer-key.js +65 -64
  7. package/api/explorer-api-hooks.js +209 -202
  8. package/api/explorer-api-paginated-list-hooks.js +441 -440
  9. package/api/explorer-batch-info-loader.js +111 -85
  10. package/api/explorer-tx-api.js +28 -28
  11. package/api/ledger-stream.js +15 -0
  12. package/asset/amount.js +56 -56
  13. package/asset/asset-icon.js +41 -41
  14. package/asset/asset-issuer.js +21 -21
  15. package/asset/asset-link.js +107 -107
  16. package/asset/asset-list-hooks.js +59 -59
  17. package/asset/asset-meta-hooks.js +88 -88
  18. package/asset/asset-selector.js +72 -71
  19. package/claimable-balance/claimable-balance-claimants.js +23 -18
  20. package/contract/contract-api.js +15 -0
  21. package/contract/invocation-info-view.js +10 -2
  22. package/contract/sc-val.js +131 -107
  23. package/controls/button-group.js +25 -19
  24. package/controls/button.js +93 -78
  25. package/controls/code-block.js +42 -34
  26. package/controls/dropdown.js +318 -287
  27. package/controls/external-link.js +10 -4
  28. package/controls/info-tooltip.js +23 -16
  29. package/controls/slider.js +29 -19
  30. package/controls/tabs.js +94 -94
  31. package/controls/tooltip.js +244 -240
  32. package/controls/update-highlighter.js +32 -27
  33. package/date/date-selector.js +56 -54
  34. package/date/elapsed-time.js +28 -21
  35. package/dex/price-dynamic.js +53 -44
  36. package/directory/directory-hooks.js +109 -97
  37. package/effect/effect-description.js +346 -344
  38. package/errors/error-boundary.js +110 -97
  39. package/horizon/horizon-account-helpers.js +104 -104
  40. package/horizon/horizon-ledger-helpers.js +35 -35
  41. package/horizon/horizon-trades-helper.js +88 -88
  42. package/horizon/horizon-transaction-helpers.js +36 -36
  43. package/index.d.ts +1241 -0
  44. package/interaction/accordion.js +43 -35
  45. package/interaction/autofocus.js +13 -9
  46. package/interaction/block-select.js +64 -53
  47. package/interaction/copy-to-clipboard.js +25 -18
  48. package/interaction/inline-progress.js +20 -15
  49. package/interaction/qr-code.js +34 -34
  50. package/interaction/responsive.js +20 -20
  51. package/interaction/spoiler.js +52 -39
  52. package/interaction/theme-selector.js +13 -10
  53. package/ledger/ledger-entry-href-formatter.js +27 -26
  54. package/ledger/ledger-entry-link.js +93 -58
  55. package/ledger/ledger-info-parser.js +46 -18
  56. package/meta/page-meta-tags.js +243 -238
  57. package/module/dynamic-module.js +47 -47
  58. package/package.json +41 -40
  59. package/state/on-screen-hooks.js +22 -22
  60. package/state/page-visibility-helpers.js +29 -16
  61. package/state/page-visibility-hooks.js +13 -11
  62. package/state/screen-orientation-hooks.js +19 -15
  63. package/state/state-hooks.js +76 -76
  64. package/state/stellar-network-hooks.js +56 -44
  65. package/state/theme.js +29 -28
  66. package/stellar/key-type.js +92 -91
  67. package/stellar/ledger-generic-id.js +39 -39
  68. package/stellar/signature-hint-utils.js +65 -65
  69. package/toast/toast-notifications-block.js +59 -59
  70. package/tx/op-description-view.js +945 -942
  71. package/tx/op-icon.js +98 -98
  72. package/tx/parser/op-balance-changes.js +66 -66
  73. package/tx/parser/op-descriptor.js +51 -48
  74. package/tx/parser/tx-details-parser.js +81 -81
  75. package/tx/parser/tx-matcher.js +371 -371
  76. package/tx/parser/type-filter-matcher.js +126 -126
  77. package/tx/tx-list-hooks.js +32 -18
  78. package/tx/tx-operations-list.js +117 -116
@@ -1,98 +1,111 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
- import {navigation as nav} from '@stellar-expert/navigation'
4
- import {BlockSelect} from '../interaction/block-select'
5
-
6
- export class ErrorBoundary extends React.Component {
7
- constructor(props) {
8
- super(props)
9
- this.state = {lastError: null, url: null}
10
- }
11
-
12
- componentDidCatch(e, errorInfo) {
13
- e.componentStack = errorInfo?.componentStack
14
- console.error(e)
15
- this.setState({lastError: e, url: window.location.href}, () => {
16
- const stopListening = nav.history.listen((location) => {
17
- if (this.state.url !== window.location.href) {
18
- stopListening()
19
- this.setState({lastError: null, url: null})
20
- }
21
- })
22
- })
23
- }
24
-
25
- renderError() {
26
- const {errorBoundarySendErrors = true} = this.props
27
- return <>
28
- {this.renderErrorDetails()}
29
- {errorBoundarySendErrors && this.renderSendErrorBlock()}
30
- </>
31
- }
32
-
33
- renderSendErrorBlock() {
34
- const {lastError, url} = this.state
35
- const {message, stack, componentStack} = lastError
36
- const compiledText = `Error details:
37
- "${message}" at ${url}
38
- ${stack}
39
- ${componentStack ? 'Components stack: ' + componentStack : ''}
40
- ${navigator.userAgent}`
41
- return <div className="space dimmed text-small text-right">
42
- If this error persists please{' '}
43
- <a href={'mailto:support@stellar.expert?subject=Exception&body=' + encodeURIComponent(compiledText)}
44
- target="_blank">contact our support</a>.
45
- </div>
46
- }
47
-
48
- renderErrorDetails() {
49
- const {lastError, url} = this.state
50
- const {message, stack, componentStack} = lastError
51
- const text = `"${message}" at ${url}`
52
- const {errorBoundaryErrorDetails = true} = this.props
53
- if (errorBoundaryErrorDetails === false)
54
- return null
55
- if (errorBoundaryErrorDetails === true) {
56
- return <div className="error space text-small" style={{overflow: 'auto', maxWidth: '100%', padding: '1rem 2rem'}}>
57
- <BlockSelect as="div">
58
- <div className="micro-space">{text}</div>
59
- <div className="text-tiny">
60
- <pre style={{whiteSpace: 'pre-wrap'}}>{stack}</pre>
61
- <div>{navigator.userAgent}</div>
62
- </div>
63
- </BlockSelect>
64
- </div>
65
- }
66
- return <>{errorBoundaryErrorDetails}</>
67
- }
68
-
69
- render() {
70
- if (!this.state.lastError)
71
- return this.props.children
72
- const {title = 'Unhandled error occurred', wrapper, ...otherProps} = this.props
73
- if (wrapper)
74
- return React.createElement(wrapper, {...otherProps}, this.renderError())
75
- return <div className="segment blank">
76
- <h3 className="color-danger">{title}</h3>
77
- <hr className="flare"/>
78
- {this.renderError()}
79
- </div>
80
- }
81
-
82
- static propTypes = {
83
- errorBoundaryTitle: PropTypes.string,
84
- errorBoundarySendErrors: PropTypes.bool,
85
- errorBoundaryErrorDetails: PropTypes.oneOfType([PropTypes.bool, PropTypes.any])
86
- }
87
- }
88
-
89
- export function withErrorBoundary(wrapped, {errorBoundaryTitle, errorBoundarySendErrors, errorBoundaryErrorDetails} = {}) {
90
- function ErrorBoundaryWrapper(props) {
91
- const nested = /*#__PURE__*/React.createElement(wrapped, props)
92
- return <ErrorBoundary {...{errorBoundaryTitle, errorBoundarySendErrors, errorBoundaryErrorDetails}}>{nested}</ErrorBoundary>
93
- }
94
-
95
- ErrorBoundaryWrapper.displayName = `withErrorBoundary(${wrapped.displayName || wrapped.name})`
96
- ErrorBoundaryWrapper.WrappedComponent = wrapped
97
- return ErrorBoundaryWrapper
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import {navigation as nav} from '@stellar-expert/navigation'
4
+ import {BlockSelect} from '../interaction/block-select'
5
+
6
+ /**
7
+ * React error boundary that catches rendering errors and displays a fallback UI
8
+ * @extends React.Component
9
+ */
10
+ export class ErrorBoundary extends React.Component {
11
+ constructor(props) {
12
+ super(props)
13
+ this.state = {lastError: null, url: null}
14
+ }
15
+
16
+ componentDidCatch(e, errorInfo) {
17
+ e.componentStack = errorInfo?.componentStack
18
+ console.error(e)
19
+ this.setState({lastError: e, url: window.location.href}, () => {
20
+ const stopListening = nav.history.listen((location) => {
21
+ if (this.state.url !== window.location.href) {
22
+ stopListening()
23
+ this.setState({lastError: null, url: null})
24
+ }
25
+ })
26
+ })
27
+ }
28
+
29
+ renderError() {
30
+ const {errorBoundarySendErrors = true} = this.props
31
+ return <>
32
+ {this.renderErrorDetails()}
33
+ {errorBoundarySendErrors && this.renderSendErrorBlock()}
34
+ </>
35
+ }
36
+
37
+ renderSendErrorBlock() {
38
+ const {lastError, url} = this.state
39
+ const {message, stack, componentStack} = lastError
40
+ const compiledText = `Error details:
41
+ "${message}" at ${url}
42
+ ${stack}
43
+ ${componentStack ? 'Components stack: ' + componentStack : ''}
44
+ ${navigator.userAgent}`
45
+ return <div className="space dimmed text-small text-right">
46
+ If this error persists please{' '}
47
+ <a href={'mailto:support@stellar.expert?subject=Exception&body=' + encodeURIComponent(compiledText)}
48
+ target="_blank">contact our support</a>.
49
+ </div>
50
+ }
51
+
52
+ renderErrorDetails() {
53
+ const {lastError, url} = this.state
54
+ const {message, stack, componentStack} = lastError
55
+ const text = `"${message}" at ${url}`
56
+ const {errorBoundaryErrorDetails = true} = this.props
57
+ if (errorBoundaryErrorDetails === false)
58
+ return null
59
+ if (errorBoundaryErrorDetails === true) {
60
+ return <div className="error space text-small" style={{overflow: 'auto', maxWidth: '100%', padding: '1rem 2rem'}}>
61
+ <BlockSelect as="div">
62
+ <div className="micro-space">{text}</div>
63
+ <div className="text-tiny">
64
+ <pre style={{whiteSpace: 'pre-wrap'}}>{stack}</pre>
65
+ <div>{navigator.userAgent}</div>
66
+ </div>
67
+ </BlockSelect>
68
+ </div>
69
+ }
70
+ return <>{errorBoundaryErrorDetails}</>
71
+ }
72
+
73
+ render() {
74
+ if (!this.state.lastError)
75
+ return this.props.children
76
+ const {title = 'Unhandled error occurred', wrapper, ...otherProps} = this.props
77
+ if (wrapper)
78
+ return React.createElement(wrapper, {...otherProps}, this.renderError())
79
+ return <div className="segment blank">
80
+ <h3 className="color-danger">{title}</h3>
81
+ <hr className="flare"/>
82
+ {this.renderError()}
83
+ </div>
84
+ }
85
+
86
+ static propTypes = {
87
+ errorBoundaryTitle: PropTypes.string,
88
+ errorBoundarySendErrors: PropTypes.bool,
89
+ errorBoundaryErrorDetails: PropTypes.oneOfType([PropTypes.bool, PropTypes.any])
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Higher-order component that wraps a component with an ErrorBoundary
95
+ * @param {function} wrapped - React component to wrap
96
+ * @param {Object} [options]
97
+ * @param {string} [options.errorBoundaryTitle] - Title displayed in the error UI
98
+ * @param {boolean} [options.errorBoundarySendErrors] - Show "contact support" link
99
+ * @param {boolean|*} [options.errorBoundaryErrorDetails] - Show error details (true), hide (false), or provide custom content
100
+ * @return {function} Wrapped component with error boundary
101
+ */
102
+ export function withErrorBoundary(wrapped, {errorBoundaryTitle, errorBoundarySendErrors, errorBoundaryErrorDetails} = {}) {
103
+ function ErrorBoundaryWrapper(props) {
104
+ const nested = /*#__PURE__*/React.createElement(wrapped, props)
105
+ return <ErrorBoundary {...{errorBoundaryTitle, errorBoundarySendErrors, errorBoundaryErrorDetails}}>{nested}</ErrorBoundary>
106
+ }
107
+
108
+ ErrorBoundaryWrapper.displayName = `withErrorBoundary(${wrapped.displayName || wrapped.name})`
109
+ ErrorBoundaryWrapper.WrappedComponent = wrapped
110
+ return ErrorBoundaryWrapper
98
111
  }
@@ -1,104 +1,104 @@
1
- import {applyListQueryParameters, loadAllHorizonRecords, initHorizon} from './horizon-client-helpers'
2
-
3
- /**
4
- * Retrieve account status from Horizon
5
- * @param {String} accountAddress
6
- * @returns {Promise<AccountResponse>}
7
- */
8
- export function loadAccount(accountAddress) {
9
- return initHorizon()
10
- .loadAccount(accountAddress)
11
- }
12
-
13
- /**
14
- * Load assets issued by the account from Horizon
15
- * @param {String} account - Account address
16
- * @param {ListQueryParams} [queryParams] - Query parameters (optional)
17
- * @return {Promise<Array<Object>>}
18
- */
19
- export function loadIssuedAssets(account, queryParams = null) {
20
- const query = applyListQueryParameters(initHorizon().assets().forIssuer(account).limit(200), queryParams)
21
- return query.call()
22
- .then(({records}) => {
23
- if (records && records.length) {
24
- records.sort((a, b) => a.asset_code - b.asset_code)
25
- }
26
- return records || []
27
- })
28
- }
29
-
30
- /**
31
- * Load offer from Horizon for the particular account
32
- * @param {String} account - Account address
33
- * @param {ListQueryParams} [queryParams] - Query parameters (optional)
34
- * @return {Promise<Array<Object>>}
35
- */
36
- export function loadAccountOffers(account, queryParams = null) {
37
- const query = applyListQueryParameters(initHorizon().offers().forAccount(account), queryParams)
38
- return loadAllHorizonRecords(query)
39
- .then(r => r.records)
40
- }
41
-
42
-
43
- /**
44
- * Load pending claimable balances for a given account pubkey
45
- * @param {String} account - Account address
46
- * @return {Promise<Array<Object>>}
47
- */
48
- export function loadAccountClaimableBalances(account) {
49
- return initHorizon().claimableBalances()
50
- .claimant(account)
51
- .limit(100)
52
- .call()
53
- .then(r => r.records)
54
- }
55
-
56
- /**
57
- * Detect account lock status based on the signers configuration
58
- * @param {AccountResponse} account - Account info from Horizon
59
- * @returns {'unlocked'|'locked'|'partially locked'}
60
- */
61
- export function getAccountLockStatus(account) {
62
- const {high_threshold, med_threshold, low_threshold} = account.thresholds
63
-
64
- //calculate the sum of all signers weights
65
- const totalSignersWeight = account.signers.reduce((res, signer) => res + signer.weight, 0)
66
-
67
- if (totalSignersWeight > 0) {
68
- //compare it with med_threshold and high_threshold
69
- if (totalSignersWeight >= med_threshold || totalSignersWeight >= high_threshold) {
70
- return 'unlocked'
71
- } else if (totalSignersWeight >= low_threshold) {
72
- return 'partially locked'
73
- }
74
- }
75
- return 'locked'
76
- }
77
-
78
- /**
79
- * Retrieve balance for a given Horizon account info
80
- * @param {AccountResponse} account - Account Horizon info
81
- * @param {{code: String, issuer: String}} asset - Asset or null (for XLM)
82
- * @returns {{total: Number, available: Number, [asset]: {code: String, issuer: String}}}
83
- */
84
- export function getAccountBalance(account, asset = null) {
85
- if (!account.balances) return {asset, total: 0, available: 0}
86
- const assetBalance = account.balances.find(b => (!asset && b.asset_type === 'native') ||
87
- (b.asset_issuer === asset.issuer && b.asset_code === asset.code)),
88
- res = {asset, total: 0, available: 0}
89
-
90
- if (assetBalance) {
91
- res.total = parseFloat(assetBalance.balance)
92
- res.available = res.total - parseFloat(assetBalance.selling_liabilities)
93
- if (!asset) { //for XLM we also need to check reserved amount
94
- res.available -= (parseInt(account.subentry_count) + 2) * baseReserve
95
- }
96
- } else if (asset && asset.issuer === account.address) {
97
- res.total = res.available = 922337203685
98
- }
99
-
100
- return res
101
- }
102
-
103
-
104
-
1
+ import {applyListQueryParameters, loadAllHorizonRecords, initHorizon} from './horizon-client-helpers'
2
+
3
+ /**
4
+ * Retrieve account status from Horizon
5
+ * @param {string} accountAddress
6
+ * @returns {Promise<AccountResponse>}
7
+ */
8
+ export function loadAccount(accountAddress) {
9
+ return initHorizon()
10
+ .loadAccount(accountAddress)
11
+ }
12
+
13
+ /**
14
+ * Load assets issued by the account from Horizon
15
+ * @param {string} account - Account address
16
+ * @param {ListQueryParams} [queryParams] - Query parameters (optional)
17
+ * @return {Promise<Array<Object>>}
18
+ */
19
+ export function loadIssuedAssets(account, queryParams = null) {
20
+ const query = applyListQueryParameters(initHorizon().assets().forIssuer(account).limit(200), queryParams)
21
+ return query.call()
22
+ .then(({records}) => {
23
+ if (records && records.length) {
24
+ records.sort((a, b) => a.asset_code - b.asset_code)
25
+ }
26
+ return records || []
27
+ })
28
+ }
29
+
30
+ /**
31
+ * Load offer from Horizon for the particular account
32
+ * @param {string} account - Account address
33
+ * @param {ListQueryParams} [queryParams] - Query parameters (optional)
34
+ * @return {Promise<Array<Object>>}
35
+ */
36
+ export function loadAccountOffers(account, queryParams = null) {
37
+ const query = applyListQueryParameters(initHorizon().offers().forAccount(account), queryParams)
38
+ return loadAllHorizonRecords(query)
39
+ .then(r => r.records)
40
+ }
41
+
42
+
43
+ /**
44
+ * Load pending claimable balances for a given account pubkey
45
+ * @param {string} account - Account address
46
+ * @return {Promise<Array<Object>>}
47
+ */
48
+ export function loadAccountClaimableBalances(account) {
49
+ return initHorizon().claimableBalances()
50
+ .claimant(account)
51
+ .limit(100)
52
+ .call()
53
+ .then(r => r.records)
54
+ }
55
+
56
+ /**
57
+ * Detect account lock status based on the signers configuration
58
+ * @param {AccountResponse} account - Account info from Horizon
59
+ * @returns {'unlocked'|'locked'|'partially locked'}
60
+ */
61
+ export function getAccountLockStatus(account) {
62
+ const {high_threshold, med_threshold, low_threshold} = account.thresholds
63
+
64
+ //calculate the sum of all signers weights
65
+ const totalSignersWeight = account.signers.reduce((res, signer) => res + signer.weight, 0)
66
+
67
+ if (totalSignersWeight > 0) {
68
+ //compare it with med_threshold and high_threshold
69
+ if (totalSignersWeight >= med_threshold || totalSignersWeight >= high_threshold) {
70
+ return 'unlocked'
71
+ } else if (totalSignersWeight >= low_threshold) {
72
+ return 'partially locked'
73
+ }
74
+ }
75
+ return 'locked'
76
+ }
77
+
78
+ /**
79
+ * Retrieve balance for a given Horizon account info
80
+ * @param {AccountResponse} account - Account Horizon info
81
+ * @param {{code: string, issuer: string}} asset - Asset or null (for XLM)
82
+ * @returns {{total: number, available: number, [asset]: {code: string, issuer: string}}}
83
+ */
84
+ export function getAccountBalance(account, asset = null) {
85
+ if (!account.balances) return {asset, total: 0, available: 0}
86
+ const assetBalance = account.balances.find(b => (!asset && b.asset_type === 'native') ||
87
+ (b.asset_issuer === asset.issuer && b.asset_code === asset.code)),
88
+ res = {asset, total: 0, available: 0}
89
+
90
+ if (assetBalance) {
91
+ res.total = parseFloat(assetBalance.balance)
92
+ res.available = res.total - parseFloat(assetBalance.selling_liabilities)
93
+ if (!asset) { //for XLM we also need to check reserved amount
94
+ res.available -= (parseInt(account.subentry_count) + 2) * baseReserve
95
+ }
96
+ } else if (asset && asset.issuer === account.address) {
97
+ res.total = res.available = 922337203685
98
+ }
99
+
100
+ return res
101
+ }
102
+
103
+
104
+
@@ -1,36 +1,36 @@
1
- import {initHorizon, applyListQueryParameters} from './horizon-client-helpers'
2
-
3
- /**
4
- * Load ledgers from Horizon
5
- * @param {ListQueryParams} [queryParams] - Query parameters (optional)
6
- * @return {Promise<Array<Object>>}
7
- */
8
- export function loadLedgers(queryParams = null) {
9
- const query = applyListQueryParameters(initHorizon().ledgers(), queryParams)
10
- return query.call()
11
- .then(r => r.records)
12
- }
13
-
14
- /**
15
- * Load ledger by its sequence
16
- * @param {Number} sequence - Sequence of the ledger to fetch
17
- * @return {Object}
18
- */
19
- export function loadLedger(sequence) {
20
- return initHorizon().ledgers()
21
- .ledger(sequence)
22
- .call()
23
- }
24
-
25
- /**
26
- * Stream ledgers from Horizon
27
- * @param {String} cursor - Cursor to start from
28
- * @param {Function} onNewLedger - Callback to invoke when new ledger arrives
29
- * @return {Function}
30
- */
31
- export function streamLedgers(cursor, onNewLedger) {
32
- return initHorizon().ledgers()
33
- .order('asc')
34
- .cursor(cursor || 'now')
35
- .stream({onmessage: ledger => onNewLedger(ledger)})
1
+ import {initHorizon, applyListQueryParameters} from './horizon-client-helpers'
2
+
3
+ /**
4
+ * Load ledgers from Horizon
5
+ * @param {ListQueryParams} [queryParams] - Query parameters (optional)
6
+ * @return {Promise<Array<Object>>}
7
+ */
8
+ export function loadLedgers(queryParams = null) {
9
+ const query = applyListQueryParameters(initHorizon().ledgers(), queryParams)
10
+ return query.call()
11
+ .then(r => r.records)
12
+ }
13
+
14
+ /**
15
+ * Load ledger by its sequence
16
+ * @param {number} sequence - Sequence of the ledger to fetch
17
+ * @return {Object}
18
+ */
19
+ export function loadLedger(sequence) {
20
+ return initHorizon().ledgers()
21
+ .ledger(sequence)
22
+ .call()
23
+ }
24
+
25
+ /**
26
+ * Stream ledgers from Horizon
27
+ * @param {string} cursor - Cursor to start from
28
+ * @param {function} onNewLedger - Callback to invoke when new ledger arrives
29
+ * @return {function}
30
+ */
31
+ export function streamLedgers(cursor, onNewLedger) {
32
+ return initHorizon().ledgers()
33
+ .order('asc')
34
+ .cursor(cursor || 'now')
35
+ .stream({onmessage: ledger => onNewLedger(ledger)})
36
36
  }