@@ -4,7 +4,7 @@ import { useFacade } from "../../data/facades";
4
4
import { useState , useMemo } from "react" ;
5
5
import { IRON_ASSET_ID_HEX } from "../../data/constants" ;
6
6
import { CurrencyUtils } from "@ironfish/sdk" ;
7
- import { useQueries , useMutation } from "@tanstack/react-query" ;
7
+ import { useQueries } from "@tanstack/react-query" ;
8
8
import { Asset } from "@/data/facades/chain/types" ;
9
9
import { AccountBalance } from "@/data/facades/wallet/types" ;
10
10
import {
@@ -20,7 +20,6 @@ import {
20
20
IndexPath ,
21
21
IconProps ,
22
22
Modal ,
23
- Spinner ,
24
23
} from "@ui-kitten/components" ;
25
24
import SendConfirmed from "../../svgs/SendConfirmed" ;
26
25
import Rubics from "../../svgs/Rubics" ;
@@ -36,19 +35,81 @@ const isValidBigInt = (num: string) => {
36
35
}
37
36
} ;
38
37
39
- const isValidAmount = ( value : string , decimals : number ) => {
38
+ const convertAmountToMinor = (
39
+ amount : string ,
40
+ assetId : string ,
41
+ assetMap : Map < string , Asset > ,
42
+ ) : [ bigint , null ] | [ null , Error ] => {
43
+ const asset =
44
+ assetId === IRON_ASSET_ID_HEX ? undefined : assetMap . get ( assetId ) ;
45
+ return CurrencyUtils . tryMajorToMinor ( amount , assetId , {
46
+ decimals : getAssetDecimals ( asset ) ,
47
+ } ) ;
48
+ } ;
49
+
50
+ const isValidAmount = (
51
+ value : string ,
52
+ assetId : string ,
53
+ assetMap : Map < string , Asset > ,
54
+ ) => {
40
55
if ( value . length === 0 ) return true ;
56
+
57
+ const asset =
58
+ assetId === IRON_ASSET_ID_HEX ? undefined : assetMap . get ( assetId ) ;
59
+
60
+ // For unverified assets, don't allow any decimals
61
+ if ( asset && asset . verification . status !== "verified" ) {
62
+ return ! value . includes ( "." ) ;
63
+ }
64
+
65
+ const decimals = getAssetDecimals ( asset ) ?? 8 ; // $IRON has 8 decimals by default
41
66
const parts = value . split ( "." ) ;
42
67
return parts . length <= 2 && ( parts [ 1 ] ?. length ?? 0 ) <= decimals ;
43
68
} ;
44
69
70
+ const enforceDecimals = (
71
+ value : string ,
72
+ assetId : string ,
73
+ assetMap : Map < string , Asset > ,
74
+ ) : string => {
75
+ if ( value . length === 0 ) return value ;
76
+
77
+ const asset =
78
+ assetId === IRON_ASSET_ID_HEX ? undefined : assetMap . get ( assetId ) ;
79
+
80
+ // For unverified assets, remove any decimal points
81
+ if ( asset && asset . verification . status !== "verified" ) {
82
+ return value . replace ( / \. / g, "" ) ;
83
+ }
84
+
85
+ const decimals = getAssetDecimals ( asset ) ?? 8 ;
86
+ const parts = value . split ( "." ) ;
87
+ if ( parts . length === 2 && parts [ 1 ] . length > decimals ) {
88
+ return `${ parts [ 0 ] } .${ parts [ 1 ] . slice ( 0 , decimals ) } ` ;
89
+ }
90
+
91
+ return value ;
92
+ } ;
93
+
45
94
const CheckIcon = ( props : IconProps ) => (
46
95
< Icon { ...props } name = "checkmark-outline" />
47
96
) ;
48
97
49
98
// First add a new type for the transaction state
50
99
type TransactionState = "sending" | "sent" | "idle" ;
51
100
101
+ // Add this helper at the top with other utility functions
102
+ const getAssetDecimals = ( asset : Asset | undefined ) : number | undefined => {
103
+ if ( ! asset ) return undefined ;
104
+ try {
105
+ return asset . verification . status === "verified"
106
+ ? asset . verification . decimals
107
+ : JSON . parse ( asset . metadata ) . decimals ;
108
+ } catch {
109
+ return undefined ;
110
+ }
111
+ } ;
112
+
52
113
export default function Send ( ) {
53
114
const facade = useFacade ( ) ;
54
115
const router = useRouter ( ) ;
@@ -139,16 +200,28 @@ export default function Send() {
139
200
) ;
140
201
} , [ selectedAssetId , assetOptions ] ) ;
141
202
142
- const decimals =
143
- selectedAssetId === IRON_ASSET_ID_HEX
144
- ? 8
145
- : assetMap . get ( selectedAssetId ) ?. verification . status === "verified"
146
- ? ( assetMap . get ( selectedAssetId ) ?. verification . decimals ?? 0 )
147
- : 0 ;
148
-
149
203
// Add the mutation
150
204
const sendTransactionMutation = facade . sendTransaction . useMutation ( ) ;
151
205
206
+ // Add amount validation
207
+ const amountError = useMemo ( ( ) => {
208
+ if ( ! amount ) return undefined ;
209
+
210
+ if ( ! isValidAmount ( amount , selectedAssetId , assetMap ) ) {
211
+ const asset =
212
+ selectedAssetId === IRON_ASSET_ID_HEX
213
+ ? undefined
214
+ : assetMap . get ( selectedAssetId ) ;
215
+ const decimals = getAssetDecimals ( asset ) ?? 8 ;
216
+ return `Maximum ${ decimals } decimal places allowed` ;
217
+ }
218
+
219
+ const [ amountInMinorUnits ] =
220
+ convertAmountToMinor ( amount , selectedAssetId , assetMap ) ?? [ ] ;
221
+
222
+ return undefined ;
223
+ } , [ amount , selectedAssetId , assetMap ] ) ;
224
+
152
225
return (
153
226
< Layout style = { styles . container } level = "1" >
154
227
< Stack . Screen options = { { headerTitle : "Send Transaction" } } />
@@ -194,13 +267,22 @@ export default function Send() {
194
267
placeholder = "Enter amount"
195
268
value = { amount }
196
269
onChangeText = { ( value ) => {
197
- if ( isValidAmount ( value , decimals ) ) {
198
- setAmount ( value ) ;
199
- }
270
+ const sanitized = enforceDecimals ( value , selectedAssetId , assetMap ) ;
271
+ setAmount ( sanitized ) ;
200
272
} }
201
273
style = { styles . input }
202
- keyboardType = "numeric"
203
- caption = { `Maximum ${ decimals } decimal places` }
274
+ status = { amountError ? "danger" : "basic" }
275
+ caption = {
276
+ amountError ||
277
+ ( assetMap . get ( selectedAssetId ) ?. verification . status === "unverified"
278
+ ? "No decimals allowed"
279
+ : `Up to ${
280
+ selectedAssetId === IRON_ASSET_ID_HEX
281
+ ? "8"
282
+ : ( getAssetDecimals ( assetMap . get ( selectedAssetId ) ) ?? "0" )
283
+ } decimal places`)
284
+ }
285
+ keyboardType = "decimal-pad"
204
286
/>
205
287
206
288
< Input
@@ -244,17 +326,25 @@ export default function Send() {
244
326
disabled = {
245
327
transactionState !== "idle" ||
246
328
isValidPublicAddress . data !== true ||
247
- ! isValidBigInt ( amount ) ||
329
+ ! amount ||
330
+ ! ! amountError ||
248
331
( selectedFee === "custom" && ! isValidBigInt ( customFee ) )
249
332
}
250
333
onPress = { async ( ) => {
251
334
try {
252
335
setTransactionState ( "sending" ) ;
253
336
337
+ const [ amountInMinorUnits ] =
338
+ convertAmountToMinor ( amount , selectedAssetId , assetMap ) ?? [ ] ;
339
+
340
+ if ( ! amountInMinorUnits ) {
341
+ throw new Error ( "Invalid amount" ) ;
342
+ }
343
+
254
344
const outputs = [
255
345
{
256
346
publicAddress : selectedRecipient ,
257
- amount : amount ,
347
+ amount : amountInMinorUnits . toString ( ) ,
258
348
memo : memo ,
259
349
assetId : selectedAssetId ,
260
350
} ,
0 commit comments