@stellar-expert/ui-framework 1.16.8 → 1.19.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 (85) hide show
  1. package/README.md +1168 -3
  2. package/account/account-address.js +127 -127
  3. package/account/available-balance.js +22 -22
  4. package/account/identicon.js +90 -90
  5. package/account/signer-key.js +65 -64
  6. package/api/explorer-api-hooks.js +209 -202
  7. package/api/explorer-api-paginated-list-hooks.js +441 -440
  8. package/api/explorer-batch-info-loader.js +111 -85
  9. package/api/explorer-tx-api.js +28 -28
  10. package/api/ledger-stream.js +15 -0
  11. package/asset/amount.js +56 -56
  12. package/asset/asset-icon.js +41 -41
  13. package/asset/asset-issuer.js +21 -21
  14. package/asset/asset-link.js +107 -107
  15. package/asset/asset-list-hooks.js +59 -59
  16. package/asset/asset-meta-hooks.js +88 -88
  17. package/asset/asset-selector.js +72 -71
  18. package/claimable-balance/claimable-balance-claimants.js +23 -18
  19. package/contract/contract-api.js +15 -0
  20. package/contract/invocation-info-view.js +10 -2
  21. package/contract/sc-val.js +131 -107
  22. package/controls/button-group.js +25 -19
  23. package/controls/button.js +93 -78
  24. package/controls/code-block.js +42 -34
  25. package/controls/dropdown.js +318 -287
  26. package/controls/external-link.js +10 -4
  27. package/controls/info-tooltip.js +23 -16
  28. package/controls/slider.js +29 -19
  29. package/controls/tabs.js +94 -94
  30. package/controls/tooltip.js +244 -240
  31. package/controls/update-highlighter.js +32 -27
  32. package/date/date-selector.js +56 -54
  33. package/date/elapsed-time.js +28 -21
  34. package/dex/price-dynamic.js +53 -44
  35. package/directory/directory-hooks.js +109 -97
  36. package/effect/effect-description.js +346 -344
  37. package/errors/error-boundary.js +110 -97
  38. package/filter/editors/account-filter-view.js +20 -0
  39. package/filter/editors/asset-filter-view.js +9 -0
  40. package/filter/editors/timestamp-filter-view.js +34 -0
  41. package/filter/editors/type-filter-view.js +148 -0
  42. package/filter/filter-editors.js +102 -0
  43. package/filter/filter-view.js +218 -0
  44. package/filter/filter.scss +57 -0
  45. package/horizon/horizon-account-helpers.js +104 -104
  46. package/horizon/horizon-ledger-helpers.js +35 -35
  47. package/horizon/horizon-trades-helper.js +88 -88
  48. package/horizon/horizon-transaction-helpers.js +36 -36
  49. package/index.d.ts +1271 -0
  50. package/index.js +95 -93
  51. package/interaction/accordion.js +43 -35
  52. package/interaction/autofocus.js +13 -9
  53. package/interaction/block-select.js +64 -53
  54. package/interaction/copy-to-clipboard.js +25 -18
  55. package/interaction/inline-progress.js +20 -15
  56. package/interaction/qr-code.js +34 -34
  57. package/interaction/responsive.js +20 -20
  58. package/interaction/spoiler.js +52 -39
  59. package/interaction/theme-selector.js +13 -10
  60. package/ledger/ledger-entry-href-formatter.js +27 -26
  61. package/ledger/ledger-entry-link.js +93 -58
  62. package/ledger/ledger-info-parser.js +46 -18
  63. package/meta/page-meta-tags.js +243 -238
  64. package/module/dynamic-module.js +47 -47
  65. package/package.json +71 -40
  66. package/state/on-screen-hooks.js +22 -22
  67. package/state/page-visibility-helpers.js +29 -16
  68. package/state/page-visibility-hooks.js +13 -11
  69. package/state/screen-orientation-hooks.js +19 -15
  70. package/state/state-hooks.js +76 -76
  71. package/state/stellar-network-hooks.js +56 -44
  72. package/state/theme.js +29 -28
  73. package/stellar/key-type.js +92 -91
  74. package/stellar/ledger-generic-id.js +39 -39
  75. package/stellar/signature-hint-utils.js +65 -65
  76. package/toast/toast-notifications-block.js +59 -59
  77. package/tx/op-description-view.js +945 -942
  78. package/tx/op-icon.js +98 -98
  79. package/tx/parser/op-balance-changes.js +66 -66
  80. package/tx/parser/op-descriptor.js +51 -48
  81. package/tx/parser/tx-details-parser.js +81 -81
  82. package/tx/parser/tx-matcher.js +371 -371
  83. package/tx/parser/type-filter-matcher.js +126 -126
  84. package/tx/tx-list-hooks.js +32 -18
  85. package/tx/tx-operations-list.js +117 -116
@@ -0,0 +1,218 @@
1
+ import React, {useCallback, useEffect, useState} from 'react'
2
+ import {parseQuery} from '@stellar-expert/navigation'
3
+ import deepmerge from 'deepmerge'
4
+ import {Dropdown} from '../controls/dropdown'
5
+ import {resolveFilterEditor} from './filter-editors'
6
+ import './filter.scss'
7
+
8
+ let fieldDescriptionMapping = {}
9
+
10
+ function FilterCondition({field, value, setValue, removeFilter, edit}) {
11
+ const updateValue = useCallback(function (value) {
12
+ setValue(field, value)
13
+ }, [field, setValue])
14
+
15
+ const removeValue = useCallback(function () {
16
+ removeFilter(field, value)
17
+ }, [field, value, removeFilter])
18
+
19
+ const childProps = {value, edit}
20
+ const filter = fieldDescriptionMapping[field]
21
+ if (edit || filter.multi === false) {
22
+ childProps.setValue = updateValue
23
+ }
24
+
25
+ const editor = resolveFilterEditor(field)
26
+ const title = !edit ? '' : (field === 'type' ? '' : filter.title)
27
+ return <span className="filter-condition condensed" title={edit ? '' : filter.description}>
28
+ <span className={'icon-' + filter.icon}/>
29
+ {title} {React.createElement(editor, childProps)}
30
+ {removeFilter ? <a href="#" className="icon-delete-circle" onClick={removeValue} title="Remove filter"/> : <>&emsp;</>}
31
+ </span>
32
+ }
33
+ /**
34
+ * Parses and validates filter parameters from the current URL query string
35
+ * @return {object}
36
+ */
37
+ export function parseFiltersFromQuery() {
38
+ const params = parseQuery()
39
+ const filters = {}
40
+ let isEmpty = true
41
+ for (const [key, value] of Object.entries(params)) {
42
+ const filterDescriptor = fieldDescriptionMapping[key]
43
+ if (!filterDescriptor)
44
+ continue //skip unrelated query parameters
45
+ filters[key] = value
46
+ isEmpty = false
47
+ }
48
+ return filters
49
+ }
50
+
51
+ function FiltersGroup({filters, replaceFilter, removeFilter, edit = false}) {
52
+ if (!filters)
53
+ return null
54
+ return <>{Object.entries(filters).map(([field, values]) => {
55
+ if (!(values instanceof Array)) {
56
+ return <FilterCondition key={field} field={field} value={values} edit={edit} setValue={replaceFilter}
57
+ removeFilter={removeFilter}/>
58
+ }
59
+ if (!values.length)
60
+ return null
61
+ return <React.Fragment key={field}>
62
+ {values.map(value => <FilterCondition key={value} field={field} value={value} edit={edit}
63
+ setValue={replaceFilter} removeFilter={removeFilter}/>)}
64
+ </React.Fragment>
65
+ })}</>
66
+ }
67
+
68
+ /**
69
+ * A component for managing search filters
70
+ * @param {object} [presetFilter] - Initial base filters
71
+ * @param {object} [fields] - Custom filter field definitions
72
+ * @param {func} [onChange] - Callback triggered when filters change
73
+ */
74
+ export function FilterView({presetFilter, fields = {}, onChange}) {
75
+ fieldDescriptionMapping = fields
76
+ const [filters, setFilters] = useState(presetFilter || {})
77
+ const [_, setSerializedFilter] = useState(JSON.stringify(filters))
78
+
79
+ const availableFields = []
80
+ const readyFilters = {}
81
+ const editFilters = {}
82
+ let editorMode = false
83
+ for (const [field, props = {}] of Object.entries(fieldDescriptionMapping)) {
84
+ const values = filters[field]
85
+ if (values !== undefined) {
86
+ if (values instanceof Array) {
87
+ const newValues = readyFilters[field] = []
88
+ for (let i = 0; i < values.length; i++) {
89
+ const value = values[i]
90
+ if (!value) {
91
+ editFilters[field] = [value]
92
+ editorMode = true
93
+ } else if (!presetFilter || !presetFilter[field]?.includes(value)) {
94
+ newValues.push(value)
95
+ }
96
+ }
97
+ } else {
98
+ if (!values) {
99
+ editFilters[field] = values
100
+ editorMode = true
101
+ } else {
102
+ readyFilters[field] = values
103
+ }
104
+ }
105
+ }
106
+ if (props.multi !== false || !values) {
107
+ availableFields.push({
108
+ value: field,
109
+ title: props.description,
110
+ icon: 'icon-' + props.icon
111
+ })
112
+ }
113
+ }
114
+
115
+ const updateExternalFilters = useCallback(function (newFilters) {
116
+ setSerializedFilter(prev => {
117
+ const newValue = JSON.stringify(newFilters)
118
+ if (prev !== newValue) {
119
+ setTimeout(() => onChange(deepmerge(presetFilter, newFilters)), 100)
120
+ return newValue
121
+ }
122
+ return prev
123
+ })
124
+ }, [onChange])
125
+
126
+ useEffect(() => {
127
+ const queryParams = parseFiltersFromQuery()
128
+ setFilters(queryParams)
129
+ updateExternalFilters(queryParams)
130
+ }, [])
131
+
132
+
133
+ const replaceFilter = useCallback((field, value) => setTimeout(() => setFilters(prev => {
134
+ const newFilters = {...prev}
135
+ const filter = fieldDescriptionMapping[field] || {}
136
+ //atomic values
137
+ if (filter.multi === false) {
138
+ if (value === null) {
139
+ delete newFilters[field]
140
+ updateExternalFilters(newFilters)
141
+ } else {
142
+ newFilters[field] = value
143
+ if (value) {
144
+ updateExternalFilters(newFilters)
145
+ }
146
+ }
147
+ return newFilters
148
+ }
149
+ //multi-values
150
+ let values = newFilters[field]
151
+ if (!values) {
152
+ values = newFilters[field] = []
153
+ }
154
+ if (value !== null) {
155
+ if (!values.includes(value)) {
156
+ if (values[values.length - 1] === '') {
157
+ values.pop()
158
+ }
159
+ values.push(value)
160
+ if (value) {
161
+ updateExternalFilters(newFilters)
162
+ }
163
+ }
164
+ } else {
165
+ const idx = values.indexOf('')
166
+ if (idx >= 0) {
167
+ values.splice(idx, 1)
168
+ updateExternalFilters(newFilters)
169
+ }
170
+ }
171
+ return newFilters
172
+ }), 100), [updateExternalFilters])
173
+
174
+ const removeFilter = useCallback((field, value) => setFilters(prev => {
175
+ const newFilters = {...prev}
176
+ const filter = fieldDescriptionMapping[field] || {}
177
+ //atomic value
178
+ if (filter.multi === false) {
179
+ if (newFilters[field] || newFilters[field] === '') {
180
+ newFilters[field] = undefined
181
+ updateExternalFilters(newFilters)
182
+ }
183
+ return newFilters
184
+ }
185
+ //multi-values
186
+ const values = newFilters[field]
187
+ const idx = values.indexOf(value)
188
+ if (idx >= 0) {
189
+ values.splice(idx, 1)
190
+ updateExternalFilters(newFilters)
191
+ }
192
+ return newFilters
193
+ }), [updateExternalFilters])
194
+
195
+ const addFilter = useCallback(field => {
196
+ replaceFilter(field, '')
197
+ }, [replaceFilter])
198
+
199
+ const title = <span className="nowrap"><span className="icon icon-add-circle"/>Add filter</span>
200
+
201
+ return <div className="op-filters">
202
+ <div className="mobile-only micro-space"/>
203
+ <span className="icon-filter"/>&nbsp;Filters&emsp;
204
+ <FiltersGroup filters={presetFilter}/>
205
+ <FiltersGroup filters={readyFilters} replaceFilter={replaceFilter} removeFilter={removeFilter}/>
206
+ {!editorMode ?
207
+ <Dropdown title={title} options={availableFields} onChange={addFilter}/> :
208
+ <div className="micro-space">
209
+ <FiltersGroup filters={editFilters} replaceFilter={replaceFilter} removeFilter={removeFilter} edit/>
210
+ </div>}
211
+ <div className="micro-space"/>
212
+ </div>
213
+ }
214
+
215
+ /**
216
+ * @callback FilterView~OnChange
217
+ * @param {Object} mergedFilters - The combination of preset filters and newly selected values.
218
+ */
@@ -0,0 +1,57 @@
1
+ .op-filters {
2
+ .icon-delete-circle {
3
+ margin-left: 0.1em;
4
+ }
5
+ }
6
+
7
+ .op-filter-container {
8
+ display: flex;
9
+ justify-content: flex-start;
10
+ background-color: var(--color-bg);
11
+ border: 1px solid var(--color-contrast-border);
12
+ border-radius: $border-radius-input;
13
+ color: var(--color-text);
14
+ height: 4rem;
15
+ padding: 0.2rem 1rem;
16
+ width: 100%;
17
+
18
+ &:focus {
19
+ border-color: var(--color-highlight);
20
+ }
21
+ }
22
+
23
+ .filter-condition {
24
+ display: inline-block;
25
+ margin-right: 0.4em;
26
+ cursor: pointer;
27
+
28
+ &:before {
29
+ content: '/';
30
+ color: var(--color-highlight);
31
+ }
32
+
33
+ .icon-delete-circle {
34
+ opacity: 0;
35
+ }
36
+
37
+ &:hover .icon-delete-circle {
38
+ opacity: 1;
39
+ }
40
+
41
+ > input {
42
+ margin-bottom: 0;
43
+ height: 3rem;
44
+ padding: 0 0.4rem;
45
+
46
+ + .icon-ok {
47
+ display: inline-block;
48
+ visibility: hidden;
49
+ transition: visibility 0.3s;
50
+ }
51
+
52
+ &:focus + .icon-ok {
53
+ visibility: visible;
54
+ margin-left: 0.2em;
55
+ }
56
+ }
57
+ }
@@ -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
  }