-
Notifications
You must be signed in to change notification settings - Fork 5k
/
Copy pathsend.js
3635 lines (3391 loc) · 126 KB
/
send.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import {
createAsyncThunk,
createSelector,
createSlice,
} from '@reduxjs/toolkit';
import BigNumber from 'bignumber.js';
import { addHexPrefix, zeroAddress } from 'ethereumjs-util';
import { cloneDeep, debounce } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { providerErrors } from '@metamask/rpc-errors';
import {
TransactionEnvelopeType,
TransactionType,
} from '@metamask/transaction-controller';
import { getErrorMessage } from '../../../shared/modules/error';
import {
decimalToHex,
hexToDecimal,
} from '../../../shared/modules/conversion.utils';
import { GasEstimateTypes, GAS_LIMITS } from '../../../shared/constants/gas';
import {
CONTRACT_ADDRESS_ERROR,
FLOAT_TOKENS_ERROR,
INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_FUNDS_FOR_GAS_ERROR,
INSUFFICIENT_TOKENS_ERROR,
NEGATIVE_OR_ZERO_AMOUNT_TOKENS_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR,
KNOWN_RECIPIENT_ADDRESS_WARNING,
RECIPIENT_TYPES,
SWAPS_NO_QUOTES,
SWAPS_QUOTES_ERROR,
} from '../../pages/confirmations/send/send.constants';
import {
isBalanceSufficient,
isERC1155BalanceSufficient,
isTokenBalanceSufficient,
} from '../../pages/confirmations/send/send.utils';
import {
getAdvancedInlineGasShown,
getCurrentChainId,
getGasPriceInHexWei,
getIsMainnet,
getTargetAccount,
getIsNonStandardEthChain,
checkNetworkAndAccountSupports1559,
getUseTokenDetection,
getTokenList,
getAddressBookEntryOrAccountName,
getEnsResolutionByAddress,
getSelectedAccount,
getSelectedInternalAccount,
getSelectedInternalAccountWithBalance,
getUnapprovedTransactions,
getSelectedNetworkClientId,
getIsSwapsChain,
getUseExternalServices,
} from '../../selectors';
import {
displayWarning,
hideLoadingIndication,
showLoadingIndication,
updateEditableParams,
updateTransactionGasFees,
addPollingTokenToAppState,
removePollingTokenFromAppState,
isNftOwner,
getTokenStandardAndDetails,
showModal,
addTransactionAndRouteToConfirmationPage,
updateTransactionSendFlowHistory,
getCurrentNetworkEIP1559Compatibility,
getLayer1GasFee,
gasFeeStopPollingByPollingToken,
gasFeeStartPollingByNetworkClientId,
getBalancesInSingleCall,
estimateGas,
addTransactionAndWaitForPublish,
setDefaultHomeActiveTabName,
rejectPendingApproval,
} from '../../store/actions';
import { setCustomGasLimit } from '../gas/gas.duck';
import {
QR_CODE_DETECTED,
SELECTED_ACCOUNT_CHANGED,
ACCOUNT_CHANGED,
ADDRESS_BOOK_UPDATED,
GAS_FEE_ESTIMATES_UPDATED,
CLEAR_SWAP_AND_SEND_STATE,
} from '../../store/actionConstants';
import {
getTokenAddressParam,
getTokenMetadata,
getTokenIdParam,
} from '../../helpers/utils/token-util';
import {
checkExistingAddresses,
isOriginContractAddress,
} from '../../helpers/utils/util';
import {
getGasEstimateType,
getNativeCurrency,
getProviderConfig,
getTokens,
} from '../metamask/metamask';
import { resetDomainResolution } from '../domains';
import {
isBurnAddress,
isPossibleAddress,
isValidHexAddress,
toChecksumHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { isSmartContractAddress } from '../../helpers/utils/transactions.util';
import {
AssetType,
TokenStandard,
} from '../../../shared/constants/transaction';
import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys';
import { SECOND } from '../../../shared/constants/time';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import { parseStandardTokenTransactionData } from '../../../shared/modules/transaction.utils';
import { getTokenValueParam } from '../../../shared/lib/metamask-controller-utils';
import {
calcGasTotal,
calcTokenAmount,
} from '../../../shared/lib/transactions-controller-utils';
import { Numeric } from '../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../shared/constants/common';
import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps';
import { setMaxValueMode } from '../confirm-transaction/confirm-transaction.duck';
// used for typing
// eslint-disable-next-line no-unused-vars
import {
CONFIRM_TRANSACTION_ROUTE,
DEFAULT_ROUTE,
} from '../../helpers/constants/routes';
import { fetchBlockedTokens } from '../../pages/swaps/swaps.util';
import {
getDisabledSwapAndSendNetworksFromAPI,
getSwapAndSendQuotes,
} from './swap-and-send-utils';
import {
estimateGasLimitForSend,
generateTransactionParams,
getRoundedGasPrice,
calculateBestQuote,
addAdjustedReturnToQuotes,
getIsDraftSwapAndSend,
} from './helpers';
const RECENT_REQUEST_ERROR =
'This has been replaced with a more recent request';
const FETCH_DELAY = SECOND;
// typedef import statements
/**
* @typedef {(
* import('immer/dist/internal').WritableDraft<SendState>
* )} SendStateDraft
* @typedef {(
* import( '../../helpers/constants/common').TokenStandardStrings
* )} TokenStandardStrings
* @typedef {(
* import( '../../../shared/constants/tokens').TokenDetails
* )} TokenDetails
* @typedef {(
* import('@metamask/gas-fee-controller').LegacyGasPriceEstimate
* )} LegacyGasPriceEstimate
* @typedef {(
* import('@metamask/gas-fee-controller').GasFeeEstimates
* )} GasFeeEstimates
* @typedef {(
* import('@metamask/gas-fee-controller').EthGasPriceEstimate
* )} EthGasPriceEstimate
* @typedef {(
* import('@metamask/gas-fee-controller').GasEstimateType
* )} GasEstimateType
* @typedef {(
* import('redux').AnyAction
* )} AnyAction
*/
/**
* @template R - Return type of the async function
* @typedef {(
* import('redux-thunk').ThunkAction<R, MetaMaskState, unknown, AnyAction>
* )} ThunkAction<R>
*/
/**
* This type will take a typical constant string mapped object and turn it into
* a union type of the values.
*
* @template O - The object to make strings out of
* @typedef {O[keyof O]} MapValuesToUnion<O>
*/
/**
* @typedef {object} SendStateStages
* @property {'ADD_RECIPIENT'} ADD_RECIPIENT - The user is selecting which
* address to send an asset to.
* @property {'DRAFT'} DRAFT - The send form is shown for a transaction yet to
* be sent to the Transaction Controller.
* @property {'EDIT'} EDIT - The send form is shown for a transaction already
* submitted to the Transaction Controller but not yet confirmed. This happens
* when a confirmation is shown for a transaction and the 'edit' button in the
* header is clicked.
* @property {'INACTIVE'} INACTIVE - The send state is idle, and hasn't yet
* fetched required data for gasPrice and gasLimit estimations, etc.
*/
/**
* The Stages that the send slice can be in
*
* @type {SendStateStages}
*/
export const SEND_STAGES = {
ADD_RECIPIENT: 'ADD_RECIPIENT',
DRAFT: 'DRAFT',
EDIT: 'EDIT',
INACTIVE: 'INACTIVE',
};
/**
* @typedef {object} DraftTxStatus
* @property {'INVALID'} INVALID - The transaction is invalid and cannot be
* submitted. There are a number of cases that would result in an invalid
* send state:
* 1. The recipient is not yet defined
* 2. The amount + gasTotal is greater than the user's balance when sending
* native currency
* 3. The gasTotal is greater than the user's *native* balance
* 4. The amount of sent asset is greater than the user's *asset* balance
* 5. Gas price estimates failed to load entirely
* 6. The gasLimit is less than 21000 (0x5208)
* @property {'VALID'} VALID - The transaction is valid and can be submitted.
*/
/**
* The status of the send slice
*
* @type {DraftTxStatus}
*/
export const SEND_STATUSES = {
INVALID: 'INVALID',
VALID: 'VALID',
};
/**
* @typedef {object} SendStateGasModes
* @property {'BASIC'} BASIC - Shows the basic estimate slow/avg/fast buttons
* when on mainnet and the metaswaps API request is successful.
* @property {'CUSTOM'} CUSTOM - Shows GasFeeDisplay component that is a read
* only display of the values the user has set in the advanced gas modal
* (stored in the gas duck under the customData key).
* @property {'INLINE'} INLINE - Shows inline gasLimit/gasPrice fields when on
* any other network or metaswaps API fails and we use eth_gasPrice.
*/
/**
* Controls what is displayed in the send-gas-row component.
*
* @type {SendStateGasModes}
*/
export const GAS_INPUT_MODES = {
BASIC: 'BASIC',
CUSTOM: 'CUSTOM',
INLINE: 'INLINE',
};
/**
* @typedef {object} SendStateAmountModes
* @property {'INPUT'} INPUT - the user provides the amount by typing in the
* field.
* @property {'MAX'} MAX - The user selects the MAX button and amount is
* calculated based on balance - (amount + gasTotal).
*/
/**
* The modes that the amount field can be set by
*
* @type {SendStateAmountModes}
*/
export const AMOUNT_MODES = {
INPUT: 'INPUT',
MAX: 'MAX',
};
/**
* @typedef {object} SendStateRecipientModes
* @property {'CONTACT_LIST'} CONTACT_LIST - The user is displayed a list of
* their contacts and addresses they have recently send to.
* @property {'MY_ACCOUNTS'} MY_ACCOUNTS - the user is displayed a list of
* their own accounts to send to.
*/
/**
* The type of recipient list that is displayed to user
*
* @type {SendStateRecipientModes}
*/
export const RECIPIENT_SEARCH_MODES = {
CONTACT_LIST: 'CONTACT_LIST',
MY_ACCOUNTS: 'MY_ACCOUNTS',
};
/**
* @typedef {object} Account
* @property {string} address - The hex address of the account.
* @property {string} balance - Hex string representing the native asset
* balance of the account the transaction will be sent from.
*/
/**
* @typedef {object} Amount
* @property {string} [error] - Error to display for the amount field.
* @property {string} value - A hex string representing the amount of the
* selected currency to send.
*/
/**
* @typedef {object} Asset
* @property {string} balance - A hex string representing the balance
* that the user holds of the asset that they are attempting to send.
* @property {TokenDetails} [details] - An object that describes the
* selected asset in the case that the user is sending a token or collectibe.
* Will be null when asset.type is 'NATIVE'.
* @property {string} [error] - Error to display when there is an issue
* with the asset.
* @property {AssetType} type - The type of asset that the user
* is attempting to send. Defaults to 'NATIVE' which represents the native
* asset of the chain. Can also be 'TOKEN' or 'NFT'.
*/
/**
* @typedef {object} GasFees
* @property {string} [error] - error to display for gas fields.
* @property {string} gasLimit - maximum gas needed for tx.
* @property {string} gasPrice - price in wei to pay per gas.
* @property {string} gasTotal - maximum total price in wei to pay.
* @property {string} maxFeePerGas - Maximum price in wei to pay per gas.
* @property {string} maxPriorityFeePerGas - Maximum priority fee in wei to pay
* per gas.
*/
/**
* An object that describes the intended recipient of a transaction.
*
* @typedef {object} Recipient
* @property {string} address - The fully qualified address of the recipient.
* This is set after the recipient.userInput is validated, the userInput field
* is quickly updated to avoid delay between keystrokes and seeing the input
* field updated. After a debounce the address typed is validated and then the
* address field is updated. The address field is also set when the user
* selects a contact or account from the list, or an ENS resolution when
* typing ENS names.
* @property {string} [error] - Error to display on the address field.
* @property {string} nickname - The nickname that the user has added to their
* address book for the recipient.address.
* @property {string} [warning] - Warning to display on the address field.
*/
/**
* @typedef {object} DraftTransaction
* @property {Amount} amount - An object containing information about the
* amount of currency to send.
* @property {Asset} sendAsset - An object that describes the asset that the user
* has selected to send.
* @property {Account} [fromAccount] - The send flow is usually only relative to
* the currently selected account. When editing a transaction, however, the
* account may differ. In that case, the details of that account will be
* stored in this object within the draftTransaction.
* @property {GasFees} gas - Details about the current gas settings
* @property {Array<{event: string, timestamp: number}>} history - An array of
* entries that describe the user's journey through the send flow. This is
* sent to the controller for attaching to state logs for troubleshooting and
* support.
* @property {string} [id] - If the transaction has already been added to the
* TransactionController this field will be populated with its id from the
* TransactionController state. This is required to be able to update the
* transaction in the controller.
* @property {boolean} isSwapQuoteLoading – is a swap quote being fetched
* @property {Quote[]} [quotes] – quotes for swaps
* @property {Asset} receiveAsset - An object that describes the asset that the user
* has selected for the recipient to receive.
* @property {Recipient} recipient - An object that describes the intended
* recipient of the transaction.
* @property {string} [swapQuotesError] - error message for swap quotes
* @property {number} [timeToFetchQuotes] time to fetch most recent swap+send quotes
* @property {MapValuesToUnion<DraftTxStatus>} status - Describes the
* validity of the draft transaction, which will be either 'VALID' or
* 'INVALID', depending on our ability to generate a valid txParams object for
* submission.
* @property {string} transactionType - Determines type of transaction being
* sent, defaulted to 0x0 (legacy).
* @property {string} [userInputHexData] - When a user has enabled custom hex
* data field in advanced options, they can supply data to the field which is
* stored under this key.
*/
/**
* @type {DraftTransaction}
*/
export const draftTransactionInitialState = {
amount: {
error: null,
value: '0x0',
},
sendAsset: {
balance: '0x0',
details: null,
error: null,
type: AssetType.native,
},
receiveAsset: {
balance: '0x0',
details: null,
error: null,
type: AssetType.native,
},
fromAccount: null,
gas: {
error: null,
gasLimit: '0x0',
gasPrice: '0x0',
gasTotal: '0x0',
maxFeePerGas: '0x0',
maxPriorityFeePerGas: '0x0',
wasManuallyEdited: false,
},
history: [],
id: null,
recipient: {
address: '',
error: null,
nickname: '',
warning: null,
type: '',
recipientWarningAcknowledged: false,
},
status: SEND_STATUSES.VALID,
transactionType: TransactionEnvelopeType.legacy,
userInputHexData: null,
isSwapQuoteLoading: false,
swapQuotesError: null,
swapQuotesLatestRequestTimestamp: null,
timeToFetchQuotes: null,
quotes: null,
};
/**
* Describes the state tree of the send slice
*
* @typedef {object} SendState
* @property {MapValuesToUnion<SendStateAmountModes>} amountMode - Describe
* whether the user has manually input an amount or if they have selected max
* to send the maximum amount of the selected currency.
* @property {string} currentTransactionUUID - The UUID of the transaction
* currently being modified by the send flow. This UUID is generated upon
* initialization of the send flow, any previous UUIDs are discarded at
* clean up AND during initialization. When a transaction is edited a new UUID
* is generated for it and the state of that transaction is copied into a new
* entry in the draftTransactions object.
* @property {string[]} disabledSwapAndSendNetworks - list of networks that are disabled for swap and send
* @property {{[key: string]: DraftTransaction}} draftTransactions - An object keyed
* by UUID with draftTransactions as the values.
* @property {boolean} eip1559support - tracks whether the current network
* supports EIP 1559 transactions.
* @property {boolean} gasEstimateIsLoading - Indicates whether the gas
* estimate is loading.
* @property {string} [gasEstimatePollToken] - String token identifying a
* listener for polling on the gasFeeController
* @property {boolean} gasIsSetInModal - true if the user set custom gas in the
* custom gas modal
* @property {string} gasLimitMinimum - minimum supported gasLimit.
* @property {string} gasPriceEstimate - Expected price in wei necessary to
* pay per gas used for a transaction to be included in a reasonable timeframe.
* Comes from the GasFeeController.
* @property {string} gasTotalForLayer1 - Layer 1 gas fee total on multi-layer
* fee networks
* @property {object} prevSwapAndSendInput - form inputs for the last submitted swap and send transaction
* @property {string} recipientInput - The user input of the recipient
* which is updated quickly to avoid delays in the UI reflecting manual entry
* of addresses.
* @property {MapValuesToUnion<SendStateRecipientModes>} recipientMode -
* Describes which list of recipients the user is shown on the add recipient
* screen. When this key is set to 'MY_ACCOUNTS' the user is shown the list of
* accounts they own. When it is 'CONTACT_LIST' the user is shown the list of
* contacts they have saved in MetaMask and any addresses they have recently
* sent to.
* @property {Account} selectedAccount - The currently selected account in
* MetaMask. Native balance and address will be pulled from this account if a
* fromAccount is not specified in the draftTransaction object. During an edit
* the fromAccount is specified.
* @property {MapValuesToUnion<SendStateStages>} stage - The stage of the
* send flow that the user has progressed to. Defaults to 'INACTIVE' which
* results in the send screen not being shown.
* @property {string[]} swapsBlockedTokens - list of tokens that are blocked by the swaps-api
*/
/**
* @type {SendState}
*/
export const initialState = {
amountMode: AMOUNT_MODES.INPUT,
currentTransactionUUID: null,
disabledSwapAndSendNetworks: [],
draftTransactions: {},
eip1559support: false,
gasEstimateIsLoading: true,
gasEstimatePollToken: null,
gasIsSetInModal: false,
gasPriceEstimate: '0x0',
gasLimitMinimum: GAS_LIMITS.SIMPLE,
gasTotalForLayer1: null,
prevSwapAndSendInput: null,
recipientMode: RECIPIENT_SEARCH_MODES.CONTACT_LIST,
recipientInput: '',
selectedAccount: {
address: null,
balance: '0x0',
},
stage: SEND_STAGES.INACTIVE,
swapsBlockedTokens: [],
};
/**
* TODO: We really need to start creating the metamask state type, and the
* entire state tree of redux. Would be *extremely* valuable in future
* typescript conversions. The metamask key is typed as an object on purpose
* here because I cannot go so far in this work as to type that entire object.
*
* @typedef {object} MetaMaskState
* @property {SendState} send - The state of the send flow.
* @property {object} metamask - The state of the metamask store.
*/
const name = 'send';
// After modification of specific fields in specific circumstances we must
// recompute the gasLimit estimate to be as accurate as possible. the cases
// that necessitate this logic are listed below:
// 1. when the amount sent changes when sending a token due to the amount being
// part of the hex encoded data property of the transaction.
// 2. when updating the data property while sending NATIVE currency (ex: ETH)
// because the data parameter defines function calls that the EVM will have
// to execute which is where a large chunk of gas is potentially consumed.
// 3. when the recipient changes while sending a token due to the recipient's
// address being included in the hex encoded data property of the
// transaction
// 4. when the asset being sent changes due to the contract address and details
// of the token being included in the hex encoded data property of the
// transaction. If switching to NATIVE currency (ex: ETH), the gasLimit will
// change due to hex data being removed (unless supplied by user).
// This method computes the gasLimit estimate which is written to state in an
// action handler in extraReducers.
export const computeEstimatedGasLimit = createAsyncThunk(
'send/computeEstimatedGasLimit',
async (_, thunkApi) => {
const state = thunkApi.getState();
const { send, metamask } = state;
const draftTransaction =
send.draftTransactions[send.currentTransactionUUID];
const unapprovedTxs = getUnapprovedTransactions(state);
const transaction = unapprovedTxs[draftTransaction.id];
const isNonStandardEthChain = getIsNonStandardEthChain(state);
const chainId = getCurrentChainId(state);
const selectedAccount = getSelectedInternalAccountWithBalance(state);
const gasTotalForLayer1 = await thunkApi.dispatch(
getLayer1GasFee({
transactionParams: {
gasPrice: draftTransaction.gas.gasPrice,
gas: draftTransaction.gas.gasLimit,
to: draftTransaction.recipient.address?.toLowerCase(),
value:
send.amountMode === AMOUNT_MODES.MAX
? send.selectedAccount.balance
: draftTransaction.amount.value,
from: send.selectedAccount.address,
data: draftTransaction.userInputHexData,
type: '0x0',
},
chainId,
}),
);
if (
send.stage !== SEND_STAGES.EDIT ||
!transaction.dappSuggestedGasFees?.gas ||
!transaction.userEditedGasLimit
) {
const gasLimit = await estimateGasLimitForSend({
gasPrice: draftTransaction.gas.gasPrice,
blockGasLimit: metamask.currentBlockGasLimit,
selectedAddress: selectedAccount.address,
sendToken: draftTransaction.sendAsset.details,
to: draftTransaction.recipient.address?.toLowerCase(),
value: draftTransaction.amount.value,
data: draftTransaction.userInputHexData,
isNonStandardEthChain,
chainId,
gasLimit: draftTransaction.gas.gasLimit,
});
await thunkApi.dispatch(setCustomGasLimit(gasLimit));
return {
gasLimit,
gasTotalForLayer1,
};
}
return null;
},
);
/**
* @typedef {object} Asset
* @property {AssetType} type - The type of asset that the user
* is attempting to send. Defaults to 'NATIVE' which represents the native
* asset of the chain. Can also be 'TOKEN' or 'NFT'.
* @property {string} balance - A hex string representing the balance
* that the user holds of the asset that they are attempting to send.
* @property {TokenDetails} [details] - An object that describes the
* selected asset in the case that the user is sending a token or collectibe.
* Will be null when asset.type is 'NATIVE'.
* @property {string} [error] - Error to display when there is an issue
* with the asset.
*/
/**
* Responsible for initializing required state for the send slice.
* This method is dispatched from the send page in the componentDidMount
* method. It is also dispatched anytime the network changes to ensure that
* the slice remains valid with changing token and account balances. To do so
* it keys into state to get necessary values and computes a starting point for
* the send slice. It returns the values that might change from this action and
* those values are written to the slice in the `initializeSendState.fulfilled`
* action handler.
*
* @type {import('@reduxjs/toolkit').AsyncThunk<any, { chainHasChanged: boolean }, {}>}
*/
export const initializeSendState = createAsyncThunk(
'send/initializeSendState',
async ({ chainHasChanged = false } = {}, thunkApi) => {
/**
* @typedef {object} ReduxState
* @property {object} metamask - Half baked type for the MetaMask object
* @property {SendState} send - the send state
*/
/**
* @type {ReduxState}
*/
const state = thunkApi.getState();
const isNonStandardEthChain = getIsNonStandardEthChain(state);
const selectedNetworkClientId = getSelectedNetworkClientId(state);
const chainId = getCurrentChainId(state);
let eip1559support = checkNetworkAndAccountSupports1559(state);
if (eip1559support === undefined) {
eip1559support = await getCurrentNetworkEIP1559Compatibility();
}
const account = getSelectedAccount(state);
const { send: sendState, metamask } = state;
const draftTransaction =
sendState.draftTransactions[sendState.currentTransactionUUID];
// If the draft transaction is not present, then this action has been
// dispatched out of sync with the intended flow. This is not always a bug.
// For instance, in the actions.js file we dispatch this action anytime the
// chain changes.
if (!draftTransaction) {
return thunkApi.rejectWithValue(
'draftTransaction not found, possibly not on send flow',
);
}
// Default gasPrice to 1 gwei if all estimation fails, this is only used
// for gasLimit estimation and won't be set directly in state. Instead, we
// will return the gasFeeEstimates and gasEstimateType so that the reducer
// can set the appropriate gas fees in state.
let gasPrice =
sendState.stage === SEND_STAGES.EDIT
? draftTransaction.gas.gasPrice
: '0x1';
let gasEstimatePollToken = null;
// Instruct the background process that polling for gas prices should begin
gasEstimatePollToken = await gasFeeStartPollingByNetworkClientId(
selectedNetworkClientId,
);
addPollingTokenToAppState(gasEstimatePollToken);
const {
metamask: { gasFeeEstimates, gasEstimateType },
} = thunkApi.getState();
if (sendState.stage !== SEND_STAGES.EDIT) {
// Because we are only interested in getting a gasLimit estimation we only
// need to worry about gasPrice. So we use maxFeePerGas as gasPrice if we
// have a fee market estimation.
if (gasEstimateType === GasEstimateTypes.legacy) {
gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GasEstimateTypes.ethGasPrice) {
gasPrice = getRoundedGasPrice(gasFeeEstimates.gasPrice);
} else if (gasEstimateType === GasEstimateTypes.feeMarket) {
gasPrice = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
} else {
gasPrice = gasFeeEstimates.gasPrice
? getRoundedGasPrice(gasFeeEstimates.gasPrice)
: '0x0';
}
}
// Set a basic gasLimit in the event that other estimation fails
let { gasLimit } = draftTransaction.gas;
if (
gasEstimateType !== GasEstimateTypes.none &&
sendState.stage !== SEND_STAGES.EDIT &&
draftTransaction.recipient.address
) {
gasLimit =
draftTransaction.sendAsset.type === AssetType.token ||
draftTransaction.sendAsset.type === AssetType.NFT
? GAS_LIMITS.BASE_TOKEN_ESTIMATE
: GAS_LIMITS.SIMPLE;
// Run our estimateGasLimit logic to get a more accurate estimation of
// required gas. If this value isn't nullish, set it as the new gasLimit
const estimatedGasLimit = await estimateGasLimitForSend({
gasPrice,
blockGasLimit: metamask.currentBlockGasLimit,
selectedAddress: getSender(state),
sendToken: draftTransaction.sendAsset.details,
to: draftTransaction.recipient.address.toLowerCase(),
value: draftTransaction.amount.value,
data: draftTransaction.userInputHexData,
isNonStandardEthChain,
chainId,
});
gasLimit = estimatedGasLimit || gasLimit;
}
// We have to keep the gas slice in sync with the send slice state
// so that it'll be initialized correctly if the gas modal is opened.
await thunkApi.dispatch(setCustomGasLimit(gasLimit));
// There may be a case where the send has been canceled by the user while
// the gas estimate is being computed. So we check again to make sure that
// a currentTransactionUUID exists and matches the previous tx.
const newState = thunkApi.getState();
if (
newState.send.currentTransactionUUID !== sendState.currentTransactionUUID
) {
return thunkApi.rejectWithValue(
`draftTransaction changed during initialization.
A new initializeSendState action must be dispatched.`,
);
}
const swapsBlockedTokens =
getIsSwapsChain(state) && getUseExternalServices(state)
? (await fetchBlockedTokens(chainId)).map((t) => t.toLowerCase())
: [];
const disabledSwapAndSendNetworks =
await getDisabledSwapAndSendNetworksFromAPI();
return {
account,
chainId: getCurrentChainId(state),
tokens: getTokens(state),
chainHasChanged,
disabledSwapAndSendNetworks,
gasFeeEstimates,
gasEstimateType,
gasLimit,
gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)),
gasEstimatePollToken,
eip1559support,
useTokenDetection: getUseTokenDetection(state),
tokenAddressList: Object.keys(getTokenList(state)),
swapsBlockedTokens,
};
},
);
// variable tracking the latestFetchTime
let latestFetchTime;
/**
* Fetch the swap and send transaction if the source and destination token do not match
*
* @param {string} requestTimestamp - the timestamp of the request
* @returns {ThunkAction<void>}
*/
const fetchSwapAndSendQuotes = createAsyncThunk(
'send/fetchSwapAndSendQuotes',
async ({ requestTimestamp }, thunkApi) => {
const state = thunkApi.getState();
const sendState = state[name];
const chainId = getCurrentChainId(state);
const draftTransaction =
sendState.draftTransactions[sendState.currentTransactionUUID];
const sender = getSender(state);
const sourceAmount = hexToDecimal(draftTransaction.amount.value);
// return early if form isn't filled out
if (
!Number(sourceAmount) ||
!draftTransaction.sendAsset ||
!draftTransaction.receiveAsset ||
!draftTransaction.recipient.address
) {
return { quotes: null, requestTimestamp };
}
let quotes = await new Promise((resolve, reject) =>
setTimeout(async () => {
if (requestTimestamp !== latestFetchTime) {
reject(new Error(RECENT_REQUEST_ERROR));
}
getSwapAndSendQuotes({
chainId,
sourceAmount,
sourceToken:
draftTransaction.sendAsset?.details?.address ||
SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].address,
destinationToken:
draftTransaction.receiveAsset?.details?.address ||
SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].address,
sender,
recipient: draftTransaction.recipient.address,
})
.then((response) => resolve(response))
.catch(() => reject(SWAPS_QUOTES_ERROR));
}, FETCH_DELAY),
);
for (const quote of quotes) {
if (quote.approvalNeeded) {
quote.approvalNeeded.gas = addHexPrefix(
await estimateGas(quote.approvalNeeded),
);
}
}
quotes = await addAdjustedReturnToQuotes(
quotes,
state,
draftTransaction.receiveAsset?.details,
);
if (!quotes?.length) {
throw new Error(SWAPS_NO_QUOTES);
}
return { quotes, requestTimestamp };
},
);
// Action Payload Typedefs
/**
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<string>
* )} SimpleStringPayload
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<MapValuesToUnion<SendStateAmountModes>>
* )} SendStateAmountModePayload
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<DraftTransaction['asset']>
* )} UpdateAssetPayload
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<Partial<
* Pick<DraftTransaction['recipient'], 'address' | 'nickname'>>
* >
* )} updateRecipientPayload
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<SendState['recipientMode']>
* )} UpdateRecipientModePayload
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<SendState['prevSwapAndSendInput']>
* )} PrevSwapAndSendPayload
*/
/**
* @typedef {object} GasFeeUpdateParams
* @property {TransactionType} transactionType - The transaction type
* @property {string} [maxFeePerGas] - The maximum amount in hex wei to pay
* per gas on a FEE_MARKET transaction.
* @property {string} [maxPriorityFeePerGas] - The maximum amount in hex
* wei to pay per gas as an incentive to miners on a FEE_MARKET
* transaction.
* @property {string} [gasPrice] - The amount in hex wei to pay per gas on
* a LEGACY transaction.
* @property {boolean} [isAutomaticUpdate] - true if the update is the
* result of a gas estimate update from the controller.
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<GasFeeUpdateParams>
* )} GasFeeUpdatePayload
*/
/**
* @typedef {object} GasEstimateUpdateParams
* @property {GasEstimateType} gasEstimateType - The type of gas estimation
* provided by the controller.
* @property {(
* EthGasPriceEstimate | LegacyGasPriceEstimate | GasFeeEstimates
* )} gasFeeEstimates - The gas fee estimates provided by the controller.
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<GasEstimateUpdateParams>
* )} GasEstimateUpdatePayload
*/
/**
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<DraftTransaction['asset']>
* )} UpdateAssetPayload
* @typedef {(
* import('@reduxjs/toolkit').PayloadAction<DraftTransaction>
* )} DraftTransactionPayload
*/
const slice = createSlice({
name,
initialState,
reducers: {
/**
* Adds a new draft transaction to state, first generating a new UUID for
* the transaction and setting that as the currentTransactionUUID. If the
* draft has an id property set, the stage is set to EDIT.
*
* @param {SendStateDraft} state - A writable draft of the send state to be
* updated.
* @param {DraftTransactionPayload} action - An action with payload that is
* a new draft transaction that will be added to state.
* @returns {void}
*/
addNewDraft: (state, action) => {
state.currentTransactionUUID = uuidv4();
state.draftTransactions[state.currentTransactionUUID] = action.payload;
if (action.payload.id) {
state.stage = SEND_STAGES.EDIT;
} else {
state.stage = SEND_STAGES.ADD_RECIPIENT;
}
},
/**
* Adds an entry, with timestamp, to the draftTransaction history.
*
* @param {SendStateDraft} state - A writable draft of the send state to be
* updated.
* @param {SimpleStringPayload} action - An action with payload that is
* a string to be added to the history of the draftTransaction
* @returns {void}
*/
addHistoryEntry: (state, action) => {
const draftTransaction =
state.draftTransactions[state.currentTransactionUUID];
if (draftTransaction) {
draftTransaction.history.push({
entry: action.payload,
timestamp: Date.now(),
});
}
},
/**
* gasTotal is computed based on gasPrice and gasLimit and set in state
* recomputes the maximum amount if the current amount mode is 'MAX' and
* sending the native token. ERC20 assets max amount is unaffected by
* gasTotal so does not need to be recomputed. Finally, validates the gas
* field and send state.
*
* @param {SendStateDraft} state - A writable draft of the send state to be
* updated.
* @returns {void}
*/
calculateGasTotal: (state) => {
const draftTransaction =
state.draftTransactions[state.currentTransactionUUID];
if (!draftTransaction) {
return;
}
// use maxFeePerGas as the multiplier if working with a FEE_MARKET transaction
// otherwise use gasPrice
if (
draftTransaction.transactionType === TransactionEnvelopeType.feeMarket
) {
draftTransaction.gas.gasTotal = addHexPrefix(
calcGasTotal(