2
2
/* eslint-disable react/no-multi-comp */
3
3
/* eslint-disable complexity */
4
4
/* eslint-disable no-magic-numbers */
5
+ /* eslint-disable max-statements */
5
6
import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
6
7
import { Button , Input , Search } from '@lace/common' ;
7
8
import styles from './SendStepOne.module.scss' ;
8
9
import mainStyles from './SendFlow.module.scss' ;
9
- import { AssetInput } from '@lace/core' ;
10
+ import { AssetInput , HANDLE_DEBOUNCE_TIME , isHandle } from '@lace/core' ;
10
11
import BigNumber from 'bignumber.js' ;
11
- import { useFetchCoinPrice } from '@hooks' ;
12
+ import { useFetchCoinPrice , useHandleResolver } from '@hooks' ;
12
13
import { CoreTranslationKey } from '@lace/translation' ;
13
14
import { Box , Flex , Text , ToggleButtonGroup } from '@input-output-hk/lace-ui-toolkit' ;
14
15
import { Bitcoin } from '@lace/bitcoin' ;
15
16
import { useTranslation } from 'react-i18next' ;
16
17
import { formatNumberForDisplay } from '@utils/format-number' ;
18
+ import { isAdaHandleEnabled } from '@src/features/ada-handle/config' ;
19
+ import { Asset } from '@cardano-sdk/core' ;
20
+ import debounce from 'lodash/debounce' ;
21
+ import { CustomConflictError , CustomError , ensureHandleOwnerHasntChanged , verifyHandle } from '@utils/validators' ;
22
+ import { AddressValue , HandleVerificationState } from './types' ;
23
+ import { CheckCircleOutlined , ExclamationCircleOutlined , LoadingOutlined } from '@ant-design/icons' ;
17
24
18
25
const SATS_IN_BTC = 100_000_000 ;
19
26
@@ -34,9 +41,9 @@ const fees: RecommendedFee[] = [
34
41
interface SendStepOneProps {
35
42
amount : string ;
36
43
onAmountChange : ( value : string ) => void ;
37
- address : string ;
44
+ address : AddressValue ;
38
45
availableBalance : number ;
39
- onAddressChange : ( value : string ) => void ;
46
+ onAddressChange : ( value : AddressValue ) => void ;
40
47
feeMarkets : Bitcoin . EstimatedFees | null ;
41
48
onEstimatedTimeChange : ( value : string ) => void ;
42
49
onContinue : ( feeRate : number ) => void ;
@@ -64,6 +71,10 @@ const InputError = ({
64
71
</ Box >
65
72
) ;
66
73
74
+ type HandleIcons = {
75
+ [ key in HandleVerificationState ] : JSX . Element | undefined ;
76
+ } ;
77
+
67
78
export const SendStepOne : React . FC < SendStepOneProps > = ( {
68
79
amount,
69
80
onAmountChange,
@@ -83,6 +94,7 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
83
94
const hasNoValue = numericAmount === 0 ;
84
95
const exceedsBalance = numericAmount > availableBalance / SATS_IN_BTC ;
85
96
const [ feeRate , setFeeRate ] = useState < number > ( 1 ) ;
97
+ const handleResolver = useHandleResolver ( ) ;
86
98
87
99
const getFees = useCallback (
88
100
( ) =>
@@ -93,6 +105,7 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
93
105
[ feeMarkets ]
94
106
) ;
95
107
108
+ const [ handleVerificationState , setHandleVerificationState ] = useState < HandleVerificationState | undefined > ( ) ;
96
109
const [ recommendedFees , setRecommendedFees ] = useState < RecommendedFee [ ] > ( getFees ( ) ) ;
97
110
const [ selectedFeeKey , setSelectedFeeKey ] = useState < RecommendedFee [ 'key' ] > (
98
111
recommendedFees . find ( ( f ) => f . feeRate === feeRate ) ?. key || recommendedFees [ 1 ] ?. key
@@ -101,10 +114,28 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
101
114
( ) => recommendedFees . find ( ( f ) => f . key === selectedFeeKey ) ,
102
115
[ recommendedFees , selectedFeeKey ]
103
116
) ;
117
+
104
118
const [ customFee , setCustomFee ] = useState < string > ( '0' ) ;
105
119
const [ customFeeError , setCustomFeeError ] = useState < string | undefined > ( ) ;
106
120
const [ isValidAddress , setIsValidAddress ] = useState < boolean > ( false ) ;
107
121
const [ invalidAddressError , setInvalidAddressError ] = useState < string | undefined > ( ) ;
122
+ const [ icon , setIcon ] = useState < JSX . Element | undefined > ( ) ;
123
+
124
+ const calcIcon = debounce ( ( state : HandleVerificationState | undefined ) => {
125
+ const handleIcons : HandleIcons = {
126
+ [ HandleVerificationState . VALID ] : < CheckCircleOutlined className = { styles . valid } /> ,
127
+ [ HandleVerificationState . CHANGED_OWNERSHIP ] : < CheckCircleOutlined className = { styles . valid } /> ,
128
+ [ HandleVerificationState . INVALID ] : < ExclamationCircleOutlined className = { styles . invalid } /> ,
129
+ [ HandleVerificationState . VERIFYING ] : < LoadingOutlined className = { styles . loading } />
130
+ } ;
131
+ const handleIcon = ( state && handleIcons [ state ] ) || undefined ;
132
+ setIcon ( handleIcon ) ;
133
+ } , HANDLE_DEBOUNCE_TIME ) ;
134
+
135
+ useEffect ( ( ) => {
136
+ calcIcon ( handleVerificationState ) ;
137
+ return ( ) => calcIcon . cancel ( ) ;
138
+ } , [ handleVerificationState , calcIcon ] ) ;
108
139
109
140
useEffect ( ( ) => {
110
141
// eslint-disable-next-line unicorn/no-useless-undefined
@@ -125,7 +156,12 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
125
156
} , [ getFees ] ) ;
126
157
127
158
const handleNext = ( ) => {
128
- if ( hasNoValue || exceedsBalance || address . trim ( ) === '' ) return ;
159
+ if (
160
+ hasNoValue || exceedsBalance || address ?. isHandle
161
+ ? address ?. resolvedAddress . trim ( ) === ''
162
+ : address ?. address . trim ( ) === ''
163
+ )
164
+ return ;
129
165
130
166
const newFeeRate =
131
167
selectedFeeKey === 'custom'
@@ -149,8 +185,102 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
149
185
150
186
const fiatValue = `≈ ${ new BigNumber ( enteredAmount . toString ( ) ) . toFixed ( 2 , BigNumber . ROUND_HALF_UP ) } USD` ;
151
187
188
+ const isAddressInputInvalidHandle =
189
+ isAdaHandleEnabled &&
190
+ isHandle ( address ?. address ) &&
191
+ ! Asset . util . isValidHandle ( address ?. address ?. toString ( ) . slice ( 1 ) . toLowerCase ( ) ) ;
192
+
193
+ const isAddressInputValueHandle = isAdaHandleEnabled && isHandle ( address ?. address ) ;
194
+
195
+ const resolveHandle = useMemo (
196
+ ( ) =>
197
+ debounce ( async ( ) => {
198
+ setHandleVerificationState ( HandleVerificationState . VERIFYING ) ;
199
+
200
+ if ( isAddressInputInvalidHandle ) {
201
+ setHandleVerificationState ( HandleVerificationState . INVALID ) ;
202
+ }
203
+ if ( ! address ?. handleResolution ) {
204
+ const { valid, handles } = await verifyHandle ( address ?. address , handleResolver ) ;
205
+
206
+ if ( valid && ! ! handles [ 0 ] . addresses ?. bitcoin ) {
207
+ setHandleVerificationState ( HandleVerificationState . VALID ) ;
208
+
209
+ onAddressChange ( {
210
+ address : address ?. address ,
211
+ resolvedAddress : handles [ 0 ] . addresses ?. bitcoin ,
212
+ isHandle : true ,
213
+ handleResolution : handles [ 0 ]
214
+ } ) ;
215
+ } else {
216
+ setHandleVerificationState ( HandleVerificationState . INVALID ) ;
217
+ }
218
+ return ;
219
+ }
220
+
221
+ try {
222
+ await ensureHandleOwnerHasntChanged ( {
223
+ force : true ,
224
+ handleResolution : address ?. handleResolution ,
225
+ handleResolver
226
+ } ) ;
227
+
228
+ setHandleVerificationState ( HandleVerificationState . VALID ) ;
229
+ } catch ( error ) {
230
+ if ( error instanceof CustomError && error . isValidHandle === false ) {
231
+ setHandleVerificationState ( HandleVerificationState . INVALID ) ;
232
+ }
233
+ if ( error instanceof CustomConflictError ) {
234
+ setHandleVerificationState ( HandleVerificationState . CHANGED_OWNERSHIP ) ;
235
+ }
236
+ }
237
+ } , HANDLE_DEBOUNCE_TIME ) ,
238
+ [ address ?. address , address ?. handleResolution , onAddressChange , handleResolver , isAddressInputInvalidHandle ]
239
+ ) ;
240
+
241
+ useEffect ( ( ) => {
242
+ if ( ! address ) {
243
+ return ;
244
+ }
245
+
246
+ if ( isAddressInputValueHandle ) {
247
+ resolveHandle ( ) ;
248
+ } else {
249
+ // eslint-disable-next-line unicorn/no-useless-undefined
250
+ setHandleVerificationState ( undefined ) ;
251
+ }
252
+
253
+ // eslint-disable-next-line consistent-return
254
+ return ( ) => {
255
+ resolveHandle && resolveHandle . cancel ( ) ;
256
+ } ;
257
+ } , [ address , setHandleVerificationState , resolveHandle , isAddressInputValueHandle ] ) ;
258
+
152
259
useEffect ( ( ) => {
153
- const result = Bitcoin . validateBitcoinAddress ( address , network ) ;
260
+ if ( handleVerificationState === HandleVerificationState . VERIFYING ) return ;
261
+
262
+ if ( isAddressInputValueHandle ) {
263
+ if (
264
+ ! address ?. isHandle ||
265
+ isAddressInputInvalidHandle ||
266
+ handleVerificationState === HandleVerificationState . INVALID
267
+ ) {
268
+ setIsValidAddress ( false ) ;
269
+ setInvalidAddressError ( t ( 'core.destinationAddressInput.invalidBitcoinHandle' ) ) ;
270
+ return ;
271
+ }
272
+
273
+ if ( handleVerificationState === HandleVerificationState . CHANGED_OWNERSHIP ) {
274
+ setIsValidAddress ( false ) ;
275
+ setInvalidAddressError ( t ( 'core.destinationAddressInput.handleChangedOwner' ) ) ;
276
+ return ;
277
+ }
278
+ }
279
+
280
+ const addressToValidate = address ?. isHandle ? address ?. resolvedAddress : address ?. address ;
281
+
282
+ if ( ! addressToValidate ) return ;
283
+ const result = Bitcoin . validateBitcoinAddress ( addressToValidate , network ) ;
154
284
155
285
switch ( result ) {
156
286
case Bitcoin . AddressValidationResult . Valid :
@@ -167,7 +297,15 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
167
297
setInvalidAddressError ( t ( 'general.errors.incorrectAddress' ) ) ;
168
298
break ;
169
299
}
170
- } , [ address , network , onAddressChange , t ] ) ;
300
+ } , [
301
+ isAddressInputValueHandle ,
302
+ isAddressInputInvalidHandle ,
303
+ handleVerificationState ,
304
+ address ,
305
+ network ,
306
+ onAddressChange ,
307
+ t
308
+ ] ) ;
171
309
172
310
const handleCustomFeeKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
173
311
const disallowedKeys = [ '-' , '+' , 'e' , ',' ] ;
@@ -196,14 +334,22 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
196
334
197
335
< Search
198
336
disabled = { hasUtxosInMempool }
199
- value = { address }
337
+ value = { address ?. address }
200
338
data-testid = "btc-address-input"
201
339
label = { t ( 'core.destinationAddressInput.recipientAddressOnly' ) }
202
- onChange = { ( value ) => onAddressChange ( value ) }
340
+ onChange = { ( value ) => {
341
+ setHandleVerificationState ( HandleVerificationState . VERIFYING ) ;
342
+ // eslint-disable-next-line unicorn/no-useless-undefined
343
+ setInvalidAddressError ( undefined ) ;
344
+ onAddressChange ( { isHandle : false , address : value , resolvedAddress : '' } ) ;
345
+ } }
203
346
style = { { width : '100%' } }
347
+ customIcon = { icon }
204
348
/>
205
349
206
- { ! isValidAddress && ! ! address ?. length && < InputError error = { invalidAddressError } isPopupView = { isPopupView } /> }
350
+ { ! isValidAddress && ! ! address ?. address ?. length && (
351
+ < InputError error = { invalidAddressError } isPopupView = { isPopupView } />
352
+ ) }
207
353
208
354
< Box w = "$fill" mt = { isPopupView ? '$16' : '$40' } py = "$24" px = "$32" className = { styles . amountSection } >
209
355
< AssetInput
@@ -299,7 +445,7 @@ export const SendStepOne: React.FC<SendStepOneProps> = ({
299
445
className = { mainStyles . buttons }
300
446
>
301
447
< Button
302
- disabled = { hasNoValue || exceedsBalance || address . trim ( ) === '' || ! isValidAddress }
448
+ disabled = { hasNoValue || exceedsBalance || ! address || address ?. address ? .trim ( ) === '' || ! isValidAddress }
303
449
color = "primary"
304
450
block
305
451
size = "medium"
0 commit comments