@@ -11,8 +11,9 @@ import { RenderError } from "@/ui/Forms/FormField/FormField";
11
11
import { Select } from "@/ui/Forms/Select/Select" ;
12
12
import { TextInput } from "@/ui/Forms/TextInput/TextInput" ;
13
13
import { PillButton } from "@/ui/PillButton/PillButton" ;
14
+ import { CurrencyUtils } from "@/utils/currency" ;
14
15
import { hexToUTF16String } from "@/utils/hexToUTF16String" ;
15
- import { formatOre , parseIron } from "@/utils/ironUtils" ;
16
+ import { formatOre } from "@/utils/ironUtils" ;
16
17
import { asQueryString } from "@/utils/parseRouteQuery" ;
17
18
import { sliceToUtf8Bytes } from "@/utils/sliceToUtf8Bytes" ;
18
19
import { truncateString } from "@/utils/truncateString" ;
@@ -23,6 +24,9 @@ import {
23
24
TransactionData ,
24
25
TransactionFormData ,
25
26
transactionSchema ,
27
+ AccountType ,
28
+ BalanceType ,
29
+ AssetOptionType ,
26
30
} from "./transactionSchema" ;
27
31
import {
28
32
AccountSyncingMessage ,
@@ -64,17 +68,11 @@ const messages = defineMessages({
64
68
fastFeeLabel : {
65
69
defaultMessage : "Fast" ,
66
70
} ,
67
- unknownAsset : {
68
- defaultMessage : "unknown asset" ,
69
- } ,
70
71
estimatedFeeDefaultError : {
71
72
defaultMessage : "An error occurred while estimating the transaction fee" ,
72
73
} ,
73
74
} ) ;
74
75
75
- type AccountType = TRPCRouterOutputs [ "getAccounts" ] [ number ] ;
76
- type BalanceType = AccountType [ "balances" ] [ "iron" ] ;
77
-
78
76
function getAccountBalances ( account : AccountType ) : {
79
77
[ key : string ] : BalanceType ;
80
78
} {
@@ -134,7 +132,7 @@ export function SendAssetsFormContent({
134
132
} = useForm < TransactionFormData > ( {
135
133
resolver : zodResolver ( transactionSchema ) ,
136
134
defaultValues : {
137
- amount : 0 ,
135
+ amount : "0" ,
138
136
fromAccount : defaultAccount ,
139
137
toAccount : defaultToAddress ?? "" ,
140
138
assetId : defaultAssetId ,
@@ -143,26 +141,97 @@ export function SendAssetsFormContent({
143
141
} ) ;
144
142
145
143
const fromAccountValue = watch ( "fromAccount" ) ;
146
- const assetValue = watch ( "assetId" ) ;
144
+ const assetIdValue = watch ( "assetId" ) ;
147
145
const feeValue = watch ( "fee" ) ;
148
146
const amountValue = watch ( "amount" ) ;
149
147
const toAccountValue = watch ( "toAccount" ) ;
150
148
149
+ useEffect ( ( ) => {
150
+ // If the 'assetId' changes, reset the 'amount' field
151
+ // to prevent issues if there is a mismatch in decimals
152
+ // between two assets.
153
+ const _unused = assetIdValue ;
154
+ resetField ( "amount" ) ;
155
+ } , [ resetField , assetIdValue ] ) ;
156
+
157
+ const selectedAccount = useMemo ( ( ) => {
158
+ const match = accountsData ?. find (
159
+ ( account ) => account . name === fromAccountValue ,
160
+ ) ;
161
+ if ( ! match ) {
162
+ return accountsData [ 0 ] ;
163
+ }
164
+ return match ;
165
+ } , [ accountsData , fromAccountValue ] ) ;
166
+
167
+ const accountBalances = useMemo ( ( ) => {
168
+ return getAccountBalances ( selectedAccount ) ;
169
+ } , [ selectedAccount ] ) ;
170
+
171
+ const assetOptionsMap = useMemo ( ( ) => {
172
+ const entries : Array < [ string , AssetOptionType ] > = Object . values (
173
+ accountBalances ,
174
+ ) . map ( ( balance ) => {
175
+ const assetName = hexToUTF16String ( balance . asset . name ) ;
176
+ const confirmed = CurrencyUtils . render (
177
+ BigInt ( balance . confirmed ) ,
178
+ balance . asset . id ,
179
+ balance . asset . verification ,
180
+ ) ;
181
+ return [
182
+ balance . asset . id ,
183
+ {
184
+ assetName : assetName ,
185
+ label : assetName + ` (${ confirmed } )` ,
186
+ value : balance . asset . id ,
187
+ asset : balance . asset ,
188
+ } ,
189
+ ] ;
190
+ } ) ;
191
+ return new Map ( entries ) ;
192
+ } , [ accountBalances ] ) ;
193
+
194
+ const assetOptions = useMemo (
195
+ ( ) => Array . from ( assetOptionsMap . values ( ) ) ,
196
+ [ assetOptionsMap ] ,
197
+ ) ;
198
+
199
+ const assetAmountToSend = useMemo ( ( ) => {
200
+ const assetToSend = assetOptionsMap . get ( assetIdValue ) ;
201
+ if ( ! assetToSend ) {
202
+ return 0n ;
203
+ }
204
+
205
+ const [ amountToSend , conversionError ] = CurrencyUtils . tryMajorToMinor (
206
+ amountValue . toString ( ) ,
207
+ assetIdValue ,
208
+ assetToSend . asset . verification ,
209
+ ) ;
210
+
211
+ if ( conversionError ) {
212
+ return 0n ;
213
+ }
214
+
215
+ return amountToSend ;
216
+ } , [ amountValue , assetIdValue , assetOptionsMap ] ) ;
217
+
151
218
const { data : estimatedFeesData , error : estimatedFeesError } =
152
219
trpcReact . getEstimatedFees . useQuery (
153
220
{
154
221
accountName : fromAccountValue ,
155
222
output : {
156
- amount : parseIron ( amountValue ) ,
157
- assetId : assetValue ,
223
+ amount : Number ( assetAmountToSend ) ,
224
+ assetId : assetIdValue ,
158
225
memo : "" ,
159
- publicAddress : toAccountValue ,
226
+ // For fee estimation, the actual address of the recipient is not important, is just has to be
227
+ // a valid address. Therefore, we're just going to use the address of the first account.
228
+ publicAddress : accountsData [ 0 ] . address ,
160
229
} ,
161
230
} ,
162
231
{
163
232
retry : false ,
164
233
enabled :
165
- amountValue > 0 &&
234
+ Number ( amountValue ) > 0 &&
166
235
! errors . memo &&
167
236
! errors . amount &&
168
237
! errors . toAccount &&
@@ -191,37 +260,12 @@ export function SendAssetsFormContent({
191
260
return null ;
192
261
} , [ isAccountSynced , nodeStatusData ?. blockchain . synced ] ) ;
193
262
194
- const selectedAccount = useMemo ( ( ) => {
195
- const match = accountsData ?. find (
196
- ( account ) => account . name === fromAccountValue ,
197
- ) ;
198
- if ( ! match ) {
199
- return accountsData [ 0 ] ;
200
- }
201
- return match ;
202
- } , [ accountsData , fromAccountValue ] ) ;
203
-
204
- const accountBalances = useMemo ( ( ) => {
205
- return getAccountBalances ( selectedAccount ) ;
206
- } , [ selectedAccount ] ) ;
207
-
208
- const assetOptions = useMemo ( ( ) => {
209
- return Object . values ( accountBalances ) . map ( ( balance ) => {
210
- const assetName = hexToUTF16String ( balance . asset . name ) ;
211
- return {
212
- assetName : assetName ,
213
- label : assetName + ` (${ formatOre ( balance . confirmed ) } )` ,
214
- value : balance . asset . id ,
215
- } ;
216
- } ) ;
217
- } , [ accountBalances ] ) ;
218
-
219
263
// Resets asset field to $IRON if a newly selected account does not have the selected asset
220
264
useEffect ( ( ) => {
221
- if ( ! Object . hasOwn ( accountBalances , assetValue ) ) {
265
+ if ( ! Object . hasOwn ( accountBalances , assetIdValue ) ) {
222
266
resetField ( "assetId" ) ;
223
267
}
224
- } , [ assetValue , resetField , selectedAccount , accountBalances ] ) ;
268
+ } , [ assetIdValue , resetField , selectedAccount , accountBalances ] ) ;
225
269
226
270
const { data : contactsData } = trpcReact . getContacts . useQuery ( ) ;
227
271
const formattedContacts = useMemo ( ( ) => {
@@ -242,9 +286,8 @@ export function SendAssetsFormContent({
242
286
const currentBalance = Number (
243
287
accountBalances [ data . assetId ] . confirmed ,
244
288
) ;
245
- const amountAsIron = parseIron ( data . amount ) ;
246
289
247
- if ( currentBalance < amountAsIron ) {
290
+ if ( currentBalance < assetAmountToSend ) {
248
291
setError ( "amount" , {
249
292
type : "custom" ,
250
293
message : formatMessage ( messages . insufficientFundsError ) ,
@@ -262,11 +305,12 @@ export function SendAssetsFormContent({
262
305
}
263
306
264
307
const fee = estimatedFeesData [ data . fee ] ;
308
+
265
309
setPendingTransaction ( {
266
310
fromAccount : data . fromAccount ,
267
311
toAccount : data . toAccount ,
268
312
assetId : data . assetId ,
269
- amount : parseIron ( data . amount ) ,
313
+ amount : assetAmountToSend . toString ( ) ,
270
314
fee : fee ,
271
315
memo : data . memo ?? "" ,
272
316
} ) ;
@@ -292,7 +336,7 @@ export function SendAssetsFormContent({
292
336
293
337
< Select
294
338
{ ...register ( "assetId" ) }
295
- value = { assetValue }
339
+ value = { assetIdValue }
296
340
label = { formatMessage ( messages . assetLabel ) }
297
341
options = { assetOptions }
298
342
error = { errors . assetId ?. message }
@@ -317,27 +361,37 @@ export function SendAssetsFormContent({
317
361
return ;
318
362
}
319
363
364
+ const assetToSend = assetOptionsMap . get ( assetIdValue ) ;
365
+ const decimals =
366
+ assetToSend ?. asset . verification ?. decimals ?? 0 ;
367
+
320
368
let finalValue = azValue ;
321
369
322
- // only allow up to 8 decimal places
323
- const parts = azValue . split ( "." ) ;
324
- if ( parts [ 1 ] ?. length > 8 ) {
325
- finalValue = `${ parts [ 0 ] } .${ parts [ 1 ] . slice ( 0 , 8 ) } ` ;
370
+ if ( decimals === 0 ) {
371
+ // If decimals is 0, take the left side of the decimal.
372
+ // If no decimal is present, this will still work correctly.
373
+ finalValue = azValue . split ( "." ) [ 0 ] ;
374
+ } else {
375
+ // Otherwise, take the left side of the decimal and up to the correct number of decimal places.
376
+ const parts = azValue . split ( "." ) ;
377
+ if ( parts [ 1 ] ?. length > decimals ) {
378
+ finalValue = `${ parts [ 0 ] } .${ parts [ 1 ] . slice ( 0 , decimals ) } ` ;
379
+ }
326
380
}
327
381
328
382
field . onChange ( finalValue ) ;
329
383
} }
330
384
onFocus = { ( ) => {
331
- if ( field . value === 0 ) {
385
+ if ( field . value === "0" ) {
332
386
field . onChange ( "" ) ;
333
387
}
334
388
} }
335
389
onBlur = { ( ) => {
336
390
if ( ! field . value ) {
337
- field . onChange ( 0 ) ;
391
+ field . onChange ( "0" ) ;
338
392
}
339
- if ( String ( field . value ) . endsWith ( "." ) ) {
340
- field . onChange ( String ( field . value ) . slice ( 0 , - 1 ) ) ;
393
+ if ( field . value . endsWith ( "." ) ) {
394
+ field . onChange ( field . value . slice ( 0 , - 1 ) ) ;
341
395
}
342
396
} }
343
397
label = { formatMessage ( messages . amountLabel ) }
@@ -417,17 +471,16 @@ export function SendAssetsFormContent({
417
471
</ PillButton >
418
472
</ HStack >
419
473
</ chakra . form >
420
- < ConfirmTransactionModal
421
- isOpen = { ! ! pendingTransaction }
422
- transactionData = { pendingTransaction }
423
- selectedAssetName = {
424
- assetOptions . find ( ( { value } ) => value === assetValue ) ?. assetName ??
425
- formatMessage ( messages . unknownAsset )
426
- }
427
- onCancel = { ( ) => {
428
- setPendingTransaction ( null ) ;
429
- } }
430
- />
474
+ { pendingTransaction && (
475
+ < ConfirmTransactionModal
476
+ isOpen
477
+ transactionData = { pendingTransaction }
478
+ selectedAsset = { assetOptionsMap . get ( assetIdValue ) }
479
+ onCancel = { ( ) => {
480
+ setPendingTransaction ( null ) ;
481
+ } }
482
+ />
483
+ ) }
431
484
</ >
432
485
) ;
433
486
}
0 commit comments