@stellar-expert/ui-framework 1.16.7 → 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 (92) 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/basic-styles/base.scss +50 -50
  20. package/basic-styles/grid.scss +1 -1
  21. package/basic-styles/table.scss +1 -1
  22. package/claimable-balance/claimable-balance-claimants.js +5 -0
  23. package/contract/contract-api.js +15 -0
  24. package/contract/invocation-info-view.js +10 -2
  25. package/contract/sc-val.js +24 -0
  26. package/controls/button-group.js +25 -19
  27. package/controls/button-group.scss +46 -46
  28. package/controls/button.js +93 -78
  29. package/controls/button.scss +173 -173
  30. package/controls/code-block.js +42 -34
  31. package/controls/code-block.scss +71 -71
  32. package/controls/dropdown.js +318 -287
  33. package/controls/dropdown.scss +159 -159
  34. package/controls/external-link.js +10 -4
  35. package/controls/info-tooltip.js +23 -16
  36. package/controls/slider.js +29 -19
  37. package/controls/tabs.js +94 -94
  38. package/controls/tabs.scss +70 -70
  39. package/controls/tooltip.js +244 -240
  40. package/controls/tooltip.scss +116 -116
  41. package/controls/update-highlighter.js +32 -27
  42. package/controls/update-highlighter.scss +7 -7
  43. package/date/date-selector.js +56 -54
  44. package/date/elapsed-time.js +28 -21
  45. package/dex/price-dynamic.js +53 -44
  46. package/dex/price-dynamic.scss +33 -33
  47. package/directory/directory-hooks.js +109 -97
  48. package/effect/effect-description.js +5 -3
  49. package/errors/error-boundary.js +110 -97
  50. package/horizon/horizon-account-helpers.js +104 -104
  51. package/horizon/horizon-ledger-helpers.js +35 -35
  52. package/horizon/horizon-trades-helper.js +88 -88
  53. package/horizon/horizon-transaction-helpers.js +36 -36
  54. package/index.d.ts +1241 -0
  55. package/interaction/accordion.js +43 -35
  56. package/interaction/autofocus.js +13 -9
  57. package/interaction/block-select.js +64 -53
  58. package/interaction/block-select.scss +21 -21
  59. package/interaction/copy-to-clipboard.js +25 -18
  60. package/interaction/inline-progress.js +20 -15
  61. package/interaction/qr-code.js +34 -34
  62. package/interaction/responsive.js +20 -20
  63. package/interaction/spoiler.js +52 -39
  64. package/interaction/spoiler.scss +6 -6
  65. package/interaction/theme-selector.js +13 -10
  66. package/ledger/ledger-entry-href-formatter.js +4 -3
  67. package/ledger/ledger-entry-link.js +93 -58
  68. package/ledger/ledger-info-parser.js +28 -0
  69. package/meta/page-meta-tags.js +243 -238
  70. package/module/dynamic-module.js +47 -47
  71. package/package.json +3 -2
  72. package/state/on-screen-hooks.js +22 -22
  73. package/state/page-visibility-helpers.js +29 -16
  74. package/state/page-visibility-hooks.js +13 -11
  75. package/state/screen-orientation-hooks.js +19 -15
  76. package/state/state-hooks.js +76 -76
  77. package/state/stellar-network-hooks.js +56 -44
  78. package/state/theme.js +29 -28
  79. package/stellar/key-type.js +92 -91
  80. package/stellar/ledger-generic-id.js +39 -39
  81. package/stellar/signature-hint-utils.js +65 -65
  82. package/toast/toast-notifications-block.js +59 -59
  83. package/toast/toast-notifications.scss +1 -1
  84. package/tx/op-description-view.js +84 -81
  85. package/tx/op-icon.js +98 -98
  86. package/tx/parser/op-balance-changes.js +66 -66
  87. package/tx/parser/op-descriptor.js +51 -48
  88. package/tx/parser/tx-details-parser.js +81 -81
  89. package/tx/parser/tx-matcher.js +371 -371
  90. package/tx/parser/type-filter-matcher.js +126 -126
  91. package/tx/tx-list-hooks.js +32 -18
  92. package/tx/tx-operations-list.js +117 -116
@@ -1,98 +1,110 @@
1
- import {useEffect, useState} from 'react'
2
- import {StrKey} from '@stellar/stellar-base'
3
- import {stringifyQuery} from '@stellar-expert/navigation'
4
- import {InMemoryClientCache} from '@stellar-expert/client-cache'
5
- import {fetchExplorerApi} from '../api/explorer-api-call'
6
- import {ExplorerBatchInfoLoader} from '../api/explorer-batch-info-loader'
7
-
8
- const cache = new InMemoryClientCache({})
9
-
10
- function isValidAddress(address) {
11
- if (!address)
12
- return false
13
- if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
14
- return false
15
- return true
16
- }
17
-
18
- const loader = new ExplorerBatchInfoLoader(batch => {
19
- return fetchExplorerApi('directory' + stringifyQuery({address: batch}))
20
- }, entry => {
21
- cache.set(entry.address, entry)
22
- return {key: entry.address, info: entry}
23
- })
24
-
25
- export async function getDirectoryEntry(address, options) {
26
- const {forceRefresh = false, extended = false} = options || {}
27
- //ignore invalid addresses
28
- if (!isValidAddress(address))
29
- return null
30
- if (extended) {
31
- return fetchExplorerApi(`directory/${address}` + stringifyQuery({
32
- extended: true,
33
- s: Math.random().toString(36).substr(2)
34
- }))
35
- .catch(err => {
36
- console.error(err)
37
- return null
38
- })
39
- }
40
- //try to load from the shared cache
41
- if (forceRefresh) {
42
- const cachedEntry = cache.get(address)
43
- if (cachedEntry && !cachedEntry.isExpired) return cachedEntry.data
44
- }
45
- //load from the server
46
- return loader.loadEntry(address)
47
- }
48
-
49
- /**
50
- *
51
- * @param {String} address
52
- * @param {{[forceRefresh]: Boolean}} [options]
53
- * @return {String|null}
54
- */
55
- export function useDirectory(address, options) {
56
- const {forceRefresh = false} = options || {}
57
- const [directoryInfo, setDirectoryInfo] = useState(null)
58
- let unloaded = false
59
- useEffect(function () {
60
- if (!isValidAddress(address))
61
- return
62
- if (!forceRefresh) {
63
- const cachedEntry = cache.get(address)
64
- if (cachedEntry && !cachedEntry.isStale) {
65
- if (!cachedEntry.isExpired)
66
- return setDirectoryInfo(cachedEntry.data) //everything is up to date - no need to re-fetch
67
- }
68
- }
69
- //load from the server
70
- loader.loadEntry(address)
71
- .then(di => {
72
- if (!unloaded) {
73
- setDirectoryInfo(di)
74
- }
75
- })
76
- return () => {
77
- unloaded = true
78
- }
79
- }, [address, forceRefresh])
80
- return directoryInfo
81
- }
82
-
83
- export function useDirectoryTags() {
84
- const [tags, setTags] = useState(() => {
85
- const cachedEntry = cache.get('tags')
86
- if (cachedEntry && !cachedEntry.isExpired) {
87
- return cachedEntry.data
88
- }
89
- fetchExplorerApi('directory/tags')
90
- .then(data => {
91
- cache.set('tags', data, 10 * 60) //10 minutes
92
- setTags(data)
93
- })
94
- .catch(err => console.error(err))
95
- return []
96
- })
97
- return tags
1
+ import {useEffect, useState} from 'react'
2
+ import {StrKey} from '@stellar/stellar-base'
3
+ import {stringifyQuery} from '@stellar-expert/navigation'
4
+ import {InMemoryClientCache} from '@stellar-expert/client-cache'
5
+ import {fetchExplorerApi} from '../api/explorer-api-call'
6
+ import {ExplorerBatchInfoLoader} from '../api/explorer-batch-info-loader'
7
+
8
+ const cache = new InMemoryClientCache({})
9
+
10
+ function isValidAddress(address) {
11
+ if (!address)
12
+ return false
13
+ if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
14
+ return false
15
+ return true
16
+ }
17
+
18
+ const loader = new ExplorerBatchInfoLoader(batch => {
19
+ return fetchExplorerApi('directory' + stringifyQuery({address: batch}))
20
+ }, entry => {
21
+ cache.set(entry.address, entry)
22
+ return {key: entry.address, info: entry}
23
+ })
24
+
25
+ /**
26
+ * Fetch a directory entry for a Stellar address
27
+ * @param {string} address - Stellar account or contract address
28
+ * @param {Object} [options]
29
+ * @param {boolean} [options.forceRefresh=false] - Bypass cache and fetch fresh data
30
+ * @param {boolean} [options.extended=false] - Fetch extended directory information
31
+ * @return {Promise<Object|null>} Directory entry or null if not found
32
+ */
33
+ export async function getDirectoryEntry(address, options) {
34
+ const {forceRefresh = false, extended = false} = options || {}
35
+ //ignore invalid addresses
36
+ if (!isValidAddress(address))
37
+ return null
38
+ if (extended) {
39
+ return fetchExplorerApi(`directory/${address}` + stringifyQuery({
40
+ extended: true,
41
+ s: Math.random().toString(36).substr(2)
42
+ }))
43
+ .catch(err => {
44
+ console.error(err)
45
+ return null
46
+ })
47
+ }
48
+ //try to load from the shared cache
49
+ if (forceRefresh) {
50
+ const cachedEntry = cache.get(address)
51
+ if (cachedEntry && !cachedEntry.isExpired) return cachedEntry.data
52
+ }
53
+ //load from the server
54
+ return loader.loadEntry(address)
55
+ }
56
+
57
+ /**
58
+ *
59
+ * @param {string} address
60
+ * @param {{[forceRefresh]: boolean}} [options]
61
+ * @return {string|null}
62
+ */
63
+ export function useDirectory(address, options) {
64
+ const {forceRefresh = false} = options || {}
65
+ const [directoryInfo, setDirectoryInfo] = useState(null)
66
+ let unloaded = false
67
+ useEffect(function () {
68
+ if (!isValidAddress(address))
69
+ return
70
+ if (!forceRefresh) {
71
+ const cachedEntry = cache.get(address)
72
+ if (cachedEntry && !cachedEntry.isStale) {
73
+ if (!cachedEntry.isExpired)
74
+ return setDirectoryInfo(cachedEntry.data) //everything is up to date - no need to re-fetch
75
+ }
76
+ }
77
+ //load from the server
78
+ loader.loadEntry(address)
79
+ .then(di => {
80
+ if (!unloaded) {
81
+ setDirectoryInfo(di)
82
+ }
83
+ })
84
+ return () => {
85
+ unloaded = true
86
+ }
87
+ }, [address, forceRefresh])
88
+ return directoryInfo
89
+ }
90
+
91
+ /**
92
+ * React hook that fetches and returns all available directory tags
93
+ * @return {Array<Object>} Array of directory tag objects
94
+ */
95
+ export function useDirectoryTags() {
96
+ const [tags, setTags] = useState(() => {
97
+ const cachedEntry = cache.get('tags')
98
+ if (cachedEntry && !cachedEntry.isExpired) {
99
+ return cachedEntry.data
100
+ }
101
+ fetchExplorerApi('directory/tags')
102
+ .then(data => {
103
+ cache.set('tags', data, 10 * 60) //10 minutes
104
+ setTags(data)
105
+ })
106
+ .catch(err => console.error(err))
107
+ return []
108
+ })
109
+ return tags
98
110
  }
@@ -14,10 +14,12 @@ import {AssetLink} from '../asset/asset-link'
14
14
  import {Amount} from '../asset/amount'
15
15
 
16
16
  /**
17
- * @param {{}} effect
18
- * @param {{}} operation
17
+ * Renders a human-readable description of a Stellar transaction effect
18
+ * @param {Object} props
19
+ * @param {Object} props.effect - Parsed transaction effect object
20
+ * @param {string} props.effect.type - Effect type identifier
21
+ * @param {Object} [props.operation] - Parent operation context
19
22
  * @return {JSX.Element}
20
- * @constructor
21
23
  */
22
24
  export function EffectDescription({effect, operation}) {
23
25
  switch (effect.type) {
@@ -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
+