@stellar-expert/ui-framework 1.12.9 → 1.13.1

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.
@@ -0,0 +1,33 @@
1
+ import {useEffect, useState} from 'react'
2
+ import {useExplorerApi} from '../api/explorer-api-hooks'
3
+ import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
4
+
5
+ export function useContractInfo(address) {
6
+ return useExplorerApi('contract/' + address, {
7
+ processResult(data) {
8
+ if (data?.error && (data.status || data.error.status) === 404) {
9
+ data.nonExistentContract = true
10
+ }
11
+ data.address = address
12
+ return data
13
+ }
14
+ })
15
+ }
16
+
17
+ export function generateContractSourceLink(hash) {
18
+ return `${explorerApiOrigin}/explorer/${getCurrentStellarNetwork()}/contract/wasm/${hash}`
19
+ }
20
+
21
+ export function useContractSource(hash) {
22
+ const [source, setSource] = useState()
23
+ useEffect(() => {
24
+ if (!hash) {
25
+ setSource(null)
26
+ return
27
+ }
28
+ fetch(generateContractSourceLink(hash))
29
+ .then(res => res.ok && res.arrayBuffer())
30
+ .then(src => setSource(src))
31
+ }, [hash])
32
+ return source
33
+ }
@@ -0,0 +1,74 @@
1
+ import React from 'react'
2
+ import {parseContractMetadata} from '@stellar-expert/contract-wasm-interface-parser'
3
+ import {shortenString} from '@stellar-expert/formatter'
4
+ import {AccountAddress} from '../account/account-address'
5
+ import {AssetLink} from '../asset/asset-link'
6
+ import {Tooltip} from '../controls/tooltip'
7
+ import {parseScValValue, primitiveTypes, ScVal, ScValStruct} from './sc-val'
8
+ import {useContractSource} from './contract-api'
9
+ import {sacInterface} from './sac-interface'
10
+
11
+ export default function InvocationInfoView({contract, func, args, result, sac}) {
12
+ if (typeof args === 'string') {
13
+ args = parseScValValue(args)
14
+ if (args._arm !== 'vec') {
15
+ args = [args]
16
+ } else {
17
+ args = args._value
18
+ }
19
+ }
20
+ return <>
21
+ {!!sac && <><AssetLink asset={sac}/> </>}
22
+ <code>{func}({args.map((arg, i) => <ScValStruct key={i} separate={args.length - i}>
23
+ <ScVal value={arg} nested/>
24
+ </ScValStruct>)})<InvocationResult result={result}/></code>
25
+ <Tooltip trigger={<i className="trigger icon-info"/>} desiredPlace="top" activation="click" maxWidth="90vw">
26
+ <ExtendedInvocationInfoView {...{contract, func, args, result, sac}}/>
27
+ </Tooltip>
28
+ </>
29
+ }
30
+
31
+ const InvocationResult = React.memo(function ({result, indent}) {
32
+ if (result === undefined)
33
+ return null
34
+ return <> →&nbsp;{!result ? 'void' : <ScVal value={result} nested indent={indent}/>}</>
35
+ })
36
+
37
+ const ExtendedInvocationInfoView = React.memo(function ({contract, func, args, result, sac}) {
38
+ const source = useContractSource(sac ? undefined : contract)
39
+ if (!source && !sac)
40
+ return <div className="segment blank text-center">
41
+ <div className="loader large"/>
42
+ <div className="text-small">Loading contract...</div>
43
+ </div>
44
+ const meta = sac ? sacInterface : parseContractMetadata(Buffer.from(source))
45
+ const fd = meta.functions[func]
46
+ if (!fd)
47
+ return <div className="error"><i className="icon-warning-circle"/> Failed to parse contract code</div>
48
+ const shouldIndent = args.length > 2 || Object.values(fd.inputs).some(i => !primitiveTypes.has(i.type)) // multi-line display for large argument number or complex types
49
+ const contractInfo = sac ?
50
+ <> from asset <AssetLink asset={sac}/></> :
51
+ <span className="text-tiny dimmed">
52
+ &emsp;SDK v{meta.sdkVersion.split('#')[0]}&emsp;RUST v{shortenString(meta.rustVersion, 12)}
53
+ </span>
54
+ const contractArgNames = Object.keys(fd.inputs)
55
+ //TODO: show contract validation info
56
+ return <div>
57
+ <div>
58
+ Contract <AccountAddress account={contract}/>{contractInfo}
59
+ </div>
60
+ <div className="space">
61
+ <code className="block" style={{padding: '0.4em 0.8em'}}>{func}({args.map((arg, i) =>
62
+ <ScValStruct key={i} separate={args.length - i}>
63
+ {shouldIndent ? <><br/>&emsp;</> : null}
64
+ <span className="dimmed">{contractArgNames[i]}: </span>
65
+ <ScVal value={arg} nested indent={shouldIndent}/>
66
+ </ScValStruct>)})<InvocationResult indent result={result}/></code>
67
+ </div>
68
+ <div className="text-small space">
69
+ {fd.doc ?
70
+ <>{fd.doc.split('\n').filter(v => !!v).map((v, i) => <div key={i}>{v}</div>)}</> :
71
+ <span className="dimmed">Function documentation is not included into the WASM</span>}
72
+ </div>
73
+ </div>
74
+ })
@@ -0,0 +1,131 @@
1
+ export const sacInterface = {
2
+ functions: {
3
+ allowance: {
4
+ inputs: {
5
+ from: {type: 'address'},
6
+ spender: {type: 'address'}
7
+ },
8
+ outputs: ['i128'],
9
+ doc: `Returns the allowance for "spender" to transfer from "from".
10
+ # Arguments
11
+ * "from" - The address holding the balance of tokens to be drawn from.
12
+ * "spender" - The address spending the tokens held by "from".`
13
+ },
14
+ approve: {
15
+ inputs: {
16
+ from: {type: 'address'},
17
+ spender: {type: 'address'},
18
+ amount: {type: 'i128'},
19
+ expiration_ledger: {type: 'u32'}
20
+ },
21
+ doc: `Set the allowance by "amount" for "spender" to transfer/burn from "from".
22
+ # Arguments
23
+ * "from" - The address holding the balance of tokens to be drawn from.
24
+ * "spender" - The address being authorized to spend the tokens held by "from".
25
+ * "amount" - The tokens to be made available to "spender".
26
+ * "expiration_ledger" - The ledger number where this allowance expires. Cannot be less than the current ledger number unless the amount is being set to 0. An expired entry (where expiration_ledger < the current ledger number) should be treated as a 0 amount allowance.
27
+ # Events
28
+ Emits an event with topics "["approve", from: Address, spender: Address], data = [amount: i128, expiration_ledger: u32]"`
29
+ },
30
+ balance: {
31
+ inputs: {
32
+ id: {type: 'address'}
33
+ },
34
+ outputs: ['i128'],
35
+ doc: `Returns the balance of "id".
36
+ # Arguments
37
+ * "id" - The address for which a balance is being queried. If the address has no existing balance, returns 0.`
38
+ },
39
+ transfer: {
40
+ inputs: {
41
+ from: {type: 'address'},
42
+ to: {type: 'address'},
43
+ amount: {type: 'i128'}
44
+ },
45
+ doc: `Transfer "amount" from "from" to "to".
46
+ # Arguments
47
+ * "from" - The address holding the balance of tokens which will be withdrawn from.
48
+ * "to" - The address which will receive the transferred tokens.
49
+ * "amount" - The amount of tokens to be transferred.
50
+ # Events
51
+ Emits an event with topics "["transfer", from: Address, to: Address], data = [amount: i128]"`
52
+ },
53
+ transfer_from: {
54
+ inputs: {
55
+ spender: {type: 'address'},
56
+ from: {type: 'address'},
57
+ to: {type: 'address'},
58
+ amount: {type: 'i128'}
59
+ },
60
+ doc: `Transfer "amount" from "from" to "to", consuming the allowance of "spender". Authorized by spender ("spender.require_auth()").
61
+ # Arguments
62
+ * "spender" - The address authorizing the transfer, and having its allowance consumed during the transfer.
63
+ * "from" - The address holding the balance of tokens which will be withdrawn from.
64
+ * "to" - The address which will receive the transferred tokens.
65
+ * "amount" - The amount of tokens to be transferred.
66
+ # Events
67
+ Emits an event with topics "["transfer", from: Address, to: Address], data = [amount: i128]"`
68
+ },
69
+ mint: {
70
+ inputs: {
71
+ spender: {type: 'address'},
72
+ to: {type: 'address'},
73
+ amount: {type: 'i128'}
74
+ },
75
+ doc: `Mint "amount" of tokens to "to".
76
+ # Arguments
77
+ * "spender" - The address authorizing the mint.
78
+ * "to" - The address which will receive the minted tokens.
79
+ * "amount" - The amount of tokens to be minted.
80
+ # Events
81
+ Emits an event with topics "["mint", spender: Address, from: Address], data = [amount: i128]"`
82
+ },
83
+ burn: {
84
+ inputs: {
85
+ from: {type: 'address'},
86
+ amount: {type: 'i128'}
87
+ },
88
+ doc: `Burn "amount" from "from".
89
+ # Arguments
90
+ * "from" - The address holding the balance of tokens which will be burned from.
91
+ * "amount" - The amount of tokens to be burned.
92
+ # Events
93
+ Emits an event with topics "["burn", from: Address], data = [amount: i128]"`
94
+ },
95
+ burn_from: {
96
+ inputs: {
97
+ spender: {type: 'address'},
98
+ from: {type: 'address'},
99
+ amount: {type: 'i128'}
100
+ },
101
+ doc: `Burn "amount" from "from", consuming the allowance of "spender".
102
+ # Arguments
103
+ * "spender" - The address authorizing the burn, and having its allowance consumed during the burn.
104
+ * "from" - The address holding the balance of tokens which will be burned from.
105
+ * "amount" - The amount of tokens to be burned.
106
+ # Events
107
+ Emits an event with topics "["burn", from: Address], data = [amount: i128]"`
108
+ },
109
+ decimals: {
110
+ inputs: {},
111
+ outputs: ['u32'],
112
+ doc: `Returns the number of decimals used to represent amounts of this token.
113
+ # Panics
114
+ If the contract has not yet been initialized.`
115
+ },
116
+ name: {
117
+ inputs: {},
118
+ outputs: ['String'],
119
+ doc: `Returns the name for this token.
120
+ # Panics
121
+ If the contract has not yet been initialized.`
122
+ },
123
+ symbol: {
124
+ inputs: {},
125
+ outputs: ['String'],
126
+ doc: `Returns the symbol for this token.
127
+ # Panics
128
+ If the contract has not yet been initialized.`
129
+ }
130
+ }
131
+ }
@@ -6,7 +6,7 @@ import {shortenString} from '@stellar-expert/formatter'
6
6
  import {AccountAddress} from '../account/account-address'
7
7
  import './sc-val.scss'
8
8
 
9
- export const ScVal = React.memo(function ScVal({value, nested = false, indent = false}) {
9
+ export const ScVal = React.memo(function ScVal({value, nested = false, indent = false, wrapObjects = true}) {
10
10
  if (!nested)
11
11
  return <code className={cn('sc-val', {block: indent})}><ScVal value={value} indent={indent} nested/></code>
12
12
  if (!value)
@@ -14,10 +14,12 @@ export const ScVal = React.memo(function ScVal({value, nested = false, indent =
14
14
  if (typeof value === 'string') {
15
15
  value = xdr.ScVal.fromXDR(value, 'base64')
16
16
  }
17
- if (value instanceof Array)
18
- return <>[{value.map((v, i) => <ScValStruct key={i} indent={indent} separate={value.length - i}>
17
+ if (value instanceof Array) {
18
+ const values = value.map((v, i) => <ScValStruct key={i} indent={indent} separate={value.length - i}>
19
19
  <ScVal value={v} indent={indent} nested/>
20
- </ScValStruct>)}]</>
20
+ </ScValStruct>)
21
+ return wrapObjects ? <>[{values}]</> : <>{values}</>
22
+ }
21
23
  const val = value._value
22
24
  switch (value._arm) {
23
25
  case 'vec':
@@ -25,11 +27,11 @@ export const ScVal = React.memo(function ScVal({value, nested = false, indent =
25
27
  <ScVal value={v} indent={indent} nested/>
26
28
  </ScValStruct>)}]</>
27
29
  case 'map':
28
- return <>&#123;{val.map((kv, i) =>
30
+ const values = val.map((kv, i) =>
29
31
  <ScValStruct key={i} indent={indent} separate={val.length - i}>
30
32
  <ScVal value={kv.key()} indent={indent} nested/>: <ScVal value={kv.val()} indent={indent} nested/>
31
- </ScValStruct>)}
32
- &#125;</>
33
+ </ScValStruct>)
34
+ return wrapObjects ? <>&#123;{values}&#125;</> : <>{values}</>
33
35
  case 'b':
34
36
  return <>{val.toString()}<ScValType type="bool"/></>
35
37
  case 'i32':
@@ -57,7 +59,7 @@ export const ScVal = React.memo(function ScVal({value, nested = false, indent =
57
59
  return <><span className="condensed">{shortenString(asBytes, 86)}</span><ScValType type="bytes"/></>
58
60
  case 'str':
59
61
  case 'sym':
60
- return <>"{val.toString()}"<ScValType type={value._arm}/></>
62
+ return <span className="word-break">"{val.toString()}"<ScValType type={value._arm}/></span>
61
63
  case 'nonceKey':
62
64
  return <>{val.nonce()._value.toString()}<ScValType type="nonce"/></>
63
65
  case 'instance':
@@ -74,15 +76,21 @@ export const ScVal = React.memo(function ScVal({value, nested = false, indent =
74
76
  }
75
77
  })
76
78
 
79
+ export function parseScValValue(value) {
80
+ return xdr.ScVal.fromXDR(value, 'base64')
81
+ }
82
+
77
83
  const ScValType = React.memo(function ScValType({type}) {
78
84
  return <sub className="dimmed text-tiny" style={{padding: '0 0.2em'}}>{type}</sub>
79
85
  })
80
86
 
81
- const ScValStruct = React.memo(function ScValStruct({indent, children, separate}) {
87
+ export const ScValStruct = React.memo(function ScValStruct({indent, children, separate}) {
82
88
  const separator = separate > 1 ? <>, </> : null
83
89
  if (!indent)
84
90
  return <>{children}{separator}</>
85
91
  return <div className="block-indent">
86
92
  {children}{separator}
87
93
  </div>
88
- })
94
+ })
95
+
96
+ export const primitiveTypes = new Set(['b', 'i32', 'u32', 'i256', 'u256', 'i128', 'u128', 'i64', 'u64', 'timepoint', 'duration', 'address', 'bytes', 'str', 'sym', 'nonceKey', 'contractId', 'instance'])
@@ -1,4 +1,4 @@
1
- import React, {useState, useRef} from 'react'
1
+ import React, {useState, useRef, useCallback, useMemo} from 'react'
2
2
  import cn from 'classnames'
3
3
  import './tooltip.scss'
4
4
 
@@ -152,47 +152,69 @@ function parseOffset(offset) {
152
152
 
153
153
  /**
154
154
  * Tooltip component
155
- * @param {any} trigger
155
+ * @param {HTMLElement} trigger
156
156
  * @param {PositionDescriptor} desiredPlace
157
- * @param {PositionOffset} offset?
158
- * @param {String} maxWidth?
159
- * @param {any} children?
157
+ * @param {PositionOffset} [offset]
158
+ * @param {'hover'|'click'} [activation]
159
+ * @param {String} [maxWidth]
160
+ * @param {*} [children]
160
161
  * @constructor
161
162
  */
162
- export const Tooltip = React.memo(function Tooltip({trigger, desiredPlace = 'top', offset = {}, children, maxWidth = '20em', ...op}) {
163
- const [visible, setVisible] = useState(false),
164
- [place, setPlace] = useState('top'),
165
- [position, setPosition] = useState({top: 0, left: 0}),
166
- content = useRef(null)
167
-
168
- function mouseEnter(e) {
163
+ export const Tooltip = React.memo(function Tooltip({
164
+ trigger,
165
+ desiredPlace = 'top',
166
+ offset = {},
167
+ activation = 'hover',
168
+ children,
169
+ maxWidth = '20em',
170
+ ...op
171
+ }) {
172
+ const [visible, setVisible] = useState(false)
173
+ const [rendered, setRendered] = useState(false)
174
+ const [place, setPlace] = useState('top')
175
+ const [position, setPosition] = useState({top: 0, left: 0})
176
+ const content = useRef(null)
177
+
178
+ function activate(e) {
169
179
  if (visible)
170
180
  return
171
- const {place, position} = calculateTooltipPosition(e.currentTarget, content.current, desiredPlace, offset)
172
- setVisible(true)
173
- setPosition(position)
174
- setPlace(place)
181
+ const {currentTarget} = e
182
+ setRendered(true)
183
+ setTimeout(() => {
184
+ if (visible)
185
+ return
186
+ const {place, position} = calculateTooltipPosition(currentTarget, content.current, desiredPlace, offset)
187
+ setVisible(true)
188
+ setPosition(position)
189
+ setPlace(place)
190
+ }, 250)
175
191
  }
176
192
 
177
- function mouseLeave(e) {
193
+ function onMouseLeave(e) {
178
194
  if (!visible)
179
195
  return
180
196
  setVisible(false)
197
+ setRendered(false)
181
198
  }
182
199
 
183
200
  const triggerProps = {
184
- onMouseEnter: e => mouseEnter(e),
185
- onMouseLeave: e => mouseLeave(e),
201
+ onMouseLeave,
186
202
  ...op
187
203
  }
204
+ if (activation === 'hover') {
205
+ triggerProps.onMouseEnter = activate
206
+ } else {
207
+ triggerProps.onClick = activate
208
+ }
188
209
  const contentStyle = {
189
210
  maxWidth,
190
211
  left: position.left + 'px',
191
212
  top: position.top + 'px'
192
213
  }
214
+
193
215
  return React.cloneElement(trigger, triggerProps, <div className="tooltip-wrapper" style={contentStyle}>
194
- <div ref={content} className={cn('tooltip', place)}>
195
- <div className="tooltip-content">{children}</div>
216
+ <div ref={content} className={cn('tooltip', place, {visible})}>
217
+ <div className="tooltip-content">{rendered ? children : null}</div>
196
218
  </div>
197
219
  </div>)
198
220
  })
@@ -21,7 +21,7 @@ $tooltip-notch-shadow-color: var(--color-border-shadow);
21
21
  background: var(--color-bg);
22
22
  border-color: var(--color-contrast-border);
23
23
  border-radius: $border-radius-input;
24
- box-shadow: 0 2px 4px -1px var(--color-backdrop);
24
+ box-shadow: 0 1px 6px 2px var(--color-backdrop);
25
25
  padding: 1rem 1.4rem;
26
26
  letter-spacing: normal;
27
27
  word-break: normal;
@@ -69,6 +69,10 @@ $tooltip-notch-shadow-color: var(--color-border-shadow);
69
69
  z-index: 5;
70
70
  position: relative;
71
71
  }
72
+
73
+ &.visible {
74
+ visibility: visible;
75
+ }
72
76
  }
73
77
 
74
78
  .trigger {
@@ -78,19 +82,28 @@ $tooltip-notch-shadow-color: var(--color-border-shadow);
78
82
  top: -.3rem;
79
83
  left: .2rem;
80
84
  color: var(--color-primary);
81
- transition: color 0.15s ease 0.3s;
82
85
 
83
86
  &:before {
84
87
  opacity: 0.4;
85
88
  }
86
89
 
90
+ &:after {
91
+ content: '';
92
+ position: absolute;
93
+ opacity: 0.01;
94
+ display: block;
95
+ width: 200%;
96
+ height: 200%;
97
+ left: -50%;
98
+ top: -50%;
99
+ }
100
+
87
101
  &:hover {
88
102
  &:before {
89
103
  opacity: 1;
90
104
  }
91
105
 
92
106
  & > .tooltip-wrapper {
93
- visibility: visible;
94
107
  opacity: 1;
95
108
  }
96
109
  }
@@ -2,19 +2,12 @@ import React, {useCallback, useEffect, useState} from 'react'
2
2
  import {debounce} from 'throttle-debounce'
3
3
  import {normalizeDate, toUnixTimestamp} from '@stellar-expert/formatter'
4
4
 
5
- export function trimIsoDateSeconds(date) {
6
- if (typeof date === 'number') {
7
- date = new Date(date)
8
- }
9
- return date.toISOString().replace(/:\d{2}\.\d*Z/, '')
10
- }
11
-
12
- const minSelectableValue = trimIsoDateSeconds(new Date('2015-09-30T16:46:00Z'))
13
-
14
5
  /**
15
- * @param {String} value
16
- * @param {Function} onChange
17
- * @constructor
6
+ * @param {String} value - Current input value
7
+ * @param {DateSelector~OnChange} onChange - Onchange event handler
8
+ * @param {String} [min] - Minimum allowed date
9
+ * @param {String} [max] - Maximum allowed date
10
+ * @param {*} [ref]
18
11
  */
19
12
  export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
20
13
  const [date, setDate] = useState(value ? trimIsoDateSeconds(normalizeDate(value)) : '')
@@ -30,7 +23,7 @@ export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
30
23
  if (value !== newDate) {
31
24
  setDate(newDate)
32
25
  if (onChange) {
33
- onChange(toUnixTimestamp(normalizeDate(newDate)))
26
+ onChange(newDate ? toUnixTimestamp(normalizeDate(newDate)) : null)
34
27
  }
35
28
  }
36
29
  }), [onChange, value])
@@ -41,4 +34,22 @@ export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
41
34
  return <input type="datetime-local" value={date} className="date-selector condensed" step={60} ref={ref}
42
35
  min={min || minSelectableValue} max={max} onChange={e => selectDate(e.target.value)}
43
36
  style={{width: '11em', overflow: 'hidden'}} {...otherProps}/>
44
- }
37
+ }
38
+
39
+ /**
40
+ * @param {Date|Number} date
41
+ * @return {String}
42
+ */
43
+ export function trimIsoDateSeconds(date) {
44
+ if (typeof date === 'number') {
45
+ date = new Date(date)
46
+ }
47
+ return date.toISOString().replace(/:\d{2}\.\d*Z/, '')
48
+ }
49
+
50
+ const minSelectableValue = trimIsoDateSeconds(new Date('2015-09-30T16:46:00Z'))
51
+
52
+ /**
53
+ * @callback DateSelector~OnChange
54
+ * @param {number|null} value - Selected value represented as a Unix timestamp or null for invalid date value
55
+ */
@@ -5,12 +5,18 @@ import {normalizeDate, formatDateUTC} from '@stellar-expert/formatter'
5
5
  import {BlockSelect} from '../interaction/block-select'
6
6
 
7
7
  export const UtcTimestamp = React.memo(function UtcTimestamp({date, dateOnly, className}) {
8
- date = normalizeDate(date)
9
- let formatted = formatDateUTC(date)
10
- if (dateOnly) {
11
- formatted = formatted.split(' ')[0]
12
- } else {
13
- formatted += ' UTC'
8
+ let formatted
9
+ try {
10
+ date = normalizeDate(date)
11
+ formatted = formatDateUTC(date)
12
+ if (dateOnly) {
13
+ formatted = formatted.split(' ')[0]
14
+ } else {
15
+ formatted += ' UTC'
16
+ }
17
+ } catch (e) {
18
+ console.error(e)
19
+ return null
14
20
  }
15
21
  const localTime = date.toString().replace(/ \(.+\)/, '').replace(/\w+ /, '')
16
22
  return <BlockSelect className={cn('condensed nowrap', className)} title={localTime}>{formatted}</BlockSelect>
@@ -10,13 +10,14 @@ import {AssetLink} from '../asset/asset-link'
10
10
  import {Amount} from '../asset/amount'
11
11
  import {CopyToClipboard} from '../interaction/copy-to-clipboard'
12
12
  import {ScVal} from '../contract/sc-val'
13
+ import InvocationInfoView from '../contract/invocation-info-view'
13
14
 
14
15
  /**
15
16
  * @param {{}} effect
16
17
  * @return {JSX.Element}
17
18
  * @constructor
18
19
  */
19
- export function EffectDescription({effect}) {
20
+ export function EffectDescription({effect, operation}) {
20
21
  switch (effect.type) {
21
22
  case 'accountCreated':
22
23
  return <>Account <AccountAddress account={effect.account}/> created</>
@@ -216,18 +217,20 @@ export function EffectDescription({effect}) {
216
217
  case 'contractInvoked':
217
218
  return <>{effect.depth > 0 &&
218
219
  <i className="icon-level-down text-tiny color-primary" style={{paddingLeft: (effect.depth - 1) + 'em'}}/>}
219
- Contract <AccountAddress account={effect.contract}/>{' '} function <code>{effect.function}</code> invoked
220
- {!!effect.rawArgs && <> with arguments <ScVal value={effect.rawArgs}/></>}</>
220
+ Contract <AccountAddress account={effect.contract}/> invoked{' '}
221
+ <InvocationInfoView func={effect.function} args={effect.rawArgs} contract={effect.contract} result={effect.result} sac={operation.operation.sacMap?.[effect.contract]}/>
222
+ </>
221
223
  case 'contractEvent':
222
224
  return <>Contract <AccountAddress account={effect.contract}/> raised event <ScVal value={effect.rawTopics}/>{' '}
223
225
  with data <ScVal value={effect.rawData}/></>
224
226
  case 'contractDataCreated':
225
227
  case 'contractDataUpdated':
226
- return <>Contract <AccountAddress account={effect.owner}/> {effect.type === 'contractDataCreated' ? 'created ' : 'updated '}
227
- data <ScVal value={effect.key}/> with value <ScVal value={effect.value}/>
228
+ return <>Contract <AccountAddress account={effect.owner}/>
229
+ {effect.type === 'contractDataCreated' ? ' created ' : ' updated '} {effect.durability} data{' '}
230
+ <ScVal value={effect.key}/> with value <ScVal value={effect.value}/>
228
231
  </>
229
232
  case 'contractDataRemoved':
230
- return <>Contract <AccountAddress account={effect.owner}/> removed data <ScVal value={effect.key}/></>
233
+ return <>Contract <AccountAddress account={effect.owner}/> removed {effect.durability} data <ScVal value={effect.key}/></>
231
234
  case 'contractError':
232
235
  return <>Execution error {effect.code ? effect.code + ': ' : ''}"{effect.details[0]}"{' '}
233
236
  <code>{JSON.stringify(effect.details.slice(1))}</code> in <AccountAddress account={effect.contract}/></>
@@ -3,7 +3,8 @@ import {EffectDescription} from './effect-description'
3
3
  import SorobanTxMetricsView from './soroban-tx-metrics-view'
4
4
  import './op-effects.scss'
5
5
 
6
- export function OpEffectsView({effects}) {
6
+ export function OpEffectsView({operation}) {
7
+ const effects = getEffects(operation)
7
8
  if (!effects.length)
8
9
  return <div className="op-effects">
9
10
  <div className="dimmed">(no effects)</div>
@@ -13,9 +14,35 @@ export function OpEffectsView({effects}) {
13
14
  if (e.type === 'contractMetrics')
14
15
  return null
15
16
  return <div key={i}>
16
- <i className={e.type === 'contractError' ? 'icon-warning' : 'icon-puzzle'}/> <EffectDescription effect={e}/>
17
+ <i className={e.type === 'contractError' ? 'icon-warning' : 'icon-puzzle'}/>
18
+ <EffectDescription effect={e} operation={operation}/>
17
19
  </div>
18
20
  })}
19
21
  <SorobanTxMetricsView metrics={effects.find(e => e.type === 'contractMetrics')}/>
20
22
  </div>
23
+ }
24
+
25
+ function getEffects(op) {
26
+ let {effects} = op.operation
27
+ if (op.operation.type === 'invokeHostFunction')
28
+ return sortContractEffects([...effects])
29
+ return effects
30
+ }
31
+
32
+ function sortContractEffects(effects) {
33
+ effects.sort((a, b) => {
34
+ const aInvocation = a.type === 'contractInvoked'
35
+ const bInvocation = b.type === 'contractInvoked'
36
+ if (aInvocation && bInvocation)
37
+ return 0
38
+ if (!aInvocation && !bInvocation){
39
+ const aData = a.type.startsWith('contractData')
40
+ const bData = b.type.startsWith('contractData')
41
+ if (aData === bData)
42
+ return 0
43
+ return aData ? 1 : -1
44
+ }
45
+ return aInvocation ? -1 : 1
46
+ })
47
+ return effects
21
48
  }
package/index.js CHANGED
@@ -81,6 +81,8 @@ export * from './tx/tx-operations-list'
81
81
  export * from './tx/parser/tx-details-parser'
82
82
  export * from './tx/tx-list-hooks'
83
83
  export * from './effect/effect-description'
84
+ //contract-related components
85
+ export * from './contract/contract-api'
84
86
  //Stellar-specific utils
85
87
  export * from './stellar/key-type'
86
88
  export * from './stellar/signature-hint-utils'
@@ -1,7 +1,7 @@
1
1
  import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
2
2
 
3
3
  /**
4
- * @param {'account'|'asset'|'ledger'|'tx'|'op'|'offer'} type
4
+ * @param {'account'|'asset'|'ledger'|'tx'|'op'|'offer'|'contract'} type
5
5
  * @param {String|Number} id
6
6
  * @param {String} [network]
7
7
  * @return {String}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stellar-expert/ui-framework",
3
- "version": "1.12.9",
3
+ "version": "1.13.1",
4
4
  "description": "StellarExpert shared UI components library",
5
5
  "main": "index.js",
6
6
  "module": "./index.js",
@@ -14,14 +14,15 @@
14
14
  "author": "orbitlens <orbit@stellar.expert>",
15
15
  "license": "MIT",
16
16
  "peerDependencies": {
17
- "@stellar/stellar-base": "^12.0.0",
18
- "@stellar/stellar-sdk": "^12.0.1",
19
- "@stellar-expert/asset-descriptor": "^1.3.0",
17
+ "@stellar/stellar-base": "^12.1.0",
18
+ "@stellar/stellar-sdk": "^12.2.0",
19
+ "@stellar-expert/asset-descriptor": "^1.3.3",
20
20
  "@stellar-expert/claimable-balance-utils": "^1.4.1",
21
21
  "@stellar-expert/client-cache": "github:stellar-expert/client-cache",
22
+ "@stellar-expert/contract-wasm-interface-parser": "^3.1.0",
22
23
  "@stellar-expert/formatter": "^2.3.0",
23
24
  "@stellar-expert/navigation": "github:stellar-expert/navigation#v1.0.2",
24
- "@stellar-expert/tx-meta-effects-parser": "^5.5.0",
25
+ "@stellar-expert/tx-meta-effects-parser": "5.6.5",
25
26
  "classnames": ">=2",
26
27
  "prop-types": ">=15",
27
28
  "qrcode.react": "^3.1.0",
@@ -15,6 +15,7 @@ import {ClaimableBalanceClaimants} from '../claimable-balance/claimable-balance-
15
15
  import {CopyToClipboard} from '../interaction/copy-to-clipboard'
16
16
  import {useStellarNetwork} from '../state/stellar-network-hooks'
17
17
  import {ScVal} from '../contract/sc-val'
18
+ import InvocationInfoView from '../contract/invocation-info-view'
18
19
 
19
20
  function formatBalanceId(balance) {
20
21
  return `${balance.substr(8, 4)}…${balance.substr(-4)}`
@@ -46,7 +47,7 @@ function OpSourceAccount({op, compact}) {
46
47
 
47
48
  if (!op.isEphemeral)
48
49
  return accountItem
49
- return <> on behalf of account {accountItem}</>
50
+ return <> for account {accountItem}</>
50
51
  }
51
52
 
52
53
 
@@ -453,13 +454,13 @@ function CreateClaimableBalanceDescriptionView({op, compact}) {
453
454
  return <>
454
455
  <b>Create claimable balance</b> <Amount amount={amount} asset={AssetDescriptor.parse(asset)} issuer={!compact}/>
455
456
  <OpSourceAccount op={op}/>{' '}
456
- for claimants <ClaimableBalanceClaimants claimants={claimants}/>
457
+ claimable by <ClaimableBalanceClaimants claimants={claimants}/>
457
458
 
458
459
  </>
459
460
  return <>
460
461
  <OpSourceAccount op={op}/> created claimable balance{' '}
461
462
  <Amount amount={amount} asset={AssetDescriptor.parse(asset)} issuer={!compact}/>{' '}
462
- for claimants <ClaimableBalanceClaimants claimants={claimants}/>
463
+ claimable by <ClaimableBalanceClaimants claimants={claimants}/>
463
464
  </>
464
465
  }
465
466
 
@@ -775,18 +776,25 @@ function InvokeHostFunctionView({op, compact}) {
775
776
  const value = func.value()
776
777
  switch (func.arm()) {
777
778
  case 'invokeContract':
778
- const invocation = <>
779
- <AccountAddress account={xdrParserUtils.xdrParseScVal(value.contractAddress())}/>{' '}
780
- <code>{value.functionName().toString()}(<ScVal value={value.args()}/>)</code>
781
- </>
779
+ const contractAddress = xdrParserUtils.xdrParseScVal(value._attributes.contractAddress)
780
+ const sac = op.operation.sacMap?.[contractAddress]
781
+ const invocationArgs = {
782
+ contract: contractAddress,
783
+ func: value.functionName().toString(),
784
+ args: value.args(),
785
+ sac
786
+ }
782
787
  if (op.isEphemeral)
783
788
  return <>
784
- <b>Invoke contract</b> {invocation}<OpSourceAccount op={op}/>
789
+ <b>Invoke contract</b> <AccountAddress account={contractAddress}/>{' '}
790
+ <InvocationInfoView {...invocationArgs}/>
791
+ <OpSourceAccount op={op}/>
785
792
  </>
793
+ const invocationEffect = op.operation.effects.find(e => e.type === 'contractInvoked' && e.contract === contractAddress)
786
794
  return <>
787
- <OpSourceAccount op={op}/> invoked contract {invocation}
795
+ <OpSourceAccount op={op}/> invoked contract <AccountAddress account={contractAddress}/>{' '}
796
+ <InvocationInfoView {...invocationArgs} result={invocationEffect?.result}/>
788
797
  </>
789
- break
790
798
  case 'wasm':
791
799
  const wasmCode = value.toString('base64')
792
800
  const codeReference = <>
@@ -800,7 +808,6 @@ function InvokeHostFunctionView({op, compact}) {
800
808
  return <>
801
809
  <OpSourceAccount op={op}/> uploaded contract code {codeReference}
802
810
  </>
803
- break
804
811
  case 'createContract':
805
812
  const preimage = value.contractIdPreimage()
806
813
  const executable = value.executable()
@@ -43,7 +43,7 @@ import TxMatcher from './tx-matcher'
43
43
  * @return {ParsedTxDetails}
44
44
  */
45
45
  export function parseTxDetails({network, txEnvelope, result, meta, id, context, createdAt, skipUnrelated, protocol}) {
46
- const parsedTx = parseTxOperationsMeta({network, tx: txEnvelope, meta, result, protocol, processSystemEvents: true})
46
+ const parsedTx = parseTxOperationsMeta({network, tx: txEnvelope, meta, result, protocol, processSystemEvents: true, mapSac: true})
47
47
  const {tx, effects, operations, isEphemeral, failed} = parsedTx
48
48
  const txHash = tx.hash().toString('hex')
49
49
  const txMatcher = new TxMatcher(context, skipUnrelated)
@@ -106,11 +106,11 @@ export const TxOperationsList = React.memo(function TxOperationsList({
106
106
  </div>
107
107
  {!!compact && !op.isEphemeral && <OpAccountingChanges op={op}/>}
108
108
  </div>
109
- {effectsExpanded && <OpEffectsView effects={op.operation.effects}/>}
109
+ {effectsExpanded && <OpEffectsView operation={op}/>}
110
110
  </div>)}
111
111
  {showFees && <TxChargedFee {...{parsedTx, compact}}/>}
112
112
  </div>
113
113
  {(opsExpanded || opdiff > 0) && <Spoiler className="text-tiny" expanded={opsExpanded} onChange={toggleAdditionalOps}
114
- showMore={`${opdiff} more operation${opdiff > 1 && 's'} in this transaction`} showLess="Hide additional operations"/>}
114
+ showMore={`${opdiff} more operation${opdiff > 1 ? 's' : ''} in this transaction`} showLess="Hide additional operations"/>}
115
115
  </div>
116
116
  })