@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.
Files changed (124) hide show
  1. package/.npmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -0
  4. package/account/account-address.js +127 -0
  5. package/account/account-address.scss +20 -0
  6. package/account/available-balance.js +21 -0
  7. package/account/identicon.js +91 -0
  8. package/account/identicon.scss +6 -0
  9. package/account/signer-key.js +65 -0
  10. package/api/api-cache.js +5 -0
  11. package/api/explorer-api-call.js +37 -0
  12. package/api/explorer-api-hooks.js +202 -0
  13. package/api/explorer-api-paginated-list-hooks.js +439 -0
  14. package/api/explorer-batch-info-loader.js +85 -0
  15. package/api/explorer-tx-api.js +29 -0
  16. package/asset/amount.js +53 -0
  17. package/asset/asset-icon.js +34 -0
  18. package/asset/asset-issuer.js +22 -0
  19. package/asset/asset-link.js +93 -0
  20. package/asset/asset-link.scss +26 -0
  21. package/asset/asset-list-hooks.js +60 -0
  22. package/asset/asset-meta-hooks.js +78 -0
  23. package/asset/asset-selector.js +72 -0
  24. package/asset/asset-selector.scss +41 -0
  25. package/basic-styles/base.scss +53 -0
  26. package/basic-styles/form.scss +90 -0
  27. package/basic-styles/grid.scss +388 -0
  28. package/basic-styles/link.scss +17 -0
  29. package/basic-styles/lists.scss +68 -0
  30. package/basic-styles/loader.scss +22 -0
  31. package/basic-styles/responsive.scss +29 -0
  32. package/basic-styles/scrollbars.scss +19 -0
  33. package/basic-styles/spacing.scss +50 -0
  34. package/basic-styles/table.scss +104 -0
  35. package/basic-styles/themes.scss +59 -0
  36. package/basic-styles/typography.scss +191 -0
  37. package/basic-styles/variables.scss +26 -0
  38. package/claimable-balance/claimable-balance-claimants.js +12 -0
  39. package/contract/sc-val.js +70 -0
  40. package/contract/wasm-section-parser.js +159 -0
  41. package/contract/wasm-section-reader.js +79 -0
  42. package/contract/xdr-reader.js +148 -0
  43. package/controls/active-icon.scss +23 -0
  44. package/controls/button-group.js +20 -0
  45. package/controls/button-group.scss +48 -0
  46. package/controls/button.js +74 -0
  47. package/controls/button.scss +148 -0
  48. package/controls/code-block.js +35 -0
  49. package/controls/code-block.scss +74 -0
  50. package/controls/dropdown.js +281 -0
  51. package/controls/dropdown.scss +161 -0
  52. package/controls/external-link.js +5 -0
  53. package/controls/info-tooltip.js +17 -0
  54. package/controls/info-tooltip.scss +4 -0
  55. package/controls/slider.js +20 -0
  56. package/controls/slider.scss +22 -0
  57. package/controls/tabs.js +94 -0
  58. package/controls/tabs.scss +73 -0
  59. package/controls/tooltip.js +214 -0
  60. package/controls/tooltip.scss +97 -0
  61. package/controls/update-highlighter.js +28 -0
  62. package/controls/update-highlighter.scss +10 -0
  63. package/date/date-selector.js +44 -0
  64. package/date/elapsed-time.js +22 -0
  65. package/date/utc-timestamp.js +23 -0
  66. package/dex/price-dynamic.js +45 -0
  67. package/dex/price-dynamic.scss +36 -0
  68. package/directory/directory-hooks.js +89 -0
  69. package/effect/effect-description.js +289 -0
  70. package/effect/op-effects-view.js +11 -0
  71. package/effect/op-effects.scss +8 -0
  72. package/errors/error-boundary.js +98 -0
  73. package/fonts/font.scss +23 -0
  74. package/fonts/icons/config.json +1858 -0
  75. package/fonts/icons/icons-embedded.scss +644 -0
  76. package/horizon/horizon-account-helpers.js +104 -0
  77. package/horizon/horizon-client-helpers.js +52 -0
  78. package/horizon/horizon-ledger-helpers.js +36 -0
  79. package/horizon/horizon-orderbook-helpers.js +14 -0
  80. package/horizon/horizon-trades-helper.js +89 -0
  81. package/horizon/horizon-transaction-helpers.js +36 -0
  82. package/index.js +85 -0
  83. package/index.scss +16 -0
  84. package/interaction/autofocus.js +9 -0
  85. package/interaction/block-select.js +54 -0
  86. package/interaction/block-select.scss +24 -0
  87. package/interaction/copy-to-clipboard.js +19 -0
  88. package/interaction/inline-progress.js +16 -0
  89. package/interaction/qr-code.js +45 -0
  90. package/interaction/responsive.js +21 -0
  91. package/interaction/spoiler.js +40 -0
  92. package/interaction/spoiler.scss +9 -0
  93. package/interaction/theme-selector.js +11 -0
  94. package/ledger/ledger-entry-href-formatter.js +22 -0
  95. package/ledger/ledger-entry-link.js +59 -0
  96. package/meta/page-meta-tags.js +229 -0
  97. package/module/dynamic-module.js +48 -0
  98. package/package.json +41 -0
  99. package/state/on-screen-hooks.js +23 -0
  100. package/state/page-visibility-helpers.js +17 -0
  101. package/state/page-visibility-hooks.js +12 -0
  102. package/state/screen-orientation-hooks.js +16 -0
  103. package/state/state-hooks.js +77 -0
  104. package/state/stellar-network-hooks.js +45 -0
  105. package/state/theme.js +29 -0
  106. package/stellar/key-type.js +92 -0
  107. package/stellar/ledger-generic-id.js +40 -0
  108. package/stellar/signature-hint-utils.js +65 -0
  109. package/toast/toast-notification-instance.js +43 -0
  110. package/toast/toast-notification.js +46 -0
  111. package/toast/toast-notifications-block.js +55 -0
  112. package/toast/toast-notifications.scss +166 -0
  113. package/tx/op-accounting-changes.js +27 -0
  114. package/tx/op-description-view.js +925 -0
  115. package/tx/op-description.scss +98 -0
  116. package/tx/op-icon.js +93 -0
  117. package/tx/parser/op-balance-changes.js +62 -0
  118. package/tx/parser/op-descriptor.js +49 -0
  119. package/tx/parser/tx-details-parser.js +75 -0
  120. package/tx/parser/tx-matcher.js +372 -0
  121. package/tx/parser/type-filter-matcher.js +127 -0
  122. package/tx/tx-fee-effect.js +14 -0
  123. package/tx/tx-list-hooks.js +19 -0
  124. 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
+ }
@@ -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
+ })