@metamask/transaction-pay-controller 16.2.0 → 16.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/TransactionPayController.cjs +11 -0
  3. package/dist/TransactionPayController.cjs.map +1 -1
  4. package/dist/TransactionPayController.d.cts +2 -1
  5. package/dist/TransactionPayController.d.cts.map +1 -1
  6. package/dist/TransactionPayController.d.mts +2 -1
  7. package/dist/TransactionPayController.d.mts.map +1 -1
  8. package/dist/TransactionPayController.mjs +11 -0
  9. package/dist/TransactionPayController.mjs.map +1 -1
  10. package/dist/actions/update-fiat-payment.cjs +29 -0
  11. package/dist/actions/update-fiat-payment.cjs.map +1 -0
  12. package/dist/actions/update-fiat-payment.d.cts +14 -0
  13. package/dist/actions/update-fiat-payment.d.cts.map +1 -0
  14. package/dist/actions/update-fiat-payment.d.mts +14 -0
  15. package/dist/actions/update-fiat-payment.d.mts.map +1 -0
  16. package/dist/actions/update-fiat-payment.mjs +25 -0
  17. package/dist/actions/update-fiat-payment.mjs.map +1 -0
  18. package/dist/actions/update-payment-token.cjs +1 -0
  19. package/dist/actions/update-payment-token.cjs.map +1 -1
  20. package/dist/actions/update-payment-token.d.cts.map +1 -1
  21. package/dist/actions/update-payment-token.d.mts.map +1 -1
  22. package/dist/actions/update-payment-token.mjs +1 -0
  23. package/dist/actions/update-payment-token.mjs.map +1 -1
  24. package/dist/constants.cjs +1 -0
  25. package/dist/constants.cjs.map +1 -1
  26. package/dist/constants.d.cts +1 -0
  27. package/dist/constants.d.cts.map +1 -1
  28. package/dist/constants.d.mts +1 -0
  29. package/dist/constants.d.mts.map +1 -1
  30. package/dist/constants.mjs +1 -0
  31. package/dist/constants.mjs.map +1 -1
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +1 -1
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.mts +1 -1
  36. package/dist/index.d.mts.map +1 -1
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/strategy/across/AcrossStrategy.cjs +28 -0
  39. package/dist/strategy/across/AcrossStrategy.cjs.map +1 -0
  40. package/dist/strategy/across/AcrossStrategy.d.cts +8 -0
  41. package/dist/strategy/across/AcrossStrategy.d.cts.map +1 -0
  42. package/dist/strategy/across/AcrossStrategy.d.mts +8 -0
  43. package/dist/strategy/across/AcrossStrategy.d.mts.map +1 -0
  44. package/dist/strategy/across/AcrossStrategy.mjs +24 -0
  45. package/dist/strategy/across/AcrossStrategy.mjs.map +1 -0
  46. package/dist/strategy/across/across-quotes.cjs +356 -0
  47. package/dist/strategy/across/across-quotes.cjs.map +1 -0
  48. package/dist/strategy/across/across-quotes.d.cts +10 -0
  49. package/dist/strategy/across/across-quotes.d.cts.map +1 -0
  50. package/dist/strategy/across/across-quotes.d.mts +10 -0
  51. package/dist/strategy/across/across-quotes.d.mts.map +1 -0
  52. package/dist/strategy/across/across-quotes.mjs +352 -0
  53. package/dist/strategy/across/across-quotes.mjs.map +1 -0
  54. package/dist/strategy/across/across-submit.cjs +241 -0
  55. package/dist/strategy/across/across-submit.cjs.map +1 -0
  56. package/dist/strategy/across/across-submit.d.cts +13 -0
  57. package/dist/strategy/across/across-submit.d.cts.map +1 -0
  58. package/dist/strategy/across/across-submit.d.mts +13 -0
  59. package/dist/strategy/across/across-submit.d.mts.map +1 -0
  60. package/dist/strategy/across/across-submit.mjs +237 -0
  61. package/dist/strategy/across/across-submit.mjs.map +1 -0
  62. package/dist/strategy/across/types.cjs +3 -0
  63. package/dist/strategy/across/types.cjs.map +1 -0
  64. package/dist/strategy/across/types.d.cts +89 -0
  65. package/dist/strategy/across/types.d.cts.map +1 -0
  66. package/dist/strategy/across/types.d.mts +89 -0
  67. package/dist/strategy/across/types.d.mts.map +1 -0
  68. package/dist/strategy/across/types.mjs +2 -0
  69. package/dist/strategy/across/types.mjs.map +1 -0
  70. package/dist/strategy/relay/RelayStrategy.cjs +5 -0
  71. package/dist/strategy/relay/RelayStrategy.cjs.map +1 -1
  72. package/dist/strategy/relay/RelayStrategy.d.cts +1 -0
  73. package/dist/strategy/relay/RelayStrategy.d.cts.map +1 -1
  74. package/dist/strategy/relay/RelayStrategy.d.mts +1 -0
  75. package/dist/strategy/relay/RelayStrategy.d.mts.map +1 -1
  76. package/dist/strategy/relay/RelayStrategy.mjs +5 -0
  77. package/dist/strategy/relay/RelayStrategy.mjs.map +1 -1
  78. package/dist/strategy/relay/gas-station.cjs +92 -0
  79. package/dist/strategy/relay/gas-station.cjs.map +1 -0
  80. package/dist/strategy/relay/gas-station.d.cts +22 -0
  81. package/dist/strategy/relay/gas-station.d.cts.map +1 -0
  82. package/dist/strategy/relay/gas-station.d.mts +22 -0
  83. package/dist/strategy/relay/gas-station.d.mts.map +1 -0
  84. package/dist/strategy/relay/gas-station.mjs +87 -0
  85. package/dist/strategy/relay/gas-station.mjs.map +1 -0
  86. package/dist/strategy/relay/relay-max-gas-station.cjs +242 -0
  87. package/dist/strategy/relay/relay-max-gas-station.cjs.map +1 -0
  88. package/dist/strategy/relay/relay-max-gas-station.d.cts +22 -0
  89. package/dist/strategy/relay/relay-max-gas-station.d.cts.map +1 -0
  90. package/dist/strategy/relay/relay-max-gas-station.d.mts +22 -0
  91. package/dist/strategy/relay/relay-max-gas-station.d.mts.map +1 -0
  92. package/dist/strategy/relay/relay-max-gas-station.mjs +238 -0
  93. package/dist/strategy/relay/relay-max-gas-station.mjs.map +1 -0
  94. package/dist/strategy/relay/relay-quotes.cjs +38 -59
  95. package/dist/strategy/relay/relay-quotes.cjs.map +1 -1
  96. package/dist/strategy/relay/relay-quotes.d.cts.map +1 -1
  97. package/dist/strategy/relay/relay-quotes.d.mts.map +1 -1
  98. package/dist/strategy/relay/relay-quotes.mjs +35 -56
  99. package/dist/strategy/relay/relay-quotes.mjs.map +1 -1
  100. package/dist/strategy/relay/types.cjs.map +1 -1
  101. package/dist/strategy/relay/types.d.cts +1 -0
  102. package/dist/strategy/relay/types.d.cts.map +1 -1
  103. package/dist/strategy/relay/types.d.mts +1 -0
  104. package/dist/strategy/relay/types.d.mts.map +1 -1
  105. package/dist/strategy/relay/types.mjs.map +1 -1
  106. package/dist/types.cjs.map +1 -1
  107. package/dist/types.d.cts +44 -1
  108. package/dist/types.d.cts.map +1 -1
  109. package/dist/types.d.mts +44 -1
  110. package/dist/types.d.mts.map +1 -1
  111. package/dist/types.mjs.map +1 -1
  112. package/dist/utils/amounts.cjs +40 -0
  113. package/dist/utils/amounts.cjs.map +1 -0
  114. package/dist/utils/amounts.d.cts +18 -0
  115. package/dist/utils/amounts.d.cts.map +1 -0
  116. package/dist/utils/amounts.d.mts +18 -0
  117. package/dist/utils/amounts.d.mts.map +1 -0
  118. package/dist/utils/amounts.mjs +35 -0
  119. package/dist/utils/amounts.mjs.map +1 -0
  120. package/dist/utils/feature-flags.cjs +45 -6
  121. package/dist/utils/feature-flags.cjs.map +1 -1
  122. package/dist/utils/feature-flags.d.cts +45 -2
  123. package/dist/utils/feature-flags.d.cts.map +1 -1
  124. package/dist/utils/feature-flags.d.mts +45 -2
  125. package/dist/utils/feature-flags.d.mts.map +1 -1
  126. package/dist/utils/feature-flags.mjs +42 -5
  127. package/dist/utils/feature-flags.mjs.map +1 -1
  128. package/dist/utils/gas.cjs +46 -1
  129. package/dist/utils/gas.cjs.map +1 -1
  130. package/dist/utils/gas.d.cts +18 -0
  131. package/dist/utils/gas.d.cts.map +1 -1
  132. package/dist/utils/gas.d.mts +18 -0
  133. package/dist/utils/gas.d.mts.map +1 -1
  134. package/dist/utils/gas.mjs +44 -0
  135. package/dist/utils/gas.mjs.map +1 -1
  136. package/dist/utils/quotes.cjs +8 -3
  137. package/dist/utils/quotes.cjs.map +1 -1
  138. package/dist/utils/quotes.d.cts.map +1 -1
  139. package/dist/utils/quotes.d.mts.map +1 -1
  140. package/dist/utils/quotes.mjs +8 -3
  141. package/dist/utils/quotes.mjs.map +1 -1
  142. package/dist/utils/strategy.cjs +3 -0
  143. package/dist/utils/strategy.cjs.map +1 -1
  144. package/dist/utils/strategy.d.cts.map +1 -1
  145. package/dist/utils/strategy.d.mts.map +1 -1
  146. package/dist/utils/strategy.mjs +3 -0
  147. package/dist/utils/strategy.mjs.map +1 -1
  148. package/dist/utils/totals.cjs +4 -19
  149. package/dist/utils/totals.cjs.map +1 -1
  150. package/dist/utils/totals.d.cts.map +1 -1
  151. package/dist/utils/totals.d.mts.map +1 -1
  152. package/dist/utils/totals.mjs +1 -16
  153. package/dist/utils/totals.mjs.map +1 -1
  154. package/package.json +3 -3
@@ -0,0 +1,238 @@
1
+ import { createModuleLogger } from "@metamask/utils";
2
+ import { BigNumber } from "bignumber.js";
3
+ import { getGasStationEligibility, getGasStationCostInSourceTokenRaw } from "./gas-station.mjs";
4
+ import { projectLogger } from "../../logger.mjs";
5
+ import { getNativeToken, getTokenBalance, getTokenInfo } from "../../utils/token.mjs";
6
+ const log = createModuleLogger(projectLogger, 'relay-max-gas-station');
7
+ const PROBE_AMOUNT_PERCENTAGE = 0.25;
8
+ var GasCostEstimateSource;
9
+ (function (GasCostEstimateSource) {
10
+ GasCostEstimateSource["GasStation"] = "gas-station";
11
+ GasCostEstimateSource["Probe"] = "probe";
12
+ GasCostEstimateSource["Quote"] = "quote";
13
+ })(GasCostEstimateSource || (GasCostEstimateSource = {}));
14
+ /**
15
+ * Returns a Relay max-amount quote using a two-phase gas-station fallback.
16
+ *
17
+ * It first requests a standard max quote (phase 1), then when needed estimates
18
+ * gas in source-token units (directly or via a probe quote), requests an
19
+ * adjusted max quote (phase 2), and accepts phase 2 only if validation passes
20
+ * (source gas fee token selected, gas-limit checks, affordability).
21
+ *
22
+ * If any step fails or validation is unsafe, it safely falls back to phase 1.
23
+ * Successful phase-2 quotes are tagged with `metamask.isMaxGasStation = true`.
24
+ *
25
+ * @param request - Relay quote request for a max-amount flow.
26
+ * @param fullRequest - Full quote request context including messenger and transaction.
27
+ * @param getSingleQuote - Quote fetcher used for phase-1, phase-2, and probe quotes.
28
+ * @returns The validated adjusted phase-2 quote, or the original phase-1 quote on fallback.
29
+ */
30
+ export async function getRelayMaxGasStationQuote(request, fullRequest, getSingleQuote) {
31
+ const { messenger } = fullRequest;
32
+ const { sourceChainId, sourceTokenAmount } = request;
33
+ const context = {
34
+ fullRequest,
35
+ getSingleQuote,
36
+ messenger,
37
+ request,
38
+ };
39
+ const phase1Quote = await getSingleQuote(request, fullRequest);
40
+ const nativeBalanceCheck = checkEnoughNativeBalanceIfSourceGasFeeTokenNotUsed(phase1Quote, messenger, request);
41
+ if (nativeBalanceCheck.hasEnoughNativeBalance) {
42
+ return fallbackToPhase1(phase1Quote, 'Native balance is sufficient for gas', {
43
+ nativeBalance: nativeBalanceCheck.nativeBalance,
44
+ nativeGasCost: nativeBalanceCheck.nativeGasCostRaw,
45
+ });
46
+ }
47
+ const gasStationEligibility = getGasStationEligibility(messenger, sourceChainId);
48
+ if (!gasStationEligibility.isEligible) {
49
+ return fallbackToPhase1(phase1Quote, 'Gas station is disabled or unsupported');
50
+ }
51
+ const sourceAmountBN = new BigNumber(sourceTokenAmount);
52
+ const probeCost = await getProbeGasCostInSourceTokenRaw(context);
53
+ if (!probeCost) {
54
+ return fallbackToPhase1(phase1Quote, 'Unable to estimate gas-station source token cost');
55
+ }
56
+ const initialGasEstimate = {
57
+ amount: probeCost,
58
+ source: GasCostEstimateSource.Probe,
59
+ };
60
+ const adjustedSourceAmount = getAdjustedSourceAmount(sourceAmountBN, initialGasEstimate.amount);
61
+ if (!adjustedSourceAmount.isGreaterThan(0)) {
62
+ return fallbackToPhase1(phase1Quote, 'Insufficient balance for gas station after max adjustment');
63
+ }
64
+ const phase2Quote = await getAdjustedPhase2Quote(adjustedSourceAmount, initialGasEstimate, context);
65
+ if (!phase2Quote) {
66
+ return fallbackToPhase1(phase1Quote, 'Adjusted phase-2 quote request failed');
67
+ }
68
+ if (!phase2Quote.fees.isSourceGasFeeToken) {
69
+ return fallbackToPhase1(phase1Quote, 'Adjusted quote did not return source gas fee token pricing');
70
+ }
71
+ const phase1GasLimits = phase1Quote.original.metamask.gasLimits ?? [];
72
+ const phase2GasLimits = phase2Quote.original.metamask.gasLimits ?? [];
73
+ if (!hasMatchingGasLimitShape(phase1GasLimits, phase2GasLimits)) {
74
+ return fallbackToPhase1(phase1Quote, 'Adjusted quote gas limit shape changed between phases', {
75
+ phase1GasLimits,
76
+ phase2GasLimits,
77
+ });
78
+ }
79
+ const phase1TotalGasLimit = getTotalGasLimit(phase1GasLimits);
80
+ const phase2TotalGasLimit = getTotalGasLimit(phase2GasLimits);
81
+ if (phase2TotalGasLimit > phase1TotalGasLimit) {
82
+ return fallbackToPhase1(phase1Quote, 'Adjusted quote total gas limit increased between phases', {
83
+ phase1TotalGasLimit,
84
+ phase2TotalGasLimit,
85
+ });
86
+ }
87
+ const validationGasEstimate = new BigNumber(phase2Quote.fees.sourceNetwork.max.raw);
88
+ if (!isAdjustedAmountAffordable(adjustedSourceAmount, validationGasEstimate, sourceAmountBN)) {
89
+ return fallbackToPhase1(phase1Quote, 'Adjusted quote fails affordability validation', {
90
+ adjustedSourceAmount: adjustedSourceAmount.toString(10),
91
+ validationGasCost: validationGasEstimate.toString(10),
92
+ });
93
+ }
94
+ markQuoteAsMaxGasStation(phase2Quote);
95
+ return phase2Quote;
96
+ }
97
+ function checkEnoughNativeBalanceIfSourceGasFeeTokenNotUsed(quote, messenger, request) {
98
+ if (quote.fees.isSourceGasFeeToken) {
99
+ return { hasEnoughNativeBalance: false };
100
+ }
101
+ const nativeGasCostRaw = quote.fees.sourceNetwork.max.raw;
102
+ const nativeBalance = getTokenBalance(messenger, request.from, request.sourceChainId, getNativeToken(request.sourceChainId));
103
+ return {
104
+ hasEnoughNativeBalance: new BigNumber(nativeBalance).isGreaterThanOrEqualTo(nativeGasCostRaw),
105
+ nativeBalance,
106
+ nativeGasCostRaw,
107
+ };
108
+ }
109
+ function getAdjustedSourceAmount(sourceAmount, estimatedGasCost) {
110
+ return sourceAmount
111
+ .minus(estimatedGasCost)
112
+ .integerValue(BigNumber.ROUND_DOWN);
113
+ }
114
+ function isAdjustedAmountAffordable(adjustedSourceAmount, gasCost, originalSourceAmount) {
115
+ return adjustedSourceAmount
116
+ .plus(gasCost)
117
+ .isLessThanOrEqualTo(originalSourceAmount);
118
+ }
119
+ async function getAdjustedPhase2Quote(adjustedSourceAmount, initialGasEstimate, context) {
120
+ const { fullRequest, getSingleQuote, request } = context;
121
+ log('Requesting adjusted max quote', {
122
+ adjustedAmount: adjustedSourceAmount.toString(10),
123
+ gasCostInSourceToken: initialGasEstimate.amount.toString(10),
124
+ gasEstimateSource: initialGasEstimate.source,
125
+ originalAmount: request.sourceTokenAmount,
126
+ });
127
+ try {
128
+ return await getSingleQuote({
129
+ ...request,
130
+ sourceTokenAmount: adjustedSourceAmount.toFixed(0, BigNumber.ROUND_DOWN),
131
+ }, fullRequest);
132
+ }
133
+ catch (error) {
134
+ log('Adjusted quote request failed, falling back to phase-1 quote', {
135
+ error,
136
+ });
137
+ return undefined;
138
+ }
139
+ }
140
+ async function getGasCostFromQuoteOrGasStation(quote, messenger, request) {
141
+ const gasCost = quote.fees.sourceNetwork.max;
142
+ if (quote.fees.isSourceGasFeeToken) {
143
+ log('Gas cost already in source token units', { raw: gasCost.raw });
144
+ return {
145
+ amount: new BigNumber(gasCost.raw),
146
+ source: GasCostEstimateSource.Quote,
147
+ };
148
+ }
149
+ const firstStepData = quote.original.steps[0]?.items[0]?.data;
150
+ if (!firstStepData) {
151
+ return undefined;
152
+ }
153
+ const totalItemCount = quote.original.steps.reduce((count, step) => count + step.items.length, 0);
154
+ const totalGasEstimate = (quote.original.metamask.gasLimits ?? []).reduce((acc, gasLimit) => acc + gasLimit, 0);
155
+ const gasStationCost = await getGasStationCostInSourceTokenRaw({
156
+ firstStepData,
157
+ messenger,
158
+ request: {
159
+ from: request.from,
160
+ sourceChainId: request.sourceChainId,
161
+ sourceTokenAddress: request.sourceTokenAddress,
162
+ },
163
+ totalGasEstimate,
164
+ totalItemCount,
165
+ });
166
+ if (!gasStationCost) {
167
+ return undefined;
168
+ }
169
+ return {
170
+ amount: new BigNumber(gasStationCost.raw),
171
+ source: GasCostEstimateSource.GasStation,
172
+ };
173
+ }
174
+ async function getProbeGasCostInSourceTokenRaw(context) {
175
+ const { fullRequest, getSingleQuote, messenger, request } = context;
176
+ const sourceTokenInfo = getTokenInfo(messenger, request.sourceTokenAddress, request.sourceChainId);
177
+ if (!sourceTokenInfo) {
178
+ return undefined;
179
+ }
180
+ const probeAmount = getProbeSourceAmountRaw(request.sourceTokenAmount, sourceTokenInfo.decimals);
181
+ if (!probeAmount || probeAmount === request.sourceTokenAmount) {
182
+ return undefined;
183
+ }
184
+ log('Requesting probe quote for gas station estimation', {
185
+ originalSourceAmount: request.sourceTokenAmount,
186
+ probeAmount,
187
+ });
188
+ let probeQuote;
189
+ try {
190
+ probeQuote = await getSingleQuote({
191
+ ...request,
192
+ sourceTokenAmount: probeAmount,
193
+ }, fullRequest);
194
+ }
195
+ catch (error) {
196
+ log('Probe quote request failed', { error });
197
+ return undefined;
198
+ }
199
+ if (!probeQuote) {
200
+ return undefined;
201
+ }
202
+ const probeEstimate = await getGasCostFromQuoteOrGasStation(probeQuote, messenger, request);
203
+ return probeEstimate?.amount;
204
+ }
205
+ function getProbeSourceAmountRaw(sourceAmountRaw, sourceDecimals) {
206
+ const sourceAmount = new BigNumber(sourceAmountRaw);
207
+ if (sourceAmount.isLessThanOrEqualTo(0)) {
208
+ return undefined;
209
+ }
210
+ const probeRawAmount = sourceAmount
211
+ .multipliedBy(PROBE_AMOUNT_PERCENTAGE)
212
+ .integerValue(BigNumber.ROUND_FLOOR);
213
+ // Minimum probe size: ~0.01 token for tokens with >=2 decimals,
214
+ // otherwise one raw unit for low-decimal tokens.
215
+ const minimumProbeRaw = new BigNumber(1).shiftedBy(Math.max(sourceDecimals - 2, 0));
216
+ const probeRaw = BigNumber.minimum(sourceAmount, BigNumber.maximum(probeRawAmount, minimumProbeRaw)).integerValue(BigNumber.ROUND_FLOOR);
217
+ if (probeRaw.isLessThanOrEqualTo(0)) {
218
+ return undefined;
219
+ }
220
+ return probeRaw.toFixed(0);
221
+ }
222
+ function fallbackToPhase1(phase1Quote, message, paramsToLog) {
223
+ log(message, paramsToLog);
224
+ return phase1Quote;
225
+ }
226
+ function hasMatchingGasLimitShape(phase1, phase2) {
227
+ return phase1.length === phase2.length;
228
+ }
229
+ function getTotalGasLimit(gasLimits) {
230
+ return gasLimits.reduce((total, gasLimit) => total + gasLimit, 0);
231
+ }
232
+ function markQuoteAsMaxGasStation(quote) {
233
+ quote.original.metamask = {
234
+ ...quote.original.metamask,
235
+ isMaxGasStation: true,
236
+ };
237
+ }
238
+ //# sourceMappingURL=relay-max-gas-station.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay-max-gas-station.mjs","sourceRoot":"","sources":["../../../src/strategy/relay/relay-max-gas-station.ts"],"names":[],"mappings":"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","sourcesContent":["import { createModuleLogger } from '@metamask/utils';\nimport { BigNumber } from 'bignumber.js';\n\nimport {\n getGasStationEligibility,\n getGasStationCostInSourceTokenRaw,\n} from './gas-station';\nimport type { RelayQuote } from './types';\nimport { projectLogger } from '../../logger';\nimport type {\n PayStrategyGetQuotesRequest,\n QuoteRequest,\n TransactionPayControllerMessenger,\n TransactionPayQuote,\n} from '../../types';\nimport {\n getNativeToken,\n getTokenBalance,\n getTokenInfo,\n} from '../../utils/token';\n\nconst log = createModuleLogger(projectLogger, 'relay-max-gas-station');\n\nconst PROBE_AMOUNT_PERCENTAGE = 0.25;\n\nenum GasCostEstimateSource {\n GasStation = 'gas-station',\n Probe = 'probe',\n Quote = 'quote',\n}\n\ntype GasCostEstimate = {\n amount: BigNumber;\n source: GasCostEstimateSource;\n};\n\ntype NativeBalanceCheckResult = {\n hasEnoughNativeBalance: boolean;\n nativeBalance?: string;\n nativeGasCostRaw?: string;\n};\n\ntype GetSingleQuoteFn = (\n request: QuoteRequest,\n fullRequest: PayStrategyGetQuotesRequest,\n) => Promise<TransactionPayQuote<RelayQuote>>;\n\ntype MaxAmountQuoteContext = {\n fullRequest: PayStrategyGetQuotesRequest;\n getSingleQuote: GetSingleQuoteFn;\n messenger: TransactionPayControllerMessenger;\n request: QuoteRequest;\n};\n\n/**\n * Returns a Relay max-amount quote using a two-phase gas-station fallback.\n *\n * It first requests a standard max quote (phase 1), then when needed estimates\n * gas in source-token units (directly or via a probe quote), requests an\n * adjusted max quote (phase 2), and accepts phase 2 only if validation passes\n * (source gas fee token selected, gas-limit checks, affordability).\n *\n * If any step fails or validation is unsafe, it safely falls back to phase 1.\n * Successful phase-2 quotes are tagged with `metamask.isMaxGasStation = true`.\n *\n * @param request - Relay quote request for a max-amount flow.\n * @param fullRequest - Full quote request context including messenger and transaction.\n * @param getSingleQuote - Quote fetcher used for phase-1, phase-2, and probe quotes.\n * @returns The validated adjusted phase-2 quote, or the original phase-1 quote on fallback.\n */\nexport async function getRelayMaxGasStationQuote(\n request: QuoteRequest,\n fullRequest: PayStrategyGetQuotesRequest,\n getSingleQuote: GetSingleQuoteFn,\n): Promise<TransactionPayQuote<RelayQuote>> {\n const { messenger } = fullRequest;\n const { sourceChainId, sourceTokenAmount } = request;\n const context: MaxAmountQuoteContext = {\n fullRequest,\n getSingleQuote,\n messenger,\n request,\n };\n\n const phase1Quote = await getSingleQuote(request, fullRequest);\n\n const nativeBalanceCheck = checkEnoughNativeBalanceIfSourceGasFeeTokenNotUsed(\n phase1Quote,\n messenger,\n request,\n );\n\n if (nativeBalanceCheck.hasEnoughNativeBalance) {\n return fallbackToPhase1(\n phase1Quote,\n 'Native balance is sufficient for gas',\n {\n nativeBalance: nativeBalanceCheck.nativeBalance,\n nativeGasCost: nativeBalanceCheck.nativeGasCostRaw,\n },\n );\n }\n\n const gasStationEligibility = getGasStationEligibility(\n messenger,\n sourceChainId,\n );\n\n if (!gasStationEligibility.isEligible) {\n return fallbackToPhase1(\n phase1Quote,\n 'Gas station is disabled or unsupported',\n );\n }\n\n const sourceAmountBN = new BigNumber(sourceTokenAmount);\n const probeCost = await getProbeGasCostInSourceTokenRaw(context);\n\n if (!probeCost) {\n return fallbackToPhase1(\n phase1Quote,\n 'Unable to estimate gas-station source token cost',\n );\n }\n\n const initialGasEstimate: GasCostEstimate = {\n amount: probeCost,\n source: GasCostEstimateSource.Probe,\n };\n\n const adjustedSourceAmount = getAdjustedSourceAmount(\n sourceAmountBN,\n initialGasEstimate.amount,\n );\n\n if (!adjustedSourceAmount.isGreaterThan(0)) {\n return fallbackToPhase1(\n phase1Quote,\n 'Insufficient balance for gas station after max adjustment',\n );\n }\n\n const phase2Quote = await getAdjustedPhase2Quote(\n adjustedSourceAmount,\n initialGasEstimate,\n context,\n );\n\n if (!phase2Quote) {\n return fallbackToPhase1(\n phase1Quote,\n 'Adjusted phase-2 quote request failed',\n );\n }\n\n if (!phase2Quote.fees.isSourceGasFeeToken) {\n return fallbackToPhase1(\n phase1Quote,\n 'Adjusted quote did not return source gas fee token pricing',\n );\n }\n\n const phase1GasLimits = phase1Quote.original.metamask.gasLimits ?? [];\n const phase2GasLimits = phase2Quote.original.metamask.gasLimits ?? [];\n\n if (!hasMatchingGasLimitShape(phase1GasLimits, phase2GasLimits)) {\n return fallbackToPhase1(\n phase1Quote,\n 'Adjusted quote gas limit shape changed between phases',\n {\n phase1GasLimits,\n phase2GasLimits,\n },\n );\n }\n\n const phase1TotalGasLimit = getTotalGasLimit(phase1GasLimits);\n const phase2TotalGasLimit = getTotalGasLimit(phase2GasLimits);\n\n if (phase2TotalGasLimit > phase1TotalGasLimit) {\n return fallbackToPhase1(\n phase1Quote,\n 'Adjusted quote total gas limit increased between phases',\n {\n phase1TotalGasLimit,\n phase2TotalGasLimit,\n },\n );\n }\n\n const validationGasEstimate = new BigNumber(\n phase2Quote.fees.sourceNetwork.max.raw,\n );\n\n if (\n !isAdjustedAmountAffordable(\n adjustedSourceAmount,\n validationGasEstimate,\n sourceAmountBN,\n )\n ) {\n return fallbackToPhase1(\n phase1Quote,\n 'Adjusted quote fails affordability validation',\n {\n adjustedSourceAmount: adjustedSourceAmount.toString(10),\n validationGasCost: validationGasEstimate.toString(10),\n },\n );\n }\n\n markQuoteAsMaxGasStation(phase2Quote);\n\n return phase2Quote;\n}\n\nfunction checkEnoughNativeBalanceIfSourceGasFeeTokenNotUsed(\n quote: TransactionPayQuote<RelayQuote>,\n messenger: TransactionPayControllerMessenger,\n request: QuoteRequest,\n): NativeBalanceCheckResult {\n if (quote.fees.isSourceGasFeeToken) {\n return { hasEnoughNativeBalance: false };\n }\n\n const nativeGasCostRaw = quote.fees.sourceNetwork.max.raw;\n const nativeBalance = getTokenBalance(\n messenger,\n request.from,\n request.sourceChainId,\n getNativeToken(request.sourceChainId),\n );\n\n return {\n hasEnoughNativeBalance: new BigNumber(nativeBalance).isGreaterThanOrEqualTo(\n nativeGasCostRaw,\n ),\n nativeBalance,\n nativeGasCostRaw,\n };\n}\n\nfunction getAdjustedSourceAmount(\n sourceAmount: BigNumber,\n estimatedGasCost: BigNumber,\n): BigNumber {\n return sourceAmount\n .minus(estimatedGasCost)\n .integerValue(BigNumber.ROUND_DOWN);\n}\n\nfunction isAdjustedAmountAffordable(\n adjustedSourceAmount: BigNumber,\n gasCost: BigNumber,\n originalSourceAmount: BigNumber,\n): boolean {\n return adjustedSourceAmount\n .plus(gasCost)\n .isLessThanOrEqualTo(originalSourceAmount);\n}\n\nasync function getAdjustedPhase2Quote(\n adjustedSourceAmount: BigNumber,\n initialGasEstimate: GasCostEstimate,\n context: MaxAmountQuoteContext,\n): Promise<TransactionPayQuote<RelayQuote> | undefined> {\n const { fullRequest, getSingleQuote, request } = context;\n\n log('Requesting adjusted max quote', {\n adjustedAmount: adjustedSourceAmount.toString(10),\n gasCostInSourceToken: initialGasEstimate.amount.toString(10),\n gasEstimateSource: initialGasEstimate.source,\n originalAmount: request.sourceTokenAmount,\n });\n\n try {\n return await getSingleQuote(\n {\n ...request,\n sourceTokenAmount: adjustedSourceAmount.toFixed(\n 0,\n BigNumber.ROUND_DOWN,\n ),\n },\n fullRequest,\n );\n } catch (error) {\n log('Adjusted quote request failed, falling back to phase-1 quote', {\n error,\n });\n return undefined;\n }\n}\n\nasync function getGasCostFromQuoteOrGasStation(\n quote: TransactionPayQuote<RelayQuote>,\n messenger: TransactionPayControllerMessenger,\n request: QuoteRequest,\n): Promise<GasCostEstimate | undefined> {\n const gasCost = quote.fees.sourceNetwork.max;\n\n if (quote.fees.isSourceGasFeeToken) {\n log('Gas cost already in source token units', { raw: gasCost.raw });\n return {\n amount: new BigNumber(gasCost.raw),\n source: GasCostEstimateSource.Quote,\n };\n }\n\n const firstStepData = quote.original.steps[0]?.items[0]?.data;\n\n if (!firstStepData) {\n return undefined;\n }\n\n const totalItemCount = quote.original.steps.reduce(\n (count, step) => count + step.items.length,\n 0,\n );\n\n const totalGasEstimate = (quote.original.metamask.gasLimits ?? []).reduce(\n (acc, gasLimit) => acc + gasLimit,\n 0,\n );\n\n const gasStationCost = await getGasStationCostInSourceTokenRaw({\n firstStepData,\n messenger,\n request: {\n from: request.from,\n sourceChainId: request.sourceChainId,\n sourceTokenAddress: request.sourceTokenAddress,\n },\n totalGasEstimate,\n totalItemCount,\n });\n\n if (!gasStationCost) {\n return undefined;\n }\n\n return {\n amount: new BigNumber(gasStationCost.raw),\n source: GasCostEstimateSource.GasStation,\n };\n}\n\nasync function getProbeGasCostInSourceTokenRaw(\n context: MaxAmountQuoteContext,\n): Promise<BigNumber | undefined> {\n const { fullRequest, getSingleQuote, messenger, request } = context;\n\n const sourceTokenInfo = getTokenInfo(\n messenger,\n request.sourceTokenAddress,\n request.sourceChainId,\n );\n\n if (!sourceTokenInfo) {\n return undefined;\n }\n\n const probeAmount = getProbeSourceAmountRaw(\n request.sourceTokenAmount,\n sourceTokenInfo.decimals,\n );\n\n if (!probeAmount || probeAmount === request.sourceTokenAmount) {\n return undefined;\n }\n\n log('Requesting probe quote for gas station estimation', {\n originalSourceAmount: request.sourceTokenAmount,\n probeAmount,\n });\n\n let probeQuote: TransactionPayQuote<RelayQuote> | undefined;\n\n try {\n probeQuote = await getSingleQuote(\n {\n ...request,\n sourceTokenAmount: probeAmount,\n },\n fullRequest,\n );\n } catch (error) {\n log('Probe quote request failed', { error });\n return undefined;\n }\n\n if (!probeQuote) {\n return undefined;\n }\n\n const probeEstimate = await getGasCostFromQuoteOrGasStation(\n probeQuote,\n messenger,\n request,\n );\n\n return probeEstimate?.amount;\n}\n\nfunction getProbeSourceAmountRaw(\n sourceAmountRaw: string,\n sourceDecimals: number,\n): string | undefined {\n const sourceAmount = new BigNumber(sourceAmountRaw);\n\n if (sourceAmount.isLessThanOrEqualTo(0)) {\n return undefined;\n }\n\n const probeRawAmount = sourceAmount\n .multipliedBy(PROBE_AMOUNT_PERCENTAGE)\n .integerValue(BigNumber.ROUND_FLOOR);\n\n // Minimum probe size: ~0.01 token for tokens with >=2 decimals,\n // otherwise one raw unit for low-decimal tokens.\n const minimumProbeRaw = new BigNumber(1).shiftedBy(\n Math.max(sourceDecimals - 2, 0),\n );\n\n const probeRaw = BigNumber.minimum(\n sourceAmount,\n BigNumber.maximum(probeRawAmount, minimumProbeRaw),\n ).integerValue(BigNumber.ROUND_FLOOR);\n\n if (probeRaw.isLessThanOrEqualTo(0)) {\n return undefined;\n }\n\n return probeRaw.toFixed(0);\n}\n\nfunction fallbackToPhase1(\n phase1Quote: TransactionPayQuote<RelayQuote>,\n message: string,\n paramsToLog?: Record<string, unknown>,\n): TransactionPayQuote<RelayQuote> {\n log(message, paramsToLog);\n return phase1Quote;\n}\n\nfunction hasMatchingGasLimitShape(phase1: number[], phase2: number[]): boolean {\n return phase1.length === phase2.length;\n}\n\nfunction getTotalGasLimit(gasLimits: number[]): number {\n return gasLimits.reduce((total, gasLimit) => total + gasLimit, 0);\n}\n\nfunction markQuoteAsMaxGasStation(\n quote: TransactionPayQuote<RelayQuote>,\n): void {\n quote.original.metamask = {\n ...quote.original.metamask,\n isMaxGasStation: true,\n };\n}\n"]}
@@ -7,9 +7,12 @@ const controller_utils_1 = require("@metamask/controller-utils");
7
7
  const utils_1 = require("@metamask/utils");
8
8
  const bignumber_js_1 = require("bignumber.js");
9
9
  const constants_1 = require("./constants.cjs");
10
+ const gas_station_1 = require("./gas-station.cjs");
11
+ const relay_max_gas_station_1 = require("./relay-max-gas-station.cjs");
10
12
  const __1 = require("../../index.cjs");
11
13
  const constants_2 = require("../../constants.cjs");
12
14
  const logger_1 = require("../../logger.cjs");
15
+ const amounts_1 = require("../../utils/amounts.cjs");
13
16
  const feature_flags_1 = require("../../utils/feature-flags.cjs");
14
17
  const gas_1 = require("../../utils/gas.cjs");
15
18
  const token_1 = require("../../utils/token.cjs");
@@ -31,7 +34,7 @@ async function getRelayQuotes(request) {
31
34
  singleRequest.isPostQuote)
32
35
  .map((singleRequest) => normalizeRequest(singleRequest));
33
36
  log('Normalized requests', normalizedRequests);
34
- return await Promise.all(normalizedRequests.map((singleRequest) => getSingleQuote(singleRequest, request)));
37
+ return await Promise.all(normalizedRequests.map((singleRequest) => getQuoteWithMaxAmountHandling(singleRequest, request)));
35
38
  }
36
39
  catch (error) {
37
40
  log('Error fetching quotes', { error });
@@ -39,6 +42,13 @@ async function getRelayQuotes(request) {
39
42
  }
40
43
  }
41
44
  exports.getRelayQuotes = getRelayQuotes;
45
+ async function getQuoteWithMaxAmountHandling(request, fullRequest) {
46
+ const { isMaxAmount } = request;
47
+ if (!isMaxAmount) {
48
+ return getSingleQuote(request, fullRequest);
49
+ }
50
+ return (0, relay_max_gas_station_1.getRelayMaxGasStationQuote)(request, fullRequest, getSingleQuote);
51
+ }
42
52
  /**
43
53
  * Fetches a single Relay quote.
44
54
  *
@@ -73,6 +83,12 @@ async function getSingleQuote(request, fullRequest) {
73
83
  if (!request.isPostQuote) {
74
84
  await processTransactions(transaction, request, body, messenger);
75
85
  }
86
+ else if (request.refundTo) {
87
+ // For post-quote flows, honour the caller-specified refund address so that
88
+ // failed Relay transactions refund to the correct account (e.g. the Predict
89
+ // Safe proxy) rather than defaulting to the EOA.
90
+ body.refundTo = request.refundTo;
91
+ }
76
92
  const url = (0, feature_flags_1.getFeatureFlags)(messenger).relayQuoteUrl;
77
93
  log('Request body', { body, url });
78
94
  const response = await (0, controller_utils_1.successfulFetch)(url, {
@@ -191,15 +207,15 @@ async function normalizeQuote(quote, request, fullRequest) {
191
207
  const { details } = quote;
192
208
  const { currencyIn, currencyOut } = details;
193
209
  const { usdToFiatRate } = getFiatRates(messenger, request);
194
- const dust = getFiatValueFromUsd(calculateDustUsd(quote, request), usdToFiatRate);
210
+ const dust = (0, amounts_1.getFiatValueFromUsd)(calculateDustUsd(quote, request), usdToFiatRate);
195
211
  const subsidizedFeeUsd = getSubsidizedFeeAmountUsd(quote);
196
212
  const appFeeUsd = new bignumber_js_1.BigNumber(quote.fees?.app?.amountUsd ?? '0');
197
- const metaMaskFee = getFiatValueFromUsd(appFeeUsd, usdToFiatRate);
213
+ const metaMaskFee = (0, amounts_1.getFiatValueFromUsd)(appFeeUsd, usdToFiatRate);
198
214
  // Subtract app fee from provider fee since totalImpact.usd already includes it
199
215
  const providerFeeUsd = calculateProviderFee(quote).minus(appFeeUsd);
200
216
  const provider = subsidizedFeeUsd.gt(0)
201
217
  ? { usd: '0', fiat: '0' }
202
- : getFiatValueFromUsd(providerFeeUsd, usdToFiatRate);
218
+ : (0, amounts_1.getFiatValueFromUsd)(providerFeeUsd, usdToFiatRate);
203
219
  const { gasLimits, isGasFeeToken: isSourceGasFeeToken, ...sourceNetwork } = await calculateSourceNetworkCost(quote, messenger, request, fullRequest.transaction);
204
220
  const targetNetwork = {
205
221
  usd: '0',
@@ -208,7 +224,7 @@ async function normalizeQuote(quote, request, fullRequest) {
208
224
  const sourceAmount = {
209
225
  human: currencyIn.amountFormatted,
210
226
  raw: currencyIn.amount,
211
- ...getFiatValueFromUsd(new bignumber_js_1.BigNumber(currencyIn.amountUsd), usdToFiatRate),
227
+ ...(0, amounts_1.getFiatValueFromUsd)(new bignumber_js_1.BigNumber(currencyIn.amountUsd), usdToFiatRate),
212
228
  };
213
229
  const isTargetStablecoin = isStablecoin(request.targetChainId, request.targetTokenAddress);
214
230
  const additionalTargetAmountUsd = quote.request.tradeType === 'EXACT_INPUT'
@@ -221,7 +237,7 @@ async function normalizeQuote(quote, request, fullRequest) {
221
237
  ? new bignumber_js_1.BigNumber(currencyOut.amountFormatted)
222
238
  : new bignumber_js_1.BigNumber(currencyOut.amountUsd);
223
239
  const targetAmountUsd = baseTargetAmountUsd.plus(additionalTargetAmountUsd);
224
- const targetAmount = getFiatValueFromUsd(targetAmountUsd, usdToFiatRate);
240
+ const targetAmount = (0, amounts_1.getFiatValueFromUsd)(targetAmountUsd, usdToFiatRate);
225
241
  const metamask = {
226
242
  gasLimits,
227
243
  };
@@ -260,20 +276,6 @@ function calculateDustUsd(quote, request) {
260
276
  const dustRaw = new bignumber_js_1.BigNumber(minimumAmount).minus(request.targetAmountMinimum);
261
277
  return dustRaw.shiftedBy(-targetDecimals).multipliedBy(targetUsdRate);
262
278
  }
263
- /**
264
- * Converts USD value to fiat value.
265
- *
266
- * @param usdValue - USD value.
267
- * @param usdToFiatRate - USD to fiat rate.
268
- * @returns Fiat value.
269
- */
270
- function getFiatValueFromUsd(usdValue, usdToFiatRate) {
271
- const fiatValue = usdValue.multipliedBy(usdToFiatRate);
272
- return {
273
- usd: usdValue.toString(10),
274
- fiat: fiatValue.toString(10),
275
- };
276
- }
277
279
  /**
278
280
  * Calculates USD to fiat rate.
279
281
  *
@@ -314,7 +316,6 @@ async function calculateSourceNetworkCost(quote, messenger, request, transaction
314
316
  const relayParams = quote.steps
315
317
  .flatMap((step) => step.items)
316
318
  .map((item) => item.data);
317
- const { relayDisabledGasStationChains } = (0, feature_flags_1.getFeatureFlags)(messenger);
318
319
  const { chainId, data, maxFeePerGas, maxPriorityFeePerGas, to, value } = relayParams[0];
319
320
  const { totalGasEstimate, totalGasLimit, gasLimits } = await calculateSourceNetworkGasLimit(relayParams, messenger, request.isPostQuote ? transaction : undefined);
320
321
  log('Gas limit', {
@@ -342,16 +343,14 @@ async function calculateSourceNetworkCost(quote, messenger, request, transaction
342
343
  if (new bignumber_js_1.BigNumber(nativeBalance).isGreaterThanOrEqualTo(max.raw)) {
343
344
  return result;
344
345
  }
345
- if (relayDisabledGasStationChains.includes(sourceChainId)) {
346
+ const gasStationEligibility = (0, gas_station_1.getGasStationEligibility)(messenger, sourceChainId);
347
+ if (gasStationEligibility.isDisabledChain) {
346
348
  log('Skipping gas station as disabled chain', {
347
349
  sourceChainId,
348
- disabledChainIds: relayDisabledGasStationChains,
349
350
  });
350
351
  return result;
351
352
  }
352
- const supportedChains = (0, feature_flags_1.getEIP7702SupportedChains)(messenger);
353
- const chainSupportsGasStation = supportedChains.some((supportedChainId) => supportedChainId.toLowerCase() === sourceChainId.toLowerCase());
354
- if (!chainSupportsGasStation) {
353
+ if (!gasStationEligibility.chainSupportsGasStation) {
355
354
  log('Skipping gas station as chain does not support EIP-7702', {
356
355
  sourceChainId,
357
356
  });
@@ -361,40 +360,20 @@ async function calculateSourceNetworkCost(quote, messenger, request, transaction
361
360
  nativeBalance,
362
361
  max: max.raw,
363
362
  });
364
- const gasFeeTokens = await messenger.call('TransactionController:getGasFeeTokens', {
365
- chainId: sourceChainId,
366
- data,
367
- from,
368
- to,
369
- value: (0, controller_utils_1.toHex)(value ?? '0'),
370
- });
371
- log('Source gas fee tokens', { gasFeeTokens });
372
- const gasFeeToken = gasFeeTokens.find((singleGasFeeToken) => singleGasFeeToken.tokenAddress.toLowerCase() ===
373
- sourceTokenAddress.toLowerCase());
374
- if (!gasFeeToken) {
375
- log('No matching gas fee token found', {
376
- sourceTokenAddress,
377
- gasFeeTokens,
378
- });
379
- return result;
380
- }
381
- let finalAmount = gasFeeToken.amount;
382
- const hasMultipleTransactions = relayParams.length > 1 || gasLimits.length > 1;
383
- if (hasMultipleTransactions) {
384
- const gasRate = new bignumber_js_1.BigNumber(gasFeeToken.amount, 16).dividedBy(gasFeeToken.gas, 16);
385
- const finalAmountValue = gasRate.multipliedBy(totalGasEstimate);
386
- finalAmount = (0, controller_utils_1.toHex)(finalAmountValue.toFixed(0));
387
- log('Estimated gas fee token amount for batch', {
388
- finalAmount: finalAmountValue.toString(10),
389
- gasRate: gasRate.toString(10),
390
- totalGasEstimate,
391
- });
392
- }
393
- const finalGasFeeToken = { ...gasFeeToken, amount: finalAmount };
394
- const gasFeeTokenCost = (0, gas_1.calculateGasFeeTokenCost)({
395
- chainId: sourceChainId,
396
- gasFeeToken: finalGasFeeToken,
363
+ const gasFeeTokenCost = await (0, gas_station_1.getGasStationCostInSourceTokenRaw)({
364
+ firstStepData: {
365
+ data,
366
+ to,
367
+ value,
368
+ },
397
369
  messenger,
370
+ request: {
371
+ from,
372
+ sourceChainId,
373
+ sourceTokenAddress,
374
+ },
375
+ totalGasEstimate,
376
+ totalItemCount: Math.max(relayParams.length, gasLimits.length),
398
377
  });
399
378
  if (!gasFeeTokenCost) {
400
379
  return result;