@d13co/liquid-ui 0.0.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.
- package/LICENSE +21 -0
- package/package.json +22 -0
- package/src/components/AlgoSymbol.tsx +26 -0
- package/src/components/ManagePanel.tsx +203 -0
- package/src/components/OptInPanel.tsx +112 -0
- package/src/components/SendPanel.tsx +171 -0
- package/src/components/Spinner.tsx +21 -0
- package/src/components/TransactionFlow.tsx +124 -0
- package/src/components/TransactionReview.tsx +159 -0
- package/src/components/TransactionStatus.tsx +61 -0
- package/src/components/WelcomeContent.tsx +171 -0
- package/src/formatters.ts +19 -0
- package/src/hooks/useAssets.ts +43 -0
- package/src/hooks/useTransactionData.ts +40 -0
- package/src/index.ts +29 -0
- package/src/types.ts +42 -0
- package/tsconfig.json +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TxnLab, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@d13co/liquid-ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Shared UI components for use-wallet-ui and Liquid Wallet Companion extensions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"@tanstack/react-query": ">=5",
|
|
11
|
+
"react": "^18 || ^19"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@tanstack/react-query": "5.90.21",
|
|
15
|
+
"@types/react": "19.2.14",
|
|
16
|
+
"react": "19.2.4",
|
|
17
|
+
"typescript": "5.9.3"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface AlgoSymbolProps {
|
|
2
|
+
className?: string
|
|
3
|
+
/**
|
|
4
|
+
* Size relative to current font size (1em = 100%)
|
|
5
|
+
* @default 0.85
|
|
6
|
+
*/
|
|
7
|
+
scale?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AlgoSymbol({ className, scale = 0.85 }: AlgoSymbolProps) {
|
|
11
|
+
return (
|
|
12
|
+
<svg
|
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
className={`inline-block align-baseline ${className ?? ''}`}
|
|
16
|
+
style={{
|
|
17
|
+
width: `${scale}em`,
|
|
18
|
+
height: `${scale}em`,
|
|
19
|
+
}}
|
|
20
|
+
aria-label="Algorand"
|
|
21
|
+
fill="currentColor"
|
|
22
|
+
>
|
|
23
|
+
<path d="M23.98 23.99h-3.75l-2.44-9.07-5.25 9.07H8.34l8.1-14.04-1.3-4.88L4.22 24H.02L13.88 0h3.67l1.61 5.96h3.79l-2.59 4.5 3.62 13.53z" />
|
|
24
|
+
</svg>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { AlgoSymbol } from './AlgoSymbol'
|
|
3
|
+
import { OptInPanel, type OptInPanelProps } from './OptInPanel'
|
|
4
|
+
import { SendPanel, type SendPanelProps } from './SendPanel'
|
|
5
|
+
|
|
6
|
+
export interface ManagePanelProps {
|
|
7
|
+
displayBalance: number | null
|
|
8
|
+
showAvailableBalance: boolean
|
|
9
|
+
onToggleBalance: () => void
|
|
10
|
+
onBack: () => void
|
|
11
|
+
send?: Omit<SendPanelProps, 'onBack'>
|
|
12
|
+
optIn?: Omit<OptInPanelProps, 'onBack'>
|
|
13
|
+
onBridge?: () => void
|
|
14
|
+
onExplore?: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const balanceFormatter = new Intl.NumberFormat(undefined, {
|
|
18
|
+
minimumFractionDigits: 4,
|
|
19
|
+
maximumFractionDigits: 4,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export function ManagePanel({
|
|
23
|
+
displayBalance,
|
|
24
|
+
showAvailableBalance,
|
|
25
|
+
onToggleBalance,
|
|
26
|
+
onBack,
|
|
27
|
+
send,
|
|
28
|
+
optIn,
|
|
29
|
+
onBridge,
|
|
30
|
+
onExplore,
|
|
31
|
+
}: ManagePanelProps) {
|
|
32
|
+
const [mode, setMode] = useState<'main' | 'send' | 'opt-in'>('main')
|
|
33
|
+
|
|
34
|
+
const backToMain = (resetFn?: () => void) => {
|
|
35
|
+
setMode('main')
|
|
36
|
+
resetFn?.()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (mode === 'send' && send) {
|
|
40
|
+
return <SendPanel {...send} onBack={() => backToMain(send.reset)} />
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (mode === 'opt-in' && optIn) {
|
|
44
|
+
return <OptInPanel {...optIn} onBack={() => backToMain(optIn.reset)} />
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
{/* Header with back arrow */}
|
|
50
|
+
<div className="flex items-center gap-2 mb-4">
|
|
51
|
+
<button
|
|
52
|
+
onClick={onBack}
|
|
53
|
+
className="-ml-1 p-1 rounded-lg hover:bg-[var(--wui-color-bg-secondary)] transition-colors text-[var(--wui-color-text-secondary)] flex items-center justify-center"
|
|
54
|
+
title="Back"
|
|
55
|
+
>
|
|
56
|
+
<svg
|
|
57
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
58
|
+
width="20"
|
|
59
|
+
height="20"
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
strokeWidth="2"
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
66
|
+
>
|
|
67
|
+
<path d="m15 18-6-6 6-6" />
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
<h3 className="text-lg font-bold leading-none text-[var(--wui-color-text)] wallet-custom-font">
|
|
71
|
+
Manage Liquid Account
|
|
72
|
+
</h3>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Balance display */}
|
|
76
|
+
<div className="mb-4 bg-[var(--wui-color-bg-secondary)] rounded-lg p-3">
|
|
77
|
+
<div className="flex justify-between items-center">
|
|
78
|
+
{displayBalance !== null && (
|
|
79
|
+
<span className="text-base font-medium text-[var(--wui-color-text)] flex items-center gap-1">
|
|
80
|
+
{balanceFormatter.format(displayBalance)}
|
|
81
|
+
<AlgoSymbol />
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
<button
|
|
85
|
+
onClick={onToggleBalance}
|
|
86
|
+
className="flex items-center gap-1 text-sm text-[var(--wui-color-text-secondary)] bg-[var(--wui-color-bg-tertiary)] py-1 pl-2.5 pr-2 rounded-md hover:brightness-90 transition-all focus:outline-none"
|
|
87
|
+
title={showAvailableBalance ? 'Show total balance' : 'Show available balance'}
|
|
88
|
+
>
|
|
89
|
+
{showAvailableBalance ? 'Available' : 'Total'}
|
|
90
|
+
<svg
|
|
91
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
92
|
+
width="10"
|
|
93
|
+
height="10"
|
|
94
|
+
viewBox="0 0 24 24"
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
strokeWidth="2"
|
|
98
|
+
strokeLinecap="round"
|
|
99
|
+
strokeLinejoin="round"
|
|
100
|
+
className="ml-0.5 opacity-80"
|
|
101
|
+
>
|
|
102
|
+
<path d="m17 10-5-5-5 5" />
|
|
103
|
+
<path d="m17 14-5 5-5-5" />
|
|
104
|
+
</svg>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Divider */}
|
|
110
|
+
<div className="border-t border-[var(--wui-color-border)] mb-3" />
|
|
111
|
+
|
|
112
|
+
{/* Action buttons grid */}
|
|
113
|
+
<div className="grid grid-cols-2 gap-2">
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => setMode('send')}
|
|
116
|
+
disabled={!send}
|
|
117
|
+
className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
|
|
118
|
+
>
|
|
119
|
+
<svg
|
|
120
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
121
|
+
className="h-4 w-4 mr-1.5"
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
fill="none"
|
|
124
|
+
stroke="currentColor"
|
|
125
|
+
strokeWidth="2"
|
|
126
|
+
strokeLinecap="round"
|
|
127
|
+
strokeLinejoin="round"
|
|
128
|
+
>
|
|
129
|
+
<path d="M5 12h14" />
|
|
130
|
+
<path d="m12 5 7 7-7 7" />
|
|
131
|
+
</svg>
|
|
132
|
+
Send
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
onClick={() => setMode('opt-in')}
|
|
136
|
+
disabled={!optIn}
|
|
137
|
+
className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
|
|
138
|
+
>
|
|
139
|
+
<svg
|
|
140
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
141
|
+
className="h-4 w-4 mr-1.5"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
fill="none"
|
|
144
|
+
stroke="currentColor"
|
|
145
|
+
strokeWidth="2"
|
|
146
|
+
strokeLinecap="round"
|
|
147
|
+
strokeLinejoin="round"
|
|
148
|
+
>
|
|
149
|
+
<path d="M12 5v14" />
|
|
150
|
+
<path d="M5 12h14" />
|
|
151
|
+
</svg>
|
|
152
|
+
Opt In
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
onClick={onBridge}
|
|
156
|
+
disabled={!onBridge}
|
|
157
|
+
className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
|
|
158
|
+
>
|
|
159
|
+
<svg
|
|
160
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
161
|
+
className="h-4 w-4 mr-1.5"
|
|
162
|
+
viewBox="0 0 24 24"
|
|
163
|
+
fill="none"
|
|
164
|
+
stroke="currentColor"
|
|
165
|
+
strokeWidth="2"
|
|
166
|
+
strokeLinecap="round"
|
|
167
|
+
strokeLinejoin="round"
|
|
168
|
+
>
|
|
169
|
+
<path d="M8 3l4 4-4 4" />
|
|
170
|
+
<path d="M16 3l-4 4 4 4" />
|
|
171
|
+
<path d="M12 7H4" />
|
|
172
|
+
<path d="M12 7h8" />
|
|
173
|
+
<path d="M8 21l4-4-4-4" />
|
|
174
|
+
<path d="M16 21l-4-4 4-4" />
|
|
175
|
+
<path d="M12 17H4" />
|
|
176
|
+
<path d="M12 17h8" />
|
|
177
|
+
</svg>
|
|
178
|
+
Bridge
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
onClick={onExplore}
|
|
182
|
+
disabled={!onExplore}
|
|
183
|
+
className="py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm flex items-center justify-center disabled:opacity-40 disabled:pointer-events-none"
|
|
184
|
+
>
|
|
185
|
+
<svg
|
|
186
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
187
|
+
className="h-4 w-4 mr-1.5"
|
|
188
|
+
viewBox="0 0 24 24"
|
|
189
|
+
fill="none"
|
|
190
|
+
stroke="currentColor"
|
|
191
|
+
strokeWidth="2"
|
|
192
|
+
strokeLinecap="round"
|
|
193
|
+
strokeLinejoin="round"
|
|
194
|
+
>
|
|
195
|
+
<circle cx="11" cy="11" r="8" />
|
|
196
|
+
<path d="m21 21-4.3-4.3" />
|
|
197
|
+
</svg>
|
|
198
|
+
Explore
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Spinner } from './Spinner'
|
|
2
|
+
import { TransactionStatus, type TransactionStatusValue } from './TransactionStatus'
|
|
3
|
+
|
|
4
|
+
export interface OptInPanelProps {
|
|
5
|
+
assetIdInput: string
|
|
6
|
+
setAssetIdInput: (v: string) => void
|
|
7
|
+
assetInfo: { name: string; unitName: string; index: number } | null
|
|
8
|
+
assetLookupLoading: boolean
|
|
9
|
+
assetLookupError: string | null
|
|
10
|
+
status: TransactionStatusValue
|
|
11
|
+
error: string | null
|
|
12
|
+
handleOptIn: () => void
|
|
13
|
+
reset: () => void
|
|
14
|
+
retry: () => void
|
|
15
|
+
onBack: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function OptInPanel({
|
|
19
|
+
assetIdInput,
|
|
20
|
+
setAssetIdInput,
|
|
21
|
+
assetInfo,
|
|
22
|
+
assetLookupLoading,
|
|
23
|
+
assetLookupError,
|
|
24
|
+
status,
|
|
25
|
+
error,
|
|
26
|
+
handleOptIn,
|
|
27
|
+
reset: _reset,
|
|
28
|
+
retry,
|
|
29
|
+
onBack,
|
|
30
|
+
}: OptInPanelProps) {
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{/* Header */}
|
|
34
|
+
<div className="flex items-center gap-2 mb-4">
|
|
35
|
+
<button
|
|
36
|
+
onClick={onBack}
|
|
37
|
+
className="-ml-1 p-1 rounded-lg hover:bg-[var(--wui-color-bg-secondary)] transition-colors text-[var(--wui-color-text-secondary)] flex items-center justify-center"
|
|
38
|
+
title="Back"
|
|
39
|
+
>
|
|
40
|
+
<svg
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
width="20"
|
|
43
|
+
height="20"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
strokeWidth="2"
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
>
|
|
51
|
+
<path d="m15 18-6-6 6-6" />
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
54
|
+
<h3 className="text-lg font-bold leading-none text-[var(--wui-color-text)] wallet-custom-font">Opt In Asset</h3>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Asset ID input */}
|
|
58
|
+
<div className="mb-4">
|
|
59
|
+
<input
|
|
60
|
+
type="text"
|
|
61
|
+
inputMode="numeric"
|
|
62
|
+
pattern="[0-9]*"
|
|
63
|
+
placeholder="Enter Asset ID"
|
|
64
|
+
value={assetIdInput}
|
|
65
|
+
onChange={(e) => setAssetIdInput(e.target.value.replace(/[^0-9]/g, ''))}
|
|
66
|
+
className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Loading */}
|
|
71
|
+
{assetLookupLoading && (
|
|
72
|
+
<div className="flex items-center justify-center py-4 text-sm text-[var(--wui-color-text-secondary)]">
|
|
73
|
+
<Spinner className="h-4 w-4 mr-2" />
|
|
74
|
+
Looking up asset...
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{/* Lookup error */}
|
|
79
|
+
{assetLookupError && (
|
|
80
|
+
<div className="py-3 text-center text-sm text-[var(--wui-color-danger-text)]">{assetLookupError}</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/* Asset result */}
|
|
84
|
+
{assetInfo && status === 'idle' && (
|
|
85
|
+
<div className="bg-[var(--wui-color-bg-secondary)] rounded-lg p-3">
|
|
86
|
+
<div className="flex justify-between items-start mb-3">
|
|
87
|
+
<div>
|
|
88
|
+
<p className="text-sm font-medium text-[var(--wui-color-text)]">{assetInfo.name}</p>
|
|
89
|
+
{assetInfo.unitName && (
|
|
90
|
+
<p className="text-xs text-[var(--wui-color-text-secondary)]">{assetInfo.unitName}</p>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
<span className="text-xs text-[var(--wui-color-text-tertiary)]">ID: {assetInfo.index}</span>
|
|
94
|
+
</div>
|
|
95
|
+
<button
|
|
96
|
+
onClick={handleOptIn}
|
|
97
|
+
className="w-full py-2 px-4 bg-[var(--wui-color-primary)] text-white font-medium rounded-xl hover:brightness-90 transition-all text-sm"
|
|
98
|
+
>
|
|
99
|
+
Opt In
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<TransactionStatus
|
|
105
|
+
status={status}
|
|
106
|
+
error={error}
|
|
107
|
+
successMessage="Opted in successfully!"
|
|
108
|
+
onRetry={retry}
|
|
109
|
+
/>
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Spinner } from './Spinner'
|
|
2
|
+
import { TransactionStatus, type TransactionStatusValue } from './TransactionStatus'
|
|
3
|
+
|
|
4
|
+
export interface SendPanelProps {
|
|
5
|
+
sendType: 'algo' | 'asa'
|
|
6
|
+
setSendType: (type: 'algo' | 'asa') => void
|
|
7
|
+
receiver: string
|
|
8
|
+
setReceiver: (v: string) => void
|
|
9
|
+
amount: string
|
|
10
|
+
setAmount: (v: string) => void
|
|
11
|
+
assetIdInput: string
|
|
12
|
+
setAssetIdInput: (v: string) => void
|
|
13
|
+
assetInfo: { name: string; unitName: string; index: number } | null
|
|
14
|
+
assetLookupLoading: boolean
|
|
15
|
+
assetLookupError: string | null
|
|
16
|
+
status: TransactionStatusValue
|
|
17
|
+
error: string | null
|
|
18
|
+
handleSend: () => void
|
|
19
|
+
reset: () => void
|
|
20
|
+
retry: () => void
|
|
21
|
+
onBack: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SendPanel({
|
|
25
|
+
sendType,
|
|
26
|
+
setSendType,
|
|
27
|
+
receiver,
|
|
28
|
+
setReceiver,
|
|
29
|
+
amount,
|
|
30
|
+
setAmount,
|
|
31
|
+
assetIdInput,
|
|
32
|
+
setAssetIdInput,
|
|
33
|
+
assetInfo,
|
|
34
|
+
assetLookupLoading,
|
|
35
|
+
assetLookupError,
|
|
36
|
+
status,
|
|
37
|
+
error,
|
|
38
|
+
handleSend,
|
|
39
|
+
reset: _reset,
|
|
40
|
+
retry,
|
|
41
|
+
onBack,
|
|
42
|
+
}: SendPanelProps) {
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{/* Header */}
|
|
46
|
+
<div className="flex items-center gap-2 mb-4">
|
|
47
|
+
<button
|
|
48
|
+
onClick={onBack}
|
|
49
|
+
className="-ml-1 p-1 rounded-lg hover:bg-[var(--wui-color-bg-secondary)] transition-colors text-[var(--wui-color-text-secondary)] flex items-center justify-center"
|
|
50
|
+
title="Back"
|
|
51
|
+
>
|
|
52
|
+
<svg
|
|
53
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
54
|
+
width="20"
|
|
55
|
+
height="20"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
fill="none"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
strokeWidth="2"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
>
|
|
63
|
+
<path d="m15 18-6-6 6-6" />
|
|
64
|
+
</svg>
|
|
65
|
+
</button>
|
|
66
|
+
<h3 className="text-lg font-bold leading-none text-[var(--wui-color-text)] wallet-custom-font">Send</h3>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* ALGO / Asset toggle */}
|
|
70
|
+
<div className="flex mb-4 bg-[var(--wui-color-bg-secondary)] rounded-lg p-1">
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => setSendType('algo')}
|
|
73
|
+
className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-all ${
|
|
74
|
+
sendType === 'algo'
|
|
75
|
+
? 'bg-[var(--wui-color-bg)] text-[var(--wui-color-text)] shadow-sm'
|
|
76
|
+
: 'text-[var(--wui-color-text-secondary)] hover:text-[var(--wui-color-text)]'
|
|
77
|
+
}`}
|
|
78
|
+
>
|
|
79
|
+
ALGO
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
onClick={() => setSendType('asa')}
|
|
83
|
+
className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-all ${
|
|
84
|
+
sendType === 'asa'
|
|
85
|
+
? 'bg-[var(--wui-color-bg)] text-[var(--wui-color-text)] shadow-sm'
|
|
86
|
+
: 'text-[var(--wui-color-text-secondary)] hover:text-[var(--wui-color-text)]'
|
|
87
|
+
}`}
|
|
88
|
+
>
|
|
89
|
+
Asset
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Asset ID input (ASA mode only) */}
|
|
94
|
+
{sendType === 'asa' && (
|
|
95
|
+
<div className="mb-3">
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
inputMode="numeric"
|
|
99
|
+
pattern="[0-9]*"
|
|
100
|
+
placeholder="Asset ID"
|
|
101
|
+
value={assetIdInput}
|
|
102
|
+
onChange={(e) => setAssetIdInput(e.target.value.replace(/[^0-9]/g, ''))}
|
|
103
|
+
className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
|
|
104
|
+
/>
|
|
105
|
+
{assetLookupLoading && (
|
|
106
|
+
<div className="flex items-center mt-2 text-xs text-[var(--wui-color-text-secondary)]">
|
|
107
|
+
<Spinner className="h-3 w-3 mr-1.5" />
|
|
108
|
+
Looking up asset...
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
{assetLookupError && (
|
|
112
|
+
<p className="mt-2 text-xs text-[var(--wui-color-danger-text)]">{assetLookupError}</p>
|
|
113
|
+
)}
|
|
114
|
+
{assetInfo && (
|
|
115
|
+
<div className="mt-2 flex items-center justify-between text-xs text-[var(--wui-color-text-secondary)] bg-[var(--wui-color-bg-secondary)] rounded-md px-2 py-1.5">
|
|
116
|
+
<span className="font-medium text-[var(--wui-color-text)]">{assetInfo.name}</span>
|
|
117
|
+
{assetInfo.unitName && <span>{assetInfo.unitName}</span>}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{/* Receiver address */}
|
|
124
|
+
<div className="mb-3">
|
|
125
|
+
<input
|
|
126
|
+
type="text"
|
|
127
|
+
placeholder="Receiver address"
|
|
128
|
+
value={receiver}
|
|
129
|
+
onChange={(e) => setReceiver(e.target.value)}
|
|
130
|
+
className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Amount */}
|
|
135
|
+
<div className="mb-4">
|
|
136
|
+
<input
|
|
137
|
+
type="text"
|
|
138
|
+
inputMode="decimal"
|
|
139
|
+
placeholder={
|
|
140
|
+
sendType === 'algo'
|
|
141
|
+
? 'Amount (ALGO)'
|
|
142
|
+
: assetInfo
|
|
143
|
+
? `Amount (${assetInfo.unitName || assetInfo.name})`
|
|
144
|
+
: 'Amount'
|
|
145
|
+
}
|
|
146
|
+
value={amount}
|
|
147
|
+
onChange={(e) => setAmount(e.target.value.replace(/[^0-9.]/g, ''))}
|
|
148
|
+
className="w-full rounded-lg border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] py-2.5 px-3 text-sm text-[var(--wui-color-text)] placeholder:text-[var(--wui-color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--wui-color-primary)] focus:border-transparent"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Send button */}
|
|
153
|
+
{status === 'idle' && (
|
|
154
|
+
<button
|
|
155
|
+
onClick={handleSend}
|
|
156
|
+
disabled={!receiver || !amount || (sendType === 'asa' && !assetInfo)}
|
|
157
|
+
className="w-full py-2.5 px-4 bg-[var(--wui-color-primary)] text-white font-medium rounded-xl hover:brightness-90 transition-all text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
|
158
|
+
>
|
|
159
|
+
Send {sendType === 'algo' ? 'ALGO' : assetInfo?.unitName || 'Asset'}
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<TransactionStatus
|
|
164
|
+
status={status}
|
|
165
|
+
error={error}
|
|
166
|
+
successMessage="Sent successfully!"
|
|
167
|
+
onRetry={retry}
|
|
168
|
+
/>
|
|
169
|
+
</>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface SpinnerProps {
|
|
2
|
+
className?: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function Spinner({ className }: SpinnerProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
className={`animate-spin h-4 w-4 ${className ?? ''}`}
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
fill="none"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
>
|
|
13
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
14
|
+
<path
|
|
15
|
+
className="opacity-75"
|
|
16
|
+
fill="currentColor"
|
|
17
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
18
|
+
/>
|
|
19
|
+
</svg>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { TransactionData, AssetInfo } from '../types'
|
|
2
|
+
import { formatAssetAmount, assetLabel } from '../formatters'
|
|
3
|
+
|
|
4
|
+
export interface TransactionFlowProps {
|
|
5
|
+
txn: TransactionData
|
|
6
|
+
assetInfo?: AssetInfo
|
|
7
|
+
appEscrows?: Record<string, string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TransactionFlow({ txn, assetInfo, appEscrows = {} }: TransactionFlowProps) {
|
|
11
|
+
const resolveAddr = (full: string | undefined, short: string): string =>
|
|
12
|
+
(full && appEscrows[full]) || short
|
|
13
|
+
|
|
14
|
+
const renderFlowLine = (
|
|
15
|
+
from: string,
|
|
16
|
+
label: string,
|
|
17
|
+
to: string,
|
|
18
|
+
isDanger: boolean = false,
|
|
19
|
+
isSecondary: boolean = false,
|
|
20
|
+
) => (
|
|
21
|
+
<div className={`grid grid-cols-[1fr_auto_1fr] w-full items-center font-mono text-xs ${isSecondary ? 'mt-1' : ''}`}>
|
|
22
|
+
<span className="text-[var(--wui-color-text-secondary)] text-left">{from}</span>
|
|
23
|
+
<span className="flex items-center justify-center gap-1 px-1">
|
|
24
|
+
<span className="text-[var(--wui-color-text-tertiary)]">--[</span>
|
|
25
|
+
<span className={isDanger ? 'text-[var(--wui-color-danger-text)] font-bold' : 'text-[var(--wui-color-primary)] font-medium'}>
|
|
26
|
+
{label}
|
|
27
|
+
</span>
|
|
28
|
+
<span className="text-[var(--wui-color-text-tertiary)]">]--></span>
|
|
29
|
+
</span>
|
|
30
|
+
<span className={`text-right ${isDanger ? 'text-[var(--wui-color-danger-text)] font-bold' : 'text-[var(--wui-color-text-secondary)]'}`}>{to}</span>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const renderRemainderLine = (to: string) => (
|
|
35
|
+
<div className="grid grid-cols-[1fr_auto_1fr] w-full items-center font-mono text-xs mt-1">
|
|
36
|
+
<span />
|
|
37
|
+
<span className="flex items-center justify-center gap-1 px-1">
|
|
38
|
+
<span className="text-[var(--wui-color-text-tertiary)]">--[</span>
|
|
39
|
+
<span className="text-[var(--wui-color-danger-text)] font-bold">remainder</span>
|
|
40
|
+
<span className="text-[var(--wui-color-text-tertiary)]">]--></span>
|
|
41
|
+
</span>
|
|
42
|
+
<span className="text-[var(--wui-color-danger-text)] font-bold text-right">{to}</span>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const renderRekeyLine = (from: string, to: string) => (
|
|
47
|
+
<div className="grid grid-cols-[1fr_auto_1fr] w-full items-center font-mono text-xs mt-1">
|
|
48
|
+
<span className="text-[var(--wui-color-text-secondary)] text-left">{from}</span>
|
|
49
|
+
<span className="flex items-center justify-center gap-1 px-1">
|
|
50
|
+
<span className="text-[var(--wui-color-text-tertiary)]">--[</span>
|
|
51
|
+
<span className="text-[var(--wui-color-danger-text)] font-bold">REKEY</span>
|
|
52
|
+
<span className="text-[var(--wui-color-text-tertiary)]">]--></span>
|
|
53
|
+
</span>
|
|
54
|
+
<span className="text-[var(--wui-color-danger-text)] font-bold text-right">{to}</span>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-0">
|
|
60
|
+
{/* Payment transaction */}
|
|
61
|
+
{txn.type === 'pay' && txn.receiverShort && (
|
|
62
|
+
<>
|
|
63
|
+
{renderFlowLine(txn.senderShort, txn.amount || '0 ALGO', resolveAddr(txn.receiver, txn.receiverShort))}
|
|
64
|
+
{txn.closeRemainderToShort && renderRemainderLine(txn.closeRemainderToShort)}
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Asset transfer */}
|
|
69
|
+
{txn.type === 'axfer' && txn.receiverShort && (
|
|
70
|
+
<>
|
|
71
|
+
{renderFlowLine(
|
|
72
|
+
txn.senderShort,
|
|
73
|
+
assetInfo ? formatAssetAmount(txn.rawAmount, assetInfo) : `${txn.amount || '0'} ${assetLabel(txn)}`,
|
|
74
|
+
resolveAddr(txn.receiver, txn.receiverShort),
|
|
75
|
+
)}
|
|
76
|
+
{txn.closeRemainderToShort && renderRemainderLine(txn.closeRemainderToShort)}
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Asset freeze */}
|
|
81
|
+
{txn.type === 'afrz' && txn.freezeTargetShort && (
|
|
82
|
+
<>
|
|
83
|
+
{renderFlowLine(
|
|
84
|
+
txn.senderShort,
|
|
85
|
+
`${txn.isFreezing ? 'Freeze' : 'Unfreeze'} ${assetLabel(txn, assetInfo)}`,
|
|
86
|
+
txn.freezeTargetShort,
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{/* Asset config */}
|
|
92
|
+
{txn.type === 'acfg' && (
|
|
93
|
+
<>
|
|
94
|
+
{renderFlowLine(
|
|
95
|
+
txn.senderShort,
|
|
96
|
+
`Configure ${txn.assetIndex ? assetLabel(txn, assetInfo) : 'NEW'}`,
|
|
97
|
+
txn.senderShort,
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{/* Application call */}
|
|
103
|
+
{txn.type === 'appl' && (
|
|
104
|
+
<>
|
|
105
|
+
{renderFlowLine(
|
|
106
|
+
txn.senderShort,
|
|
107
|
+
'APP CALL',
|
|
108
|
+
`App ${txn.appIndex || 'NEW'}`,
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Key registration */}
|
|
114
|
+
{txn.type === 'keyreg' && (
|
|
115
|
+
<>
|
|
116
|
+
{renderFlowLine(txn.senderShort, 'KEY REG', txn.senderShort)}
|
|
117
|
+
</>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Rekey (applies to any transaction type) */}
|
|
121
|
+
{txn.rekeyToShort && renderRekeyLine(txn.senderShort, txn.rekeyToShort)}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { TransactionFlow } from './TransactionFlow'
|
|
3
|
+
import { useTransactionData } from '../hooks/useTransactionData'
|
|
4
|
+
import type { TransactionData, TransactionDanger, AssetLookupClient } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface TransactionReviewProps {
|
|
7
|
+
transactions: TransactionData[]
|
|
8
|
+
message: string
|
|
9
|
+
dangerous: TransactionDanger
|
|
10
|
+
algodClient?: AssetLookupClient
|
|
11
|
+
getApplicationAddress?: (appId: number) => { toString(): string }
|
|
12
|
+
onApprove: () => void
|
|
13
|
+
onReject: () => void
|
|
14
|
+
signing?: boolean
|
|
15
|
+
walletName?: string
|
|
16
|
+
origin?: string
|
|
17
|
+
headerAction?: ReactNode
|
|
18
|
+
payloadVerified?: boolean | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function TransactionReview({
|
|
22
|
+
transactions,
|
|
23
|
+
message,
|
|
24
|
+
dangerous,
|
|
25
|
+
algodClient,
|
|
26
|
+
getApplicationAddress,
|
|
27
|
+
onApprove,
|
|
28
|
+
onReject,
|
|
29
|
+
signing,
|
|
30
|
+
walletName,
|
|
31
|
+
origin,
|
|
32
|
+
headerAction,
|
|
33
|
+
payloadVerified,
|
|
34
|
+
}: TransactionReviewProps) {
|
|
35
|
+
const { loading, assets, appEscrows } = useTransactionData(transactions, {
|
|
36
|
+
algodClient,
|
|
37
|
+
getApplicationAddress,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex flex-col">
|
|
42
|
+
{/* Header */}
|
|
43
|
+
<div className="flex items-center justify-between px-6 pt-5 pb-1">
|
|
44
|
+
<h2 className={`text-lg font-bold ${dangerous ? 'text-[var(--wui-color-danger-text)]' : 'text-[var(--wui-color-text)]'}`}>
|
|
45
|
+
{dangerous ? 'Review Dangerous ' : 'Review '}
|
|
46
|
+
Transaction{transactions.length > 1 ? 's' : ''}
|
|
47
|
+
</h2>
|
|
48
|
+
{headerAction}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Origin (extension shows request origin) */}
|
|
52
|
+
{origin && (
|
|
53
|
+
<div className="px-6 text-xs text-[var(--wui-color-text-tertiary)] truncate">
|
|
54
|
+
{origin}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{/* Danger description */}
|
|
59
|
+
{dangerous ? (
|
|
60
|
+
<div className="px-6 pb-3 text-sm font-bold text-[var(--wui-color-danger-text)]">
|
|
61
|
+
{dangerous === 'rekey'
|
|
62
|
+
? 'This transaction will rekey your account, transferring signing authority to a different address. You will no longer be able to sign transactions with your current key.'
|
|
63
|
+
: 'This transaction will close your account and transfer all remaining funds to another address. This action is irreversible.'}
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<div className="px-6 pb-3 text-sm text-[var(--wui-color-text-secondary)]">
|
|
67
|
+
{transactions.length === 1
|
|
68
|
+
? 'You are about to sign the following transaction:'
|
|
69
|
+
: `You are about to sign ${transactions.length} transactions:`}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{/* Transaction list */}
|
|
74
|
+
<div className="px-4 pb-4 max-h-80 overflow-y-auto">
|
|
75
|
+
{loading ? (
|
|
76
|
+
<div className="flex items-center justify-center py-6 text-sm text-[var(--wui-color-text-secondary)]">
|
|
77
|
+
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
78
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
79
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
80
|
+
</svg>
|
|
81
|
+
Loading asset info...
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
{transactions.map((txn) => (
|
|
86
|
+
<div
|
|
87
|
+
key={txn.index}
|
|
88
|
+
className="rounded-sm border p-3 border-[var(--wui-color-primary)] bg-[var(--wui-color-bg-secondary)]"
|
|
89
|
+
>
|
|
90
|
+
<TransactionFlow txn={txn} assetInfo={txn.assetIndex ? assets[txn.assetIndex.toString()] : undefined} appEscrows={appEscrows} />
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Payload verification warning */}
|
|
98
|
+
{payloadVerified === false && (
|
|
99
|
+
<div className="px-4 pb-4">
|
|
100
|
+
<div className="text-sm font-bold text-[var(--wui-color-danger-text)] border border-[var(--wui-color-danger-text)] rounded-xl p-3 bg-[var(--wui-color-danger-bg)]">
|
|
101
|
+
Payload verification failed. The provided payload was invalid and has been recalculated from the raw transactions.
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Sign payload */}
|
|
107
|
+
<div className="px-4 pb-4">
|
|
108
|
+
<div className="text-sm flex flex-col gap-2 border border-[var(--wui-color-border)] rounded-xl p-3">
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<span>Transaction ID to sign:</span>
|
|
111
|
+
{payloadVerified === true && (
|
|
112
|
+
<span className="text-xs text-green-600 font-medium">Verified</span>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
<div className="font-mono break-all text-[var(--wui-color-danger-text)]">{message}</div>
|
|
116
|
+
<div>Ensure the transaction ID is correct before approving.</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Footer */}
|
|
121
|
+
{signing ? (
|
|
122
|
+
<div className="px-6 py-4 border-t border-[var(--wui-color-border)]">
|
|
123
|
+
<div className="flex items-center gap-2 text-sm text-[var(--wui-color-text-secondary)]">
|
|
124
|
+
<svg className="animate-spin h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
125
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
126
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
127
|
+
</svg>
|
|
128
|
+
Signing...
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
) : dangerous ? (
|
|
132
|
+
<div className="px-6 py-4 border-t border-[var(--wui-color-border)] flex gap-3">
|
|
133
|
+
<button
|
|
134
|
+
onClick={onReject}
|
|
135
|
+
className="flex-1 py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text-secondary)] font-medium rounded-xl hover:brightness-90 transition-all text-sm"
|
|
136
|
+
>
|
|
137
|
+
Reject
|
|
138
|
+
</button>
|
|
139
|
+
<button
|
|
140
|
+
onClick={onApprove}
|
|
141
|
+
className="flex-1 py-2.5 px-4 bg-[var(--wui-color-danger-text)] text-white font-medium rounded-xl hover:brightness-90 transition-all text-sm"
|
|
142
|
+
>
|
|
143
|
+
Sign (Dangerous)
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<div className="px-6 py-4 border-t border-[var(--wui-color-border)]">
|
|
148
|
+
<div className="flex items-center gap-2 text-sm text-[var(--wui-color-text-secondary)]">
|
|
149
|
+
<svg className="animate-spin h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
150
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
151
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
152
|
+
</svg>
|
|
153
|
+
Review in {walletName || 'wallet'}...
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Spinner } from './Spinner'
|
|
2
|
+
|
|
3
|
+
export type TransactionStatusValue = 'idle' | 'signing' | 'sending' | 'success' | 'error'
|
|
4
|
+
|
|
5
|
+
interface TransactionStatusProps {
|
|
6
|
+
status: TransactionStatusValue
|
|
7
|
+
error: string | null
|
|
8
|
+
successMessage: string
|
|
9
|
+
onRetry: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TransactionStatus({ status, error, successMessage, onRetry }: TransactionStatusProps) {
|
|
13
|
+
if (status === 'idle') return null
|
|
14
|
+
|
|
15
|
+
if (status === 'signing') {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center justify-center py-4 text-sm text-[var(--wui-color-text-secondary)]">
|
|
18
|
+
<Spinner className="h-4 w-4 mr-2" />
|
|
19
|
+
Waiting for signature...
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (status === 'sending') {
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex items-center justify-center py-4 text-sm text-[var(--wui-color-text-secondary)]">
|
|
27
|
+
<Spinner className="h-4 w-4 mr-2" />
|
|
28
|
+
Sending transaction...
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (status === 'success') {
|
|
34
|
+
return (
|
|
35
|
+
<div className="text-center py-4">
|
|
36
|
+
<svg
|
|
37
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
38
|
+
className="h-8 w-8 mx-auto mb-2 text-green-500"
|
|
39
|
+
viewBox="0 0 20 20"
|
|
40
|
+
fill="currentColor"
|
|
41
|
+
>
|
|
42
|
+
<path
|
|
43
|
+
fillRule="evenodd"
|
|
44
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
45
|
+
clipRule="evenodd"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
<p className="text-sm font-medium text-[var(--wui-color-text)]">{successMessage}</p>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="text-center py-3">
|
|
55
|
+
<p className="text-sm text-[var(--wui-color-danger-text)] mb-2">{error}</p>
|
|
56
|
+
<button onClick={onRetry} className="text-sm text-[var(--wui-color-primary)] hover:underline">
|
|
57
|
+
Try again
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface WelcomeContentProps {
|
|
4
|
+
algorandAddress: string
|
|
5
|
+
evmAddress: string
|
|
6
|
+
onClose: () => void
|
|
7
|
+
labelId?: string
|
|
8
|
+
descriptionId?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function shortAddress(address: string) {
|
|
12
|
+
return `${address.slice(0, 6)}...${address.slice(-6)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function WelcomeContent({
|
|
16
|
+
algorandAddress,
|
|
17
|
+
evmAddress,
|
|
18
|
+
onClose,
|
|
19
|
+
labelId,
|
|
20
|
+
descriptionId,
|
|
21
|
+
}: WelcomeContentProps) {
|
|
22
|
+
const [copiedField, setCopiedField] = useState<'evm' | 'algorand' | null>(null)
|
|
23
|
+
|
|
24
|
+
const handleCopy = useCallback((address: string, field: 'evm' | 'algorand') => {
|
|
25
|
+
navigator.clipboard.writeText(address)
|
|
26
|
+
setCopiedField(field)
|
|
27
|
+
setTimeout(() => setCopiedField(null), 1500)
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const shortAlgorandAddress = shortAddress(algorandAddress)
|
|
31
|
+
const shortEvmAddress = shortAddress(evmAddress)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{/* Header */}
|
|
36
|
+
<div className="relative flex items-center px-6 pt-5 pb-4">
|
|
37
|
+
<h2 id={labelId} className="text-xl font-bold text-[var(--wui-color-text)] wallet-custom-font">
|
|
38
|
+
Welcome to Algorand
|
|
39
|
+
</h2>
|
|
40
|
+
<button
|
|
41
|
+
onClick={onClose}
|
|
42
|
+
className="absolute right-4 top-5 w-9 h-9 flex items-center justify-center rounded-full bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text-secondary)] hover:brightness-90 transition-all"
|
|
43
|
+
aria-label="Close dialog"
|
|
44
|
+
>
|
|
45
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
46
|
+
<path
|
|
47
|
+
fillRule="evenodd"
|
|
48
|
+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
49
|
+
clipRule="evenodd"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Content */}
|
|
56
|
+
<div id={descriptionId} className="px-6 pb-4 space-y-4">
|
|
57
|
+
<p className="text-sm text-[var(--wui-color-text-secondary)]">
|
|
58
|
+
You can use Algorand with your very own Liquid EVM account, which wraps your EVM private key, preserving full
|
|
59
|
+
self-custodial control.{' '}
|
|
60
|
+
<a
|
|
61
|
+
className="text-[var(--wui-color-link)] hover:text-[var(--wui-color-link-hover)]"
|
|
62
|
+
rel="noopener noreferrer"
|
|
63
|
+
href="#"
|
|
64
|
+
onClick={() => {
|
|
65
|
+
alert('Soon')
|
|
66
|
+
return false
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
Learn more.
|
|
70
|
+
</a>
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
<div className="!my-4 rounded-xl border border-[var(--wui-color-border)] bg-[var(--wui-color-bg-secondary)] p-3 space-y-2">
|
|
74
|
+
<div className="flex justify-between items-center text-xs">
|
|
75
|
+
<span className="text-[var(--wui-color-text-tertiary)]">Your EVM Address</span>
|
|
76
|
+
<span className="flex items-center gap-1.5">
|
|
77
|
+
<span className="text-[var(--wui-color-text-secondary)] font-mono">{shortEvmAddress}</span>
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => handleCopy(evmAddress, 'evm')}
|
|
80
|
+
className="text-[var(--wui-color-text-tertiary)] hover:text-[var(--wui-color-text-secondary)] transition-colors"
|
|
81
|
+
aria-label="Copy EVM address"
|
|
82
|
+
>
|
|
83
|
+
{copiedField === 'evm' ? (
|
|
84
|
+
<svg
|
|
85
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
86
|
+
className="h-3.5 w-3.5 text-green-500"
|
|
87
|
+
viewBox="0 0 20 20"
|
|
88
|
+
fill="currentColor"
|
|
89
|
+
>
|
|
90
|
+
<path
|
|
91
|
+
fillRule="evenodd"
|
|
92
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
93
|
+
clipRule="evenodd"
|
|
94
|
+
/>
|
|
95
|
+
</svg>
|
|
96
|
+
) : (
|
|
97
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
|
98
|
+
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
99
|
+
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
100
|
+
</svg>
|
|
101
|
+
)}
|
|
102
|
+
</button>
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex justify-between items-center text-xs">
|
|
106
|
+
<span className="text-[var(--wui-color-text-tertiary)]">Your Algorand Address</span>
|
|
107
|
+
<span className="flex items-center gap-1.5">
|
|
108
|
+
<span className="text-[var(--wui-color-text-secondary)] font-mono">{shortAlgorandAddress}</span>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => handleCopy(algorandAddress, 'algorand')}
|
|
111
|
+
className="text-[var(--wui-color-text-tertiary)] hover:text-[var(--wui-color-text-secondary)] transition-colors"
|
|
112
|
+
aria-label="Copy Algorand address"
|
|
113
|
+
>
|
|
114
|
+
{copiedField === 'algorand' ? (
|
|
115
|
+
<svg
|
|
116
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
117
|
+
className="h-3.5 w-3.5 text-green-500"
|
|
118
|
+
viewBox="0 0 20 20"
|
|
119
|
+
fill="currentColor"
|
|
120
|
+
>
|
|
121
|
+
<path
|
|
122
|
+
fillRule="evenodd"
|
|
123
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
124
|
+
clipRule="evenodd"
|
|
125
|
+
/>
|
|
126
|
+
</svg>
|
|
127
|
+
) : (
|
|
128
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
|
129
|
+
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
130
|
+
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
131
|
+
</svg>
|
|
132
|
+
)}
|
|
133
|
+
</button>
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<p className="text-sm text-[var(--wui-color-text-secondary)]">
|
|
139
|
+
To get started,{' '}
|
|
140
|
+
<a className="text-[var(--wui-color-link)] hover:text-[var(--wui-color-link-hover)]" rel="noopener noreferrer" target="_blank" href="https://algorand.co/ecosystem/directory?tags=CEX">
|
|
141
|
+
fund
|
|
142
|
+
</a>{' '}
|
|
143
|
+
your new Algorand account via CEX, Card onramp, or{' '}
|
|
144
|
+
<a className="text-[var(--wui-color-link)] hover:text-[var(--wui-color-link-hover)]" rel="noopener noreferrer" target="_blank" href="https://core.allbridge.io/?ft=USDC&tt=USDC&f=BAS&t=ALG">
|
|
145
|
+
bridge USDC
|
|
146
|
+
</a>{' '}
|
|
147
|
+
in 2 minutes.
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Action buttons */}
|
|
152
|
+
<div className="px-6 py-4 border-t border-[var(--wui-color-border)] flex flex gap-2">
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => {
|
|
155
|
+
onClose()
|
|
156
|
+
window.open('https://algorand.co/algorand-start-here#hs_cos_wrapper_widget_1769533007886')
|
|
157
|
+
}}
|
|
158
|
+
className="w-full py-2.5 px-4 bg-[var(--wui-color-primary)] text-[var(--wui-color-primary-text)] font-medium rounded-xl hover:brightness-90 transition-all text-sm"
|
|
159
|
+
>
|
|
160
|
+
Get Started
|
|
161
|
+
</button>
|
|
162
|
+
<button
|
|
163
|
+
onClick={onClose}
|
|
164
|
+
className="w-full py-2.5 px-4 bg-[var(--wui-color-bg-tertiary)] text-[var(--wui-color-text-secondary)] font-medium rounded-xl hover:brightness-90 transition-all text-sm"
|
|
165
|
+
>
|
|
166
|
+
Close
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TransactionData, AssetInfo } from './types'
|
|
2
|
+
|
|
3
|
+
export function formatAssetAmount(rawAmount: bigint | string | undefined, info: AssetInfo): string {
|
|
4
|
+
if (rawAmount === undefined) return `0 ${info.unitName}`
|
|
5
|
+
const amount = typeof rawAmount === 'bigint' ? rawAmount : BigInt(rawAmount)
|
|
6
|
+
if (info.decimals === 0) return `${amount} ${info.unitName}`
|
|
7
|
+
const divisor = 10n ** BigInt(info.decimals)
|
|
8
|
+
const whole = amount / divisor
|
|
9
|
+
const remainder = amount % divisor
|
|
10
|
+
if (remainder === 0n) return `${whole} ${info.unitName}`
|
|
11
|
+
const frac = remainder.toString().padStart(info.decimals, '0').replace(/0+$/, '')
|
|
12
|
+
return `${whole}.${frac} ${info.unitName}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assetLabel(txn: TransactionData, info?: AssetInfo): string {
|
|
16
|
+
if (info?.unitName) return info.unitName
|
|
17
|
+
if (!txn.assetIndex) return 'ASA'
|
|
18
|
+
return `ASA#${txn.assetIndex}`
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useQueries } from '@tanstack/react-query'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import type { AssetInfo, AssetLookupClient } from '../types'
|
|
4
|
+
|
|
5
|
+
export function useAssets(
|
|
6
|
+
assetIds: string[],
|
|
7
|
+
algodClient: AssetLookupClient | undefined,
|
|
8
|
+
): {
|
|
9
|
+
loading: boolean
|
|
10
|
+
assets: Record<string, AssetInfo>
|
|
11
|
+
} {
|
|
12
|
+
const results = useQueries({
|
|
13
|
+
queries: assetIds.map((id) => ({
|
|
14
|
+
queryKey: ['asset', id],
|
|
15
|
+
queryFn: async (): Promise<AssetInfo> => {
|
|
16
|
+
const result = await algodClient!.getAssetByID(Number(id)).do()
|
|
17
|
+
return {
|
|
18
|
+
decimals: result.params.decimals,
|
|
19
|
+
unitName: result.params.unitName || '',
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
retries: 0,
|
|
23
|
+
enabled: !!algodClient,
|
|
24
|
+
staleTime: Infinity,
|
|
25
|
+
gcTime: Infinity,
|
|
26
|
+
})),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const loading = results.some((r) => r.isLoading)
|
|
30
|
+
|
|
31
|
+
const assets = useMemo(() => {
|
|
32
|
+
const map: Record<string, AssetInfo> = {}
|
|
33
|
+
for (let i = 0; i < assetIds.length; i++) {
|
|
34
|
+
const data = results[i]?.data
|
|
35
|
+
if (data) {
|
|
36
|
+
map[assetIds[i]] = data
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return map
|
|
40
|
+
}, [assetIds, results])
|
|
41
|
+
|
|
42
|
+
return { loading, assets }
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useAssets } from './useAssets'
|
|
3
|
+
import type { TransactionData, AssetInfo, AssetLookupClient } from '../types'
|
|
4
|
+
|
|
5
|
+
export function useTransactionData(
|
|
6
|
+
transactions: TransactionData[],
|
|
7
|
+
options?: {
|
|
8
|
+
algodClient?: AssetLookupClient
|
|
9
|
+
getApplicationAddress?: (appId: number) => { toString(): string }
|
|
10
|
+
},
|
|
11
|
+
): { loading: boolean; assets: Record<string, AssetInfo>; appEscrows: Record<string, string> } {
|
|
12
|
+
const algodClient = options?.algodClient
|
|
13
|
+
const getApplicationAddress = options?.getApplicationAddress
|
|
14
|
+
|
|
15
|
+
const assetIds = useMemo(() => {
|
|
16
|
+
const ids = new Set<string>()
|
|
17
|
+
for (const txn of transactions) {
|
|
18
|
+
if (txn.assetIndex) {
|
|
19
|
+
ids.add(txn.assetIndex.toString())
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Array.from(ids)
|
|
23
|
+
}, [transactions])
|
|
24
|
+
|
|
25
|
+
const { loading, assets } = useAssets(assetIds, algodClient)
|
|
26
|
+
|
|
27
|
+
const appEscrows = useMemo(() => {
|
|
28
|
+
if (!getApplicationAddress) return {}
|
|
29
|
+
const escrows: Record<string, string> = {}
|
|
30
|
+
for (const txn of transactions) {
|
|
31
|
+
if (txn.type === 'appl' && txn.appIndex) {
|
|
32
|
+
const escrowAddr = getApplicationAddress(txn.appIndex)
|
|
33
|
+
escrows[escrowAddr.toString()] = `App ${txn.appIndex}`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return escrows
|
|
37
|
+
}, [transactions, getApplicationAddress])
|
|
38
|
+
|
|
39
|
+
return { loading, assets, appEscrows }
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type { TransactionData, TransactionDanger, AssetInfo, AssetLookupClient } from './types'
|
|
3
|
+
|
|
4
|
+
// Formatters
|
|
5
|
+
export { formatAssetAmount, assetLabel } from './formatters'
|
|
6
|
+
|
|
7
|
+
// Components
|
|
8
|
+
export { TransactionFlow } from './components/TransactionFlow'
|
|
9
|
+
export type { TransactionFlowProps } from './components/TransactionFlow'
|
|
10
|
+
export { TransactionReview } from './components/TransactionReview'
|
|
11
|
+
export type { TransactionReviewProps } from './components/TransactionReview'
|
|
12
|
+
|
|
13
|
+
// Shared UI
|
|
14
|
+
export { AlgoSymbol } from './components/AlgoSymbol'
|
|
15
|
+
export { Spinner } from './components/Spinner'
|
|
16
|
+
export { TransactionStatus } from './components/TransactionStatus'
|
|
17
|
+
export type { TransactionStatusValue } from './components/TransactionStatus'
|
|
18
|
+
export { OptInPanel } from './components/OptInPanel'
|
|
19
|
+
export type { OptInPanelProps } from './components/OptInPanel'
|
|
20
|
+
export { SendPanel } from './components/SendPanel'
|
|
21
|
+
export type { SendPanelProps } from './components/SendPanel'
|
|
22
|
+
export { ManagePanel } from './components/ManagePanel'
|
|
23
|
+
export type { ManagePanelProps } from './components/ManagePanel'
|
|
24
|
+
export { WelcomeContent } from './components/WelcomeContent'
|
|
25
|
+
export type { WelcomeContentProps } from './components/WelcomeContent'
|
|
26
|
+
|
|
27
|
+
// Hooks
|
|
28
|
+
export { useAssets } from './hooks/useAssets'
|
|
29
|
+
export { useTransactionData } from './hooks/useTransactionData'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified transaction data type used by shared UI components.
|
|
3
|
+
*
|
|
4
|
+
* Both `DecodedTransaction` (bigint rawAmount) from the React package
|
|
5
|
+
* and `SerializableDecodedTransaction` (string rawAmount) from the extension
|
|
6
|
+
* are structurally compatible with this type.
|
|
7
|
+
*/
|
|
8
|
+
export interface TransactionData {
|
|
9
|
+
index: number
|
|
10
|
+
type: string
|
|
11
|
+
typeLabel: string
|
|
12
|
+
sender: string
|
|
13
|
+
senderShort: string
|
|
14
|
+
receiver?: string
|
|
15
|
+
receiverShort?: string
|
|
16
|
+
amount?: string
|
|
17
|
+
rawAmount?: bigint | string
|
|
18
|
+
assetIndex?: number
|
|
19
|
+
appIndex?: number
|
|
20
|
+
rekeyTo?: string
|
|
21
|
+
rekeyToShort?: string
|
|
22
|
+
closeRemainderTo?: string
|
|
23
|
+
closeRemainderToShort?: string
|
|
24
|
+
freezeTarget?: string
|
|
25
|
+
freezeTargetShort?: string
|
|
26
|
+
isFreezing?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TransactionDanger = 'rekey' | 'closeTo' | false
|
|
30
|
+
|
|
31
|
+
export interface AssetInfo {
|
|
32
|
+
decimals: number
|
|
33
|
+
unitName: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal interface for algod client asset lookup.
|
|
38
|
+
* Avoids requiring algosdk as a dependency.
|
|
39
|
+
*/
|
|
40
|
+
export interface AssetLookupClient {
|
|
41
|
+
getAssetByID(id: number): { do(): Promise<{ params: { decimals: number; unitName?: string } }> }
|
|
42
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"noFallthroughCasesInSwitch": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|