@@ -24,7 +24,11 @@ import { HumanizerMeta } from '../../libs/humanizer/interfaces'
2424import { randomId } from '../../libs/humanizer/utils'
2525import { TokenResult } from '../../libs/portfolio'
2626import { getTokenAmount , getTokenBalanceInUSD } from '../../libs/portfolio/helpers'
27- import { getSanitizedAmount } from '../../libs/transfer/amount'
27+ import {
28+ getAmountAfterFeeReserve ,
29+ getAmountAfterFeeSync ,
30+ getSanitizedAmount
31+ } from '../../libs/transfer/amount'
2832import { getTransferRequestParams } from '../../libs/transfer/userRequest'
2933import {
3034 validateSendTransferAddress ,
@@ -126,6 +130,8 @@ export class TransferController extends EventEmitter implements ITransferControl
126130
127131 #shouldSkipTransactionQueuedModal: boolean = false
128132
133+ #isMaxAmountSelected: boolean = false
134+
129135 #accounts: IAccountsController
130136
131137 #keystore: IKeystoreController
@@ -402,6 +408,7 @@ export class TransferController extends EventEmitter implements ITransferControl
402408
403409 if ( ! token || Number ( getTokenAmount ( token ) ) === 0 ) {
404410 this . #selectedToken = null
411+ this . #isMaxAmountSelected = false
405412 this . #setAmountAndNotifyUI( '' )
406413 this . #setAmountInFiatAndNotifyUI( '' )
407414 this . amountFieldMode = 'token'
@@ -416,6 +423,7 @@ export class TransferController extends EventEmitter implements ITransferControl
416423 prevSelectedToken ?. address !== token ?. address ||
417424 prevSelectedToken ?. chainId !== token ?. chainId
418425 ) {
426+ this . #isMaxAmountSelected = false
419427 if ( ! token . priceIn . length ) this . amountFieldMode = 'token'
420428 this . #setAmountAndNotifyUI( '' )
421429 this . #setAmountInFiatAndNotifyUI( '' )
@@ -431,12 +439,7 @@ export class TransferController extends EventEmitter implements ITransferControl
431439 }
432440
433441 get maxAmount ( ) : string {
434- if (
435- ! this . selectedToken ||
436- getTokenAmount ( this . selectedToken ) === 0n ||
437- typeof this . selectedToken . decimals !== 'number'
438- )
439- return '0'
442+ if ( ! this . selectedToken || getTokenAmount ( this . selectedToken ) === 0n ) return '0'
440443
441444 return formatUnits ( getTokenAmount ( this . selectedToken ) , this . selectedToken . decimals )
442445 }
@@ -461,6 +464,7 @@ export class TransferController extends EventEmitter implements ITransferControl
461464 }
462465
463466 resetForm ( shouldDestroyAccountOp = true ) {
467+ this . #isMaxAmountSelected = false
464468 this . amount = ''
465469 this . amountInFiat = ''
466470 this . amountFieldMode = 'token'
@@ -587,12 +591,14 @@ export class TransferController extends EventEmitter implements ITransferControl
587591 }
588592 // If we do a regular check the value won't update if it's '' or '0'
589593 if ( typeof amount === 'string' ) {
594+ this . #isMaxAmountSelected = false
590595 this . #setAmount( amount )
591596 }
592597
593598 if ( shouldSetMaxAmount ) {
599+ this . #isMaxAmountSelected = true
594600 this . amountFieldMode = 'token'
595- this . #setAmount ( this . maxAmount , true )
601+ this . #setTokenAmount ( this . #getMaxAmountAfterFeeReservation ( ) , true )
596602 }
597603
598604 if ( addressState ) {
@@ -760,6 +766,119 @@ export class TransferController extends EventEmitter implements ITransferControl
760766 }
761767 }
762768
769+ #setTokenAmount( amount : string , isProgrammaticUpdate = false ) {
770+ const amountFieldMode = this . amountFieldMode
771+
772+ this . amountFieldMode = 'token'
773+ this . #setAmount( amount , isProgrammaticUpdate )
774+ this . amountFieldMode = amountFieldMode
775+ }
776+
777+ #getMaxAmountAfterFeeReservation( ) {
778+ if ( ! this . selectedToken ) return this . maxAmount
779+
780+ const totalTokenAmount = getTokenAmount ( this . selectedToken )
781+ const gasFeePayment = this . signAccountOpController ?. accountOp . gasFeePayment
782+
783+ if ( ! this . #shouldReserveFeeFromTransferredToken( ) || ! gasFeePayment ) {
784+ return formatUnits ( totalTokenAmount , this . selectedToken . decimals )
785+ }
786+
787+ return formatUnits (
788+ getAmountAfterFeeReserve ( totalTokenAmount , gasFeePayment . amount ) ,
789+ this . selectedToken . decimals
790+ )
791+ }
792+
793+ #shouldReserveFeeFromTransferredToken( ) {
794+ const gasFeePayment = this . signAccountOpController ?. accountOp . gasFeePayment
795+ const selectedFeeOption = this . signAccountOpController ?. selectedOption
796+ const selectedToken = this . selectedToken
797+ const accountAddr = this . #selectedAccount. account ?. addr . toLowerCase ( )
798+
799+ if ( ! accountAddr || ! gasFeePayment || ! selectedFeeOption || ! selectedToken ) return false
800+ if ( selectedFeeOption . token . flags . onGasTank ) return false
801+ if ( selectedFeeOption . paidBy . toLowerCase ( ) !== accountAddr ) return false
802+
803+ const selectedTokenAddress = selectedToken . address . toLowerCase ( )
804+
805+ return (
806+ ! ! accountAddr &&
807+ ! ! gasFeePayment &&
808+ ! ! selectedFeeOption &&
809+ selectedFeeOption . paidBy . toLowerCase ( ) === accountAddr &&
810+ selectedFeeOption . token . chainId === selectedToken . chainId &&
811+ selectedFeeOption . token . address . toLowerCase ( ) === selectedTokenAddress &&
812+ gasFeePayment . inToken . toLowerCase ( ) === selectedTokenAddress &&
813+ ( ! gasFeePayment . feeTokenChainId || gasFeePayment . feeTokenChainId === selectedToken . chainId )
814+ )
815+ }
816+
817+ #syncAmountWithFeeReservation( forceEmit ?: boolean ) {
818+ if ( ! this . amount || ! this . selectedToken || typeof this . selectedToken . decimals !== 'number' )
819+ return false
820+
821+ const totalTokenAmount = getTokenAmount ( this . selectedToken )
822+ const shouldReserveFee = this . #shouldReserveFeeFromTransferredToken( )
823+ const gasFeePayment = this . signAccountOpController ?. accountOp . gasFeePayment
824+ const currentAmount = this . amount
825+ ? parseUnits (
826+ getSafeAmountFromFieldValue ( this . amount , this . selectedToken . decimals ) ,
827+ this . selectedToken . decimals
828+ )
829+ : 0n
830+ const desiredAmount = getAmountAfterFeeSync ( {
831+ currentAmount,
832+ totalAmount : totalTokenAmount ,
833+ fee : shouldReserveFee ? gasFeePayment ?. amount || 0n : 0n ,
834+ shouldReserveFee,
835+ isMaxAmountSelected : this . #isMaxAmountSelected
836+ } )
837+
838+ if ( currentAmount === desiredAmount ) return false
839+
840+ this . #setTokenAmount(
841+ desiredAmount === 0n ? '0' : formatUnits ( desiredAmount , this . selectedToken . decimals ) ,
842+ true
843+ )
844+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
845+ this . syncSignAccountOp ( )
846+ this . propagateUpdate ( forceEmit )
847+
848+ return true
849+ }
850+
851+ /**
852+ * When doing a MAX transfer or a close to MAX transfer out,
853+ * if the selected fee token is the same as the transfer token,
854+ * we automatically adjust the transfer amount so the user
855+ * can successfully broadcast. For that, we put an additional
856+ * warning telling him why this is happening
857+ */
858+ get amountAdjustmentWarning ( ) : Validation | null {
859+ if ( ! this . amount || ! this . selectedToken || ! this . #shouldReserveFeeFromTransferredToken( ) ) {
860+ return null
861+ }
862+
863+ const gasFeePayment = this . signAccountOpController ?. accountOp . gasFeePayment
864+ if ( ! gasFeePayment ) return null
865+
866+ const currentAmount = parseUnits (
867+ getSafeAmountFromFieldValue ( this . amount , this . selectedToken . decimals ) ,
868+ this . selectedToken . decimals
869+ )
870+ const totalTokenAmount = getTokenAmount ( this . selectedToken )
871+
872+ if ( currentAmount > 0n && currentAmount + gasFeePayment . amount >= totalTokenAmount ) {
873+ return {
874+ severity : 'warning' ,
875+ message : 'Amount adjusted to cover blockchain fees'
876+ }
877+ }
878+
879+ return null
880+ }
881+
763882 get hasPersistedState ( ) {
764883 return ! ! ( this . amount || this . amountInFiat || this . addressState . fieldValue )
765884 }
@@ -860,7 +979,8 @@ export class TransferController extends EventEmitter implements ITransferControl
860979 calls,
861980 meta : {
862981 paymasterService : getAmbirePaymasterService ( baseAcc , this . #relayerUrl) ,
863- topUpAmount
982+ topUpAmount,
983+ allowTransferFeeTokenSelfReserve : true
864984 }
865985 }
866986
@@ -899,7 +1019,7 @@ export class TransferController extends EventEmitter implements ITransferControl
8991019 shouldSimulate : false ,
9001020 onBroadcastSuccess : async ( props ) => {
9011021 const { submittedAccountOp } = props
902- this . #portfolio. simulateAccountOp ( props . accountOp ) . then ( ( ) => {
1022+ void this . #portfolio. simulateAccountOp ( props . accountOp ) . then ( ( ) => {
9031023 this . #portfolio. markSimulationAsBroadcasted ( accountOp . accountAddr , accountOp . chainId )
9041024 } )
9051025
@@ -918,6 +1038,8 @@ export class TransferController extends EventEmitter implements ITransferControl
9181038 } )
9191039
9201040 this . signAccountOpController . onUpdate ( ( forceEmit ) => {
1041+ this . #syncAmountWithFeeReservation( forceEmit )
1042+
9211043 this . propagateUpdate ( forceEmit )
9221044
9231045 if ( this . signAccountOpController ?. broadcastStatus === 'SUCCESS' ) {
@@ -1015,7 +1137,8 @@ export class TransferController extends EventEmitter implements ITransferControl
10151137 maxAmountInFiat : this . maxAmountInFiat ,
10161138 shouldSkipTransactionQueuedModal : this . shouldSkipTransactionQueuedModal ,
10171139 hasPersistedState : this . hasPersistedState ,
1018- isRecipientAddressViewOnly : this . isRecipientAddressViewOnly
1140+ isRecipientAddressViewOnly : this . isRecipientAddressViewOnly ,
1141+ amountAdjustmentWarning : this . amountAdjustmentWarning
10191142 }
10201143 }
10211144}
0 commit comments