@metamask-previews/transaction-pay-controller 25.0.0-preview-5cac90a → 25.0.0-preview-eade524

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 (74) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1 -1
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +1 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/strategy/relay/relay-quotes.cjs +2 -10
  9. package/dist/strategy/relay/relay-quotes.cjs.map +1 -1
  10. package/dist/strategy/relay/relay-quotes.d.cts.map +1 -1
  11. package/dist/strategy/relay/relay-quotes.d.mts.map +1 -1
  12. package/dist/strategy/relay/relay-quotes.mjs +2 -10
  13. package/dist/strategy/relay/relay-quotes.mjs.map +1 -1
  14. package/dist/strategy/relay/relay-submit-execute.cjs +45 -60
  15. package/dist/strategy/relay/relay-submit-execute.cjs.map +1 -1
  16. package/dist/strategy/relay/relay-submit-execute.d.cts +1 -10
  17. package/dist/strategy/relay/relay-submit-execute.d.cts.map +1 -1
  18. package/dist/strategy/relay/relay-submit-execute.d.mts +1 -10
  19. package/dist/strategy/relay/relay-submit-execute.d.mts.map +1 -1
  20. package/dist/strategy/relay/relay-submit-execute.mjs +44 -58
  21. package/dist/strategy/relay/relay-submit-execute.mjs.map +1 -1
  22. package/dist/strategy/relay/relay-submit.cjs +46 -100
  23. package/dist/strategy/relay/relay-submit.cjs.map +1 -1
  24. package/dist/strategy/relay/relay-submit.d.cts +1 -10
  25. package/dist/strategy/relay/relay-submit.d.cts.map +1 -1
  26. package/dist/strategy/relay/relay-submit.d.mts +1 -10
  27. package/dist/strategy/relay/relay-submit.d.mts.map +1 -1
  28. package/dist/strategy/relay/relay-submit.mjs +45 -98
  29. package/dist/strategy/relay/relay-submit.mjs.map +1 -1
  30. package/dist/tests/messenger-mock.cjs +0 -3
  31. package/dist/tests/messenger-mock.cjs.map +1 -1
  32. package/dist/tests/messenger-mock.d.cts +0 -3
  33. package/dist/tests/messenger-mock.d.cts.map +1 -1
  34. package/dist/tests/messenger-mock.d.mts +0 -3
  35. package/dist/tests/messenger-mock.d.mts.map +1 -1
  36. package/dist/tests/messenger-mock.mjs +0 -3
  37. package/dist/tests/messenger-mock.mjs.map +1 -1
  38. package/dist/types.cjs.map +1 -1
  39. package/dist/types.d.cts +1 -25
  40. package/dist/types.d.cts.map +1 -1
  41. package/dist/types.d.mts +1 -25
  42. package/dist/types.d.mts.map +1 -1
  43. package/dist/types.mjs.map +1 -1
  44. package/dist/utils/quotes.cjs +6 -14
  45. package/dist/utils/quotes.cjs.map +1 -1
  46. package/dist/utils/quotes.d.cts.map +1 -1
  47. package/dist/utils/quotes.d.mts.map +1 -1
  48. package/dist/utils/quotes.mjs +6 -14
  49. package/dist/utils/quotes.mjs.map +1 -1
  50. package/package.json +1 -2
  51. package/dist/strategy/relay/relay-validation.cjs +0 -128
  52. package/dist/strategy/relay/relay-validation.cjs.map +0 -1
  53. package/dist/strategy/relay/relay-validation.d.cts +0 -11
  54. package/dist/strategy/relay/relay-validation.d.cts.map +0 -1
  55. package/dist/strategy/relay/relay-validation.d.mts +0 -11
  56. package/dist/strategy/relay/relay-validation.d.mts.map +0 -1
  57. package/dist/strategy/relay/relay-validation.mjs +0 -124
  58. package/dist/strategy/relay/relay-validation.mjs.map +0 -1
  59. package/dist/utils/simulation.cjs +0 -50
  60. package/dist/utils/simulation.cjs.map +0 -1
  61. package/dist/utils/simulation.d.cts +0 -15
  62. package/dist/utils/simulation.d.cts.map +0 -1
  63. package/dist/utils/simulation.d.mts +0 -15
  64. package/dist/utils/simulation.d.mts.map +0 -1
  65. package/dist/utils/simulation.mjs +0 -45
  66. package/dist/utils/simulation.mjs.map +0 -1
  67. package/dist/utils/validation.cjs +0 -193
  68. package/dist/utils/validation.cjs.map +0 -1
  69. package/dist/utils/validation.d.cts +0 -18
  70. package/dist/utils/validation.d.cts.map +0 -1
  71. package/dist/utils/validation.d.mts +0 -18
  72. package/dist/utils/validation.d.mts.map +0 -1
  73. package/dist/utils/validation.mjs +0 -187
  74. package/dist/utils/validation.mjs.map +0 -1
@@ -1,45 +0,0 @@
1
- import { SentinelChainNotSupportedError } from "@metamask/sentinel-api-service";
2
- import { createModuleLogger } from "@metamask/utils";
3
- import { projectLogger } from "../logger.mjs";
4
- const log = createModuleLogger(projectLogger, 'simulation');
5
- export class TransactionPaySimulationError extends Error {
6
- constructor(message) {
7
- super(message);
8
- this.name = 'TransactionPaySimulationError';
9
- }
10
- }
11
- export async function simulateQuoteTransactions(request) {
12
- log('Simulating quote transactions', {
13
- chainId: request.chainId,
14
- transactions: request.transactions,
15
- });
16
- let responseTransactions;
17
- try {
18
- const response = await request.messenger.call('SentinelApiService:simulateTransactions', request.chainId, {
19
- transactions: request.transactions,
20
- withLogs: true,
21
- });
22
- responseTransactions = response.transactions;
23
- log('Sentinel simulation succeeded', { responseTransactions });
24
- }
25
- catch (error) {
26
- log('Sentinel simulation failed', { error });
27
- if (error instanceof SentinelChainNotSupportedError) {
28
- log('Skipping validation for Sentinel-unsupported chain');
29
- return;
30
- }
31
- throw new TransactionPaySimulationError(getErrorMessage(error));
32
- }
33
- for (const responseTransaction of responseTransactions) {
34
- if (responseTransaction.error) {
35
- throw new TransactionPaySimulationError(responseTransaction.error);
36
- }
37
- }
38
- }
39
- function getErrorMessage(error) {
40
- if (error instanceof Error) {
41
- return error.message;
42
- }
43
- return String(error);
44
- }
45
- //# sourceMappingURL=simulation.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"simulation.mjs","sourceRoot":"","sources":["../../src/utils/simulation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,uCAAuC;AAMhF,OAAO,EAAE,kBAAkB,EAAE,wBAAwB;AAGrD,OAAO,EAAE,aAAa,EAAE,sBAAkB;AAK1C,MAAM,GAAG,GAAG,kBAAkB,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;AAU5D,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IACtD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAC9C,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAA0B;IAE1B,GAAG,CAAC,+BAA+B,EAAE;QACnC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,YAAY,EAAE,OAAO,CAAC,YAAY;KACnC,CAAC,CAAC;IAEH,IAAI,oBAA6D,CAAC;IAElE,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,IAAI,CAC3C,yCAAyC,EACzC,OAAO,CAAC,OAAO,EACf;YACE,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,QAAQ,EAAE,IAAI;SACc,CAC/B,CAAC;QAEF,oBAAoB,GAAG,QAAQ,CAAC,YAAY,CAAC;QAE7C,GAAG,CAAC,+BAA+B,EAAE,EAAE,oBAAoB,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,4BAA4B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAE7C,IAAI,KAAK,YAAY,8BAA8B,EAAE,CAAC;YACpD,GAAG,CAAC,oDAAoD,CAAC,CAAC;YAC1D,OAAO;QACT,CAAC;QAED,MAAM,IAAI,6BAA6B,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,KAAK,MAAM,mBAAmB,IAAI,oBAAoB,EAAE,CAAC;QACvD,IAAI,mBAAmB,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,IAAI,6BAA6B,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC","sourcesContent":["import { SentinelChainNotSupportedError } from '@metamask/sentinel-api-service';\nimport type {\n SentinelSimulationRequest,\n SentinelSimulationResponseTransaction,\n SentinelSimulationTransaction,\n} from '@metamask/sentinel-api-service';\nimport { createModuleLogger } from '@metamask/utils';\nimport type { Hex } from '@metamask/utils';\n\nimport { projectLogger } from '../logger';\nimport type { TransactionPayControllerMessenger } from '../types';\n\nexport type { SentinelSimulationTransaction };\n\nconst log = createModuleLogger(projectLogger, 'simulation');\n\nexport type SimulationTransaction = SentinelSimulationTransaction;\n\nexport type SimulationRequest = {\n chainId: Hex;\n messenger: TransactionPayControllerMessenger;\n transactions: SimulationTransaction[];\n};\n\nexport class TransactionPaySimulationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TransactionPaySimulationError';\n }\n}\n\nexport async function simulateQuoteTransactions(\n request: SimulationRequest,\n): Promise<void> {\n log('Simulating quote transactions', {\n chainId: request.chainId,\n transactions: request.transactions,\n });\n\n let responseTransactions: SentinelSimulationResponseTransaction[];\n\n try {\n const response = await request.messenger.call(\n 'SentinelApiService:simulateTransactions',\n request.chainId,\n {\n transactions: request.transactions,\n withLogs: true,\n } as SentinelSimulationRequest,\n );\n\n responseTransactions = response.transactions;\n\n log('Sentinel simulation succeeded', { responseTransactions });\n } catch (error) {\n log('Sentinel simulation failed', { error });\n\n if (error instanceof SentinelChainNotSupportedError) {\n log('Skipping validation for Sentinel-unsupported chain');\n return;\n }\n\n throw new TransactionPaySimulationError(getErrorMessage(error));\n }\n\n for (const responseTransaction of responseTransactions) {\n if (responseTransaction.error) {\n throw new TransactionPaySimulationError(responseTransaction.error);\n }\n }\n}\n\nfunction getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n}\n"]}
@@ -1,193 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isQuoteError = exports.validateQuoteExecution = exports.QuoteError = void 0;
4
- const abi_1 = require("@ethersproject/abi");
5
- const metamask_eth_abis_1 = require("@metamask/metamask-eth-abis");
6
- const bignumber_js_1 = require("bignumber.js");
7
- const logger_1 = require("../logger.cjs");
8
- const simulation_1 = require("./simulation.cjs");
9
- const token_1 = require("./token.cjs");
10
- const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, 'validation');
11
- const erc20Interface = new abi_1.Interface(metamask_eth_abis_1.abiERC20);
12
- class QuoteError extends Error {
13
- constructor(info) {
14
- super(info.message);
15
- this.name = 'QuoteError';
16
- this.info = info;
17
- }
18
- }
19
- exports.QuoteError = QuoteError;
20
- async function validateQuoteExecution({ messenger, quote, signal, simulation, }) {
21
- throwIfAborted(signal);
22
- const liveBalance = await getLiveSourceBalance(quote, messenger);
23
- log('Live source balance', {
24
- from: quote.request.from,
25
- liveBalance,
26
- sourceChainId: quote.request.sourceChainId,
27
- sourceTokenAddress: quote.request.sourceTokenAddress,
28
- });
29
- throwIfAborted(signal);
30
- log('Checking quote source amount', {
31
- hasPaymentOverride: Boolean(quote.request.paymentOverride),
32
- isPostQuote: Boolean(quote.request.isPostQuote),
33
- liveBalance,
34
- requiredAmount: quote.sourceAmount.raw,
35
- });
36
- validateRequiredSourceAmount(messenger, quote, liveBalance);
37
- log('Quote source amount check passed');
38
- log('Checking decoded source transfers', {
39
- sourceChainId: quote.request.sourceChainId,
40
- sourceTokenAddress: quote.request.sourceTokenAddress,
41
- transactionCount: simulation.transactions.length,
42
- });
43
- validateDecodedSourceTransfers(messenger, quote, liveBalance, simulation.transactions);
44
- log('Decoded source transfers check passed');
45
- throwIfAborted(signal);
46
- log('Starting simulation', {
47
- chainId: quote.request.sourceChainId,
48
- transactions: simulation.transactions,
49
- });
50
- try {
51
- await (0, simulation_1.simulateQuoteTransactions)({
52
- chainId: quote.request.sourceChainId,
53
- messenger,
54
- transactions: simulation.transactions,
55
- });
56
- log('Simulation passed');
57
- }
58
- catch (error) {
59
- throwIfAborted(signal);
60
- if (error instanceof simulation_1.TransactionPaySimulationError) {
61
- throw new QuoteError({
62
- message: 'Quote simulation failed',
63
- reason: 'simulation-failed',
64
- detail: [error.message],
65
- });
66
- }
67
- throw error;
68
- }
69
- }
70
- exports.validateQuoteExecution = validateQuoteExecution;
71
- function isQuoteError(error) {
72
- return error instanceof QuoteError;
73
- }
74
- exports.isQuoteError = isQuoteError;
75
- /**
76
- * Format an amount shortfall into display-ready detail rows.
77
- *
78
- * Amounts are formatted using the source token decimals and symbol when
79
- * available (e.g. `Required: 1.5 USDC`), falling back to the raw atomic values
80
- * otherwise.
81
- *
82
- * @param messenger - Controller messenger.
83
- * @param quote - Quote being validated.
84
- * @param required - Required amount (raw string).
85
- * @param balance - Current balance (raw string).
86
- * @returns Detail rows: Required / Current / Missing.
87
- */
88
- function formatBalanceShortfall(messenger, quote, required, balance) {
89
- const { sourceChainId, sourceTokenAddress } = quote.request;
90
- const tokenInfo = (0, token_1.getTokenInfo)(messenger, sourceTokenAddress, sourceChainId);
91
- const requiredBn = new bignumber_js_1.BigNumber(required);
92
- const balanceBn = new bignumber_js_1.BigNumber(balance);
93
- // Only ever called when balance < required, so the shortfall is positive.
94
- const missing = requiredBn.minus(balanceBn);
95
- return [
96
- `Required: ${formatTokenAmount(requiredBn, tokenInfo)}`,
97
- `Current: ${formatTokenAmount(balanceBn, tokenInfo)}`,
98
- `Missing: ${formatTokenAmount(missing, tokenInfo)}`,
99
- ];
100
- }
101
- /**
102
- * Format an atomic token amount as a human-readable string with symbol.
103
- *
104
- * @param rawAmount - Amount in atomic units.
105
- * @param tokenInfo - Source token decimals and symbol, if available.
106
- * @returns Human-readable amount suffixed with the token symbol when known,
107
- * otherwise the raw atomic value.
108
- */
109
- function formatTokenAmount(rawAmount, tokenInfo) {
110
- if (!tokenInfo) {
111
- return rawAmount.toFixed();
112
- }
113
- const human = rawAmount.shiftedBy(-tokenInfo.decimals).toFixed();
114
- return `${human} ${tokenInfo.symbol}`;
115
- }
116
- async function getLiveSourceBalance(quote, messenger) {
117
- const { from, sourceChainId, sourceTokenAddress } = quote.request;
118
- const normalizedSourceTokenAddress = (0, token_1.normalizeTokenAddress)(sourceTokenAddress, sourceChainId, token_1.TokenAddressTarget.MetaMask);
119
- try {
120
- return await (0, token_1.getLiveTokenBalance)(messenger, from, sourceChainId, normalizedSourceTokenAddress);
121
- }
122
- catch (error) {
123
- throw new QuoteError({
124
- message: 'Unable to verify balance',
125
- reason: 'balance-unavailable',
126
- detail: [error.message],
127
- });
128
- }
129
- }
130
- function validateRequiredSourceAmount(messenger, quote, liveBalance) {
131
- if (quote.request.isPostQuote || quote.request.paymentOverride) {
132
- return;
133
- }
134
- const requiredAmount = new bignumber_js_1.BigNumber(quote.sourceAmount.raw);
135
- const balance = new bignumber_js_1.BigNumber(liveBalance);
136
- if (balance.isGreaterThanOrEqualTo(requiredAmount)) {
137
- return;
138
- }
139
- throw new QuoteError({
140
- message: 'Insufficient source balance for quote',
141
- reason: 'insufficient-source-balance',
142
- detail: formatBalanceShortfall(messenger, quote, requiredAmount.toFixed(), balance.toFixed()),
143
- });
144
- }
145
- function validateDecodedSourceTransfers(messenger, quote, liveBalance, transactions) {
146
- const decodedAmounts = getDecodedSourceTransferAmounts(quote, transactions);
147
- const requiredAmount = decodedAmounts
148
- .reduce((total, amount) => total.plus(amount), new bignumber_js_1.BigNumber(0))
149
- .toString(10);
150
- const balance = new bignumber_js_1.BigNumber(liveBalance);
151
- log('Decoded source transfer amounts', {
152
- decodedAmounts,
153
- liveBalance,
154
- requiredAmount,
155
- });
156
- if (balance.isGreaterThanOrEqualTo(requiredAmount)) {
157
- return;
158
- }
159
- throw new QuoteError({
160
- message: 'Insufficient source balance for decoded transfer',
161
- reason: 'insufficient-transfer-balance',
162
- detail: formatBalanceShortfall(messenger, quote, requiredAmount, liveBalance),
163
- });
164
- }
165
- function getDecodedSourceTransferAmounts(quote, transactions) {
166
- const { sourceChainId, sourceTokenAddress } = quote.request;
167
- const isNativeSource = sourceTokenAddress.toLowerCase() ===
168
- (0, token_1.getNativeToken)(sourceChainId).toLowerCase();
169
- if (isNativeSource) {
170
- return [];
171
- }
172
- const normalizedSourceTokenAddress = (0, token_1.normalizeTokenAddress)(sourceTokenAddress, sourceChainId, token_1.TokenAddressTarget.MetaMask).toLowerCase();
173
- return transactions
174
- .filter((transaction) => transaction.to &&
175
- (0, token_1.normalizeTokenAddress)(transaction.to, sourceChainId, token_1.TokenAddressTarget.MetaMask).toLowerCase() === normalizedSourceTokenAddress)
176
- .map((transaction) => transaction.data ? decodeTransferAmount(transaction.data) : undefined)
177
- .filter((amount) => amount !== undefined);
178
- }
179
- function decodeTransferAmount(data) {
180
- try {
181
- const result = erc20Interface.decodeFunctionData('transfer', data);
182
- return new bignumber_js_1.BigNumber(result._value.toString()).toString(10);
183
- }
184
- catch {
185
- return undefined;
186
- }
187
- }
188
- function throwIfAborted(signal) {
189
- if (signal?.aborted) {
190
- throw new Error('Quote validation aborted');
191
- }
192
- }
193
- //# sourceMappingURL=validation.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"validation.cjs","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Interface } from '@ethersproject/abi';\nimport { abiERC20 } from '@metamask/metamask-eth-abis';\nimport type { Hex } from '@metamask/utils';\nimport { BigNumber } from 'bignumber.js';\n\nimport { createModuleLogger, projectLogger } from '../logger';\nimport type {\n QuoteErrorInfo,\n TransactionPayControllerMessenger,\n TransactionPayQuote,\n} from '../types';\nimport {\n SimulationTransaction,\n simulateQuoteTransactions,\n TransactionPaySimulationError,\n} from './simulation';\nimport {\n getLiveTokenBalance,\n getNativeToken,\n getTokenInfo,\n normalizeTokenAddress,\n TokenAddressTarget,\n} from './token';\n\nconst log = createModuleLogger(projectLogger, 'validation');\n\nconst erc20Interface = new Interface(abiERC20);\n\nexport type QuoteSimulation = {\n transactions: SimulationTransaction[];\n};\n\nexport type QuoteExecutionRequest = {\n messenger: TransactionPayControllerMessenger;\n quote: TransactionPayQuote<unknown>;\n signal?: AbortSignal;\n simulation: QuoteSimulation;\n};\n\nexport class QuoteError extends Error {\n readonly info: QuoteErrorInfo;\n\n constructor(info: QuoteErrorInfo) {\n super(info.message);\n this.name = 'QuoteError';\n this.info = info;\n }\n}\n\nexport async function validateQuoteExecution({\n messenger,\n quote,\n signal,\n simulation,\n}: QuoteExecutionRequest): Promise<void> {\n throwIfAborted(signal);\n\n const liveBalance = await getLiveSourceBalance(quote, messenger);\n\n log('Live source balance', {\n from: quote.request.from,\n liveBalance,\n sourceChainId: quote.request.sourceChainId,\n sourceTokenAddress: quote.request.sourceTokenAddress,\n });\n\n throwIfAborted(signal);\n\n log('Checking quote source amount', {\n hasPaymentOverride: Boolean(quote.request.paymentOverride),\n isPostQuote: Boolean(quote.request.isPostQuote),\n liveBalance,\n requiredAmount: quote.sourceAmount.raw,\n });\n\n validateRequiredSourceAmount(messenger, quote, liveBalance);\n\n log('Quote source amount check passed');\n\n log('Checking decoded source transfers', {\n sourceChainId: quote.request.sourceChainId,\n sourceTokenAddress: quote.request.sourceTokenAddress,\n transactionCount: simulation.transactions.length,\n });\n\n validateDecodedSourceTransfers(\n messenger,\n quote,\n liveBalance,\n simulation.transactions,\n );\n\n log('Decoded source transfers check passed');\n\n throwIfAborted(signal);\n\n log('Starting simulation', {\n chainId: quote.request.sourceChainId,\n transactions: simulation.transactions,\n });\n\n try {\n await simulateQuoteTransactions({\n chainId: quote.request.sourceChainId,\n messenger,\n transactions: simulation.transactions,\n });\n\n log('Simulation passed');\n } catch (error) {\n throwIfAborted(signal);\n\n if (error instanceof TransactionPaySimulationError) {\n throw new QuoteError({\n message: 'Quote simulation failed',\n reason: 'simulation-failed',\n detail: [error.message],\n });\n }\n\n throw error;\n }\n}\n\nexport function isQuoteError(error: unknown): error is QuoteError {\n return error instanceof QuoteError;\n}\n\n/**\n * Format an amount shortfall into display-ready detail rows.\n *\n * Amounts are formatted using the source token decimals and symbol when\n * available (e.g. `Required: 1.5 USDC`), falling back to the raw atomic values\n * otherwise.\n *\n * @param messenger - Controller messenger.\n * @param quote - Quote being validated.\n * @param required - Required amount (raw string).\n * @param balance - Current balance (raw string).\n * @returns Detail rows: Required / Current / Missing.\n */\nfunction formatBalanceShortfall(\n messenger: TransactionPayControllerMessenger,\n quote: TransactionPayQuote<unknown>,\n required: string,\n balance: string,\n): string[] {\n const { sourceChainId, sourceTokenAddress } = quote.request;\n const tokenInfo = getTokenInfo(messenger, sourceTokenAddress, sourceChainId);\n\n const requiredBn = new BigNumber(required);\n const balanceBn = new BigNumber(balance);\n\n // Only ever called when balance < required, so the shortfall is positive.\n const missing = requiredBn.minus(balanceBn);\n\n return [\n `Required: ${formatTokenAmount(requiredBn, tokenInfo)}`,\n `Current: ${formatTokenAmount(balanceBn, tokenInfo)}`,\n `Missing: ${formatTokenAmount(missing, tokenInfo)}`,\n ];\n}\n\n/**\n * Format an atomic token amount as a human-readable string with symbol.\n *\n * @param rawAmount - Amount in atomic units.\n * @param tokenInfo - Source token decimals and symbol, if available.\n * @returns Human-readable amount suffixed with the token symbol when known,\n * otherwise the raw atomic value.\n */\nfunction formatTokenAmount(\n rawAmount: BigNumber,\n tokenInfo: { decimals: number; symbol: string } | undefined,\n): string {\n if (!tokenInfo) {\n return rawAmount.toFixed();\n }\n\n const human = rawAmount.shiftedBy(-tokenInfo.decimals).toFixed();\n\n return `${human} ${tokenInfo.symbol}`;\n}\n\nasync function getLiveSourceBalance(\n quote: TransactionPayQuote<unknown>,\n messenger: TransactionPayControllerMessenger,\n): Promise<string> {\n const { from, sourceChainId, sourceTokenAddress } = quote.request;\n const normalizedSourceTokenAddress = normalizeTokenAddress(\n sourceTokenAddress,\n sourceChainId,\n TokenAddressTarget.MetaMask,\n );\n\n try {\n return await getLiveTokenBalance(\n messenger,\n from,\n sourceChainId,\n normalizedSourceTokenAddress,\n );\n } catch (error) {\n throw new QuoteError({\n message: 'Unable to verify balance',\n reason: 'balance-unavailable',\n detail: [(error as Error).message],\n });\n }\n}\n\nfunction validateRequiredSourceAmount(\n messenger: TransactionPayControllerMessenger,\n quote: TransactionPayQuote<unknown>,\n liveBalance: string,\n): void {\n if (quote.request.isPostQuote || quote.request.paymentOverride) {\n return;\n }\n\n const requiredAmount = new BigNumber(quote.sourceAmount.raw);\n const balance = new BigNumber(liveBalance);\n\n if (balance.isGreaterThanOrEqualTo(requiredAmount)) {\n return;\n }\n\n throw new QuoteError({\n message: 'Insufficient source balance for quote',\n reason: 'insufficient-source-balance',\n detail: formatBalanceShortfall(\n messenger,\n quote,\n requiredAmount.toFixed(),\n balance.toFixed(),\n ),\n });\n}\n\nfunction validateDecodedSourceTransfers(\n messenger: TransactionPayControllerMessenger,\n quote: TransactionPayQuote<unknown>,\n liveBalance: string,\n transactions: SimulationTransaction[],\n): void {\n const decodedAmounts = getDecodedSourceTransferAmounts(quote, transactions);\n\n const requiredAmount = decodedAmounts\n .reduce((total, amount) => total.plus(amount), new BigNumber(0))\n .toString(10);\n\n const balance = new BigNumber(liveBalance);\n\n log('Decoded source transfer amounts', {\n decodedAmounts,\n liveBalance,\n requiredAmount,\n });\n\n if (balance.isGreaterThanOrEqualTo(requiredAmount)) {\n return;\n }\n\n throw new QuoteError({\n message: 'Insufficient source balance for decoded transfer',\n reason: 'insufficient-transfer-balance',\n detail: formatBalanceShortfall(\n messenger,\n quote,\n requiredAmount,\n liveBalance,\n ),\n });\n}\n\nfunction getDecodedSourceTransferAmounts(\n quote: TransactionPayQuote<unknown>,\n transactions: SimulationTransaction[],\n): string[] {\n const { sourceChainId, sourceTokenAddress } = quote.request;\n const isNativeSource =\n sourceTokenAddress.toLowerCase() ===\n getNativeToken(sourceChainId).toLowerCase();\n\n if (isNativeSource) {\n return [];\n }\n\n const normalizedSourceTokenAddress = normalizeTokenAddress(\n sourceTokenAddress,\n sourceChainId,\n TokenAddressTarget.MetaMask,\n ).toLowerCase();\n\n return transactions\n .filter(\n (transaction) =>\n transaction.to &&\n normalizeTokenAddress(\n transaction.to,\n sourceChainId,\n TokenAddressTarget.MetaMask,\n ).toLowerCase() === normalizedSourceTokenAddress,\n )\n .map((transaction) =>\n transaction.data ? decodeTransferAmount(transaction.data) : undefined,\n )\n .filter((amount): amount is string => amount !== undefined);\n}\n\nfunction decodeTransferAmount(data: Hex): string | undefined {\n try {\n const result = erc20Interface.decodeFunctionData('transfer', data);\n return new BigNumber(result._value.toString()).toString(10);\n } catch {\n return undefined;\n }\n}\n\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw new Error('Quote validation aborted');\n }\n}\n"]}
@@ -1,18 +0,0 @@
1
- import type { QuoteErrorInfo, TransactionPayControllerMessenger, TransactionPayQuote } from "../types.cjs";
2
- import { SimulationTransaction } from "./simulation.cjs";
3
- export type QuoteSimulation = {
4
- transactions: SimulationTransaction[];
5
- };
6
- export type QuoteExecutionRequest = {
7
- messenger: TransactionPayControllerMessenger;
8
- quote: TransactionPayQuote<unknown>;
9
- signal?: AbortSignal;
10
- simulation: QuoteSimulation;
11
- };
12
- export declare class QuoteError extends Error {
13
- readonly info: QuoteErrorInfo;
14
- constructor(info: QuoteErrorInfo);
15
- }
16
- export declare function validateQuoteExecution({ messenger, quote, signal, simulation, }: QuoteExecutionRequest): Promise<void>;
17
- export declare function isQuoteError(error: unknown): error is QuoteError;
18
- //# sourceMappingURL=validation.d.cts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"validation.d.cts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,cAAc,EACd,iCAAiC,EACjC,mBAAmB,EACpB,qBAAiB;AAClB,OAAO,EACL,qBAAqB,EAGtB,yBAAqB;AAatB,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,qBAAqB,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,iCAAiC,CAAC;IAC7C,KAAK,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,EAAE,eAAe,CAAC;CAC7B,CAAC;AAEF,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,IAAI,EAAE,cAAc;CAKjC;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,SAAS,EACT,KAAK,EACL,MAAM,EACN,UAAU,GACX,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEvC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE"}
@@ -1,18 +0,0 @@
1
- import type { QuoteErrorInfo, TransactionPayControllerMessenger, TransactionPayQuote } from "../types.mjs";
2
- import { SimulationTransaction } from "./simulation.mjs";
3
- export type QuoteSimulation = {
4
- transactions: SimulationTransaction[];
5
- };
6
- export type QuoteExecutionRequest = {
7
- messenger: TransactionPayControllerMessenger;
8
- quote: TransactionPayQuote<unknown>;
9
- signal?: AbortSignal;
10
- simulation: QuoteSimulation;
11
- };
12
- export declare class QuoteError extends Error {
13
- readonly info: QuoteErrorInfo;
14
- constructor(info: QuoteErrorInfo);
15
- }
16
- export declare function validateQuoteExecution({ messenger, quote, signal, simulation, }: QuoteExecutionRequest): Promise<void>;
17
- export declare function isQuoteError(error: unknown): error is QuoteError;
18
- //# sourceMappingURL=validation.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"validation.d.mts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,cAAc,EACd,iCAAiC,EACjC,mBAAmB,EACpB,qBAAiB;AAClB,OAAO,EACL,qBAAqB,EAGtB,yBAAqB;AAatB,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,qBAAqB,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,iCAAiC,CAAC;IAC7C,KAAK,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,EAAE,eAAe,CAAC;CAC7B,CAAC;AAEF,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,IAAI,EAAE,cAAc;CAKjC;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,SAAS,EACT,KAAK,EACL,MAAM,EACN,UAAU,GACX,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEvC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE"}
@@ -1,187 +0,0 @@
1
- import { Interface } from "@ethersproject/abi";
2
- import { abiERC20 } from "@metamask/metamask-eth-abis";
3
- import { BigNumber } from "bignumber.js";
4
- import { createModuleLogger, projectLogger } from "../logger.mjs";
5
- import { simulateQuoteTransactions, TransactionPaySimulationError } from "./simulation.mjs";
6
- import { getLiveTokenBalance, getNativeToken, getTokenInfo, normalizeTokenAddress, TokenAddressTarget } from "./token.mjs";
7
- const log = createModuleLogger(projectLogger, 'validation');
8
- const erc20Interface = new Interface(abiERC20);
9
- export class QuoteError extends Error {
10
- constructor(info) {
11
- super(info.message);
12
- this.name = 'QuoteError';
13
- this.info = info;
14
- }
15
- }
16
- export async function validateQuoteExecution({ messenger, quote, signal, simulation, }) {
17
- throwIfAborted(signal);
18
- const liveBalance = await getLiveSourceBalance(quote, messenger);
19
- log('Live source balance', {
20
- from: quote.request.from,
21
- liveBalance,
22
- sourceChainId: quote.request.sourceChainId,
23
- sourceTokenAddress: quote.request.sourceTokenAddress,
24
- });
25
- throwIfAborted(signal);
26
- log('Checking quote source amount', {
27
- hasPaymentOverride: Boolean(quote.request.paymentOverride),
28
- isPostQuote: Boolean(quote.request.isPostQuote),
29
- liveBalance,
30
- requiredAmount: quote.sourceAmount.raw,
31
- });
32
- validateRequiredSourceAmount(messenger, quote, liveBalance);
33
- log('Quote source amount check passed');
34
- log('Checking decoded source transfers', {
35
- sourceChainId: quote.request.sourceChainId,
36
- sourceTokenAddress: quote.request.sourceTokenAddress,
37
- transactionCount: simulation.transactions.length,
38
- });
39
- validateDecodedSourceTransfers(messenger, quote, liveBalance, simulation.transactions);
40
- log('Decoded source transfers check passed');
41
- throwIfAborted(signal);
42
- log('Starting simulation', {
43
- chainId: quote.request.sourceChainId,
44
- transactions: simulation.transactions,
45
- });
46
- try {
47
- await simulateQuoteTransactions({
48
- chainId: quote.request.sourceChainId,
49
- messenger,
50
- transactions: simulation.transactions,
51
- });
52
- log('Simulation passed');
53
- }
54
- catch (error) {
55
- throwIfAborted(signal);
56
- if (error instanceof TransactionPaySimulationError) {
57
- throw new QuoteError({
58
- message: 'Quote simulation failed',
59
- reason: 'simulation-failed',
60
- detail: [error.message],
61
- });
62
- }
63
- throw error;
64
- }
65
- }
66
- export function isQuoteError(error) {
67
- return error instanceof QuoteError;
68
- }
69
- /**
70
- * Format an amount shortfall into display-ready detail rows.
71
- *
72
- * Amounts are formatted using the source token decimals and symbol when
73
- * available (e.g. `Required: 1.5 USDC`), falling back to the raw atomic values
74
- * otherwise.
75
- *
76
- * @param messenger - Controller messenger.
77
- * @param quote - Quote being validated.
78
- * @param required - Required amount (raw string).
79
- * @param balance - Current balance (raw string).
80
- * @returns Detail rows: Required / Current / Missing.
81
- */
82
- function formatBalanceShortfall(messenger, quote, required, balance) {
83
- const { sourceChainId, sourceTokenAddress } = quote.request;
84
- const tokenInfo = getTokenInfo(messenger, sourceTokenAddress, sourceChainId);
85
- const requiredBn = new BigNumber(required);
86
- const balanceBn = new BigNumber(balance);
87
- // Only ever called when balance < required, so the shortfall is positive.
88
- const missing = requiredBn.minus(balanceBn);
89
- return [
90
- `Required: ${formatTokenAmount(requiredBn, tokenInfo)}`,
91
- `Current: ${formatTokenAmount(balanceBn, tokenInfo)}`,
92
- `Missing: ${formatTokenAmount(missing, tokenInfo)}`,
93
- ];
94
- }
95
- /**
96
- * Format an atomic token amount as a human-readable string with symbol.
97
- *
98
- * @param rawAmount - Amount in atomic units.
99
- * @param tokenInfo - Source token decimals and symbol, if available.
100
- * @returns Human-readable amount suffixed with the token symbol when known,
101
- * otherwise the raw atomic value.
102
- */
103
- function formatTokenAmount(rawAmount, tokenInfo) {
104
- if (!tokenInfo) {
105
- return rawAmount.toFixed();
106
- }
107
- const human = rawAmount.shiftedBy(-tokenInfo.decimals).toFixed();
108
- return `${human} ${tokenInfo.symbol}`;
109
- }
110
- async function getLiveSourceBalance(quote, messenger) {
111
- const { from, sourceChainId, sourceTokenAddress } = quote.request;
112
- const normalizedSourceTokenAddress = normalizeTokenAddress(sourceTokenAddress, sourceChainId, TokenAddressTarget.MetaMask);
113
- try {
114
- return await getLiveTokenBalance(messenger, from, sourceChainId, normalizedSourceTokenAddress);
115
- }
116
- catch (error) {
117
- throw new QuoteError({
118
- message: 'Unable to verify balance',
119
- reason: 'balance-unavailable',
120
- detail: [error.message],
121
- });
122
- }
123
- }
124
- function validateRequiredSourceAmount(messenger, quote, liveBalance) {
125
- if (quote.request.isPostQuote || quote.request.paymentOverride) {
126
- return;
127
- }
128
- const requiredAmount = new BigNumber(quote.sourceAmount.raw);
129
- const balance = new BigNumber(liveBalance);
130
- if (balance.isGreaterThanOrEqualTo(requiredAmount)) {
131
- return;
132
- }
133
- throw new QuoteError({
134
- message: 'Insufficient source balance for quote',
135
- reason: 'insufficient-source-balance',
136
- detail: formatBalanceShortfall(messenger, quote, requiredAmount.toFixed(), balance.toFixed()),
137
- });
138
- }
139
- function validateDecodedSourceTransfers(messenger, quote, liveBalance, transactions) {
140
- const decodedAmounts = getDecodedSourceTransferAmounts(quote, transactions);
141
- const requiredAmount = decodedAmounts
142
- .reduce((total, amount) => total.plus(amount), new BigNumber(0))
143
- .toString(10);
144
- const balance = new BigNumber(liveBalance);
145
- log('Decoded source transfer amounts', {
146
- decodedAmounts,
147
- liveBalance,
148
- requiredAmount,
149
- });
150
- if (balance.isGreaterThanOrEqualTo(requiredAmount)) {
151
- return;
152
- }
153
- throw new QuoteError({
154
- message: 'Insufficient source balance for decoded transfer',
155
- reason: 'insufficient-transfer-balance',
156
- detail: formatBalanceShortfall(messenger, quote, requiredAmount, liveBalance),
157
- });
158
- }
159
- function getDecodedSourceTransferAmounts(quote, transactions) {
160
- const { sourceChainId, sourceTokenAddress } = quote.request;
161
- const isNativeSource = sourceTokenAddress.toLowerCase() ===
162
- getNativeToken(sourceChainId).toLowerCase();
163
- if (isNativeSource) {
164
- return [];
165
- }
166
- const normalizedSourceTokenAddress = normalizeTokenAddress(sourceTokenAddress, sourceChainId, TokenAddressTarget.MetaMask).toLowerCase();
167
- return transactions
168
- .filter((transaction) => transaction.to &&
169
- normalizeTokenAddress(transaction.to, sourceChainId, TokenAddressTarget.MetaMask).toLowerCase() === normalizedSourceTokenAddress)
170
- .map((transaction) => transaction.data ? decodeTransferAmount(transaction.data) : undefined)
171
- .filter((amount) => amount !== undefined);
172
- }
173
- function decodeTransferAmount(data) {
174
- try {
175
- const result = erc20Interface.decodeFunctionData('transfer', data);
176
- return new BigNumber(result._value.toString()).toString(10);
177
- }
178
- catch {
179
- return undefined;
180
- }
181
- }
182
- function throwIfAborted(signal) {
183
- if (signal?.aborted) {
184
- throw new Error('Quote validation aborted');
185
- }
186
- }
187
- //# sourceMappingURL=validation.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"validation.mjs","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Interface } from '@ethersproject/abi';\nimport { abiERC20 } from '@metamask/metamask-eth-abis';\nimport type { Hex } from '@metamask/utils';\nimport { BigNumber } from 'bignumber.js';\n\nimport { createModuleLogger, projectLogger } from '../logger';\nimport type {\n QuoteErrorInfo,\n TransactionPayControllerMessenger,\n TransactionPayQuote,\n} from '../types';\nimport {\n SimulationTransaction,\n simulateQuoteTransactions,\n TransactionPaySimulationError,\n} from './simulation';\nimport {\n getLiveTokenBalance,\n getNativeToken,\n getTokenInfo,\n normalizeTokenAddress,\n TokenAddressTarget,\n} from './token';\n\nconst log = createModuleLogger(projectLogger, 'validation');\n\nconst erc20Interface = new Interface(abiERC20);\n\nexport type QuoteSimulation = {\n transactions: SimulationTransaction[];\n};\n\nexport type QuoteExecutionRequest = {\n messenger: TransactionPayControllerMessenger;\n quote: TransactionPayQuote<unknown>;\n signal?: AbortSignal;\n simulation: QuoteSimulation;\n};\n\nexport class QuoteError extends Error {\n readonly info: QuoteErrorInfo;\n\n constructor(info: QuoteErrorInfo) {\n super(info.message);\n this.name = 'QuoteError';\n this.info = info;\n }\n}\n\nexport async function validateQuoteExecution({\n messenger,\n quote,\n signal,\n simulation,\n}: QuoteExecutionRequest): Promise<void> {\n throwIfAborted(signal);\n\n const liveBalance = await getLiveSourceBalance(quote, messenger);\n\n log('Live source balance', {\n from: quote.request.from,\n liveBalance,\n sourceChainId: quote.request.sourceChainId,\n sourceTokenAddress: quote.request.sourceTokenAddress,\n });\n\n throwIfAborted(signal);\n\n log('Checking quote source amount', {\n hasPaymentOverride: Boolean(quote.request.paymentOverride),\n isPostQuote: Boolean(quote.request.isPostQuote),\n liveBalance,\n requiredAmount: quote.sourceAmount.raw,\n });\n\n validateRequiredSourceAmount(messenger, quote, liveBalance);\n\n log('Quote source amount check passed');\n\n log('Checking decoded source transfers', {\n sourceChainId: quote.request.sourceChainId,\n sourceTokenAddress: quote.request.sourceTokenAddress,\n transactionCount: simulation.transactions.length,\n });\n\n validateDecodedSourceTransfers(\n messenger,\n quote,\n liveBalance,\n simulation.transactions,\n );\n\n log('Decoded source transfers check passed');\n\n throwIfAborted(signal);\n\n log('Starting simulation', {\n chainId: quote.request.sourceChainId,\n transactions: simulation.transactions,\n });\n\n try {\n await simulateQuoteTransactions({\n chainId: quote.request.sourceChainId,\n messenger,\n transactions: simulation.transactions,\n });\n\n log('Simulation passed');\n } catch (error) {\n throwIfAborted(signal);\n\n if (error instanceof TransactionPaySimulationError) {\n throw new QuoteError({\n message: 'Quote simulation failed',\n reason: 'simulation-failed',\n detail: [error.message],\n });\n }\n\n throw error;\n }\n}\n\nexport function isQuoteError(error: unknown): error is QuoteError {\n return error instanceof QuoteError;\n}\n\n/**\n * Format an amount shortfall into display-ready detail rows.\n *\n * Amounts are formatted using the source token decimals and symbol when\n * available (e.g. `Required: 1.5 USDC`), falling back to the raw atomic values\n * otherwise.\n *\n * @param messenger - Controller messenger.\n * @param quote - Quote being validated.\n * @param required - Required amount (raw string).\n * @param balance - Current balance (raw string).\n * @returns Detail rows: Required / Current / Missing.\n */\nfunction formatBalanceShortfall(\n messenger: TransactionPayControllerMessenger,\n quote: TransactionPayQuote<unknown>,\n required: string,\n balance: string,\n): string[] {\n const { sourceChainId, sourceTokenAddress } = quote.request;\n const tokenInfo = getTokenInfo(messenger, sourceTokenAddress, sourceChainId);\n\n const requiredBn = new BigNumber(required);\n const balanceBn = new BigNumber(balance);\n\n // Only ever called when balance < required, so the shortfall is positive.\n const missing = requiredBn.minus(balanceBn);\n\n return [\n `Required: ${formatTokenAmount(requiredBn, tokenInfo)}`,\n `Current: ${formatTokenAmount(balanceBn, tokenInfo)}`,\n `Missing: ${formatTokenAmount(missing, tokenInfo)}`,\n ];\n}\n\n/**\n * Format an atomic token amount as a human-readable string with symbol.\n *\n * @param rawAmount - Amount in atomic units.\n * @param tokenInfo - Source token decimals and symbol, if available.\n * @returns Human-readable amount suffixed with the token symbol when known,\n * otherwise the raw atomic value.\n */\nfunction formatTokenAmount(\n rawAmount: BigNumber,\n tokenInfo: { decimals: number; symbol: string } | undefined,\n): string {\n if (!tokenInfo) {\n return rawAmount.toFixed();\n }\n\n const human = rawAmount.shiftedBy(-tokenInfo.decimals).toFixed();\n\n return `${human} ${tokenInfo.symbol}`;\n}\n\nasync function getLiveSourceBalance(\n quote: TransactionPayQuote<unknown>,\n messenger: TransactionPayControllerMessenger,\n): Promise<string> {\n const { from, sourceChainId, sourceTokenAddress } = quote.request;\n const normalizedSourceTokenAddress = normalizeTokenAddress(\n sourceTokenAddress,\n sourceChainId,\n TokenAddressTarget.MetaMask,\n );\n\n try {\n return await getLiveTokenBalance(\n messenger,\n from,\n sourceChainId,\n normalizedSourceTokenAddress,\n );\n } catch (error) {\n throw new QuoteError({\n message: 'Unable to verify balance',\n reason: 'balance-unavailable',\n detail: [(error as Error).message],\n });\n }\n}\n\nfunction validateRequiredSourceAmount(\n messenger: TransactionPayControllerMessenger,\n quote: TransactionPayQuote<unknown>,\n liveBalance: string,\n): void {\n if (quote.request.isPostQuote || quote.request.paymentOverride) {\n return;\n }\n\n const requiredAmount = new BigNumber(quote.sourceAmount.raw);\n const balance = new BigNumber(liveBalance);\n\n if (balance.isGreaterThanOrEqualTo(requiredAmount)) {\n return;\n }\n\n throw new QuoteError({\n message: 'Insufficient source balance for quote',\n reason: 'insufficient-source-balance',\n detail: formatBalanceShortfall(\n messenger,\n quote,\n requiredAmount.toFixed(),\n balance.toFixed(),\n ),\n });\n}\n\nfunction validateDecodedSourceTransfers(\n messenger: TransactionPayControllerMessenger,\n quote: TransactionPayQuote<unknown>,\n liveBalance: string,\n transactions: SimulationTransaction[],\n): void {\n const decodedAmounts = getDecodedSourceTransferAmounts(quote, transactions);\n\n const requiredAmount = decodedAmounts\n .reduce((total, amount) => total.plus(amount), new BigNumber(0))\n .toString(10);\n\n const balance = new BigNumber(liveBalance);\n\n log('Decoded source transfer amounts', {\n decodedAmounts,\n liveBalance,\n requiredAmount,\n });\n\n if (balance.isGreaterThanOrEqualTo(requiredAmount)) {\n return;\n }\n\n throw new QuoteError({\n message: 'Insufficient source balance for decoded transfer',\n reason: 'insufficient-transfer-balance',\n detail: formatBalanceShortfall(\n messenger,\n quote,\n requiredAmount,\n liveBalance,\n ),\n });\n}\n\nfunction getDecodedSourceTransferAmounts(\n quote: TransactionPayQuote<unknown>,\n transactions: SimulationTransaction[],\n): string[] {\n const { sourceChainId, sourceTokenAddress } = quote.request;\n const isNativeSource =\n sourceTokenAddress.toLowerCase() ===\n getNativeToken(sourceChainId).toLowerCase();\n\n if (isNativeSource) {\n return [];\n }\n\n const normalizedSourceTokenAddress = normalizeTokenAddress(\n sourceTokenAddress,\n sourceChainId,\n TokenAddressTarget.MetaMask,\n ).toLowerCase();\n\n return transactions\n .filter(\n (transaction) =>\n transaction.to &&\n normalizeTokenAddress(\n transaction.to,\n sourceChainId,\n TokenAddressTarget.MetaMask,\n ).toLowerCase() === normalizedSourceTokenAddress,\n )\n .map((transaction) =>\n transaction.data ? decodeTransferAmount(transaction.data) : undefined,\n )\n .filter((amount): amount is string => amount !== undefined);\n}\n\nfunction decodeTransferAmount(data: Hex): string | undefined {\n try {\n const result = erc20Interface.decodeFunctionData('transfer', data);\n return new BigNumber(result._value.toString()).toString(10);\n } catch {\n return undefined;\n }\n}\n\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw new Error('Quote validation aborted');\n }\n}\n"]}