@stellar-expert/ui-framework 1.12.8 → 1.13.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/basic-styles/typography.scss +4 -0
- package/contract/contract-api.js +33 -0
- package/contract/invocation-info-view.js +74 -0
- package/contract/sac-interface.js +131 -0
- package/contract/sc-val.js +18 -10
- package/controls/tooltip.js +43 -21
- package/controls/tooltip.scss +16 -3
- package/date/utc-timestamp.js +12 -6
- package/effect/effect-description.js +9 -6
- package/effect/op-effects-view.js +29 -2
- package/effect/soroban-tx-metrics-view.js +1 -1
- package/index.js +2 -0
- package/ledger/ledger-entry-href-formatter.js +1 -1
- package/meta/page-meta-tags.js +9 -23
- package/package.json +6 -5
- package/tx/op-description-view.js +18 -11
- package/tx/parser/tx-details-parser.js +1 -1
- package/tx/tx-operations-list.js +2 -2
|
@@ -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 <> → {!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
|
+
 SDK v{meta.sdkVersion.split('#')[0]} 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/> </> : 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
|
+
}
|
package/contract/sc-val.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
</ScValStruct>)
|
|
34
|
+
return wrapObjects ? <>{{values}}</> : <>{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
|
|
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'])
|
package/controls/tooltip.js
CHANGED
|
@@ -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 {
|
|
155
|
+
* @param {HTMLElement} trigger
|
|
156
156
|
* @param {PositionDescriptor} desiredPlace
|
|
157
|
-
* @param {PositionOffset} offset
|
|
158
|
-
* @param {
|
|
159
|
-
* @param {
|
|
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({
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
|
|
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
|
})
|
package/controls/tooltip.scss
CHANGED
|
@@ -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
|
|
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
|
}
|
package/date/utc-timestamp.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
formatted =
|
|
12
|
-
|
|
13
|
-
|
|
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}/>{' '}
|
|
220
|
-
{
|
|
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}/>
|
|
227
|
-
|
|
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({
|
|
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'}/>
|
|
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
|
}
|
|
@@ -37,7 +37,7 @@ function parseMetrics(metrics) {
|
|
|
37
37
|
break
|
|
38
38
|
case 'emit_event':
|
|
39
39
|
if (value > 0) {
|
|
40
|
-
res['Emitted events'] = value.toString()
|
|
40
|
+
res['Emitted events'] = `${value.toString()} (${formatBytes(metrics.emit_event_byte)})`
|
|
41
41
|
}
|
|
42
42
|
break
|
|
43
43
|
case 'invoke_time_nsecs':
|
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/meta/page-meta-tags.js
CHANGED
|
@@ -66,17 +66,11 @@ function generateOpenGraphMeta({description, title, facebookImage}, canonicalUrl
|
|
|
66
66
|
{name: 'og:url', content: canonicalUrl},
|
|
67
67
|
{name: 'og:site_name', content: formatPageTitle(metaProps.serviceTitle)},
|
|
68
68
|
{name: 'og:description', content: description || metaProps.description},
|
|
69
|
-
{name: 'og:type', content: 'website'}
|
|
69
|
+
{name: 'og:type', content: 'website'},
|
|
70
|
+
{name: 'og:image:width', content: 1200},
|
|
71
|
+
{name: 'og:image:height', content: 630},
|
|
72
|
+
{name: 'og:image', content: facebookImage || metaProps.facebookImage}
|
|
70
73
|
]
|
|
71
|
-
if (facebookImage) {
|
|
72
|
-
tags.push({name: 'og:image', content: facebookImage})
|
|
73
|
-
} else {
|
|
74
|
-
tags.push({name: 'og:image', content: metaProps.facebookImage})
|
|
75
|
-
/*tags = tags.concat([
|
|
76
|
-
{name: 'og:image', content: metaProps.facebookImage},
|
|
77
|
-
{name: 'og:image:width', content: 1200},
|
|
78
|
-
{name: 'og:image:height', content: 630}])*/
|
|
79
|
-
}
|
|
80
74
|
return {
|
|
81
75
|
locator: 'property',
|
|
82
76
|
tags
|
|
@@ -164,6 +158,10 @@ export function setPageMetadata(meta) {
|
|
|
164
158
|
return
|
|
165
159
|
const canonicalUrl = origin + location.pathname// + location.search
|
|
166
160
|
document.title = formatPageTitle(meta.title)
|
|
161
|
+
if (meta.image) {
|
|
162
|
+
meta.facebookImage = meta.image
|
|
163
|
+
meta.twitterImage = meta.image
|
|
164
|
+
}
|
|
167
165
|
for (const replacer of tagReplacerPipeline) {
|
|
168
166
|
replaceMetaTags(replacer(meta, canonicalUrl))
|
|
169
167
|
}
|
|
@@ -187,19 +185,6 @@ export function resetPageMetadata(meta) {
|
|
|
187
185
|
//TODO: add logic to cleanup custom page meta tags on page unload
|
|
188
186
|
}
|
|
189
187
|
|
|
190
|
-
/*export function setPageNoIndex(noIndex) {
|
|
191
|
-
if (!noIndex) {
|
|
192
|
-
removeTag('meta[name=robots]')
|
|
193
|
-
} else {
|
|
194
|
-
replaceMetaTags({
|
|
195
|
-
locator: 'name',
|
|
196
|
-
tags: [
|
|
197
|
-
{name: 'robots', content: 'noindex,nofollow'}
|
|
198
|
-
]
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
}*/
|
|
202
|
-
|
|
203
188
|
/**
|
|
204
189
|
* React hook for setting page metadata
|
|
205
190
|
* @param {PageMeta} meta - Page metadata
|
|
@@ -218,6 +203,7 @@ export function usePageMetadata(meta, dependencies = []) {
|
|
|
218
203
|
* @property {String} description - Contents description
|
|
219
204
|
* @property {String} [twitterImage] - Twitter image url
|
|
220
205
|
* @property {String} [facebookImage] - Facebook image url
|
|
206
|
+
* @property {String} [image] - Sets the image for both the Twitter image and the Facebook image
|
|
221
207
|
* @property {MetaTagReplacement} [customMeta] - Custom metadata tags
|
|
222
208
|
*/
|
|
223
209
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stellar-expert/ui-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
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.
|
|
18
|
-
"@stellar/stellar-sdk": "^12.0
|
|
19
|
-
"@stellar-expert/asset-descriptor": "^1.3.
|
|
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": "
|
|
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 <>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
779
|
-
|
|
780
|
-
|
|
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>
|
|
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 {
|
|
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)
|
package/tx/tx-operations-list.js
CHANGED
|
@@ -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
|
|
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
|
|
114
|
+
showMore={`${opdiff} more operation${opdiff > 1 ? 's' : ''} in this transaction`} showLess="Hide additional operations"/>}
|
|
115
115
|
</div>
|
|
116
116
|
})
|