@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
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import {useRef} from 'react'
|
|
2
|
+
import isEqual from 'react-fast-compare'
|
|
3
|
+
import {parseQuery, stringifyQuery, navigation} from '@stellar-expert/navigation'
|
|
4
|
+
import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
|
|
5
|
+
import {useDependantState} from '../state/state-hooks'
|
|
6
|
+
import {fetchExplorerApi} from './explorer-api-call'
|
|
7
|
+
import apiCache from './api-cache'
|
|
8
|
+
|
|
9
|
+
function inverseOrder(order) {
|
|
10
|
+
return order === 'desc' ? 'asc' : 'desc'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class PaginatedListViewModel {
|
|
14
|
+
/**
|
|
15
|
+
* Create new instance of PaginatedListViewModel
|
|
16
|
+
* @param {String} endpoint - API endpoint
|
|
17
|
+
* @param {Object} [props] - Extra model params
|
|
18
|
+
* @param {Number} [props.ttl] - Cache time-to-live period
|
|
19
|
+
* @param {Number} [props.limit] - Rows limit
|
|
20
|
+
* @param {Boolean} [props.autoReverseRecordsOrder] - Reverse order to match default grid order
|
|
21
|
+
* @param {Boolean} [props.autoLoadLastPage] - Whether to load last page flag
|
|
22
|
+
* @param {'asc'|'desc'} [props.defaultSortOrder] - Results sorting order
|
|
23
|
+
* @param {Object} [props.defaultQueryParams] - Default query values - query params not set if default
|
|
24
|
+
* @param {Function} [props.dataProcessingCallback] - Callback called for the fetched data
|
|
25
|
+
* @param {Boolean|Function} [props.updateLocation] - Whether to update browser location
|
|
26
|
+
*/
|
|
27
|
+
constructor(endpoint, props = {limit: 20}) {
|
|
28
|
+
this.endpoint = endpoint
|
|
29
|
+
this.data = []
|
|
30
|
+
this.defaultQueryParams = {}
|
|
31
|
+
this.limit = props.limit
|
|
32
|
+
this.ttl = props.ttl
|
|
33
|
+
this.query = props.query
|
|
34
|
+
this.dataProcessingCallback = props.dataProcessingCallback
|
|
35
|
+
this.defaultSortOrder = props.defaultSortOrder
|
|
36
|
+
this.updateLocation = props.updateLocation
|
|
37
|
+
if (props.autoReverseRecordsOrder !== undefined) {
|
|
38
|
+
this.autoReverseRecordsOrder = props.autoReverseRecordsOrder
|
|
39
|
+
}
|
|
40
|
+
if (props.autoLoadLastPage !== undefined) {
|
|
41
|
+
this.autoLoadLastPage = props.autoLoadLastPage
|
|
42
|
+
}
|
|
43
|
+
//reconstruct state from query
|
|
44
|
+
if (navigation.query.cursor) {
|
|
45
|
+
const {cursor, sort, order} = navigation.query
|
|
46
|
+
this.nextCursor = stringifyQuery({cursor, sort, order})
|
|
47
|
+
}
|
|
48
|
+
if (props.defaultQueryParams) {
|
|
49
|
+
this.defaultQueryParams = props.defaultQueryParams
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* API endpoint
|
|
55
|
+
* @type {String}
|
|
56
|
+
*/
|
|
57
|
+
endpoint = ''
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Batch size
|
|
61
|
+
* @type {Number}
|
|
62
|
+
*/
|
|
63
|
+
limit = 20
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Time-to-live cache period (in seconds)
|
|
67
|
+
* @type {Number}
|
|
68
|
+
*/
|
|
69
|
+
ttl = 30
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @type {Function|Object}
|
|
74
|
+
*/
|
|
75
|
+
query = null
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Automatically reverse records order
|
|
79
|
+
* @type {Boolean}
|
|
80
|
+
*/
|
|
81
|
+
autoReverseRecordsOrder = false
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sorting order
|
|
85
|
+
* @type {String}
|
|
86
|
+
*/
|
|
87
|
+
defaultSortOrder = 'desc'
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load last meaningful page if now results returned from the server
|
|
91
|
+
* @type {Boolean}
|
|
92
|
+
*/
|
|
93
|
+
autoLoadLastPage = true
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Function used to process data retrieved from the server
|
|
97
|
+
* @type {Function}
|
|
98
|
+
*/
|
|
99
|
+
dataProcessingCallback = null
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @type {Function}
|
|
103
|
+
*/
|
|
104
|
+
onError = null
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Data received from server
|
|
108
|
+
* @type {Object}
|
|
109
|
+
*/
|
|
110
|
+
data
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Response loaded flag
|
|
114
|
+
* @type {Boolean}
|
|
115
|
+
*/
|
|
116
|
+
loaded = false
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fetch-in-progress flag
|
|
120
|
+
* @type {Boolean}
|
|
121
|
+
*/
|
|
122
|
+
loading = false
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether the next page is available
|
|
126
|
+
* @type {Boolean}
|
|
127
|
+
*/
|
|
128
|
+
canLoadNextPage = false
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether the prev page is available
|
|
132
|
+
* @type {Boolean}
|
|
133
|
+
*/
|
|
134
|
+
canLoadPrevPage = false
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Next page cursor
|
|
138
|
+
* @type {String}
|
|
139
|
+
*/
|
|
140
|
+
nextCursor
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Previous page cursor
|
|
144
|
+
* @type {String}
|
|
145
|
+
*/
|
|
146
|
+
prevCursor
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @private
|
|
150
|
+
* @type {Function}
|
|
151
|
+
*/
|
|
152
|
+
updateApiResponseData = null
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Load portion of data from the server
|
|
156
|
+
* @param {1|-1} page
|
|
157
|
+
* @return {Promise<ExplorerApiListResponse>}
|
|
158
|
+
*/
|
|
159
|
+
load(page) {
|
|
160
|
+
const paginationParams = {skip: undefined},
|
|
161
|
+
navCursor = page < 0 ? this.prevCursor : this.nextCursor
|
|
162
|
+
|
|
163
|
+
if (navCursor) {
|
|
164
|
+
parseQuery(navCursor.split('?')[1] || '', paginationParams)
|
|
165
|
+
} else {
|
|
166
|
+
this.nextCursor = stringifyQuery({
|
|
167
|
+
cursor: navigation.query.cursor,
|
|
168
|
+
sort: navigation.query.sort,
|
|
169
|
+
order: navigation.query.order
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
//const externalQueryParams = typeof this.query === 'function' ? this.query() : this.query
|
|
173
|
+
//prepare query params
|
|
174
|
+
const queryParams = Object.assign({}, this.defaultQueryParams, this.query, paginationParams, {limit: this.limit})
|
|
175
|
+
return this.loadPage(queryParams)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reverse order and load the page
|
|
180
|
+
* @param queryParams
|
|
181
|
+
* @return {Promise<ExplorerApiListResponse>}
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
async loadLastPage(queryParams) {
|
|
185
|
+
const {order} = queryParams,
|
|
186
|
+
overrides = {
|
|
187
|
+
order: inverseOrder(order),
|
|
188
|
+
cursor: undefined
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.loadPage(Object.assign({}, queryParams, overrides))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Load data page from the server
|
|
196
|
+
* @param {{}} queryParams
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
async loadPage(queryParams) {
|
|
200
|
+
this.loaded = false
|
|
201
|
+
this.loading = true
|
|
202
|
+
this.updateQuery(queryParams)
|
|
203
|
+
this.updateApiResponseData(this.toJSON())
|
|
204
|
+
const [path] = this.endpoint.split('?')
|
|
205
|
+
const endpointWithQuery = path + stringifyQuery(queryParams)
|
|
206
|
+
|
|
207
|
+
//try to retrieve data from the browser cache
|
|
208
|
+
const fromCache = this.ttl && apiCache.get(endpointWithQuery)
|
|
209
|
+
//fetch from the server only if there is no data or it is expired
|
|
210
|
+
if (!fromCache || fromCache.isExpired) {
|
|
211
|
+
const data = await fetchExplorerApi(endpointWithQuery)
|
|
212
|
+
return this.processResponseData(endpointWithQuery, data, queryParams)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (fromCache && !fromCache.isStale) {
|
|
216
|
+
//if the cached data is up to date - proceed with it
|
|
217
|
+
return this.processResponseData(endpointWithQuery, fromCache.data, queryParams)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Retrieve and convert data from the API response
|
|
223
|
+
* @param {String} endpointWithQuery
|
|
224
|
+
* @param {{}} data
|
|
225
|
+
* @param {{}} queryParams
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
processResponseData(endpointWithQuery, data, queryParams) {
|
|
229
|
+
if (data.error) {
|
|
230
|
+
console.error(e)
|
|
231
|
+
this.loaded = true
|
|
232
|
+
this.loading = false
|
|
233
|
+
this.error = data
|
|
234
|
+
} else {
|
|
235
|
+
const {_links, _embedded} = data
|
|
236
|
+
let records = _embedded.records.slice()
|
|
237
|
+
//we reached the end of the query
|
|
238
|
+
if (!records.length && this.autoLoadLastPage && this.data && this.data.length) {
|
|
239
|
+
//load first/last meaningful page
|
|
240
|
+
setTimeout(() => this.loadLastPage(queryParams), 500)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (this.autoReverseRecordsOrder && _links.self.href.includes('order=' + inverseOrder(this.defaultSortOrder))) {
|
|
244
|
+
records.reverse()
|
|
245
|
+
}
|
|
246
|
+
if (this.dataProcessingCallback) {
|
|
247
|
+
records = this.dataProcessingCallback(records)
|
|
248
|
+
}
|
|
249
|
+
this.data = records
|
|
250
|
+
this.loaded = true
|
|
251
|
+
this.loading = false
|
|
252
|
+
this.updateNav(_links)
|
|
253
|
+
}
|
|
254
|
+
//in case of error set small ttl in order to try re-fetching in 4 seconds
|
|
255
|
+
if (this.ttl >= 0) {
|
|
256
|
+
apiCache.set(endpointWithQuery, data, data.error ? 4 : this.ttl)
|
|
257
|
+
}
|
|
258
|
+
//update response data
|
|
259
|
+
const res = this.toJSON()
|
|
260
|
+
this.updateApiResponseData(res)
|
|
261
|
+
return res
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Update naviagtion links retrieved from the response
|
|
266
|
+
* @param {String} self
|
|
267
|
+
* @param {String} next
|
|
268
|
+
* @param {String} prev
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
updateNav({self, next, prev}) {
|
|
272
|
+
this.canLoadNextPage = true //this.nextCursor && this.nextCursor !== self.href && records.length >= this.limit
|
|
273
|
+
this.canLoadPrevPage = true //this.prevCursor && this.prevCursor !== self.href
|
|
274
|
+
const selfQuery = parseQuery(self.href.split('?')[1])
|
|
275
|
+
if ((selfQuery.order === inverseOrder(this.defaultSortOrder))) {
|
|
276
|
+
this.prevCursor = next ? next.href : null
|
|
277
|
+
this.nextCursor = prev ? prev.href : null
|
|
278
|
+
if (!selfQuery.cursor) {
|
|
279
|
+
this.canLoadNextPage = false
|
|
280
|
+
}
|
|
281
|
+
if (this.data.length < this.limit) {
|
|
282
|
+
this.canLoadPrevPage = false
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
this.nextCursor = next ? next.href : null
|
|
286
|
+
this.prevCursor = prev ? prev.href : null
|
|
287
|
+
if (!selfQuery.cursor) {
|
|
288
|
+
this.canLoadPrevPage = false
|
|
289
|
+
}
|
|
290
|
+
if (this.data.length < this.limit) {
|
|
291
|
+
this.canLoadNextPage = false
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (selfQuery.cursor === '0') {
|
|
296
|
+
this.canLoadPrevPage = false
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Update location query string
|
|
302
|
+
* @param {{}} queryParams
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
updateQuery(queryParams) {
|
|
306
|
+
const {updateLocation} = this
|
|
307
|
+
if (!updateLocation)
|
|
308
|
+
return
|
|
309
|
+
let paramsToSet = {}
|
|
310
|
+
for (let key in queryParams)
|
|
311
|
+
if (queryParams.hasOwnProperty(key)) {
|
|
312
|
+
if (key === 'limit') continue
|
|
313
|
+
let value = queryParams[key]
|
|
314
|
+
//ignore default params
|
|
315
|
+
if (this.defaultQueryParams[key] === value) {
|
|
316
|
+
value = undefined
|
|
317
|
+
}
|
|
318
|
+
paramsToSet[key] = value
|
|
319
|
+
}
|
|
320
|
+
if (typeof updateLocation === 'function') {
|
|
321
|
+
paramsToSet = updateLocation(paramsToSet)
|
|
322
|
+
}
|
|
323
|
+
navigation.updateQuery(paramsToSet)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Convert model to a plain object representation
|
|
328
|
+
* @return {{loaded: Boolean, data: ({}[]), load: function, reset: function loading: Boolean, canLoadNextPage: Boolean, canLoadPrevPage: Boolean}}
|
|
329
|
+
*/
|
|
330
|
+
toJSON() {
|
|
331
|
+
return {
|
|
332
|
+
data: this.data || [],
|
|
333
|
+
loaded: this.loaded,
|
|
334
|
+
loading: this.loading,
|
|
335
|
+
canLoadPrevPage: this.canLoadPrevPage,
|
|
336
|
+
canLoadNextPage: this.canLoadNextPage,
|
|
337
|
+
load: this.load.bind(this),
|
|
338
|
+
reset: this.reset.bind(this)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Reset model to the initial state
|
|
344
|
+
*/
|
|
345
|
+
reset() {
|
|
346
|
+
this.data = []
|
|
347
|
+
this.loaded = false
|
|
348
|
+
this.loading = false
|
|
349
|
+
this.canLoadNextPage = false
|
|
350
|
+
this.canLoadPrevPage = false
|
|
351
|
+
this.nextCursor = undefined
|
|
352
|
+
this.prevCursor = undefined
|
|
353
|
+
this.currentQueryParams = undefined
|
|
354
|
+
this.updateQuery({cursor: undefined, sort: undefined, order: undefined})
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @typedef {Object} ExplorerApiListResponse
|
|
361
|
+
* @property {Object[]} data - Data retrieved from the server
|
|
362
|
+
* @property {Boolean} loaded - Response result loaded flag
|
|
363
|
+
* @property {Function} load - Load pgae function
|
|
364
|
+
* @property {Boolean} canLoadPrevPage - Whether the prev page is available
|
|
365
|
+
* @property {Boolean} canLoadNextPage - Whether the next page is available
|
|
366
|
+
*/
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
*
|
|
370
|
+
* @param {String|APIEndpointParams} apiEndpoint - Server API endpoint to use as a data source.
|
|
371
|
+
* @param {Number} [ttl] - Cache time-to-live in seconds.
|
|
372
|
+
* @param {Number} [limit] - Rows per batch limit.
|
|
373
|
+
* @param {Boolean} [autoReverseRecordsOrder] - Reverse order to match default grid order.
|
|
374
|
+
* @param {'asc'|'desc'} [defaultSortOrder] - Reverse order to match default grid order.
|
|
375
|
+
* @param {Boolean} [autoLoadLastPage] - Load last meaningful page if now results returned from the server.
|
|
376
|
+
* @param {Boolean} [includeNetwork] - Whether to include network prefix in the endpoint path.
|
|
377
|
+
* @param {Function} [dataProcessingCallback] - Callback called for the fetched data.
|
|
378
|
+
* @param {Object} [defaultQueryParams] - Default query values - query params not set if default.
|
|
379
|
+
* @param {Boolean} [autoLoad] - Default query values - query params not set if default.
|
|
380
|
+
* @param {Boolean} [updateLocation] - Automatically update browser query string on navigation.
|
|
381
|
+
* @param {Array} [dependencies] - Additional dependencies to track for state updates.
|
|
382
|
+
* @return {ExplorerApiListResponse}
|
|
383
|
+
*/
|
|
384
|
+
export function useExplorerPaginatedApi(apiEndpoint,
|
|
385
|
+
{
|
|
386
|
+
ttl = 30,
|
|
387
|
+
limit = 20,
|
|
388
|
+
autoReverseRecordsOrder = false,
|
|
389
|
+
defaultSortOrder = 'desc',
|
|
390
|
+
autoLoadLastPage = true,
|
|
391
|
+
includeNetwork = true,
|
|
392
|
+
defaultQueryParams = {},
|
|
393
|
+
dataProcessingCallback,
|
|
394
|
+
autoLoad = true,
|
|
395
|
+
updateLocation = true
|
|
396
|
+
} = {},
|
|
397
|
+
dependencies = []) {
|
|
398
|
+
if (!apiEndpoint)
|
|
399
|
+
throw new Error(`Invalid API endpoint: ${apiEndpoint}`)
|
|
400
|
+
const pinRef = useRef(null)
|
|
401
|
+
if (typeof apiEndpoint === 'string') {
|
|
402
|
+
const [path, query] = apiEndpoint.split('?')
|
|
403
|
+
apiEndpoint = {
|
|
404
|
+
path,
|
|
405
|
+
query: parseQuery(query)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (defaultQueryParams.order) {
|
|
409
|
+
defaultSortOrder = defaultQueryParams.order
|
|
410
|
+
}
|
|
411
|
+
const endpoint = includeNetwork ? `${getCurrentStellarNetwork()}/${apiEndpoint.path}` : apiEndpoint.path
|
|
412
|
+
|
|
413
|
+
const [apiResponseData, updateApiResponseData] = useDependantState(() => {
|
|
414
|
+
const res = new PaginatedListViewModel(endpoint, {
|
|
415
|
+
ttl,
|
|
416
|
+
limit,
|
|
417
|
+
query: apiEndpoint.query,
|
|
418
|
+
dataProcessingCallback,
|
|
419
|
+
autoLoadLastPage,
|
|
420
|
+
autoReverseRecordsOrder,
|
|
421
|
+
defaultSortOrder,
|
|
422
|
+
defaultQueryParams,
|
|
423
|
+
updateLocation
|
|
424
|
+
})
|
|
425
|
+
pinRef.current = res
|
|
426
|
+
if (autoLoad) {
|
|
427
|
+
setTimeout(() => {
|
|
428
|
+
res.load()
|
|
429
|
+
}, 100)
|
|
430
|
+
}
|
|
431
|
+
return res.toJSON()
|
|
432
|
+
}, [JSON.stringify(apiEndpoint), limit, ttl, autoReverseRecordsOrder, autoLoadLastPage, autoLoad, ...dependencies])
|
|
433
|
+
|
|
434
|
+
pinRef.current.updateApiResponseData = function (newData) {
|
|
435
|
+
updateApiResponseData(prevListData => isEqual(prevListData, newData) ? prevListData : newData)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return apiResponseData
|
|
439
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {throttle} from 'throttle-debounce'
|
|
2
|
+
|
|
3
|
+
export class ExplorerBatchInfoLoader {
|
|
4
|
+
constructor(fetchCallback, processResponseCallback) {
|
|
5
|
+
this.pendingRequests = {}
|
|
6
|
+
this.requestsQueue = []
|
|
7
|
+
this.fetchDataFromServer = throttle(500, this.fetchDataFromServer, {noLeading: true})
|
|
8
|
+
this.fetchCallback = fetchCallback
|
|
9
|
+
this.processResponseCallback = processResponseCallback
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
pendingRequests
|
|
13
|
+
|
|
14
|
+
requestsQueue = []
|
|
15
|
+
|
|
16
|
+
fetchCallback
|
|
17
|
+
|
|
18
|
+
processResponseCallback
|
|
19
|
+
|
|
20
|
+
loadEntry(key) {
|
|
21
|
+
//check whether the same address is being fetched right now
|
|
22
|
+
const pending = this.pendingRequests[key]
|
|
23
|
+
if (pending) return pending
|
|
24
|
+
const promise = new Promise(resolve => {
|
|
25
|
+
this.requestsQueue.push({
|
|
26
|
+
key,
|
|
27
|
+
finished: data => {
|
|
28
|
+
delete this.pendingRequests[key]
|
|
29
|
+
resolve(data)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
this.fetchDataFromServer()
|
|
33
|
+
})
|
|
34
|
+
this.pendingRequests[key] = promise
|
|
35
|
+
return promise
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fetchDataFromServer() {
|
|
39
|
+
if (!this.requestsQueue.length) return
|
|
40
|
+
const batch = this.requestsQueue.splice(0, 50)
|
|
41
|
+
if (!batch.length) return
|
|
42
|
+
return this.fetchCallback(batch.map(e => e.key))
|
|
43
|
+
.then(data => {
|
|
44
|
+
if (data.error)
|
|
45
|
+
return //TODO: handle errors in a more elegant way, with retries
|
|
46
|
+
//reschedule another batch fetch if the queue is not empty
|
|
47
|
+
if (this.requestsQueue.length) {
|
|
48
|
+
setTimeout(() => this.fetchDataFromServer(), 400)
|
|
49
|
+
}
|
|
50
|
+
//remap batch array to an object for the faster elements access
|
|
51
|
+
const batchMap = {}
|
|
52
|
+
for (const {key, finished} of batch) {
|
|
53
|
+
batchMap[key] = finished
|
|
54
|
+
}
|
|
55
|
+
for (const entry of data._embedded.records) {
|
|
56
|
+
const {key} = this.processResponseCallback(entry)
|
|
57
|
+
const finalizeRequest = batchMap[key]
|
|
58
|
+
if (finalizeRequest) {
|
|
59
|
+
finalizeRequest(entry)
|
|
60
|
+
}
|
|
61
|
+
//cleanup promise reference
|
|
62
|
+
delete batchMap[key]
|
|
63
|
+
delete this.pendingRequests[key]
|
|
64
|
+
}
|
|
65
|
+
//return null for all keys that have not been found
|
|
66
|
+
for (const key of Object.keys(batchMap)) {
|
|
67
|
+
const finalizeRequest = batchMap[key]
|
|
68
|
+
if (finalizeRequest) {
|
|
69
|
+
finalizeRequest(null)
|
|
70
|
+
}
|
|
71
|
+
//cleanup promise reference
|
|
72
|
+
delete this.pendingRequests[key]
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.catch(err => {
|
|
76
|
+
console.error(err)
|
|
77
|
+
//finalize all pending promises with nulls
|
|
78
|
+
for (const {key, finished} of batch) {
|
|
79
|
+
finished(null)
|
|
80
|
+
//cleanup promise reference
|
|
81
|
+
delete this.pendingRequests[key]
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {parseStellarGenericId} from '../stellar/ledger-generic-id'
|
|
2
|
+
import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
|
|
3
|
+
import {fetchExplorerApi} from './explorer-api-call'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load transaction from API
|
|
7
|
+
* @param {String} txHashOrId - Transaction hash or generic id
|
|
8
|
+
* @return {Promise<Object>}
|
|
9
|
+
*/
|
|
10
|
+
export function loadTransaction(txHashOrId) {
|
|
11
|
+
if (/^\d+$/.test(txHashOrId)) { //treat as generic tx id
|
|
12
|
+
const {type} = parseStellarGenericId(txHashOrId)
|
|
13
|
+
if (type !== 'transaction')
|
|
14
|
+
return Promise.reject(new Error('Invalid transaction id: ' + txHashOrId))
|
|
15
|
+
} else if (!/^[a-fA-F0-9]{64}$/.test(txHashOrId))
|
|
16
|
+
return Promise.reject(new Error('Invalid transaction hash or id: ' + txHashOrId))
|
|
17
|
+
|
|
18
|
+
return fetchExplorerApi(getCurrentStellarNetwork() + '/tx/' + txHashOrId)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load transactions included into the ledger
|
|
24
|
+
* @param {Number} sequence - Ledger sequence
|
|
25
|
+
* @return {Promise<Array<Object>>}
|
|
26
|
+
*/
|
|
27
|
+
export function loadLedgerTransactions(sequence) {
|
|
28
|
+
return fetchExplorerApi(getCurrentStellarNetwork() + '/ledger/' + sequence + '/tx')
|
|
29
|
+
}
|
package/asset/amount.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {formatWithPrecision, formatWithAutoPrecision, stripTrailingZeros, fromStroops} from '@stellar-expert/formatter'
|
|
3
|
+
import {AssetDescriptor, isAssetValid, isValidPoolId} from '@stellar-expert/asset-descriptor'
|
|
4
|
+
import {AssetLink} from './asset-link'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Formatted tokens amount
|
|
8
|
+
* @param {String|Number} amount - Amount of tokens
|
|
9
|
+
* @param {String|AssetDescriptor|Asset} asset - Asset name/descriptor
|
|
10
|
+
* @param {String|Number|'auto'} decimals? - Formatted number decimals to show
|
|
11
|
+
* @param {Boolean} adjust? - Denominate raw Int64 stroops to string format
|
|
12
|
+
* @param {Boolean|'floor'} round? - Round or trim the number
|
|
13
|
+
* @param {Boolean} issuer? - Whether to show asset issuer
|
|
14
|
+
* @param {Boolean} icon? - Wheter to show asset icon
|
|
15
|
+
* @constructor
|
|
16
|
+
*/
|
|
17
|
+
export const Amount = React.memo(function Amount({amount, asset, decimals, adjust, round, issuer, icon}) {
|
|
18
|
+
if (amount === undefined || amount === null)
|
|
19
|
+
return null
|
|
20
|
+
if (adjust === true) {
|
|
21
|
+
try {
|
|
22
|
+
amount = fromStroops(amount)
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(e)
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (round) {
|
|
29
|
+
let [int, fract] = (typeof amount === 'number' ? amount.toFixed(7) : amount).split('.')
|
|
30
|
+
if (fract > 0) {
|
|
31
|
+
int = parseFloat(int)
|
|
32
|
+
fract = parseFloat('0.' + fract)
|
|
33
|
+
if (amount < 0) {
|
|
34
|
+
fract *= -1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rounded = round === 'floor' ? Math.floor(fract) : Math.round(fract)
|
|
38
|
+
amount = int + rounded
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
amount = decimals === 'auto' ? formatWithAutoPrecision(amount) : formatWithPrecision(amount, decimals)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(e)
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
return <span className="amount nowrap condensed">
|
|
48
|
+
{amount}
|
|
49
|
+
{!!asset && <>
|
|
50
|
+
{' '}{isAssetValid(asset) || isValidPoolId(asset) ? <AssetLink asset={asset} icon={icon} issuer={issuer}/> : asset.toString()}
|
|
51
|
+
</>}
|
|
52
|
+
</span>
|
|
53
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {AssetDescriptor} from '@stellar-expert/asset-descriptor'
|
|
3
|
+
import {useAssetMeta} from './asset-meta-hooks'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Inline asset icon
|
|
7
|
+
* @param {String|AssetDescriptor|Asset} asset - Asset name/descriptor
|
|
8
|
+
* @param {String} [className] - Optional CSS class name
|
|
9
|
+
* @param {{}} [style] - Optional CSS inline style
|
|
10
|
+
* @param {*} [children] - Optional inner text
|
|
11
|
+
* @constructor
|
|
12
|
+
*/
|
|
13
|
+
export const AssetIcon = React.memo(function AssetIcon({asset, className, style, children}) {
|
|
14
|
+
const meta = useAssetMeta(asset)
|
|
15
|
+
const icon = meta?.toml_info?.image || meta?.toml_info?.orgLogo
|
|
16
|
+
const classes = ['asset-icon']
|
|
17
|
+
if (!style) {
|
|
18
|
+
style = {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (className) {
|
|
22
|
+
classes.push(className)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (asset.toString() === 'XLM') { // native asset
|
|
26
|
+
classes.push('icon icon-stellar')
|
|
27
|
+
} else if (icon) { // asset with an icon
|
|
28
|
+
style.backgroundImage = `url('${icon}')`
|
|
29
|
+
} else { // asset without metadata
|
|
30
|
+
classes.push('icon icon-empty-circle')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <span className={classes.join(' ')} style={style}>{children}</span>
|
|
34
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {AssetDescriptor} from '@stellar-expert/asset-descriptor'
|
|
3
|
+
import {AccountAddress} from '../account/account-address'
|
|
4
|
+
import {useAssetMeta} from './asset-meta-hooks'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inline asset issuer
|
|
8
|
+
* @param {String|AssetDescriptor|Asset} asset - Asset name/descriptor
|
|
9
|
+
* @constructor
|
|
10
|
+
*/
|
|
11
|
+
export const AssetIssuer = React.memo(function AssetIssuer({asset}) {
|
|
12
|
+
let meta = useAssetMeta(asset)
|
|
13
|
+
if (asset === 'XLM' || asset.isNative) {
|
|
14
|
+
meta = {domain: 'stellar.org'}
|
|
15
|
+
}
|
|
16
|
+
asset = AssetDescriptor.parse(asset)
|
|
17
|
+
return <span className="asset-issuer condensed">
|
|
18
|
+
{meta?.domain ?
|
|
19
|
+
<>{meta.domain}</> :
|
|
20
|
+
<><AccountAddress account={asset.issuer} link={false} chars={8} icon={false}/></>}
|
|
21
|
+
</span>
|
|
22
|
+
})
|